Skip to content

feat(meeting): VAD-driven session lifecycle — one transcript per meeting - #36

Merged
WingedGuardian merged 2 commits into
mainfrom
feat/meeting-session-lifecycle
Jul 12, 2026
Merged

feat(meeting): VAD-driven session lifecycle — one transcript per meeting#36
WingedGuardian merged 2 commits into
mainfrom
feat/meeting-session-lifecycle

Conversation

@WingedGuardian

Copy link
Copy Markdown
Owner

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

  • New bridges/meeting_bridge/vad.py (numpy-free): peak_amplitude (pure struct), SessionGate (speech/silence → open/close decision, injectable clock), GateStats (calibration roll-up).
  • server.py _handle_ws refactor: energy per frame → open a cloud session on speech, drop silent frames (never billed), finalize after MEETING_SILENCE_CLOSE_S of 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).
  • Single knob 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 adds MEETING_VAD_HANGOVER_S, MEETING_SILENCE_CLOSE_S, MEETING_VAD_LOG_INTERVAL_S.
  • Instrumentation: periodic peak/pass/gate summary in the log (works even with the gate off) to read the room's noise floor for calibration; frames_gated added to the authenticated health payload.
  • Android: runaway cap 8h → 14h (so a full workday capture isn't cut off; the VAD keeps a mostly-silent day cheap), APK v0.3.0 (also delivers the already-in-source 2s notification refresh).
  • Docs: CONTRACTS.md §1c + client README describe the lifecycle and the speaker-labels-reset-across-gaps tradeoff.

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.
  • meeting_bridge 36/36, ambient_bridge 143/143, ruff clean, compileall clean.

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

WingedGuardian and others added 2 commits July 12, 2026 17:10
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>
@WingedGuardian
WingedGuardian merged commit 9fa0754 into main Jul 12, 2026
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