AI & Machine Learning Bug Report

Whisper transcripts invent text during silence and repeat phrases

Our meeting transcripts contained sentences nobody said, usually during silence or hold music, plus the same line repeated a dozen times. Whisper conditions on its own previous output and has no idea what silence is. A VAD pre-filter and stricter decoding thresholds removed most of it.

Ryan Pham Ryan Pham @ryan_bugs · April 24, 2025 · 6 min read · 4 Views
Banner: Whisper speech recognition inventing text during silent audio segments

We transcribe customer calls for quality review using a self-hosted Whisper model. Reviewers started flagging transcripts with lines that were never spoken. Two patterns showed up again and again:

[00:41.20 -> 00:47.80]  Thank you for watching, and don't forget to subscribe.
[00:47.80 -> 00:52.10]  Thank you for watching, and don't forget to subscribe.
...
[03:12.00 -> 03:30.00]  I'll check the account and call you back. I'll check the account and call you back.

The first appeared in segments where the caller was on hold with music. The second was a real sentence from earlier in the call repeated during a long silence.

Reproducing it

We cut a 2-minute clip with 40 seconds of hold music in the middle and ran it ten times. Every run produced invented text in the music section. A clip of pure room noise produced a short "Thank you." on most runs.

What we ruled out

  • Audio corruption. Waveforms and spectrograms of the hold sections showed music and silence, nothing that sounded like speech.
  • Wrong language setting. We forced English; the problem remained.
  • Model size. The large model hallucinated less often than the small one, but still did it.

Root cause

Whisper is a sequence-to-sequence model trained on a huge amount of subtitled audio from the web. It does not have a reliable "no speech" mode: when given audio without speech, it tends to produce the most likely text, which for web videos is often an outro like "thanks for watching". On top of that, by default each 30-second window is decoded with the previous window's text as a prompt (condition_on_previous_text=True). Once a phrase is produced, the next window is primed to repeat it, and silence gives the model nothing to break the loop.

What is happening under the hood

Whisper processes audio in 30-second windows. For each window it predicts a "no speech" probability and decodes text with a beam or greedy search. The reference implementation has a few heuristics to catch bad output:

  • no_speech_threshold: if the no-speech probability is above this value and the average log probability is low, the segment is treated as silence.
  • logprob_threshold: if the average log probability of the decoded text is below this, decoding is retried at a higher temperature or the segment is considered failed.
  • compression_ratio_threshold: repeated text compresses very well with gzip; a high ratio signals a repetition loop and triggers a retry.

These help but do not solve the core problem: music and noise do not always look like "no speech" to the model. Removing non-speech audio before Whisper sees it is far more reliable than detecting bad output after.

Environment

Component Version / value
Runtime Python 3.11
Library faster-whisper 1.1 (CTranslate2 backend)
Model large-v3, float16 on an L4 GPU
Audio 8 kHz phone calls, upsampled to 16 kHz mono
Volume ~1,800 calls per day

Minimal reproduction

from faster_whisper import WhisperModel

model = WhisperModel("large-v3", device="cuda", compute_type="float16")
segments, _ = model.transcribe("call_with_hold_music.wav", language="en")
for s in segments:
    print(f"[{s.start:.2f} -> {s.end:.2f}] {s.text}")

The fix

1. Voice activity detection before decoding. faster-whisper ships with a Silero VAD integration that removes non-speech audio before decoding and maps timestamps back to the original audio.

segments, info = model.transcribe(
    "call.wav",
    language="en",
    vad_filter=True,
    vad_parameters={"min_silence_duration_ms": 700, "speech_pad_ms": 200},
    condition_on_previous_text=False,
    no_speech_threshold=0.6,
    log_prob_threshold=-1.0,
    compression_ratio_threshold=2.2,
    temperature=[0.0, 0.2, 0.4],
)

2. Do not condition on previous text. Setting condition_on_previous_text=False breaks the repetition loop at the cost of slightly less consistent spelling of names across windows. For calls, that trade-off was clearly worth it.

3. Post-filter what still slips through. A small set of rules drops segments that are almost certainly invented: known outro phrases in segments with low average log probability, and consecutive identical segments.

KNOWN_HALLUCINATIONS = {"thank you for watching", "thanks for watching", "please subscribe"}

def keep(segment, previous_text: str | None) -> bool:
    text = segment.text.strip().lower().rstrip(".!")
    if segment.avg_logprob < -0.8 and any(p in text for p in KNOWN_HALLUCINATIONS):
        return False
    if previous_text and text == previous_text:
        return False
    return True

Alternatives and trade-offs

  • Stronger VAD settings (longer minimum silence, higher speech threshold) remove more hallucinations and risk cutting quiet speech, like a customer mumbling an account number.
  • Separate music detection. For hold music specifically, a music classifier can mark sections. VAD handled most of it for us.
  • Speaker diarisation first, then transcribe per speaker turn. Higher quality for multi-speaker audio and naturally skips silence; more compute and complexity.

How to test the fix

We built an evaluation set of 150 call clips with human transcripts, including 40 with hold music or long silences, and measured word error rate plus an "inserted words in non-speech regions" count:

config                     WER     inserted_words_in_nonspeech
baseline                   14.8%   1,912
+ vad_filter               12.9%     214
+ no condition_on_prev     12.6%      61
+ post-filter              12.6%      18

Confirming in production

  • Reviewer flags for "text not spoken" dropped by about 95 percent in the first month.
  • Average transcript length per call fell by 6 percent, mostly removed invented text.
  • Processing time dropped 20 percent because VAD skips silent audio before decoding.

Regression guard

  • The 150-clip eval runs on any change to model, library version or parameters.
  • A daily job counts segments matching known hallucination phrases and alerts on spikes.
  • Transcripts store the parameters used, so we can re-run affected calls after changes.

Security implications

Invented text in a transcript is an integrity problem with real consequences: quality reviews, disputes and compliance records rely on these transcripts. We added a notice in the review UI that transcripts are machine-generated and link each segment to its audio timestamp, so reviewers can check the source before acting. Transcripts also contain personal data and follow the same retention rules as the recordings.

Prevention checklist

  • Run VAD before Whisper for any audio with silence, music or noise.
  • Turn off conditioning on previous text for long, gappy audio.
  • Use the decoding thresholds, and measure their effect.
  • Keep an eval set with non-speech sections and count inserted words.
  • Link transcript segments to audio for human verification.

References

Add a label

#python #whisper #speech-to-text #faster-whisper #vad
Written by
Ryan Pham @ryan_bugs

I build and run the platforms other engineers ship on: CI/CD pipelines, Kubernetes clusters, GPU inference services, LLM features in production, and the cloud bills that come with all of it. I have also spent years writing and operating automated trading systems, where a rounding bug or a missed websocket message is not a log line but a position you did not intend to hold. Each post here starts from a real failure: the alert, the exact error, what we ruled out, the root cause, and how we proved the fix. Same path every time: Investigate, Explain, Fix, Verify. No hype, no price predictions, just engineering.

Advertisement

Comments

0

No comments yet. Be the first to comment!

Sign In to post a comment.