Skip to content

fix(stt): kill faster-whisper silence hallucinations — VAD, no conditioning, confidence gate - #73934

Merged
teknium1 merged 2 commits into
mainfrom
fix/stt-silence-hallucinations
Jul 29, 2026
Merged

fix(stt): kill faster-whisper silence hallucinations — VAD, no conditioning, confidence gate#73934
teknium1 merged 2 commits into
mainfrom
fix/stt-silence-hallucinations

Conversation

@teknium1

Copy link
Copy Markdown
Contributor

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_local in tools/transcription_tools.py called model.transcribe with bare {"beam_size": 5} — no VAD, no confidence filtering, and condition_on_previous_text left 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:

Layer Before (bare kwargs) After (hardened)
VAD none — silence fed straight to whisper vad_filter=True (Silero VAD, bundled with faster-whisper 1.2.1) + min_silence_duration_ms: 500. Default on; stt.local.vad: false restores raw behavior for music/ambient transcription
Conditioning condition_on_previous_text default True False — one hallucinated token can't seed a self-reinforcing run; negligible cost for voice-note-length audio
Confidence gate every decoded segment kept segment dropped only when no_speech_prob > 0.6 AND avg_logprob < -1.0 (openai-whisper's own heuristic shape — the AND means quiet-but-real and mumbled-but-real speech both survive)

The WHISPER_HALLUCINATIONS blocklist in tools/voice_mode.py is deliberately untouched — it remains last-resort defense and should now almost never fire. The stt.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 (anullsrc silence, anoisesrc pink/white noise) and edge-tts speech, run through the actual _transcribe_local:

Input Before (main) After (this branch)
5s pure silence "You" (hallucinated; segment no_speech_prob=0.705, avg_logprob=-0.793) ""
5s pink noise (amp 0.05) "" ""
5s white noise (amp 0.3) "" ""
edge-tts speech ("Hello world, this is a test of the transcription system.") "Hello World, this is a test of the transcription system." "Hello World, this is a test of the transcription system." ✅ unchanged

Also 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)

stt:
  local:
    vad: true                      # Silero VAD filter — silence never reaches whisper. false = old raw behavior
    vad_min_silence_ms: 500        # min silence (ms) that splits speech chunks when vad is on
    no_speech_prob_threshold: 0.6  # drop a segment only if no_speech_prob > this...
    logprob_threshold: -1.0        # ...AND avg_logprob < this (both must hit — quiet real speech survives)

Call-site audit

_transcribe_local is 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 through transcribe_recordingtranscribe_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 use build_local_transcribe_kwargs instead 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:

Tests

19 new unit tests in tests/tools/test_stt_silence_hallucinations.py:

  • kwargs contract: VAD on by default, off via stt.local.vad: false, conditioning always off, beam_size kept, vad_min_silence_ms configurable with garbage fallback, YAML local: null safe
  • confidence gate: high-no_speech + low-logprob dropped; quiet-but-confident speech survives; low-confidence-only (mumbled real speech) survives; missing/plugin segment attrs never dropped; thresholds configurable with garbage fallback
  • _transcribe_local wiring: hardened kwargs actually reach the model; off-switch reaches the model; hallucinated segments filtered from the transcript

Sabotage-verified: flipping the VAD default to False fails 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

fix(stt): kill faster-whisper silence hallucinations — VAD, no conditioning, confidence gate

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.
@github-actions

github-actions Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

૮ >ﻌ< ა ci review

ran on bafc581

ℹ️ Info

Desktop E2E visual evidence · View test artifacts · View job

1 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)

@alt-glitch alt-glitch added type/bug Something isn't working comp/cli CLI entry point, hermes_cli/, setup wizard tool/tts Text-to-speech and transcription area/config Config system, migrations, profiles sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades P3 Low — cosmetic, nice to have labels Jul 29, 2026
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.
@teknium1
teknium1 merged commit bff2206 into main Jul 29, 2026
38 checks passed
@teknium1
teknium1 deleted the fix/stt-silence-hallucinations branch July 29, 2026 07:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/config Config system, migrations, profiles comp/cli CLI entry point, hermes_cli/, setup wizard P3 Low — cosmetic, nice to have sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades tool/tts Text-to-speech and transcription type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants