Stop Whisper misspelling the intern's name as Devin

The intern reached 21 of 58 published transcripts as "Devin". The obvious fix —
seed his name in the Whisper initial prompt — only works for one of the two
transcription paths:

- backend/services/transcription.py (live show) uses mlx_whisper and does take
  an initial_prompt. Added Devon there. This also picks up the Big Bend prompt
  rewrite from the relocation work, which was sitting uncommitted.
- publish_episode.py, which actually produces the published transcripts, uses
  LightningWhisperMLX, whose transcribe() signature is (audio_path, language).
  It accepts no initial_prompt at all, so there is nothing to seed.

So the published path gets a deterministic correction pass instead:
fix_proper_nouns() rewrites known mishearings after transcription, preserving
casing (Devin/DEVIN/devin -> Devon/DEVON/devon) and matching whole words only,
so "Devinshire" is left alone. Verified against the real episode 58 transcript:
24 occurrences to 0, output byte-identical to the manual relabel in bf1afef.

Swapping the publish path to mlx_whisper would allow a real prompt, but that
changes the transcription engine for every episode and is a bigger call than
this warrants.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-14 04:20:22 -05:00
co-authored by Claude Opus 5
parent 5f6429e6da
commit 2901e5f4fb
3 changed files with 84 additions and 3 deletions
+1 -1
View File
@@ -103,7 +103,7 @@ async def transcribe_audio(audio_data: bytes, source_sample_rate: int = None,
audio_16k = audio audio_16k = audio
# Build initial prompt — context helps Whisper with names and topic-specific words # 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: if context_hint:
initial_prompt += f" {context_hint}" initial_prompt += f" {context_hint}"
+31 -2
View File
@@ -244,6 +244,34 @@ TRANSCRIPT:
return result 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: def transcribe_audio(audio_path: str) -> dict:
"""Transcribe audio using Lightning Whisper MLX (Apple Silicon GPU).""" """Transcribe audio using Lightning Whisper MLX (Apple Silicon GPU)."""
print(f"[1/5] Transcribing {audio_path} (MLX 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", []): for segment in result.get("segments", []):
start_ms, end_ms, text = segment[0], segment[1], segment[2] start_ms, end_ms, text = segment[0], segment[1], segment[2]
text = fix_proper_nouns(text.strip())
transcript_segments.append({ transcript_segments.append({
"start": start_ms / 1000.0, "start": start_ms / 1000.0,
"end": end_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)") print(f" Transcribed {duration} seconds of audio ({len(transcript_segments)} segments)")
return { return {
+52
View File
@@ -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())