[Backend] Let GMs set a campaign's spoken language, and pass it to transcription #419
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Severity: MEDIUM. Follow-up to the 2026-08-26 production incident (see #347).
Why
On 2026-08-26 a 77-minute six-speaker session was lost because WhisperX identified three seconds of English as Icelandic at 0.95 confidence, then raised
ValueError: No default align-model for language: is. #347 fixed the mechanism — the language is now detected once from the longest span in the session and pinned for every other span, instead of being guessed per 1-3 second clip.That makes the default path safe. It does not make it certain, and the failure this protects against is worse than the one we actually saw:
The 500 was the lucky outcome. It was loud, and it stopped the pipeline. WhisperX ships aligners for German, Spanish, Dutch, French and others. Had the misdetection landed on one of those rather than Icelandic, that clip would have returned a confident, wrong transcription and flowed straight into the transcript, the summary and the lore wiki with nothing marking it — undetectable after the fact. Detection is a heuristic, and a heuristic in the attribution path is exactly what the v4.0.0 milestone exists to remove.
A GM knows what language their table speaks. Asking them beats guessing.
Design
An override, not a replacement. Detection stays the default so the product works before anyone finds a setting.
campaigns.transcription_language— nullable text, ISO 639-1 (en,pt,de, …).NULLmeans "detect", which is the default and current behaviour.process_audioreads it and passes it through. The backend already accepts it —transcribe_session_vad(..., language=...)andtranscribe_with_optional_vad(..., language=...)skip detection entirely when supplied (#347).The default must stay "detect". Defaulting to
enwould silently mangle every non-English table until they found the setting — trading a loud failure for a quiet one, which is the wrong direction and the whole point of the paragraph above.Worth considering while in here
Acceptance criteria
campaigns.transcription_languageexists, nullable, defaulting to NULLprocess_audiopasses the configured language through to transcriptionVerified against the acceptance criteria before closing. All seven met, with direct test coverage — the cleanest issue in this milestone's verification pass.
campaigns.transcription_language, nullable, defaults to NULL — migrationb3c4d5e6f7a9. Pinned bytest_the_default_is_detect_not_english(tests/test_transcription_language.py:98), which asserts bothCampaignCreate(name="Test").transcription_language is Noneandlanguage_service.DETECT is None. This was the criterion I most wanted checked: a default ofenwould have silently mistranscribed every non-English group until someone found the setting, replacing a loud failure with a quiet one.CampaignDetail.jsx:2204-2215.process_audiopasses it through —reminder_tasks.py:2192-2207.test_a_configured_language_suppresses_detection_entirelyassertsprobes == [].test_an_unset_language_still_detects; the underlying logic is gated by a parameter defaulting toNone, not rewritten.reminder_tasks.py:2195-2201) and per track when detection runs (audio_service.py:896-906).test_a_language_the_aligner_cannot_handle_is_refuseduses Icelandic, the exact #347 failure, rejected at the Pydantic boundary rather than at the aligner.That last one is the good part: the crash that cost a session is now unreachable by configuration.
Closing. Part of a full acceptance-criteria pass across the v4.0.0 milestone.