3 Commits
Author SHA1 Message Date
james.maesandClaude Opus 4.7 958da3bb95 Event bus + greeter + gestures + activity + spatial + history + recording + tests + CI + CLAUDE.md
Four parallel agents shipped Tier 1+2+3 of the punch list, coordinated
through a fixed DetectionEventBus contract created upfront.

## Detection event bus (shared contract)
- Sources/Process/DetectionEvents.swift — `DetectionEvent.Kind` enum
  covering objectAppeared/Refreshed/Disappeared, faceRecognized,
  faceSeenUnknown, textRecognized, gestureDetected, activityDetected,
  spatialRelation.
- Sources/Process/DetectionEventBus.swift — thread-safe pub/sub, Token
  pattern with auto-unsubscribe on deinit, synchronous emit on caller's
  queue. Subscribers hop to their own queue as needed.

## ObjectDetector additions (Agent A)
- Publishes events for every track-add, track-loss, face-match,
  unmatched-face, recognized-text — with per-event throttles (text 2s
  per string, face name 5s, etc.) so the bus doesn't drown in repeats.
- New `DetectorStageTiming` struct + `lastStageTiming` accessor exposes
  the visionBundleMs / featurePrintMs / trackerMs / totalMs breakdown.
  Logged once a second at DEBUG level for visibility.
- Track UUID persistence via Sources/Process/TrackStore.swift
  — label→UUID at ~/Library/Application Support/VisionPOC/tracks.json,
  MRU-pruned to 50 entries, debounced 500ms saves. Same object across
  launches gets the same trackId → consistent per-track color.
- YOLO warmup at init (dispatched on detector queue): one dummy
  inference against a 416×416 mid-gray image so the first user-visible
  detection doesn't pay model-compile latency.

## Event consumers (Agent A)
- Sources/Process/Greeter.swift — AVSpeechSynthesizer-driven "Hello,
  <name>" on faceRecognized events. Honors Theme.greeterMuted and the
  Privacy Mode toggle. Throttle: 30s per name on top of the bus-level
  5s throttle.
- Sources/Process/GestureRecognizer.swift — five gesture heuristics
  (thumbs_up, open_hand, fist, pointing, peace) over hand-pose
  PoseDetections. Throttled 2s per gesture name.
- Sources/Process/ActivityRecognizer.swift — standing/sitting/
  crouching/hand_raised over body-pose PoseDetections. Throttled 5s
  per activity.
- Sources/Process/SpatialReasoner.swift — near/above/inside/holding
  relations between Detection pairs. Throttled 10s per triple.

## Video recording (Agent B)
- Sources/Render/Recorder.swift — @MainActor AVAssetWriter pipeline
  writing H.264 / 12 Mbps / 60fps to ~/Desktop/VisionPOC-...mov.
  Captures the rendered MTKView output via a new frame-tap callback
  on Renderer. Synchronous blit→staging-texture→CVPixelBuffer→
  AVAssetWriterInputPixelBufferAdaptor pattern; ~5-15ms per frame at
  1080p, drops dynamically when active. Format-guarded for bgra8Unorm
  drawables and disabled when not recording (early return).
- Renderer.swift gained `var frameTap` callback; invoked inside
  draw(in:) just before present, when set.

## History window (Agent C)
- Sources/App/HistoryEntry.swift, HistoryStore.swift, HistoryView.swift,
  HistoryWindowController.swift — singleton ring buffer of 500 recent
  events translated into human-readable summaries with category tags
  (object/face-known/face-unknown/ocr/gesture/activity/spatial).
- Open via ⌘H from the Faces menu. Polls the store at 1Hz.
- Window position persists under VPOC.HistoryWindowFrame.

## Privacy mode (Agent C)
- Theme.Performance.faceRecognitionDisabled — new live tunable.
- Camera menu toggle (⇧⌘P) flips both faceRecognitionDisabled and
  greeterMuted. The History viewer also filters out face entries when
  the flag is set.
