Skip to content

feat: intra-host sync anchor metadata - #21

Merged
styu12 merged 17 commits into
mainfrom
feature/intra-host-anchor
Apr 24, 2026
Merged

feat: intra-host sync anchor metadata#21
styu12 merged 17 commits into
mainfrom
feature/intra-host-anchor

Conversation

@styu12

@styu12 styu12 commented Apr 23, 2026

Copy link
Copy Markdown
Contributor

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 latency
  • SessionClock.recording_armed_ns 필드 (clock.py) — frozen dataclass, dataclasses.replace 로 arm
  • StreamBase._begin_recording_window / _observe_first_frame / _recording_anchor() helper (stream.py) — idempotent + clock-skew clamp + silent no-op
  • FinalizationReport.recording_anchor 필드 (types.py)
  • Orchestrator 에서 armed_host_ns capture + manifest.json 에 per-stream anchor 기록
  • 9개 adapter 유선: oak_camera, uvc_webcam, polling_sensor, push_sensor, meta_quest, oglo_tactile, ble_imu, host_audio, meta_quest_camera
  • 2개 adapter skip (문서화): jsonl_file (passive), insta360_go3s (offline SD aggregation)
  • Integration test: 3 sensor 스트림이 동일 armed_ns 공유 검증
  • Edge case hardening: clock skew clamp, idempotent first-frame, no-window safe
  • Version: 0.3.21 → 0.3.22

Backward Compatibility

  • 신규 필드 모두 Optional + default None
  • 기존 start_recording(session_clock) 시그니처 유지
  • Helper 미사용 legacy adapter 는 그대로 동작 (anchor=None for those)

Test plan

  • Unit tests — 905 passed (baseline 13 pre-existing failures only)
  • Integration test test_anchor_sharing.py — 3 PollingSensorStreams share armed_ns ✓
  • Per-adapter anchor tests (OAK, UVC, polling, push, meta_quest, oglo, ble_imu, host_audio, meta_quest_camera) ✓
  • Edge cases — clock skew clamp, idempotent observe, no recording window ✓
  • Manifest.json 에 recording_anchor dict 기록 검증 ✓
  • 실기기 검증 (OAK 2대 + UVC 동시 녹화 → manifest.json 에서 anchor 확인)

Follow-up (별도 PR)

  • syncfield (sync service) 측 aligner 가 이 metadata 활용하도록 확장
  • meta_quest.py 가 packet 의 ts_msdevice_ns 로 plumbing (현재 None)
  • 테스트의 time.sleepthreading.Event 기반 동기화

Plan document

구현 계획 + 설계 근거: `docs/superpowers/plans/2026-04-23-recording-anchor-metadata.md`

🤖 Generated with Claude Code

styu12 and others added 17 commits April 23, 2026 22:55
… 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.
@styu12
styu12 merged commit a1608c2 into main Apr 24, 2026
0 of 4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant