From da6b6ddcd3de4865f4e4dcbed7d01d3e2c80f12a Mon Sep 17 00:00:00 2001 From: jared Date: Wed, 21 Jan 2026 15:41:18 -0500 Subject: [PATCH] Port ItemSense to iOS --- .gitignore | 18 + .../ItemSense.xcodeproj/project.pbxproj | 339 ++++++++++++++++++ .../contents.xcworkspacedata | 7 + .../UserInterfaceState.xcuserstate | Bin 0 -> 18619 bytes .../xcschemes/xcschememanagement.plist | 14 + .../AccentColor.colorset/Contents.json | 11 + .../AppIcon.appiconset/Contents.json | 35 ++ .../ItemSense/Assets.xcassets/Contents.json | 6 + .../ItemSense/Camera/CameraManager.swift | 180 ++++++++++ iOS/ItemSense/ItemSense/ContentView.swift | 111 ++++++ iOS/ItemSense/ItemSense/ItemSenseApp.swift | 17 + .../ItemSense/Models/ItemAnalysis.swift | 18 + iOS/ItemSense/ItemSense/Secrets.swift | 13 + .../ItemSense/Services/OpenAIService.swift | 104 ++++++ .../ItemSense/Views/CameraPreview.swift | 33 ++ iOS/ItemSense/ItemSense/Views/WebView.swift | 24 ++ iOS/ItemSense/check_build.sh | 27 ++ AGENTS.md => python/AGENTS.md | 0 CLAUDE.md => python/CLAUDE.md | 0 PROMPT_build.md => python/PROMPT_build.md | 0 PROMPT_plan.md => python/PROMPT_plan.md | 0 README.md => python/README.md | 0 .../implementation_plan.md | 0 main.py => python/main.py | 0 requirements.txt => python/requirements.txt | 0 .../scripts}/ralph-loop-codex.sh | 0 {scripts => python/scripts}/ralph-loop.sh | 0 {specs => python/specs}/001-core-ui-camera.md | 0 .../specs}/002-openai-integration.md | 0 {specs => python/specs}/003-result-display.md | 0 task.md => python/task.md | 0 walkthrough.md => python/walkthrough.md | 0 32 files changed, 957 insertions(+) create mode 100644 iOS/ItemSense/ItemSense.xcodeproj/project.pbxproj create mode 100644 iOS/ItemSense/ItemSense.xcodeproj/project.xcworkspace/contents.xcworkspacedata create mode 100644 iOS/ItemSense/ItemSense.xcodeproj/project.xcworkspace/xcuserdata/jared.xcuserdatad/UserInterfaceState.xcuserstate create mode 100644 iOS/ItemSense/ItemSense.xcodeproj/xcuserdata/jared.xcuserdatad/xcschemes/xcschememanagement.plist create mode 100644 iOS/ItemSense/ItemSense/Assets.xcassets/AccentColor.colorset/Contents.json create mode 100644 iOS/ItemSense/ItemSense/Assets.xcassets/AppIcon.appiconset/Contents.json create mode 100644 iOS/ItemSense/ItemSense/Assets.xcassets/Contents.json create mode 100644 iOS/ItemSense/ItemSense/Camera/CameraManager.swift create mode 100644 iOS/ItemSense/ItemSense/ContentView.swift create mode 100644 iOS/ItemSense/ItemSense/ItemSenseApp.swift create mode 100644 iOS/ItemSense/ItemSense/Models/ItemAnalysis.swift create mode 100644 iOS/ItemSense/ItemSense/Secrets.swift create mode 100644 iOS/ItemSense/ItemSense/Services/OpenAIService.swift create mode 100644 iOS/ItemSense/ItemSense/Views/CameraPreview.swift create mode 100644 iOS/ItemSense/ItemSense/Views/WebView.swift create mode 100755 iOS/ItemSense/check_build.sh rename AGENTS.md => python/AGENTS.md (100%) rename CLAUDE.md => python/CLAUDE.md (100%) rename PROMPT_build.md => python/PROMPT_build.md (100%) rename PROMPT_plan.md => python/PROMPT_plan.md (100%) rename README.md => python/README.md (100%) rename implementation_plan.md => python/implementation_plan.md (100%) rename main.py => python/main.py (100%) rename requirements.txt => python/requirements.txt (100%) rename {scripts => python/scripts}/ralph-loop-codex.sh (100%) rename {scripts => python/scripts}/ralph-loop.sh (100%) rename {specs => python/specs}/001-core-ui-camera.md (100%) rename {specs => python/specs}/002-openai-integration.md (100%) rename {specs => python/specs}/003-result-display.md (100%) rename task.md => python/task.md (100%) rename walkthrough.md => python/walkthrough.md (100%) diff --git a/.gitignore b/.gitignore index 48643ec..e8277db 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,21 @@ __pycache__/ *.pyc .DS_Store .specify/ + +# iOS / Xcode +build/ +DerivedData/ +*.moved-aside +*.rxproj +*.tps +*.xcodeproj/project.xcworkspace +*.xcodeproj/xcuserdata +*.xcworkspace/xcuserdata +*.xccheckout +*.xcscmblueprint + +# CocoaPods +Pods/ + + +iOS/ItemSense/build/ diff --git a/iOS/ItemSense/ItemSense.xcodeproj/project.pbxproj b/iOS/ItemSense/ItemSense.xcodeproj/project.pbxproj new file mode 100644 index 0000000..64693e4 --- /dev/null +++ b/iOS/ItemSense/ItemSense.xcodeproj/project.pbxproj @@ -0,0 +1,339 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 77; + objects = { + +/* Begin PBXFileReference section */ + 7F1445BE2F216C600065C89B /* ItemSense.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ItemSense.app; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFileSystemSynchronizedRootGroup section */ + 7F1445C02F216C600065C89B /* ItemSense */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = ItemSense; + sourceTree = ""; + }; +/* End PBXFileSystemSynchronizedRootGroup section */ + +/* Begin PBXFrameworksBuildPhase section */ + 7F1445BB2F216C600065C89B /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 7F1445B52F216C600065C89B = { + isa = PBXGroup; + children = ( + 7F1445C02F216C600065C89B /* ItemSense */, + 7F1445BF2F216C600065C89B /* Products */, + ); + sourceTree = ""; + }; + 7F1445BF2F216C600065C89B /* Products */ = { + isa = PBXGroup; + children = ( + 7F1445BE2F216C600065C89B /* ItemSense.app */, + ); + name = Products; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 7F1445BD2F216C600065C89B /* ItemSense */ = { + isa = PBXNativeTarget; + buildConfigurationList = 7F1445C92F216C610065C89B /* Build configuration list for PBXNativeTarget "ItemSense" */; + buildPhases = ( + 7F1445BA2F216C600065C89B /* Sources */, + 7F1445BB2F216C600065C89B /* Frameworks */, + 7F1445BC2F216C600065C89B /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + 7F1445C02F216C600065C89B /* ItemSense */, + ); + name = ItemSense; + packageProductDependencies = ( + ); + productName = ItemSense; + productReference = 7F1445BE2F216C600065C89B /* ItemSense.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 7F1445B62F216C600065C89B /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 2620; + LastUpgradeCheck = 2620; + TargetAttributes = { + 7F1445BD2F216C600065C89B = { + CreatedOnToolsVersion = 26.2; + }; + }; + }; + buildConfigurationList = 7F1445B92F216C600065C89B /* Build configuration list for PBXProject "ItemSense" */; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 7F1445B52F216C600065C89B; + minimizedProjectReferenceProxies = 1; + preferredProjectObjectVersion = 77; + productRefGroup = 7F1445BF2F216C600065C89B /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 7F1445BD2F216C600065C89B /* ItemSense */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 7F1445BC2F216C600065C89B /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 7F1445BA2F216C600065C89B /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 7F1445C72F216C610065C89B /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + DEVELOPMENT_TEAM = 7X85543FQQ; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 26.2; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 7F1445C82F216C610065C89B /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + DEVELOPMENT_TEAM = 7X85543FQQ; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 26.2; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 7F1445CA2F216C610065C89B /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 7X85543FQQ; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_NSCameraUsageDescription = "ItemSense needs camera access to identify items."; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.jaredlog.ItemSense; + PRODUCT_NAME = "$(TARGET_NAME)"; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 7F1445CB2F216C610065C89B /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = 7X85543FQQ; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_NSCameraUsageDescription = "ItemSense needs camera access to identify items."; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.jaredlog.ItemSense; + PRODUCT_NAME = "$(TARGET_NAME)"; + STRING_CATALOG_GENERATE_SYMBOLS = YES; + SWIFT_APPROACHABLE_CONCURRENCY = YES; + SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 7F1445B92F216C600065C89B /* Build configuration list for PBXProject "ItemSense" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 7F1445C72F216C610065C89B /* Debug */, + 7F1445C82F216C610065C89B /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 7F1445C92F216C610065C89B /* Build configuration list for PBXNativeTarget "ItemSense" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 7F1445CA2F216C610065C89B /* Debug */, + 7F1445CB2F216C610065C89B /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 7F1445B62F216C600065C89B /* Project object */; +} diff --git a/iOS/ItemSense/ItemSense.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/iOS/ItemSense/ItemSense.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/iOS/ItemSense/ItemSense.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ + + + + + diff --git a/iOS/ItemSense/ItemSense.xcodeproj/project.xcworkspace/xcuserdata/jared.xcuserdatad/UserInterfaceState.xcuserstate b/iOS/ItemSense/ItemSense.xcodeproj/project.xcworkspace/xcuserdata/jared.xcuserdatad/UserInterfaceState.xcuserstate new file mode 100644 index 0000000000000000000000000000000000000000..754c44e49bacc3ea458d177d4954acdf364e6491 GIT binary patch literal 18619 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 + + + + SchemeUserState + + ItemSense.xcscheme_^#shared#^_ + + orderHint + 0 + + + + diff --git a/iOS/ItemSense/ItemSense/Assets.xcassets/AccentColor.colorset/Contents.json b/iOS/ItemSense/ItemSense/Assets.xcassets/AccentColor.colorset/Contents.json new file mode 100644 index 0000000..eb87897 --- /dev/null +++ b/iOS/ItemSense/ItemSense/Assets.xcassets/AccentColor.colorset/Contents.json @@ -0,0 +1,11 @@ +{ + "colors" : [ + { + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/iOS/ItemSense/ItemSense/Assets.xcassets/AppIcon.appiconset/Contents.json b/iOS/ItemSense/ItemSense/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..2305880 --- /dev/null +++ b/iOS/ItemSense/ItemSense/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,35 @@ +{ + "images" : [ + { + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "tinted" + } + ], + "idiom" : "universal", + "platform" : "ios", + "size" : "1024x1024" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/iOS/ItemSense/ItemSense/Assets.xcassets/Contents.json b/iOS/ItemSense/ItemSense/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/iOS/ItemSense/ItemSense/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/iOS/ItemSense/ItemSense/Camera/CameraManager.swift b/iOS/ItemSense/ItemSense/Camera/CameraManager.swift new file mode 100644 index 0000000..4f1bc93 --- /dev/null +++ b/iOS/ItemSense/ItemSense/Camera/CameraManager.swift @@ -0,0 +1,180 @@ +// +// CameraManager.swift +// ItemSense +// +// Created by Jared Evans on 1/21/26. +// + +import AVFoundation +import UIKit +import Combine + + +class CameraManager: NSObject, ObservableObject { + @Published var session = AVCaptureSession() + @Published var alert: AlertError? + @Published var isAuthorizationGranted = false + + // Delegate to send frames or captured images back + @Published var currentImage: UIImage? + + private let sessionQueue = DispatchQueue(label: "com.itemsense.sessionQueue") + private var videoOutput = AVCaptureVideoDataOutput() + + enum Status { + case unconfigured + case configured + case unauthorized + case failed + } + + @Published var status = Status.unconfigured + + override init() { + super.init() + checkPermissions() + sessionQueue.async { + self.configureSession() + self.session.startRunning() + } + } + + func checkPermissions() { + switch AVCaptureDevice.authorizationStatus(for: .video) { + case .notDetermined: + sessionQueue.suspend() + AVCaptureDevice.requestAccess(for: .video) { authorized in + if !authorized { + DispatchQueue.main.async { + self.status = .unauthorized + self.set(error: .denied) + } + } + self.sessionQueue.resume() + } + case .restricted: + status = .unauthorized + set(error: .restricted) + case .denied: + status = .unauthorized + set(error: .denied) + case .authorized: + break + @unknown default: + status = .unauthorized + set(error: .unknown) + } + } + + private func configureSession() { + guard status == .unconfigured else { return } + + session.beginConfiguration() + session.sessionPreset = .photo + + guard let camera = AVCaptureDevice.default(.builtInWideAngleCamera, for: .video, position: .back) else { + set(error: .cameraUnavailable) + DispatchQueue.main.async { + self.status = .failed + } + session.commitConfiguration() + return + } + + do { + let cameraInput = try AVCaptureDeviceInput(device: camera) + if session.canAddInput(cameraInput) { + session.addInput(cameraInput) + } else { + set(error: .cannotAddInput) + DispatchQueue.main.async { + self.status = .failed + } + session.commitConfiguration() + return + } + } catch { + set(error: .createCaptureInput(error)) + DispatchQueue.main.async { + self.status = .failed + } + session.commitConfiguration() + return + } + + if session.canAddOutput(videoOutput) { + session.addOutput(videoOutput) + videoOutput.videoSettings = [kCVPixelBufferPixelFormatTypeKey as String: Int(kCVPixelFormatType_32BGRA)] + videoOutput.setSampleBufferDelegate(self, queue: sessionQueue) + } else { + set(error: .cannotAddOutput) + DispatchQueue.main.async { + self.status = .failed + } + session.commitConfiguration() + return + } + + DispatchQueue.main.async { + self.status = .configured + } + session.commitConfiguration() + } + + func set(error: CameraError?) { + DispatchQueue.main.async { + if let error = error { + self.alert = AlertError(title: "Camera Error", message: error.description) + } + } + } +} + +extension CameraManager: AVCaptureVideoDataOutputSampleBufferDelegate { + func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) { + guard let pixelBuffer = CMSampleBufferGetImageBuffer(sampleBuffer) else { return } + + let ciImage = CIImage(cvPixelBuffer: pixelBuffer) + let context = CIContext() + + // Correct orientation (back camera typically needs 90 deg rotation on portrait) + // For simplicity we deliver the image and let the UI handle valid orientation or simple display + // Note: Real-world apps need careful orientation handling. + + if let cgImage = context.createCGImage(ciImage, from: ciImage.extent) { + let uiImage = UIImage(cgImage: cgImage, scale: 1.0, orientation: .right) + DispatchQueue.main.async { + self.currentImage = uiImage + } + } + } +} + +// Error Handling +struct AlertError: Identifiable { + let id = UUID() + let title: String + let message: String +} + +enum CameraError: Error { + case cameraUnavailable + case cannotAddInput + case cannotAddOutput + case createCaptureInput(Error) + case denied + case restricted + case unknown + + var description: String { + switch self { + case .cameraUnavailable: return "Camera unavailable" + case .cannotAddInput: return "Cannot add input" + case .cannotAddOutput: return "Cannot add output" + case .createCaptureInput(let error): return "Creating input failed: \(error.localizedDescription)" + case .denied: return "Camera access denied" + case .restricted: return "Camera access restricted" + case .unknown: return "Unknown error" + } + } +} diff --git a/iOS/ItemSense/ItemSense/ContentView.swift b/iOS/ItemSense/ItemSense/ContentView.swift new file mode 100644 index 0000000..8cf6595 --- /dev/null +++ b/iOS/ItemSense/ItemSense/ContentView.swift @@ -0,0 +1,111 @@ +// +// ContentView.swift +// ItemSense +// +// Created by Jared Evans on 1/21/26. +// + +import SwiftUI + +struct ContentView: View { + @StateObject private var cameraManager = CameraManager() + @State private var descriptionText: String = "Ready to scan..." + @State private var isProcessing = false + @State private var showWebView = false + @State private var amazonURL: String = "" + + private let openAIService = OpenAIService() + + var body: some View { + ZStack { + // Camera Preview + CameraPreview(session: cameraManager.session) + .ignoresSafeArea() + + VStack { + // Top: Description Area + VStack { + ScrollView { + Text(descriptionText) + .padding() + .frame(maxWidth: .infinity, alignment: .leading) + .foregroundStyle(.white) + } + .frame(height: 150) + .background(.ultraThinMaterial) + .clipShape(RoundedRectangle(cornerRadius: 15)) + .padding() + } + + Spacer() + + // Bottom: Controls + HStack { + if isProcessing { + ProgressView() + .progressViewStyle(CircularProgressViewStyle(tint: .white)) + .scaleEffect(1.5) + } else { + Button(action: captureAndAnalyze) { + Circle() + .strokeBorder(.white, lineWidth: 3) + .background(Circle().fill(.red)) + .frame(width: 70, height: 70) + } + } + } + .padding(.bottom, 30) + } + } + .sheet(isPresented: $showWebView) { + WebView(urlString: amazonURL) + } + .alert(item: $cameraManager.alert) { alert in + Alert(title: Text(alert.title), message: Text(alert.message), dismissButton: .default(Text("OK"))) + } + } + + private func captureAndAnalyze() { + guard let image = cameraManager.currentImage else { + descriptionText = "No image available from camera." + return + } + + isProcessing = true + descriptionText = "Analyzing..." + + // Convert UIImage to Base64 + guard let imageData = image.jpegData(compressionQuality: 0.5) else { + descriptionText = "Failed to process image data." + isProcessing = false + return + } + + let base64String = imageData.base64EncodedString() + + Task { + do { + let analysis = try await openAIService.analyzeImage(base64Image: base64String) + + await MainActor.run { + self.descriptionText = analysis.description + if !analysis.searchTerm.isEmpty { + let query = analysis.searchTerm.replacingOccurrences(of: " ", with: "+") + self.amazonURL = "https://www.amazon.com/s?k=\(query)" + self.showWebView = true + } + self.isProcessing = false + } + } catch { + await MainActor.run { + self.descriptionText = "Error: \(error.localizedDescription)" + self.isProcessing = false + } + } + } + } +} + +#Preview { + ContentView() +} diff --git a/iOS/ItemSense/ItemSense/ItemSenseApp.swift b/iOS/ItemSense/ItemSense/ItemSenseApp.swift new file mode 100644 index 0000000..4c38e21 --- /dev/null +++ b/iOS/ItemSense/ItemSense/ItemSenseApp.swift @@ -0,0 +1,17 @@ +// +// ItemSenseApp.swift +// ItemSense +// +// Created by Jared Evans on 1/21/26. +// + +import SwiftUI + +@main +struct ItemSenseApp: App { + var body: some Scene { + WindowGroup { + ContentView() + } + } +} diff --git a/iOS/ItemSense/ItemSense/Models/ItemAnalysis.swift b/iOS/ItemSense/ItemSense/Models/ItemAnalysis.swift new file mode 100644 index 0000000..0bce29c --- /dev/null +++ b/iOS/ItemSense/ItemSense/Models/ItemAnalysis.swift @@ -0,0 +1,18 @@ +// +// ItemAnalysis.swift +// ItemSense +// +// Created by Jared Evans on 1/21/26. +// + +import Foundation + +struct ItemAnalysis: Codable { + let description: String + let searchTerm: String + + enum CodingKeys: String, CodingKey { + case description + case searchTerm = "search_term" + } +} diff --git a/iOS/ItemSense/ItemSense/Secrets.swift b/iOS/ItemSense/ItemSense/Secrets.swift new file mode 100644 index 0000000..92415d1 --- /dev/null +++ b/iOS/ItemSense/ItemSense/Secrets.swift @@ -0,0 +1,13 @@ +// +// Secrets.swift +// ItemSense +// +// Created by Jared Evans on 1/21/26. +// + +import Foundation + +struct Secrets { + // TODO: Replace with your actual OpenAI API Key + static let openAIAPIKey = "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" +} diff --git a/iOS/ItemSense/ItemSense/Services/OpenAIService.swift b/iOS/ItemSense/ItemSense/Services/OpenAIService.swift new file mode 100644 index 0000000..0dd3723 --- /dev/null +++ b/iOS/ItemSense/ItemSense/Services/OpenAIService.swift @@ -0,0 +1,104 @@ +// +// OpenAIService.swift +// ItemSense +// +// Created by Jared Evans on 1/21/26. +// + +import Foundation + +class OpenAIService { + + enum ServiceError: Error { + case invalidURL + case invalidResponse + case noData + case decodingError(Error) + case apiError(String) + } + + private let apiKey: String + + init(apiKey: String = Secrets.openAIAPIKey) { + self.apiKey = apiKey + } + + func analyzeImage(base64Image: String) async throws -> ItemAnalysis { + guard let url = URL(string: "https://api.openai.com/v1/chat/completions") else { + throw ServiceError.invalidURL + } + + var request = URLRequest(url: url) + request.httpMethod = "POST" + request.addValue("Bearer \(apiKey)", forHTTPHeaderField: "Authorization") + request.addValue("application/json", forHTTPHeaderField: "Content-Type") + + let promptText = """ + Identify the main item in the foreground, including the brand name if visible. Ignore the background and any people present. Return a JSON object with two keys: 'description' (a brief description of the item including brand) and 'search_term' (keywords to search for this item on Amazon, including brand). Return ONLY the JSON. Do not wrap in markdown code blocks. + """ + + // This payload structure mimics the python script's logic + let payload: [String: Any] = [ + "model": "gpt-4o-mini", + "messages": [ + [ + "role": "user", + "content": [ + ["type": "text", "text": promptText], + [ + "type": "image_url", + "image_url": [ + "url": "data:image/jpeg;base64,\(base64Image)" + ] + ] + ] + ] + ], + "max_tokens": 300 + ] + + request.httpBody = try JSONSerialization.data(withJSONObject: payload) + + let (data, response) = try await URLSession.shared.data(for: request) + + guard let httpResponse = response as? HTTPURLResponse, (200...299).contains(httpResponse.statusCode) else { + if let errorText = String(data: data, encoding: .utf8) { + throw ServiceError.apiError("Status: \((response as? HTTPURLResponse)?.statusCode ?? 0), Body: \(errorText)") + } + throw ServiceError.apiError("Status: \((response as? HTTPURLResponse)?.statusCode ?? 0), No Body") + } + + do { + let apiResponse = try JSONDecoder().decode(OpenAIResponse.self, from: data) + guard let content = apiResponse.choices.first?.message.content else { + throw ServiceError.noData + } + + // Clean up content if it contains markdown code blocks + let cleanContent = content + .replacingOccurrences(of: "```json", with: "") + .replacingOccurrences(of: "```", with: "") + .trimmingCharacters(in: .whitespacesAndNewlines) + + guard let jsonData = cleanContent.data(using: .utf8) else { + throw ServiceError.decodingError(NSError(domain: "DataError", code: 0)) + } + + return try JSONDecoder().decode(ItemAnalysis.self, from: jsonData) + + } catch { + throw ServiceError.decodingError(error) + } + } +} + +// Helper structs for OpenAI Response +struct OpenAIResponse: Decodable { + struct Choice: Decodable { + struct Message: Decodable { + let content: String + } + let message: Message + } + let choices: [Choice] +} diff --git a/iOS/ItemSense/ItemSense/Views/CameraPreview.swift b/iOS/ItemSense/ItemSense/Views/CameraPreview.swift new file mode 100644 index 0000000..b12ae4e --- /dev/null +++ b/iOS/ItemSense/ItemSense/Views/CameraPreview.swift @@ -0,0 +1,33 @@ +// +// CameraPreview.swift +// ItemSense +// +// Created by Jared Evans on 1/21/26. +// + +import SwiftUI +import AVFoundation + +struct CameraPreview: UIViewRepresentable { + class VideoPreviewView: UIView { + override class var layerClass: AnyClass { + AVCaptureVideoPreviewLayer.self + } + + var videoPreviewLayer: AVCaptureVideoPreviewLayer { + return layer as! AVCaptureVideoPreviewLayer + } + } + + let session: AVCaptureSession + + func makeUIView(context: Context) -> VideoPreviewView { + let view = VideoPreviewView() + + view.videoPreviewLayer.session = session + view.videoPreviewLayer.videoGravity = .resizeAspectFill + return view + } + + func updateUIView(_ uiView: VideoPreviewView, context: Context) { } +} diff --git a/iOS/ItemSense/ItemSense/Views/WebView.swift b/iOS/ItemSense/ItemSense/Views/WebView.swift new file mode 100644 index 0000000..9f92a6f --- /dev/null +++ b/iOS/ItemSense/ItemSense/Views/WebView.swift @@ -0,0 +1,24 @@ +// +// WebView.swift +// ItemSense +// +// Created by Jared Evans on 1/21/26. +// + +import SwiftUI +import WebKit + +struct WebView: UIViewRepresentable { + let urlString: String + + func makeUIView(context: Context) -> WKWebView { + return WKWebView() + } + + func updateUIView(_ uiView: WKWebView, context: Context) { + if let url = URL(string: urlString) { + let request = URLRequest(url: url) + uiView.load(request) + } + } +} diff --git a/iOS/ItemSense/check_build.sh b/iOS/ItemSense/check_build.sh new file mode 100755 index 0000000..022406d --- /dev/null +++ b/iOS/ItemSense/check_build.sh @@ -0,0 +1,27 @@ +#!/bin/zsh +set -o pipefail # Fail if xcodebuild fails, even with xcbeautify + +# --- Configuration --- +SCHEME="ItemSense" +DEVICE_NAME="iPhone 17 Pro" +BUILD_PATH="./build" + +echo "🔍 Checking compilation for $SCHEME..." + +# Build Only (No Install/Launch) +# We use 'env -u' to hide Homebrew variables +# We use '-derivedDataPath' to keep it isolated +env -u CC -u CXX -u LIBCLANG_PATH xcodebuild \ + -scheme "$SCHEME" \ + -destination "platform=iOS Simulator,name=$DEVICE_NAME" \ + -configuration Debug \ + -derivedDataPath "$BUILD_PATH" \ + build | xcbeautify + +# Check exit code of the pipeline +if [ $? -eq 0 ]; then + echo "✅ Build Succeeded. No errors found." +else + echo "❌ Build Failed." + exit 1 +fi diff --git a/AGENTS.md b/python/AGENTS.md similarity index 100% rename from AGENTS.md rename to python/AGENTS.md diff --git a/CLAUDE.md b/python/CLAUDE.md similarity index 100% rename from CLAUDE.md rename to python/CLAUDE.md diff --git a/PROMPT_build.md b/python/PROMPT_build.md similarity index 100% rename from PROMPT_build.md rename to python/PROMPT_build.md diff --git a/PROMPT_plan.md b/python/PROMPT_plan.md similarity index 100% rename from PROMPT_plan.md rename to python/PROMPT_plan.md diff --git a/README.md b/python/README.md similarity index 100% rename from README.md rename to python/README.md diff --git a/implementation_plan.md b/python/implementation_plan.md similarity index 100% rename from implementation_plan.md rename to python/implementation_plan.md diff --git a/main.py b/python/main.py similarity index 100% rename from main.py rename to python/main.py diff --git a/requirements.txt b/python/requirements.txt similarity index 100% rename from requirements.txt rename to python/requirements.txt diff --git a/scripts/ralph-loop-codex.sh b/python/scripts/ralph-loop-codex.sh similarity index 100% rename from scripts/ralph-loop-codex.sh rename to python/scripts/ralph-loop-codex.sh diff --git a/scripts/ralph-loop.sh b/python/scripts/ralph-loop.sh similarity index 100% rename from scripts/ralph-loop.sh rename to python/scripts/ralph-loop.sh diff --git a/specs/001-core-ui-camera.md b/python/specs/001-core-ui-camera.md similarity index 100% rename from specs/001-core-ui-camera.md rename to python/specs/001-core-ui-camera.md diff --git a/specs/002-openai-integration.md b/python/specs/002-openai-integration.md similarity index 100% rename from specs/002-openai-integration.md rename to python/specs/002-openai-integration.md diff --git a/specs/003-result-display.md b/python/specs/003-result-display.md similarity index 100% rename from specs/003-result-display.md rename to python/specs/003-result-display.md diff --git a/task.md b/python/task.md similarity index 100% rename from task.md rename to python/task.md diff --git a/walkthrough.md b/python/walkthrough.md similarity index 100% rename from walkthrough.md rename to python/walkthrough.md