feat: intra-host sync anchor metadata - #21
Merged
Conversation
… manifest Capture a single shared ``armed_host_ns`` via ``time.monotonic_ns()`` immediately before fanning ``start_recording`` out to every stream, and broadcast it by replacing ``self._session_clock`` with a dataclass copy that carries the new ``recording_armed_ns``. Every adapter therefore receives the same armed value through its ``SessionClock`` argument, which ``StreamBase._begin_recording_window`` / ``_observe_first_frame`` turn into a per-stream :class:`RecordingAnchor` attached to each ``FinalizationReport``. Propagate that anchor to ``manifest.json`` by emitting a ``recording_anchor`` key on every stream entry in ``_persist_session_artifacts`` — ``None`` for empty recordings, the full ``RecordingAnchor.to_dict()`` otherwise. Downstream sync tooling reads ``first_frame_latency_ns`` to bias-correct per-adapter pipeline latency without re-reading each stream's JSONL. The project has no ``src/syncfield/manifest.py``; the per-stream entry builder lives inline in ``orchestrator._persist_session_artifacts``. The new field follows the same ``if final is not None`` block as ``status`` / ``frame_count``. Test ``tests/unit/test_orchestrator_anchor.py`` exercises the two invariants end-to-end via a ``StreamBase`` subclass that records the clock it receives: - both streams receive a ``SessionClock`` whose ``recording_armed_ns`` equals the same value - ``manifest.json`` carries per-stream ``recording_anchor`` with ``armed_host_ns`` / ``first_frame_host_ns`` / ``first_frame_latency_ns`` Full unit suite: 892 passed, 13 failed — all 13 are pre-existing and unrelated (OAK camera mocks, partial-connect rollback, viewer poller). Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
…pters
Extends the intra-host sync anchor pattern established for OAK / UVC /
generic sensors to the remaining live adapters:
- meta_quest.py — UDP hand/head tracking; device_ns=None
(parser doesn't currently surface ts_ms).
- oglo_tactile.py — MCU hardware clock interpolated per sample
is captured as the anchor's device_ns.
- ble_imu.py — generic BLE IMU; sample_ns is derived from
recv_ns, so device_ns=None.
- host_audio.py — PortAudio host mic; no device clock, so
device_ns=None.
- meta_quest_camera/stream — quest_native_ns on MjpegFrame captured
as device_ns alongside host capture_ns.
Skipped (offline ingest — no live host capture path):
- jsonl_file.py — bring-your-own-writer passive wrapper,
no frame-arrival concept.
- insta360_go3s/ — BLE trigger + deferred SD aggregation,
no live frames through the host during
the recording window.
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
여러 카메라/센서를 동시에 녹화할 때, 마이크 없이도 같은 호스트에 연결된 스트림들의 타임스탬프를 더 정교하게 정렬할 수 있도록 anchor metadata를 추가했습니다.
무엇이 문제였나
각 어댑터(OAK, UVC, iPhone, IMU 등)는 하드웨어/USB/인코더 latency가 다릅니다. 지금까지는 각 스트림의 첫 프레임 도착 시각을 anchor로 삼았는데, 어댑터마다 latency bias가 섞여서 스트림 간 정렬에 오차가 생겼어요.
이번 변경의 핵심
Orchestrator가
start_recording()을 broadcast 하기 직전에 공통armed_host_ns = time.monotonic_ns()을 한 번 찍고, 모든 스트림에게 동일한 값으로 전달합니다. 각 스트림은 이 armed 시각과 자신의 첫 기록 프레임 (host_ts, device_ts) pair를manifest.json에 기록해, 다운스트림 sync service가 어댑터별 bias를 상쇄할 수 있게 합니다.변경 사항
RecordingAnchor타입 추가 (types.py) — armed/first-frame 쌍 + computed latencySessionClock.recording_armed_ns필드 (clock.py) — frozen dataclass,dataclasses.replace로 armStreamBase._begin_recording_window/_observe_first_frame/_recording_anchor()helper (stream.py) — idempotent + clock-skew clamp + silent no-opFinalizationReport.recording_anchor필드 (types.py)armed_host_nscapture + manifest.json 에 per-stream anchor 기록oak_camera,uvc_webcam,polling_sensor,push_sensor,meta_quest,oglo_tactile,ble_imu,host_audio,meta_quest_camerajsonl_file(passive),insta360_go3s(offline SD aggregation)Backward Compatibility
Optional+ defaultNonestart_recording(session_clock)시그니처 유지Test plan
test_anchor_sharing.py— 3 PollingSensorStreams share armed_ns ✓Follow-up (별도 PR)
meta_quest.py가 packet 의ts_ms를device_ns로 plumbing (현재 None)time.sleep→threading.Event기반 동기화Plan document
구현 계획 + 설계 근거: `docs/superpowers/plans/2026-04-23-recording-anchor-metadata.md`
🤖 Generated with Claude Code