- Documented limitation: bounding-box labels may still show enrolled
  names if the detector doesn't consult faceRecognitionDisabled (this
  round it doesn't; flag is for History + TTS only).

## Recording menu wiring (Agent C)
- App menu gains "Start Recording" / "Stop Recording" (⇧⌘R). Title
  flips based on the Recorder's isRecording state.
- Start: instantiates the recorder, calls startRecording at the
  current drawable size, hooks renderer.frameTap.
- Stop: clears frameTap first (no more frames queue) then awaits the
  asset writer's finalize. On completion (MainActor hop), shows an
  NSAlert with the saved-path or failure.

## Settings panel additions (Agent C)
- New "Faces" subsection: "Mute greeter (TTS)" toggle bound to
  TunableSettings.greeterMuted (writes through to Theme).
- New "Diagnostics" section: per-stage detector timing readout polled
  at 2Hz. Decoupled from ObjectDetector via a closure-based timing
  provider so the App layer doesn't depend on the Process module's
  DetectorStageTiming type directly.

## Hygiene (Agent D)
- Tests/IoUTests.swift, AspectFitTests.swift, LogStreamTests.swift,
  DetectionEventBusTests.swift, FaceRegistryDistanceTests.swift
  — 24 tests covering pure-logic algorithms. Private methods are
  mirrored verbatim into the test files with a "keep in sync" comment.
- project.yml gains a `VisionPOCTests` target of type
  `bundle.unit-test`, depending on the VisionPOC application target.
- .github/workflows/build.yml — macos-15 runner, brew install xcodegen,
  generate, build, test. LFS skipped to save bandwidth (the build
  tolerates a missing model since ObjectDetector falls back to Vision
  built-ins).
- CLAUDE.md — onboarding doc covering project layout, theme tunables,
  pipelines, event vocabulary, observability, build idiosyncrasies,
  and "how to add a new detector / overlay" recipes.

## Integration fix
- Recorder.swift's `stopRecording` completion path crossed actor
  isolation boundaries — captured a `(URL?) -> Void` closure and the
  AVAssetWriter instance into a @Sendable closure. Fixed by extracting
  `writer.status` / `writer.error?.localizedDescription` on the
  writer's queue before hopping, and wrapping the completion in a
  `nonisolated(unsafe)` shuttle (it's called from MainActor only, so
  the unsafe annotation is honest).

## Build status
- `xcodebuild build` — clean
- `xcodebuild test` — 24 / 24 passing

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 17:20:54 -05:00
james.maesandClaude Opus 4.7 79ecd0184a Tier 1 + Tier 2 polish: 3 parallel agents — overlays, snapshots, menus
Coordinated three agents working in parallel on non-overlapping files,
with a shared `ProcessedFrame.swift` contract locking the API surface
between detector and renderer. All build clean on first integrate.

## Detector (ObjectDetector.swift)
- `VNRecognizeTextRequest` runs alongside YOLO at ~5 Hz; recognized
  strings + boxes published via `currentTextDetections()`
- `VNDetectHumanBodyPoseRequest` produces a 14-segment skeleton (nose-
  neck-shoulders-arms, neck-root-hips-legs); confidence-gated at 0.3
- `VNDetectHumanHandPoseRequest` produces 5 finger chains + palm ring
  per hand, up to 2 hands; published as kind=.hand poses
- Body + hand published as `[PoseDetection]` via `currentPoses()`
- New `lastDetectMode: DetectMode { .yolo | .track }` so the footer can
  distinguish the two latency regimes
- YOLO warmup runs one dummy inference at startup on the detector
  queue — first user-visible detection no longer pays the ~200ms
  model-compile latency

## Renderer (Renderer.swift + Render/ColorHash.swift)
- Box labels include 10%-bucketed confidence: "CUP 80%", "MOBILE PHONE
  60%". Bucket keeps the label texture cache from exploding.
- Per-track stable color via UUID-byte → golden-ratio hash → HSV(h,
  0.8, 1.0). Face labels stay cyan (calmer when JARVIS pane has
  multiple labeled people in frame).
- Detection labels clamp to pane edges so boxes near the right margin
  don't overflow into the next pane.
- Footer now reads "DET 28.4ms (yolo)" or "DET 2.1ms (track)" — the
  flicker between the two values is informative, not a bug.
- LRU bound on `labelTextureCache` (128 entries, parallel access-order
  array, drop-head-on-insert when at capacity).
- OCR text rendered as magenta boxes with truncated text labels (32
  chars).
- Body pose drawn in mint, hand pose in amber; axis-aligned segment
  rects with a 0.003 minimum thickness so horizontal/vertical lines
  don't vanish at small viewport sizes. Joint dots at each unique
  point.
- New `snapshotPNG() -> Data?` public method. Render loop piggybacks
  one extra blit pass + waitUntilCompleted + CGImage encoding. <50ms
  in practice, 1-second semaphore timeout if rendering stalls.

## App / Window / Capture
- Camera menu with a dynamic Devices submenu (rebuilt via
  NSMenuDelegate.menuNeedsUpdate). Active device gets a checkmark,
  Refresh List forces re-enumeration.
- CameraCapture.switchToDevice(_:) stops the session, swaps the input,
  restarts. activeDeviceUniqueID exposed so the menu can match.
- Save Snapshot (⌘S) writes the 4-pane composite to
  ~/Desktop/VisionPOC-YYYY-MM-DD-HHMMSS.png
- Window-frame persistence for all three windows via UserDefaults
  keys VPOC.MainWindowFrame / VPOC.SettingsWindowFrame /
  VPOC.LogWindowFrame
- AVCaptureDevice.wasDisconnectedNotification → session stop + flag
  set; permissionDenied returns true so the existing
  "WAITING FOR CAMERA…" status path triggers without renderer changes

## Shared contract (new file)
- Sources/Process/ProcessedFrame.swift — TextDetection, PoseDetection,
  PoseDetection.Segment, PoseKind (.body | .hand), DetectMode
  (.yolo | .track). Sendable, value types, normalized [0..1] Vision
  coords throughout.

## Theme
- Palette gains textStroke (magenta), bodyPose (mint), handPose (amber)
  for the new overlays.

Note: Orbitron font and app icon (also Tier 2 items) deferred — no
clean source available for either without manual work.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 16:39:43 -05:00
james.maesandClaude Opus 4.7 ba1ecac72a Add a live Settings panel for runtime tuning
⌘, or VisionPOC → Settings… opens a SwiftUI panel with sliders for
every value that's worth tuning at runtime: YOLO frequency, detection
min confidence, tracker age/confidence/IoU, face match threshold,
edge Sobel threshold, ASCII column count. Reset-to-defaults button at
the bottom.

Plumbing
- Theme.Performance's runtime-tunable values converted from `static let`
  to `nonisolated(unsafe) static var`. Acknowledges the threading model:
  cooperative single-writer (MainActor UI) and many readers (detector
  queue, render queue). All values are word-sized primitives, atomic on
  ARM at the hardware level — no data races in practice.
- New @Observable @MainActor `TunableSettings` mirrors those statics for
  SwiftUI binding; each property's didSet writes back through so detector
  and renderer code paths see the change on the next frame.
- New `SettingsWindowController` hosts the panel as a separate floating
  window with a dark-aqua appearance to match the HUD aesthetic.
- FaceRegistry.matchThreshold becomes a thin getter/setter passthrough
  to Theme.Performance.faceMatchThreshold so the face slider drives it
  directly.

UI niceties
- Sections grouped by concern (Detection, Tracking, Faces, Edges, ASCII)
- Each slider shows its current value to two decimal places (or the
  natural format for Hz / seconds / integer counts)
- Face section includes an in-context caption explaining the typical
  distance range so the user knows where to aim the slider

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-16 15:06:20 -05:00