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:
@@ -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
@@ -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 {
|
||||||
|
|||||||
@@ -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())
|
||||||
Reference in New Issue
Block a user