feat(meeting): VAD-driven session lifecycle — one transcript per meeting - #36
Merged
Conversation
The meeting bridge held one Speechmatics session open for the whole WS connection, so a long capture billed continuously and produced a single transcript. For the workday-capture use case (phone streaming for hours), add a server-side energy VAD that drives session lifecycle: - Open a cloud session on speech, drop silent frames (never billed by Speechmatics), finalize after MEETING_SILENCE_CLOSE_S of silence → each meeting lands in its own transcript and cost tracks talk time. - Single knob MEETING_VAD_THRESHOLD: 0 (default) disables gating = legacy one-session-per-connection behavior, so this deploy is behavior-neutral until the threshold is calibrated on a real capture. Peak/pass/gate instrumentation (MEETING_VAD_LOG_INTERVAL_S) makes the noise floor observable even with the gate off. - Lazy session creation: a connection that never carries speech opens no billed cloud session. A marker with no active session opens one so it's never lost. - Inline silence-close (no background timer), sound because the client streams continuously; documented that a future client-side VAD needs a timer instead. Speaker labels are consistent within a meeting, reset across gaps (fine for this use; enrolled-name relabel is a later follow-on). Also raise the Android runaway cap 8h→14h so a full workday capture isn't cut off (the VAD keeps a mostly-silent day cheap), bump the APK to v0.3.0 (also ships the already-in-source 2s notification refresh), and document the lifecycle in CONTRACTS.md 1c + the client README. New numpy-free VAD module with unit tests; server integration tests for gating, segmentation, marker-opens-session, and no-session-on-silence. meeting_bridge 36/36, ambient_bridge 143/143, ruff clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nalize Review follow-ups (no correctness bugs found; doc drift + one defensive nit): - server.py module docstring described the old one-session-per-connection behavior — updated to the VAD-driven lifecycle. - bridges/meeting_bridge/README.md (the second README I'd missed): update the /meeting handler description, document /health vs /health/<token>, and add the four MEETING_VAD_* vars + a calibration workflow to the config table. - deploy/meeting.env.example: add the commented VAD vars + calibration note so the canonical deploy template surfaces the feature. - Wrap the silence-triggered session.finalize() in try/except to mirror the teardown finalize's containment (a future pluggable backend whose finalize() raises must not kill the connection; session=None still prevents double-close). - Add the missing sessions[1].finalized assertion to the segmentation test. meeting_bridge 36/36, ruff clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.
Stage 2 of the meeting-capture follow-on. Turns the meeting bridge from "one Speechmatics session per WS connection" into a VAD-driven, session-per-meeting lifecycle, so a long workday capture only bills while someone is talking and each meeting lands in its own transcript.
What changed
bridges/meeting_bridge/vad.py(numpy-free):peak_amplitude(purestruct),SessionGate(speech/silence → open/close decision, injectable clock),GateStats(calibration roll-up).server.py_handle_wsrefactor: energy per frame → open a cloud session on speech, drop silent frames (never billed), finalize afterMEETING_SILENCE_CLOSE_Sof silence. Lazy creation — a connection that never carries speech opens no billed session; a marker with no active session opens one so it's never lost. Inline silence-close (no background timer) — sound because the phone streams continuously (documented that a future client-side VAD would need a timer).MEETING_VAD_THRESHOLD:0(default) = gating OFF = legacy one-session-per-connection, so this deploy is behavior-neutral until the threshold is calibrated on a real capture. Config also addsMEETING_VAD_HANGOVER_S,MEETING_SILENCE_CLOSE_S,MEETING_VAD_LOG_INTERVAL_S.frames_gatedadded to the authenticated health payload.8h → 14h(so a full workday capture isn't cut off; the VAD keeps a mostly-silent day cheap), APKv0.3.0(also delivers the already-in-source 2s notification refresh).Tradeoff (accepted)
Speaker labels (S1/S2) are consistent within a meeting but reset across the silence gaps between meetings. Fine for this use; enrolled-name relabel is a later follow-on.
Tests
test_vad.py— pure gate/stats logic, deterministic (injected clock).test_server.py— gating, no-session-on-pure-silence, speech-opens-session, marker-opens-session, and two-sessions-across-silence; the 3 tests whose contract assumed eager creation were updated to lazy creation.Pending (this session, after review)
E2E on the edge with a real Speechmatics session: confirm session-per-meeting produces multiple files, and empirically measure the Speechmatics idle-timeout to set a safe
SILENCE_CLOSE_S. Then the phone gets the v0.3.0 APK for the real-day calibration capture.🤖 Generated with Claude Code