diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d84378..53fe653 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,3 +16,4 @@ The format is based on Keep a Changelog and this project uses Semantic Versionin - Agent loop timeout and cancellation controls with deterministic recovery to `idle`. - Persona-driven face expression mapping and interruption-safe TTS output behavior. - Settings panel, startup validation gates, and hardened release-preview packaging workflow. +- Vision v1.1 foundation with feature-flagged on-demand frame capture wiring. diff --git a/Packages/VisionPipeline/Sources/VisionPipeline/VisionServices.swift b/Packages/VisionPipeline/Sources/VisionPipeline/VisionServices.swift index 12186bb..2d365fa 100644 --- a/Packages/VisionPipeline/Sources/VisionPipeline/VisionServices.swift +++ b/Packages/VisionPipeline/Sources/VisionPipeline/VisionServices.swift @@ -4,14 +4,21 @@ import Foundation public enum VisionPipelineError: Error, Equatable { case disabled case captureUnavailable + case captureFailed(String) +} + +public protocol FrameCapturing: Sendable { + func captureCurrentFrame() async throws -> Data } public actor SnapshotVisionService: VisionService { private let enabled: Bool + private let frameCapturer: FrameCapturing? private var queuedSnapshot: VisionContext? - public init(enabled: Bool = false) { + public init(enabled: Bool = false, frameCapturer: FrameCapturing? = nil) { self.enabled = enabled + self.frameCapturer = frameCapturer } public func queueSnapshotSummary(_ summary: String) { @@ -26,6 +33,16 @@ public actor SnapshotVisionService: VisionService { self.queuedSnapshot = nil return queuedSnapshot } - throw VisionPipelineError.captureUnavailable + + guard let frameCapturer else { + throw VisionPipelineError.captureUnavailable + } + + do { + let frameData = try await frameCapturer.captureCurrentFrame() + return VisionContext(summary: "Captured on-demand frame (\(frameData.count) bytes).") + } catch { + throw VisionPipelineError.captureFailed(error.localizedDescription) + } } } diff --git a/Packages/VisionPipeline/Tests/VisionPipelineTests/VisionPipelineTests.swift b/Packages/VisionPipeline/Tests/VisionPipelineTests/VisionPipelineTests.swift index c2d6013..6881fe9 100644 --- a/Packages/VisionPipeline/Tests/VisionPipelineTests/VisionPipelineTests.swift +++ b/Packages/VisionPipeline/Tests/VisionPipelineTests/VisionPipelineTests.swift @@ -1,6 +1,14 @@ import XCTest @testable import VisionPipeline +private struct MockFrameCapturer: FrameCapturing { + let payload: Data + + func captureCurrentFrame() async throws -> Data { + payload + } +} + final class VisionPipelineTests: XCTestCase { func testVisionFeatureFlag() async { let disabled = SnapshotVisionService(enabled: false) @@ -22,4 +30,28 @@ final class VisionPipelineTests: XCTestCase { XCTFail("Unexpected error: \(error)") } } + + func testOnDemandCaptureUsesFrameCapturerWhenQueueIsEmpty() async { + let capturer = MockFrameCapturer(payload: Data([0, 1, 2, 3, 4])) + let service = SnapshotVisionService(enabled: true, frameCapturer: capturer) + + do { + let context = try await service.captureSnapshotDescription() + XCTAssertTrue(context.summary.contains("5 bytes")) + } catch { + XCTFail("Expected frame capture summary: \(error)") + } + } + + func testCaptureUnavailableWithoutQueuedOrFrameSource() async { + let service = SnapshotVisionService(enabled: true, frameCapturer: nil) + do { + _ = try await service.captureSnapshotDescription() + XCTFail("Expected captureUnavailable") + } catch VisionPipelineError.captureUnavailable { + // expected + } catch { + XCTFail("Unexpected error: \(error)") + } + } } diff --git a/docs/architecture.md b/docs/architecture.md index cd82f85..201b73e 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -85,3 +85,4 @@ Timeout and cancellation guards: - `AVSpeechSynthesizerService` supports interruption-aware speaking and explicit stop behavior. - `SettingsStore` persists wake-word and vision toggles while enforcing telemetry-off policy. - `StartupValidator` gates agent startup on policy and manifest checks. +- `SnapshotVisionService` now supports on-demand frame-capture source wiring for v1.1.