fix(meeting): gate transcript-idle close on ASR partials, not just finals - #39
Conversation
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 0b7b77c6b5
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if len(self._acc.committed) != before: | ||
| self._last_activity = time.monotonic() | ||
| self._last_partial_text = "" # partial restarts after a commit — the next one counts anew |
There was a problem hiding this comment.
Count merged finals as ASR activity
When Speechmatics sends another final for the same speaker, TranscriptAccumulator.add_final() merges it into the existing committed run instead of appending a new entry, so len(self._acc.committed) does not change even though new transcript text was committed. In that single-speaker/merged-turn case, neither last_activity nor the turns fallback advances, and the meeting bridge can hit MEETING_TRANSCRIPT_IDLE_CLOSE_S and finalize/dormant a still-active session if partials are absent or not changing. Track whether add_final actually added text/results rather than using only the committed-list length.
Useful? React with 👍 / 👎.
…nals The #38 idle-close used the committed-turn count as its "meeting still going?" signal. Day-one real use showed that fails on quiet/far-field audio: Speechmatics can go many minutes hearing speech it never commits as a final, so a LIVE meeting read as idle — the session closed, went dormant, and ~15min of a real meeting were silently dropped until a quiet gap re-armed it. Switch the idle signal to ASR SPEECH EVIDENCE: ActiveSession now stamps last_activity (monotonic) on every NON-EMPTY partial and every committed final, and the meeting server gates the idle close on that — with the turn count kept as fallback for backends without it. Partials fire whenever the ASR hears anything speech-like, so a hard-to-hear meeting stays open while a post-meeting noisy room (no partials) still closes and stops billing. ambient_bridge/active_session.py change is additive (a timestamp + property; the partial lambda became a bound method with identical accumulator/flush behavior) — ambient bridge behavior is unchanged. Empty/keep-alive partials don't count as evidence (same extraction as set_partial). Tests cover the evidence rules on ActiveSession and the keep-open/close/dormant lifecycle on the server; docs updated (config comment, README, meeting.env.example, CONTRACTS 1c). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
0b7b77c to
5944d5b
Compare
Why
The #38 transcript-idle close keyed on the committed-turn count. Day-one real use dropped ~15 min of a live meeting: on quiet/far-field audio Speechmatics can go many minutes without committing a FINAL, so a live meeting read as "idle", the session closed, and the dormant state (correctly refusing to reopen on room noise) kept it closed until a quiet gap finally re-armed it.
What
Evidence-based idle signal, verified against the live Speechmatics API:
ambient_bridge/active_session.py(additive; ambient path unaffected — nothing there reads the new surface):last_activity(monotonic) stamps ASR speech evidence. Two live-API behaviors shape the predicate, both observed with a direct probe:len(acc.committed)grew).meeting_bridge/server.py: the idle close now trackslast_evidence_tsfed bylast_activity(primary) with the turns count as fallback for backends without it; both absent ⇒ idle-close disabled for that backend. Seeded at session open; dormant/re-arm machinery unchanged.MeetingSessionprotocol documents the optional liveness attributes.Verification
E2E against real Speechmatics on the edge (isolated instance, synthesized-speech PCM):
Direct-probe transcript of the live API (empty finals every ~2s during tone; frozen partial re-emission) is what drove the predicate — both behaviors are pinned by unit tests.
Reviewed twice: full review (DONE, no findings) + focused delta re-review of the post-E2E predicate changes.
Rollout
Edge currently runs with
MEETING_TRANSCRIPT_IDLE_CLOSE_S=0(idle-close disabled as the stop-the-bleed mitigation). After this deploys, the env override is removed to re-enable at the 300s default on the new signal.🤖 Generated with Claude Code