Implement Spec 003: Result Display
This commit is contained in:
67
main.py
67
main.py
@@ -17,7 +17,7 @@ from AppKit import (
|
|||||||
NSUserInterfaceLayoutOrientationVertical, NSLayoutAttributeCenterX,
|
NSUserInterfaceLayoutOrientationVertical, NSLayoutAttributeCenterX,
|
||||||
NSLayoutAttributeCenterY, NSLayoutAttributeWidth, NSLayoutAttributeHeight,
|
NSLayoutAttributeCenterY, NSLayoutAttributeWidth, NSLayoutAttributeHeight,
|
||||||
NSLayoutAttributeTop, NSLayoutAttributeBottom, NSLayoutAttributeLeading,
|
NSLayoutAttributeTop, NSLayoutAttributeBottom, NSLayoutAttributeLeading,
|
||||||
NSLayoutAttributeTrailing
|
NSLayoutAttributeTrailing, NSScrollView, NSTextView, NB
|
||||||
)
|
)
|
||||||
from Foundation import NSObject, NSTimer, NSDate
|
from Foundation import NSObject, NSTimer, NSDate
|
||||||
|
|
||||||
@@ -26,7 +26,7 @@ load_dotenv()
|
|||||||
class ItemSenseApp(NSObject):
|
class ItemSenseApp(NSObject):
|
||||||
def applicationDidFinishLaunching_(self, notification):
|
def applicationDidFinishLaunching_(self, notification):
|
||||||
self.window = NSWindow.alloc().initWithContentRect_styleMask_backing_defer_(
|
self.window = NSWindow.alloc().initWithContentRect_styleMask_backing_defer_(
|
||||||
NSMakeRect(0, 0, 800, 600),
|
NSMakeRect(0, 0, 800, 700), # Increased height for text view
|
||||||
NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskResizable | NSWindowStyleMaskMiniaturizable,
|
NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskResizable | NSWindowStyleMaskMiniaturizable,
|
||||||
NSBackingStoreBuffered,
|
NSBackingStoreBuffered,
|
||||||
False
|
False
|
||||||
@@ -44,8 +44,42 @@ class ItemSenseApp(NSObject):
|
|||||||
# Image View for Camera Feed
|
# Image View for Camera Feed
|
||||||
self.image_view = NSImageView.alloc().init()
|
self.image_view = NSImageView.alloc().init()
|
||||||
self.image_view.setImageScaling_(0) # NSImageScaleProportionallyDown
|
self.image_view.setImageScaling_(0) # NSImageScaleProportionallyDown
|
||||||
|
# Add constraint for height logic later if needed, but stackview handles it well enough for basic
|
||||||
self.stack_view.addView_inGravity_(self.image_view, 1) # Top gravity
|
self.stack_view.addView_inGravity_(self.image_view, 1) # Top gravity
|
||||||
|
|
||||||
|
# Result View (Scrollable Text)
|
||||||
|
self.scroll_view = NSScrollView.alloc().init()
|
||||||
|
self.scroll_view.setHasVerticalScroller_(True)
|
||||||
|
self.scroll_view.setBorderType_(2) # NSBezelBorder
|
||||||
|
|
||||||
|
# Determine size (approx) - needed for the content view mostly
|
||||||
|
# But StackView will resize it. Let's give it an intrinsic size or min size constraint if possible.
|
||||||
|
# For simplicity in PyObjC without autolayout constraints helpers:
|
||||||
|
self.scroll_view.setHeightAdjustLimit_(1.0) # ??? No, simple init is usually fine in stackview
|
||||||
|
|
||||||
|
# Text View
|
||||||
|
content_size = self.scroll_view.contentSize()
|
||||||
|
self.text_view = NSTextView.alloc().initWithFrame_(NSMakeRect(0, 0, content_size.width, content_size.height))
|
||||||
|
self.text_view.setMinSize_(NSMakeSize(0.0, content_size.height))
|
||||||
|
self.text_view.setMaxSize_(NSMakeSize(float("inf"), float("inf")))
|
||||||
|
self.text_view.setVerticallyResizable_(True)
|
||||||
|
self.text_view.setHorizontallyResizable_(False)
|
||||||
|
self.text_view.setAutoresizingMask_(18) # NSViewWidthSizable | NSViewHeightSizable
|
||||||
|
self.text_view.textContainer().setContainerSize_(NSMakeSize(content_size.width, float("inf")))
|
||||||
|
self.text_view.textContainer().setWidthTracksTextView_(True)
|
||||||
|
self.text_view.setEditable_(False)
|
||||||
|
self.text_view.setRichText_(False)
|
||||||
|
|
||||||
|
self.scroll_view.setDocumentView_(self.text_view)
|
||||||
|
|
||||||
|
# We need to ensure the scroll view has some height. StackView might crush it if not careful.
|
||||||
|
# We can set a frame size, or add a constraint.
|
||||||
|
# Let's simple try adding it to stack view.
|
||||||
|
self.stack_view.addView_inGravity_(self.scroll_view, 2)
|
||||||
|
|
||||||
|
# Hide initially or just empty? Let's keep it visible so layout stabilizes.
|
||||||
|
self.text_view.setString_("")
|
||||||
|
|
||||||
# Capture Button
|
# Capture Button
|
||||||
self.capture_button = NSButton.buttonWithTitle_target_action_("Capture", self, "captureClicked:")
|
self.capture_button = NSButton.buttonWithTitle_target_action_("Capture", self, "captureClicked:")
|
||||||
self.stack_view.addView_inGravity_(self.capture_button, 3) # Bottom gravity
|
self.stack_view.addView_inGravity_(self.capture_button, 3) # Bottom gravity
|
||||||
@@ -56,6 +90,7 @@ class ItemSenseApp(NSObject):
|
|||||||
self.cap = cv2.VideoCapture(0)
|
self.cap = cv2.VideoCapture(0)
|
||||||
if not self.cap.isOpened():
|
if not self.cap.isOpened():
|
||||||
NSLog("Error: Could not open camera")
|
NSLog("Error: Could not open camera")
|
||||||
|
self.text_view.setString_("Error: Could not open camera.")
|
||||||
|
|
||||||
# State
|
# State
|
||||||
self.is_capturing = True
|
self.is_capturing = True
|
||||||
@@ -81,12 +116,9 @@ class ItemSenseApp(NSObject):
|
|||||||
ret, frame = self.cap.read()
|
ret, frame = self.cap.read()
|
||||||
if ret:
|
if ret:
|
||||||
self.current_frame = frame # Store BGR frame
|
self.current_frame = frame # Store BGR frame
|
||||||
|
|
||||||
# Convert BGR to RGB
|
|
||||||
rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
|
rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
|
||||||
height, width, channels = rgb_frame.shape
|
height, width, channels = rgb_frame.shape
|
||||||
|
|
||||||
# Using PPM format is fast (uncompressed)
|
|
||||||
header = f"P6 {width} {height} 255 ".encode()
|
header = f"P6 {width} {height} 255 ".encode()
|
||||||
data = header + rgb_frame.tobytes()
|
data = header + rgb_frame.tobytes()
|
||||||
ns_data = objc.lookUpClass("NSData").dataWithBytes_length_(data, len(data))
|
ns_data = objc.lookUpClass("NSData").dataWithBytes_length_(data, len(data))
|
||||||
@@ -100,10 +132,21 @@ class ItemSenseApp(NSObject):
|
|||||||
self.is_capturing = False
|
self.is_capturing = False
|
||||||
self.capture_button.setTitle_("Processing...")
|
self.capture_button.setTitle_("Processing...")
|
||||||
self.capture_button.setEnabled_(False)
|
self.capture_button.setEnabled_(False)
|
||||||
|
self.text_view.setString_("Analyzing image...")
|
||||||
|
|
||||||
# Start background processing
|
# Start background processing
|
||||||
threading.Thread(target=self.processImage).start()
|
threading.Thread(target=self.processImage).start()
|
||||||
|
|
||||||
|
def resetScan_(self, sender):
|
||||||
|
print("Resetting...")
|
||||||
|
self.text_view.setString_("")
|
||||||
|
self.capture_button.setTitle_("Capture")
|
||||||
|
self.capture_button.setAction_("captureClicked:")
|
||||||
|
self.is_capturing = True
|
||||||
|
|
||||||
|
# Restart timer if it was invalidated? No, we didn't invalidate it, just guarded with is_capturing.
|
||||||
|
# So it should resume immediately.
|
||||||
|
|
||||||
def processImage(self):
|
def processImage(self):
|
||||||
try:
|
try:
|
||||||
if self.current_frame is None:
|
if self.current_frame is None:
|
||||||
@@ -117,7 +160,7 @@ class ItemSenseApp(NSObject):
|
|||||||
client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
|
client = OpenAI(api_key=os.getenv("OPENAI_API_KEY"))
|
||||||
|
|
||||||
response = client.chat.completions.create(
|
response = client.chat.completions.create(
|
||||||
model="gpt-4o-mini", # Fallback to 4o-mini as 5-mini is hypothetical/beta
|
model="gpt-4o-mini",
|
||||||
messages=[
|
messages=[
|
||||||
{
|
{
|
||||||
"role": "user",
|
"role": "user",
|
||||||
@@ -142,16 +185,18 @@ class ItemSenseApp(NSObject):
|
|||||||
self.performSelectorOnMainThread_withObject_waitUntilDone_("handleError:", str(e), False)
|
self.performSelectorOnMainThread_withObject_waitUntilDone_("handleError:", str(e), False)
|
||||||
|
|
||||||
def handleResponse_(self, result):
|
def handleResponse_(self, result):
|
||||||
print(f"OpenAI Response: {result}")
|
print(f"OpenAI Response received")
|
||||||
# For now, just reset state (waiting for Spec 003 for UI display)
|
self.text_view.setString_(result)
|
||||||
self.capture_button.setTitle_("Captured (Check Console)")
|
self.capture_button.setTitle_("Scan Another")
|
||||||
# In reality, we'd wait for user to reset, but let's re-enable reset flow in Spec 003.
|
self.capture_button.setEnabled_(True)
|
||||||
# For this spec, just showing we got the response is enough.
|
self.capture_button.setAction_("resetScan:")
|
||||||
|
|
||||||
def handleError_(self, error_msg):
|
def handleError_(self, error_msg):
|
||||||
print(f"Error: {error_msg}")
|
print(f"Error: {error_msg}")
|
||||||
|
self.text_view.setString_(f"Error: {error_msg}")
|
||||||
self.capture_button.setTitle_("Error - Try Again")
|
self.capture_button.setTitle_("Error - Try Again")
|
||||||
self.capture_button.setEnabled_(True)
|
self.capture_button.setEnabled_(True)
|
||||||
|
self.capture_button.setAction_("captureClicked:") # Ensure it resets to capture logic
|
||||||
self.is_capturing = True
|
self.is_capturing = True
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
# Feature: Result Display (PyObjC)
|
# Feature: Result Display (PyObjC)
|
||||||
|
|
||||||
|
## Status: COMPLETE
|
||||||
|
|
||||||
## Description
|
## Description
|
||||||
Display the analysis results natively in the AppKit UI.
|
Display the analysis results natively in the AppKit UI.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user