[Backend] Stop the ASR server's speaker label overriding the known track owner #342

Closed
opened 2026-08-25 20:39:00 +00:00 by claude-bot · 1 comment
Contributor

Severity: HIGH. Found in the August 2026 session lifecycle review (#319).

The defect

The backend knows exactly who owns each track — it sent one file per speaker — and then discards that knowledge in favour of whatever the remote server echoes back.

  • audio_service.py:535transcribe_session takes seg.get("speaker", "Unknown") with no cross-check
  • audio_service.py:460 — on the VAD path, seg.get("speaker") or display_name gives the server's value precedence over the known owner

If the server runs diarization or normalises to a label like SPEAKER_00, that silently replaces the real name. _apply_character_names (reminder_tasks.py:2121-2130) then fails to match, and the character mapping is lost for that whole track. If the server associates results by completion order rather than by the order files were sent, every line of a track is attributed to the wrong player — wholesale, plausible-looking, and undetectable from inside the backend.

Proposed fix

The track owner is authoritative. Make the fallback unconditional: use display_name always, and treat any server-supplied speaker label as advisory metadata at most. On the session endpoint, zip results back to tracks by an explicit identifier rather than by position.

This is a one-character change on the VAD path and a small one on the session path, and it removes an entire class of silent misattribution.

Acceptance criteria

  • Speaker labels derive from track ownership, never from the server echo
  • Results are associated with tracks by explicit id, not by ordering
  • A server returning SPEAKER_00 or a wrong name does not change attribution
  • Tests cover a server echoing a diarization label and a server returning results out of order
**Severity: HIGH.** Found in the August 2026 session lifecycle review (#319). ## The defect The backend knows exactly who owns each track — it sent one file per speaker — and then **discards that knowledge in favour of whatever the remote server echoes back**. - `audio_service.py:535` — `transcribe_session` takes `seg.get("speaker", "Unknown")` with no cross-check - `audio_service.py:460` — on the VAD path, `seg.get("speaker") or display_name` gives the **server's value precedence** over the known owner If the server runs diarization or normalises to a label like `SPEAKER_00`, that silently replaces the real name. `_apply_character_names` (`reminder_tasks.py:2121-2130`) then fails to match, and the character mapping is lost for that whole track. If the server associates results by completion order rather than by the order files were sent, every line of a track is attributed to the wrong player — wholesale, plausible-looking, and undetectable from inside the backend. ## Proposed fix The track owner is authoritative. Make the fallback unconditional: use `display_name` always, and treat any server-supplied speaker label as advisory metadata at most. On the session endpoint, zip results back to tracks by an explicit identifier rather than by position. This is a one-character change on the VAD path and a small one on the session path, and it removes an entire class of silent misattribution. ## Acceptance criteria - [ ] Speaker labels derive from track ownership, never from the server echo - [ ] Results are associated with tracks by explicit id, not by ordering - [ ] A server returning `SPEAKER_00` or a wrong name does not change attribution - [ ] Tests cover a server echoing a diarization label and a server returning results out of order
Author
Contributor

Verified, and closed in dd8f305 + 38631ba — but not by the fix this issue proposes.

The proposed fix would not have worked

This issue asks to "zip results back to tracks by an explicit identifier rather than by position". Implementing that literally would have ticked the criterion and changed nothing: a permutation of ids survives an id-join exactly as a permutation of names survives a name-join. Any echo-based join is unverifiable in principle, because the only thing that could confirm the association is the association being asserted.

Commit 3ee27ff recorded the partial-swap case and called it unfixable by a test. It understates the exposure. Consider the failure this issue's own body names as its headline — "if the server associates results by completion order rather than by the order files were sent, every line of a track is attributed to the wrong player — wholesale, plausible-looking, and undetectable":

A symmetric swap returns Kira's audio labelled "Bryn" and Bryn's labelled "Kira". Every label sent comes back, so by_label resolves every segment, every track receives segments, check_every_speaker_was_transcribed passes cleanly, and the whole session is misattributed with nothing raised and nothing logged. A one-directional swap was caught; this was not, and it was still fully live.

What was done instead

transcribe_session now sends one file per request through transcribe_track, which stamps each segment with the label of the file it sent. A response can only be about the file that was sent, so there is nothing to permute and no join to get wrong.

Not a new design: it is what the VAD path has always done, and VAD is on by default since #323 — the safe shape was already shipping for most deployments, and this brings the last path in line. /transcribe per file is also the ordinary Whisper contract; /transcribe/session was a custom multi-file extension.

Cost, flagged plainly: VAD-off sessions now make one request per speaker instead of one per session. Against the same endpoint the VAD path already calls many times per session, so a proven load shape rather than a new one. Sequential, matching transcribe_session_vad; concurrency belongs with #356.

Criteria

  • Speaker labels derive from track ownership, never from the server echo — and now structurally, not by discipline: there is no echo consulted anywhere.
  • Results are associated with tracks by explicit id, not by orderingsatisfied by removing the association step, since neither an id nor a name can make an echo-join verifiable.
  • A server returning SPEAKER_00 or a wrong name does not change attribution — previously this raised and lost the session; now the label is simply not consulted. Also fixes the three cases where raising was avoidable: a single-track session (attribution needs no information from the server at all), a server that omits speaker, and cosmetic normalisation.
  • Tests cover a server echoing a diarization label and results out of order — plus the symmetric swap, which had no coverage and could not be defended against before.

The one that mattered most had no test at all

transcribe_track — the function whose entire claim is that it never reads the echo, and the default production path — had no direct test. Every VAD test patches it out and the doubles return a pre-baked "speaker": track.label, so the fixture supplied the answer the assertion checked.

Verified by mutation: reverting it to seg.get("speaker") or track.label — the exact defect this issue's body names as its second site — passed all 1,300 tests. Now covered directly.

A bug this uncovered

The non-VAD path never passed a language, because the multi-file endpoint took none. A campaign that had pinned one under #419 had it honoured on the VAD path and silently ignored on the other. Per-file requests make it the same call, so it now applies on both.

Also corrected

get_vad_config's docstring claimed enabled "defaults to False so the transcribe path stays byte-identical to the untrimmed behaviour" — backwards since #323, and exactly the wrong thing to read when sizing the blast radius of a change to either transcription path.

Closing.

**Verified, and closed in `dd8f305` + `38631ba` — but not by the fix this issue proposes.** ## The proposed fix would not have worked This issue asks to *"zip results back to tracks by an explicit identifier rather than by position"*. Implementing that literally would have ticked the criterion and changed nothing: **a permutation of ids survives an id-join exactly as a permutation of names survives a name-join.** Any echo-based join is unverifiable in principle, because the only thing that could confirm the association is the association being asserted. Commit `3ee27ff` recorded the partial-swap case and called it unfixable by a test. It understates the exposure. Consider the failure this issue's own body names as its headline — *"if the server associates results by completion order rather than by the order files were sent, every line of a track is attributed to the wrong player — wholesale, plausible-looking, and undetectable"*: A **symmetric** swap returns Kira's audio labelled "Bryn" and Bryn's labelled "Kira". Every label sent comes back, so `by_label` resolves every segment, every track receives segments, `check_every_speaker_was_transcribed` passes cleanly, and the whole session is misattributed with **nothing raised and nothing logged**. A one-directional swap was caught; this was not, and it was still fully live. ## What was done instead `transcribe_session` now sends **one file per request** through `transcribe_track`, which stamps each segment with the label of the file it sent. A response can only be about the file that was sent, so there is nothing to permute and no join to get wrong. Not a new design: it is what the VAD path has always done, and VAD is on by default since #323 — the safe shape was already shipping for most deployments, and this brings the last path in line. `/transcribe` per file is also the ordinary Whisper contract; `/transcribe/session` was a custom multi-file extension. **Cost, flagged plainly: VAD-off sessions now make one request per speaker instead of one per session.** Against the same endpoint the VAD path already calls many times per session, so a proven load shape rather than a new one. Sequential, matching `transcribe_session_vad`; concurrency belongs with #356. ## Criteria - [x] **Speaker labels derive from track ownership, never from the server echo** — and now structurally, not by discipline: there is no echo consulted anywhere. - [x] **Results are associated with tracks by explicit id, not by ordering** — **satisfied by removing the association step**, since neither an id nor a name can make an echo-join verifiable. - [x] **A server returning `SPEAKER_00` or a wrong name does not change attribution** — previously this *raised* and lost the session; now the label is simply not consulted. Also fixes the three cases where raising was avoidable: a single-track session (attribution needs no information from the server at all), a server that omits `speaker`, and cosmetic normalisation. - [x] **Tests cover a server echoing a diarization label and results out of order** — plus the symmetric swap, which had no coverage and could not be defended against before. ## The one that mattered most had no test at all `transcribe_track` — the function whose entire claim is that it never reads the echo, and the **default** production path — had no direct test. Every VAD test patches it out and the doubles return a pre-baked `"speaker": track.label`, so the fixture supplied the answer the assertion checked. Verified by mutation: reverting it to `seg.get("speaker") or track.label` — the exact defect this issue's body names as its second site — **passed all 1,300 tests**. Now covered directly. ## A bug this uncovered The non-VAD path never passed a language, because the multi-file endpoint took none. A campaign that had pinned one under #419 had it honoured on the VAD path and **silently ignored** on the other. Per-file requests make it the same call, so it now applies on both. ## Also corrected `get_vad_config`'s docstring claimed `enabled` "defaults to False so the transcribe path stays byte-identical to the untrimmed behaviour" — backwards since #323, and exactly the wrong thing to read when sizing the blast radius of a change to either transcription path. Closing.
Sign in to join this conversation.
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
rbrooks/Quest-Board#342
No description provided.