fix(stt): kill faster-whisper silence hallucinations — VAD, no conditioning, confidence gate - #73934
Merged
Merged
Conversation
Local faster-whisper called model.transcribe with bare {'beam_size': 5}:
no VAD, cross-window conditioning on, no confidence filtering. Pure
silence produced hallucinated tokens (E2E: 5s anullsrc WAV -> 'You',
no_speech_prob=0.705) and noisy clips could produce runs of junk, often
in other languages.
Three-layer class fix, one shared owner for every local-whisper call
site (build_local_transcribe_kwargs):
1. Silero VAD filter (bundled with faster-whisper) on by default —
silence never reaches the model. stt.local.vad: false restores the
raw behavior for music/ambient transcription.
stt.local.vad_min_silence_ms tunes chunk splitting (default 500).
2. condition_on_previous_text=False — one hallucinated token can no
longer seed a self-reinforcing run; negligible cost for
voice-note-length audio.
3. Segment confidence gate (_join_confident_segments): drop a segment
only when no_speech_prob > 0.6 AND avg_logprob < -1.0 (openai-whisper's
own heuristic shape; both must hit so quiet-but-real speech survives).
Config: stt.local.no_speech_prob_threshold / logprob_threshold.
The WHISPER_HALLUCINATIONS blocklist in voice_mode.py stays as
last-resort defense but should now almost never fire.
E2E (real faster-whisper 'base', CPU int8):
silence.wav before 'You' -> after ''
noise.wav before '' -> after ''
speech.wav before/after 'Hello World, this is a test of the
transcription system.' (unchanged)
Docs (EN + zh-Hans), DEFAULT_CONFIG, cli-config.yaml.example updated;
19 unit tests (kwargs contract, off-switch, confidence gate incl.
quiet-speech survival, _transcribe_local wiring), sabotage-verified.
Contributor
૮ >ﻌ< ა ci reviewran on bafc581 ℹ️ InfoDesktop E2E visual evidence · View test artifacts · View job1 visual diff. inline evidence upload failed. Failed to upload diff-665a0833239e-onboarding-overlay-diff.png with gh image (exit code 1): Error uploading /home/runner/work/_temp/e2e-evidence/diff-665a0833239e-onboarding-overlay-diff.png: step 0 (get upload token): uploadToken not found on repo page — do you have write access to NousResearch/hermes-agent? (or, if NousResearch enforces SAML SSO, authorize at https://github.com/orgs/NousResearch/sso) |
The exact kwargs snapshot broke when VAD hardening added keys — the test's real contract is 'null stt.local: must not crash or force language/prompt'. Baseline kwargs are pinned by the dedicated suite.
13 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
fix(stt): kill faster-whisper silence hallucinations at the source
Summary
Local faster-whisper STT hallucinated random words — often in other languages — from silence and background noise (reported by Teknium as unacceptable). Root cause:
_transcribe_localintools/transcription_tools.pycalledmodel.transcribewith bare{"beam_size": 5}— no VAD, no confidence filtering, andcondition_on_previous_textleft default-on (a hallucination amplifier: one junk token seeds a run of them).This is a class fix at the source, not a blocklist extension. Three layers, all owned by one shared kwargs helper (
build_local_transcribe_kwargs) that every local-whisper call site goes through:vad_filter=True(Silero VAD, bundled with faster-whisper 1.2.1) +min_silence_duration_ms: 500. Default on;stt.local.vad: falserestores raw behavior for music/ambient transcriptioncondition_on_previous_textdefault Trueno_speech_prob > 0.6ANDavg_logprob < -1.0(openai-whisper's own heuristic shape — the AND means quiet-but-real and mumbled-but-real speech both survive)The
WHISPER_HALLUCINATIONSblocklist intools/voice_mode.pyis deliberately untouched — it remains last-resort defense and should now almost never fire. Thestt.language: "en"default from #73100 already reduced cross-language hallucinations; this completes the fix.E2E evidence (real faster-whisper
base, CPU int8, real WAVs)Test audio generated with ffmpeg (
anullsrcsilence,anoisesrcpink/white noise) and edge-tts speech, run through the actual_transcribe_local:"You"(hallucinated; segmentno_speech_prob=0.705,avg_logprob=-0.793)""✅""""✅""""✅"Hello World, this is a test of the transcription system.""Hello World, this is a test of the transcription system."✅ unchangedAlso verified with
stt.language: ""(auto-detect — the worst case for cross-language junk): silence/noise → empty, speech unchanged.Config keys (all under
stt.local, wired into DEFAULT_CONFIG + cli-config.yaml.example + docs EN/zh-Hans)Call-site audit
_transcribe_localis the only in-repo faster-whisper invocation — the TUI gateway (tui_gateway/server.py) and dashboard (hermes_cli/web_server.py /api/audio/transcribe) both route throughtranscribe_recording→transcribe_audio→_transcribe_local, so every surface (gateway voice notes, CLI voice mode, desktop, dashboard, wake-word loop) inherits the hardening from the one helper. Any future local-whisper call site must usebuild_local_transcribe_kwargsinstead of hand-rolling kwargs.Dupe sweep
Searched open PRs/issues for
vad,vad_filter,hallucination,no_speech,silence whisper,condition_on_previous_text,faster-whisper. No existing PR implements VAD filtering or confidence gating for the local STT transcription path — nothing to salvage:/voice vadhands-free recording mode (capture-side VAD, not transcription filtering)Tests
19 new unit tests in
tests/tools/test_stt_silence_hallucinations.py:stt.local.vad: false, conditioning always off,beam_sizekept,vad_min_silence_msconfigurable with garbage fallback, YAMLlocal: nullsafe_transcribe_localwiring: hardened kwargs actually reach the model; off-switch reaches the model; hallucinated segments filtered from the transcriptSabotage-verified: flipping the VAD default to
Falsefails 5 tests. Full STT suites green (203 passed: test_transcription_tools, test_stt_silence_hallucinations, test_stt_default_language, test_stt_language_resolution, test_managed_media_gateways). Ruff clean.Infographic