Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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)
}
}
}
Original file line number Diff line number Diff line change
@@ -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)
Expand All @@ -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)")
}
}
}
1 change: 1 addition & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.