diff --git a/backend/services/transcription.py b/backend/services/transcription.py index d9d04d3..8582157 100644 --- a/backend/services/transcription.py +++ b/backend/services/transcription.py @@ -103,7 +103,7 @@ async def transcribe_audio(audio_data: bytes, source_sample_rate: int = None, audio_16k = audio # Build initial prompt — context helps Whisper with names and topic-specific words - initial_prompt = "Luke at the Roost, a late-night radio talk show in New Mexico. The host Luke talks to callers about life, relationships, sports, politics, and pop culture." + initial_prompt = "Luke at the Roost, a late-night radio talk show in Alpine, Texas, in the Big Bend region. The host Luke talks to callers about life, relationships, sports, politics, and pop culture, with his intern Devon. Callers reference Alpine, Marfa, Marathon, Terlingua, Fort Stockton, and Big Bend." if context_hint: initial_prompt += f" {context_hint}" diff --git a/publish_episode.py b/publish_episode.py index 951b17a..4308795 100755 --- a/publish_episode.py +++ b/publish_episode.py @@ -244,6 +244,34 @@ TRANSCRIPT: return result +# Recurring show names Whisper reliably mishears. LightningWhisperMLX takes no +# initial_prompt (its transcribe() signature is just audio_path + language), so +# unlike make_clips.py we cannot condition the model on these up front — they +# have to be corrected afterwards. "Devin" reached 21 of 58 published +# transcripts before this existed. +PROPER_NOUN_FIXES = { + "devin": "Devon", +} + + +def fix_proper_nouns(text: str) -> str: + """Correct known Whisper mishearings, preserving the original casing style.""" + if not text: + return "" + + def _sub(match): + word = match.group(0) + correct = PROPER_NOUN_FIXES[word.lower()] + if word.isupper(): + return correct.upper() + if word.islower(): + return correct.lower() + return correct + + pattern = r"\b(" + "|".join(PROPER_NOUN_FIXES) + r")\b" + return re.sub(pattern, _sub, text, flags=re.IGNORECASE) + + def transcribe_audio(audio_path: str) -> dict: """Transcribe audio using Lightning Whisper MLX (Apple Silicon GPU).""" print(f"[1/5] Transcribing {audio_path} (MLX GPU)...") @@ -268,12 +296,13 @@ def transcribe_audio(audio_path: str) -> dict: for segment in result.get("segments", []): start_ms, end_ms, text = segment[0], segment[1], segment[2] + text = fix_proper_nouns(text.strip()) transcript_segments.append({ "start": start_ms / 1000.0, "end": end_ms / 1000.0, - "text": text.strip() + "text": text }) - full_text.append(text.strip()) + full_text.append(text) print(f" Transcribed {duration} seconds of audio ({len(transcript_segments)} segments)") return { diff --git a/tests/test_proper_nouns.py b/tests/test_proper_nouns.py new file mode 100644 index 0000000..8dd91a8 --- /dev/null +++ b/tests/test_proper_nouns.py @@ -0,0 +1,52 @@ +"""Whisper mishears recurring show names; correct them after transcription. + +publish_episode.py transcribes with LightningWhisperMLX, whose transcribe() +signature is (audio_path, language) — it accepts no initial_prompt, so there is +no way to condition it on proper nouns the way make_clips.py does with +mlx_whisper. The intern came out as "Devin" in 21 of 58 published transcripts +because of this. + +A deterministic pass over the finished text fixes the known names without +swapping the transcription engine. +""" + +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) + +from publish_episode import fix_proper_nouns + + +def test_corrects_the_intern_name_in_all_casings(): + assert fix_proper_nouns("Devin, where's my coffee?") == "Devon, where's my coffee?" + assert fix_proper_nouns("DEVIN: Hey Luke.") == "DEVON: Hey Luke." + assert fix_proper_nouns("devin said so") == "devon said so" + + +def test_leaves_correct_spelling_alone(): + text = "Devon is the intern. DEVON: Hi." + assert fix_proper_nouns(text) == text + + +def test_only_matches_whole_words(): + """Must not corrupt a longer word that happens to contain the name.""" + assert fix_proper_nouns("Devinshire") == "Devinshire" + assert fix_proper_nouns("mcdevins") == "mcdevins" + + +def test_preserves_surrounding_text_exactly(): + src = "LUKE: Alright. Let's check in with Devin and see how he's doing.\n\nDEVIN: Hey!" + out = fix_proper_nouns(src) + assert out == "LUKE: Alright. Let's check in with Devon and see how he's doing.\n\nDEVON: Hey!" + assert len(out) == len(src) + + +def test_empty_and_none_safe(): + assert fix_proper_nouns("") == "" + assert fix_proper_nouns(None) == "" + + +def test_word_count_is_never_changed(): + src = "Devin talked to Devin about Devin. " * 20 + assert len(fix_proper_nouns(src).split()) == len(src.split())