feat: capture orchestration framework redesign - #1
Merged
Conversation
Delete obsolete SyncSession capture.py and its tests. Reorganize tests into unit/ and integration/ subdirectories. Create adapters/ package. Update pyproject.toml with optional extras for audio/uvc/ble backends. Breaking change: SyncSession is removed. New SessionOrchestrator API replaces it in subsequent tasks.
…Event, SampleEvent, FinalizationReport, ChirpSpec, SessionReport Add new dataclasses and enums for the SessionOrchestrator + Stream SPI. Existing SyncPoint/FrameTimestamp/SensorSample types are preserved unchanged to keep output file compatibility with the sync core.
Immutable clock handle distributed to Streams at session.start(). Wraps the session's SyncPoint and provides now_ns / elapsed_ns helpers so streams don't import time directly. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
- write_sync_point accepts optional chirp_start_ns / chirp_stop_ns / chirp_spec and omits fields when None (clean output for silent/single-host sessions) - SessionLogWriter writes orchestrator-level events (state transitions, health, rollbacks) to session_log.jsonl with per-write flush for crash safety - write_manifest stores per-stream capabilities via the existing streams dict Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
generate_chirp_samples produces mono float PCM for a linear FM sweep with cosine envelope, matching the egonaut SoundFeedbackModule design (400↔2500Hz, 500ms, 15ms envelope). Pure Python math — no numpy dependency — so the core SDK stays lightweight. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Used by playback backends (sounddevice or system audio) and for debugging chirp signals. Clamps floats to [-1, 1] before int16 conversion so amplitude overflows never corrupt output. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Frozen dataclass with default() and silent() factory methods. Defaults match the egonaut production chirp spec (400↔2500Hz, 500ms, 0.8 amplitude, 15ms envelope, 200ms stabilization/tail margins). Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
- ChirpPlayer is a runtime_checkable Protocol - SoundDeviceChirpPlayer wraps the optional sounddevice library (non-blocking) - SilentChirpPlayer is a no-op fallback for headless machines - create_default_player chooses the right backend at runtime, logging at INFO when sounddevice is unavailable (never raises) Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
- Stream is a runtime_checkable Protocol describing the SPI contract - StreamBase provides callback registration and health buffering so concrete adapters only implement prepare/start/stop - Sample and health events flow to all registered callbacks; health events also accumulate in an internal buffer used by FinalizationReport Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Programmable in-memory Stream with lifecycle tracking, sample/health injection, and failure-injection flags. Ships in syncfield.testing so third-party adapter authors can reuse it for their own tests. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Class definition, state enum, add() with duplicate-id rejection, output directory creation on construction. Thread-safe via internal RLock; start/stop lifecycle methods added in subsequent tasks. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
- Captures a SyncPoint and builds a shared SessionClock - Calls prepare() then start() on every registered stream sequentially - On any failure: rolls back previously-started streams in reverse order (best-effort stop), returns state to IDLE, re-raises the original exception - The failed stream itself is NOT rolled back since it never reached a successfully-started state Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Calls stop() on each stream (best-effort — a broken stream becomes a failed FinalizationReport instead of blocking the others), writes sync_point.json and manifest.json with per-stream capabilities, and transitions through STOPPING to STOPPED. Returns an aggregated SessionReport. Logic is split into _finalize_streams and _persist_session_artifacts helpers to keep stop() itself readable as a state-machine overview. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
- Chirp eligibility is host-level: requires at least one registered stream with provides_audio_track=True. When no audio stream exists, log INFO and skip the chirp. Single-host sessions are unaffected. - Start chirp plays after all streams start, following post_start_stabilization_ms. - Stop chirp plays BEFORE stream.stop() so it is captured in recording audio tracks; orchestrator then sleeps duration + pre_stop_tail_margin_ms. - chirp_start_ns / chirp_stop_ns / chirp_spec flow to sync_point.json and to the returned SessionReport when a chirp was played. - Split into _maybe_play_start_chirp / _maybe_play_stop_chirp_and_wait so start() and stop() read as clean state-machine overviews. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
- Opens SessionLogWriter during start() BEFORE any state change so failures are still recorded on disk - Every state transition flows through _transition() which logs immediately and flushes per-write, giving a recoverable timeline even if the process dies mid-recording - Rollbacks are logged with the failing exception message and the number of streams that had to be torn down Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
- Every stream registered via add() has its on_health callback wired to the orchestrator's session log writer - Drops, reconnects, warnings, and errors flow into session_log.jsonl so the core can enrich sync_report.json with observability data - Health events emitted before start() are still buffered by StreamBase and surface through the FinalizationReport, so nothing is lost even in the unusual case of early emission Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Wraps a customer-owned JSONL file as a Stream. No I/O of its own during recording — just tracks lifecycle and counts lines on stop(). Missing file yields status='partial'. Useful for customers who already have their own sample-writing code and just want orchestrator coordination. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
OpenCV-based adapter for USB/UVC webcams. Background capture thread reads frames, timestamps each read with time.monotonic_ns(), emits a SampleEvent, and writes MP4 via cv2.VideoWriter. Declares produces_file=True and provides_audio_track=False (OpenCV has no audio path). Gated behind optional 'uvc' extra — raises a clear ImportError with the install hint when opencv-python is missing, so users know exactly how to resolve it. Internals are split into prepare/start/stop/_capture_loop plus small helper methods (_resolve_frame_geometry, _release_cv2_resources) so the lifecycle reads cleanly and the capture loop stays focused on its tight inner path. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Generic BLE IMU adapter using bleak. Bridges async bleak into the sync orchestrator by running an asyncio event loop on an internal background thread. Subscribes to a notify characteristic and decodes payloads with a configurable struct format and configurable channel names. - Connection/disconnection errors become ERROR health events - Payload decode errors (malformed notifications) become WARNING health events — never tear down the stream - Channel-name / format-length mismatches raise ValueError at construction - Payload decoding logic is split into _handle_payload so it is unit testable via a synchronous _dispatch_notification_for_test hook with no asyncio required - Gated behind optional 'ble' extra with clear install hint Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Top-level exports: SessionOrchestrator, Stream, StreamBase, types, SyncToneConfig, ChirpSpec, SessionClock. Adapters subpackage always re-exports JSONLFileStream; UVC and BLE are re-exported lazily so an uninstalled extra does not break 'import syncfield.adapters'. Users needing a specific optional adapter can always import it directly from its module — that path raises a clear ImportError with an install hint when the dependency is missing. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Drives a two-stream session through the full lifecycle using FakeStream, asserts sync_point.json / manifest.json / session_log.jsonl match the schema the sync core expects. Covers: happy path with audio-capable stream, silent-mode session (no chirp fields), and audio-less single-host session (chirp skipped, session still completes). Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
The module docstring and stop() docstring still referenced "added in subsequent tasks" from the incremental development. Replace them with accurate overviews that reflect the final file organization and the fully-integrated stop sequence including chirp playback. Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
sounddevice.play() internally requires numpy even when the caller passes a
Python list, so SoundDeviceChirpPlayer now prepares a float32 ndarray
explicitly. numpy is imported once at module load time (guarded by try/except)
instead of inline inside play(), because doing it inline triggered numpy's
C-extension one-time-init failure when unit tests patched sys.modules
inside with-blocks.
- pyproject.toml: syncfield[audio] now depends on numpy>=1.21 as well as
sounddevice>=0.4.6. The [all] extra is kept in sync.
- tone.py: module-level `try: import numpy as _np` once; play() uses _np
to build the buffer, falling back to passing the raw list on machines
that somehow have sounddevice but not numpy (real-world impossible,
but the fallback keeps the contract honest).
- scripts/e2e_chirp_check.py: standalone verification script that runs a
real session against sounddevice on a headful machine. Checks:
1. create_default_player() picks SoundDeviceChirpPlayer
2. sync_point.json carries chirp_{start,stop}_ns and chirp_spec
3. stop_chirp_ns > start_chirp_ns with plausible spacing
4. (best-effort) a parallel sounddevice InputStream cross-correlates
against the reference chirp — WARNs on quiet rooms rather than
failing so the script stays usable in varied environments
5. audio-less sessions cleanly skip the chirp path
Verified on macOS with MacBook Pro built-in speakers: all 5 checks pass.
Co-Authored-By: Claude Opus 4.6 (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
SyncSessioncapture API with a pluggableSessionOrchestratorframework built around aStreamSPI, so new modalities can be added as adapters instead of patched into a monolith.SessionClock,SessionLogWriter, sync-tone chirp player) and three reference adapters:UVCWebcamStream,BLEImuGenericStream,JSONLFileStream.scripts/e2e_chirp_check.pyfor verifying the audio chirp path on real hardware.Changes
types.py,stream.py,clock.py):Streamprotocol +StreamBase,StreamCapabilities,SessionState,HealthEvent,SampleEvent,FinalizationReport,SessionReport,ChirpSpec,SessionClock.orchestrator.py):add(), atomicstart()with rollback on partial failure,stop()finalization sequence, crash-safe session log, stream health event routing, integrated audio sync chirp.tone.py): stdlib-only linear FM chirp generator, 16-bit PCM WAV writer,ChirpPlayerprotocol withsounddeviceand silent backends,SyncToneConfigwith egonaut defaults. Playback now handssounddevicea numpy buffer.writer.py): extendedsync_point+ newSessionLogWriter.adapters/):UVCWebcamStream,BLEImuGenericStream,JSONLFileStreamreference implementations with their own unit tests.testing.py):FakeStreamused by orchestrator unit tests.__init__.py): re-exports the new framework surface; oldSyncSession/capture.pyremoved.tests/unit/+tests/integration/, added round-trip E2E coverage, clock/tone/writer/stream/orchestrator/public-API unit tests.stop()docstrings.pyproject.toml,uv.lock): adds runtime deps required by the new adapters/tone path.Test Plan
uv run pytest tests/unituv run pytest tests/integration/test_round_trip.pyuv run python scripts/e2e_chirp_check.pyon a machine with audio output (manual hardware check).github/workflows/test.ymlpasses on this branch🤖 Generated with Claude Code