●SONNET — Claude Sonnet 5 is now the default model in Claude Code, with a native 1M-token context window and introductory pricing through August 31●CHROME — Claude in Chrome reaches general availability, letting you hand browser work directly to Claude●COWORK — Cowork expands to mobile and web so sessions and files follow you across devices, starting in beta for Max users●DATAVIZ — Claude Code adds a /dataviz skill offering guidance for designing charts and dashboards●AGENTS — Agent workflows gain background notifications, draft PR handoff, and improved failover●ENTERPRISE — Claude Enterprise adds richer admin analytics, model-level entitlements, and spend alerts●SONNET — Claude Sonnet 5 is now the default model in Claude Code, with a native 1M-token context window and introductory pricing through August 31●CHROME — Claude in Chrome reaches general availability, letting you hand browser work directly to Claude●COWORK — Cowork expands to mobile and web so sessions and files follow you across devices, starting in beta for Max users●DATAVIZ — Claude Code adds a /dataviz skill offering guidance for designing charts and dashboards●AGENTS — Agent workflows gain background notifications, draft PR handoff, and improved failover●ENTERPRISE — Claude Enterprise adds richer admin analytics, model-level entitlements, and spend alerts
When Whisper Wrote 'Thanks for Watching' Into the Silence — Measuring and Killing Japanese Transcription Hallucinations
Whisper inserts phantom boilerplate into silent or noisy segments of Japanese audio. Here is how to measure hallucinations with no_speech_prob and avg_logprob, strip silence with VAD, and have Claude validate what remains — with the implementation and the routine that pulled it back.
One day, while running a pipeline that auto-generates meeting minutes, an unfamiliar line appeared in the output: "Thanks for watching." It was an internal recurring meeting. Nobody said that.
Replaying the audio at that timestamp, it was a few seconds of silence while the presenter switched slides. Whisper had written words that did not exist into a stretch where nothing was spoken.
This is a well-known behavior in Whisper's Japanese transcription. On silence, noise, or music-only stretches, it emits boilerplate common in its training data — "Thanks for watching," "Please subscribe" — like a phantom. In English this is called hallucination.
What follows is how I handle this when embedding Whisper in an audio pipeline built with Rork or Cowork: measure the hallucination, then kill it with thresholds, preprocessing, and Claude validation. Not "it feels gone now," but a record of pulling the insertion rate back while tracking it as a number.
Why Whisper adds words to silence
Whisper splits audio into 30-second chunks and tries to turn each into "some text." The problem is a model that struggles to choose "no output" even when the input is silent.
Its training data includes huge amounts of YouTube audio, where boilerplate recurs at the ends and silent parts of videos. As a result, the model has learned a skewed mapping: "low audio information equals boilerplate."
Segment state
Typical Whisper behavior
Common inserted phrase
Full silence
Repeats the prior sentence, or boilerplate
Thanks for watching
Music / noise only
Meaningless fragments or boilerplate
Please subscribe
A faint backchannel only
Balloons into an over-long sentence
(repeat of prior speech)
So this is not "mishearing." It is "filling a gap when information is scarce." The countermeasure, then, is not to make the audio clearer — it is to detect and drop the segments where gap-filling happened.
First, measure it — get per-segment confidence
Measure before you act. Whisper's verbose_json response carries confidence hints per segment. Without logging these, you cannot see where hallucination occurs.
Two signals matter.
Signal
Meaning
Sign of hallucination
no_speech_prob
Probability the segment is silence
High (over 0.6) yet text was produced
avg_logprob
Average log-probability of output tokens (confidence)
Low (below -1.0) = close to guessing
High no_speech_prob with text present, and a low avg_logprob on top — a segment where both overlap is the most typical shape of hallucination.
from openai import OpenAIclient = OpenAI(api_key="YOUR_API_KEY")def transcribe_with_confidence(audio_path): with open(audio_path, "rb") as f: result = client.audio.transcriptions.create( model="whisper-1", file=f, language="ja", response_format="verbose_json", # get per-segment confidence timestamp_granularities=["segment"], ) flagged = [] for seg in result.segments: # record segments strongly suspected of hallucination suspicious = seg.no_speech_prob > 0.6 and seg.avg_logprob < -1.0 if suspicious: flagged.append({ "start": round(seg.start, 1), "text": seg.text.strip(), "no_speech_prob": round(seg.no_speech_prob, 3), "avg_logprob": round(seg.avg_logprob, 3), }) return result, flagged
Start by counting flagged. In my minutes pipeline, one hour of meeting audio averaged about 12 hits, most of them the boilerplate or a repeat of the prior sentence. Knowing the number first lets you compare whether later fixes actually work.
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦Why Whisper inserts boilerplate on silence and noise, and how to measure hallucinations per segment with no_speech_prob and avg_logprob
✦A two-stage implementation: VAD preprocessing plus threshold filtering, then Claude validating the transcript for context fit
✦A measured drop from ~12 to ~2 insertions per hour of audio, plus a weekly routine that avoids over-deletion
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
Once measured, drop segments by threshold. The point is to carefully do the opposite of "when in doubt, keep it": drop only what is both silent and low-confidence, and keep anything that might contain real speech.
def filter_hallucinations(result): kept, removed = [], [] for seg in result.segments: is_silence = seg.no_speech_prob > 0.6 is_low_conf = seg.avg_logprob < -1.0 # drop only when silence probability is high AND confidence is low if is_silence and is_low_conf: removed.append(seg.text.strip()) continue kept.append(seg.text.strip()) cleaned = " ".join(t for t in kept if t) return cleaned, removed
Thresholds need tuning per source. A quiet meeting room is fine at no_speech_prob > 0.6, but outdoor or phone audio lowers no_speech_prob via background noise and lets hallucinations slip through. I first dropped it to 0.5 and cut real backchannels, then went back to 0.6. Do not set a threshold once and forget it; hold it on the assumption you will re-measure when the source changes.
⚠️
Cutting mechanically on confidence alone can drop fast or accented real speech as "low confidence." Trigger the filter only when both silence probability and low confidence align — never on one alone.
Strip silence with VAD before Whisper sees it
Threshold filtering is after the fact. One layer more effective is preprocessing that never hands silence to Whisper at all — VAD (Voice Activity Detection).
Cut the silence first, then transcribe, and you reduce Whisper's very opportunity to fill gaps.
import webrtcvadimport waveimport contextlibdef has_speech(frame_bytes, vad, sample_rate=16000): return vad.is_speech(frame_bytes, sample_rate)def strip_silence(pcm_path, aggressiveness=2): # aggressiveness 0 (lax) to 3 (strict). 2 is easy to work with for meetings vad = webrtcvad.Vad(aggressiveness) speech_frames = [] with contextlib.closing(wave.open(pcm_path, "rb")) as wf: sample_rate = wf.getframerate() frame_ms = 30 frame_bytes = int(sample_rate * frame_ms / 1000) * 2 # 16bit = 2 bytes while True: frame = wf.readframes(frame_bytes // 2) if len(frame) < frame_bytes: break if has_speech(frame, vad, sample_rate): speech_frames.append(frame) return b"".join(speech_frames)
VAD is not a cure-all. Cutting breaths and short pauses joins speech unnaturally, so start with a middling aggressiveness of 2. I run both VAD and the threshold filter: preprocessing drops most silence, and the after-the-fact confidence filter catches what remains. A two-stage setup lets each cover the other's misses. The overall pipeline design overlaps with voice agent production architecture.
Have Claude validate what is left
Even through thresholds and VAD, a contextually odd sentence rarely survives. This is where Claude comes in. Whisper's job is "sound to text"; Claude's job is judging whether the "text" fits the context.
Pass the surrounding segments and have it point out insertions that clearly do not fit. Receiving the result via a tool (structured output) lets you process it mechanically downstream.
import anthropic, jsonclient = anthropic.Anthropic(api_key="YOUR_API_KEY")def validate_transcript(segments): # segments: list of {"start": 12.4, "text": "..."} numbered = "\n".join(f"[{i}] {s['text']}" for i, s in enumerate(segments)) resp = client.messages.create( model="claude-sonnet-5", max_tokens=1024, tools=[{ "name": "report_hallucinations", "description": "return indices of insertions that do not fit context", "input_schema": { "type": "object", "properties": { "suspect_indices": { "type": "array", "items": {"type": "integer"}, "description": "segments that do not connect, or are pure boilerplate", } }, "required": ["suspect_indices"], }, }], tool_choice={"type": "tool", "name": "report_hallucinations"}, messages=[{ "role": "user", "content": ( "The following is a transcript of meeting audio. Detect segments that " "do not connect with the surrounding context and consist only of streaming " "boilerplate like 'Thanks for watching.' Keep genuine speech.\n\n" + numbered ), }], ) for block in resp.content: if block.type == "tool_use": return block.input["suspect_indices"] return []
This stage catches "normal confidence but clearly out of context" insertions that the confidence signals miss. Still, I do not hand everything to Claude; the final call always pairs it with the confidence signals. The structured-output design benefits from the thinking in structured responses with schema validation.
Keep the boilerplate blocklist as a last resort
It is tempting to just string-match "Thanks for watching" and delete them all. I did that first. But it should stay a last resort.
The reason is simple: some meetings genuinely say it. In a video-production planning call or a stream retrospective, someone means it. Delete unconditionally by string match and you cut real utterances.
BLOCKLIST = ["ご視聴ありがとうございました", "チャンネル登録"]def blocklist_filter(segments): result = [] for s in segments: text = s["text"].strip() # drop only if it is boilerplate ONLY and silence probability is high only_boilerplate = any(text == b for b in BLOCKLIST) if only_boilerplate and s.get("no_speech_prob", 0) > 0.6: continue result.append(s) return result
If you use a blocklist, always pair it with the confidence signals and drop only when both conditions align: boilerplate-only and high silence probability. Deleting on a bare string match will someday cut something real and cost you trust.
The numbers after rolling it out (field notes)
I put this into one minutes pipeline and ran it for about two weeks as an indie developer. I make every change myself and check the answers with my own ears.
Before the fix, one hour of meeting audio averaged about 12 hallucinations — streaming boilerplate most, then repeats of prior speech.
After VAD preprocessing and the confidence filter (no_speech_prob > 0.6 and avg_logprob < -1.0), insertions fell to about 4 on average. Adding Claude's context validation brought it to about 2. It does not reach zero. Outdoor and phone audio inevitably leave misses as background noise pushes confidence up.
What matters is measuring, at the same time, that you are not over-cutting. I compared body character counts before and after to confirm real speech was not excessively dropped. Fewer hallucinations but a thinner transcript is no win. Not relaxing just because a number improved is, I feel, a shared trap in this kind of quality work.
ℹ️
Hallucination patterns differ by source type — meeting room, outdoor, phone, stream. I split thresholds by source type, and when I start handling a new kind of audio I first listen to 10 minutes by hand, count the insertions, and re-measure the threshold.
Turn it into a weekly check
Finally, the minimal routine so this does not stay a one-off. Keep it coarse enough to run quickly.
Cadence
Do this
Criterion
Every run
Log the flagged segment count
Keep recording insertions per hour of audio
Weekly
Eyeball the contents of flagged
Are real utterances being dropped by mistake?
When the source changes
Listen to 10 minutes and re-measure the threshold
no_speech_prob distribution shifts by source
Monthly
Compare body length before and after
Is over-cutting (a thinner transcript) happening?
Transcript quality is decided not by the number of hallucinations removed but by whether the remaining body is correct. Killing hallucinations while measuring them is a quiet but effective way to hold that state. I prefer this "count before you cut" ordering — being able to explain it with numbers is what lets me tune it safely later.
Enable verbose_json in your own pipeline and count the high-no_speech_prob segments across one hour. The moment the real count is visible, the countermeasures become concrete. I hope this helps your implementation. Thank you for reading.
Share
Thank You for Reading
Claude Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.