Skip to content

fix(voice): rolling-window VAD to prevent false barge-in during TTS - #71083

Closed
beardedeagle wants to merge 1 commit into
NousResearch:mainfrom
beardedeagle:fix/vad-rolling-window
Closed

fix(voice): rolling-window VAD to prevent false barge-in during TTS#71083
beardedeagle wants to merge 1 commit into
NousResearch:mainfrom
beardedeagle:fix/vad-rolling-window

Conversation

@beardedeagle

Copy link
Copy Markdown
Contributor

Problem

The barge-in VAD used one-shot calibration that froze the noise floor from the first 800ms of TTS playback. Later louder passages exceeded the stale floor and false-triggered barge-in, cutting TTS mid-sentence. The barge-in monitor also opened the mic immediately when TTS started, causing speaker bleed to falsely trigger VAD. Additionally, when token streaming was enabled, TTS sentences were rendered twice — once by the token stream and once by the TTS display callback.

Solution

Rolling-window VAD — 90th percentile of a rolling deque window (~3s), 8x multiplier, 4000 RMS trigger ceiling, min_floor clamped to SILENCE_RMS_THRESHOLD * 2, sustained_ms=1000, calibration_ms=800.

Barge-in grace period — barge_in_grace_seconds (default 2.0s) delays VAD activation so TTS playback establishes first.

Duplicate render suppression — pass display_callback=None to stream_tts_to_speaker when streaming_enabled, making the token stream the sole display path.

Normal-exit flag — _tts_normal_exit prevents the exception finally block from setting stop_event on clean exit.

TTS CUT diagnostic logging at all barge-in cut paths.

Regression tests

  • test_quiet_then_loud_playback_does_not_trip
  • test_8x_multiplier_absorbs_tts_volume_spikes
  • test_trigger_ceiling_lets_genuine_speech_trip
  • test_silence_calibration_does_not_false_trip_on_tts

Copilot AI review requested due to automatic review settings July 25, 2026 00:04
@alt-glitch alt-glitch added type/bug Something isn't working P3 Low — cosmetic, nice to have comp/cli CLI entry point, hermes_cli/, setup wizard tool/tts Text-to-speech and transcription labels Jul 25, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Improves voice-mode TTS barge-in behavior by making the VAD floor adaptive during playback, adding a short grace period before the mic starts listening, and preventing duplicate text rendering when token streaming is enabled.

Changes:

  • Replaces one-shot VAD calibration with a rolling-window (90th percentile) floor and adds diagnostic logging for VAD trips.
  • Adds a configurable barge-in grace period and tunes barge-in VAD parameters from the CLI monitor.
  • Suppresses duplicate sentence rendering during streaming TTS and adds a “normal exit” flag to avoid cutting audio on clean shutdown.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 5 comments.

File Description
tools/voice_mode.py Implements rolling-window VAD floor + trigger ceiling and adds diagnostics.
tests/tools/test_voice_mode.py Adds regression coverage for new VAD behavior (needs one test adjustment to truly cover the multiplier).
cli.py Adds barge-in grace period + TTS cut diagnostics; avoids duplicate rendering; refines streaming-TTS shutdown behavior (normal-exit flag logic needs a small fix).
Comments suppressed due to low confidence (1)

tools/voice_mode.py:1226

  • The comment says the multiplier "was 5.0", but the removed code used a different calibration approach (median * 3.5). Since it's not a direct like-for-like multiplier change, the "was 5.0" parenthetical is misleading and should be removed.
                # 8.0x multiplier (was 5.0): TTS speaker bleed has wide
                # volume variation between sentences and within sentences.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread tools/voice_mode.py
Comment thread tools/voice_mode.py Outdated
Comment thread tests/tools/test_voice_mode.py
Comment thread cli.py
Comment thread cli.py
@beardedeagle
beardedeagle force-pushed the fix/vad-rolling-window branch from b2b308f to 70d7785 Compare July 25, 2026 00:17
…eway mirror

Replace one-shot VAD calibration with a rolling deque window that
continuously recalibrates the noise floor throughout TTS playback,
preventing false barge-in triggers from stale calibration. Add a
grace period before VAD activates so TTS playback establishes first.
Suppress duplicate text rendering when token streaming is enabled.
Mirror the barge-in and TTS stream stop logic to the TUI gateway path
so both CLI and gateway use the same VAD semantics.

Rolling-window VAD:
- 90th percentile of rolling window (~3s) for noise floor
- 8x multiplier (was 5x) for TTS volume variation headroom
- 4000 RMS trigger ceiling so genuine speech can still trip
- min_floor clamped to SILENCE_RMS_THRESHOLD * 2
- sustained_ms=1000, calibration_ms=800

Barge-in grace period (barge_in_grace_seconds, default 2.0s):
Delays VAD activation so TTS playback establishes before the mic opens.

Duplicate render suppression:
When streaming_enabled, pass display_callback=None to
stream_tts_to_speaker so the token stream is the sole display path.

TUI gateway mirror:
Mirror _tts_stream_stop and _tts_stream_barge_in_monitor changes to
tui_gateway/server.py so both code paths use the same VAD parameters,
grace period, and TTS CUT diagnostic logging.

Profile-scoped session DB and MoA progress events in tui_gateway/server.py
were necessitated by the TTS pipeline changes affecting session state
and event routing.

Normal-exit flag and TTS CUT diagnostic logging at all cut paths.

Regression tests:
- test_quiet_then_loud_playback_does_not_trip
- test_8x_multiplier_absorbs_tts_volume_spikes
- test_trigger_ceiling_lets_genuine_speech_trip
- test_silence_calibration_does_not_false_trip_on_tts
- test_tts_stream_stop_latches_interruption_for_next_turn
- test_tts_stream_stop_after_natural_finish_does_not_latch
- Profile-scoped session DB tests (10 tests)
@beardedeagle
beardedeagle force-pushed the fix/vad-rolling-window branch from 70d7785 to 070370f Compare July 25, 2026 00:28
@beardedeagle

Copy link
Copy Markdown
Contributor Author

Note on suppressed Copilot comment about 'was 5.0' parenthetical in voice_mode.py: True positive — the old code used median * 3.5, not 5.0. Removed the misleading '(was 5.0)' from the comment. Fixed in 070370f.

teknium1 added a commit that referenced this pull request Jul 29, 2026
…or TTS is playing

The continuous-voice no-speech counter (3 strikes -> voice off) counted
every silent capture cycle unconditionally. During a long agent turn
(thinking/tool-calling for minutes) or while TTS is speaking, the user
is CORRECTLY silent — those cycles ended the voice chat under them.

- hermes_cli/voice.py: new set_voice_busy_probe() seam + _voice_activity_held()
  (TTS-playing via the existing _tts_playing Event, agent-busy via the
  registered probe). Both the continuous-loop strike path and the
  force-transcribe single-shot strike path skip counting while held.
  Fail-open: a broken probe counts cycles as before.
- tui_gateway/server.py: registers _any_session_running() as the probe
  on voice.record start (voice is process-global; any running session holds).
- cli.py: classic CLI strike path skips counting while _agent_running
  or TTS playback is in flight.

Stop phrase and barge-in still work during the hold (own paths).
Includes a fixture fix for the #71083 cherry-pick: the fake tools.tts_tool
module needs _load_tts_config (main's tts_streaming imports it).
teknium1 added a commit that referenced this pull request Jul 29, 2026
…ateway/desktop backends)

Premise check on live main: barge-in machinery EXISTS for the per-turn
STREAMING pipeline only — cli.py chat() arms _voice_barge_in_monitor and
tui_gateway _tts_stream_begin arms _tts_stream_barge_in_monitor. What was
actually broken for spoken interruptions:

1. CLI whole-file fallback (_voice_speak_response_async — used whenever
   streaming TTS cannot start: sounddevice missing, requirement probe
   fails): NO monitor was ever armed, so talking over the reply did
   nothing. Now arms _voice_barge_in_monitor in continuous voice mode.
2. Gateway fallback speak (tts_queue None → speak_text thread) and the
   voice.tts RPC (desktop-triggered speech): speak_text ran bare, and
   its internal streaming dispatch created a PRIVATE stop event nothing
   could reach — uninterruptible even by stop_playback(). New
   _speak_text_with_barge() runs the same barge monitor beside the speak
   thread; hermes_cli.voice.speak_text/_speak_text_streaming accept an
   external stop_event so a barge cuts the streaming pipeline too.
   Stop-phrase handling and voice.transcript submission are inherited
   from the shared monitor (merged #73933 behavior preserved).
3. False barge during TTS (the reason interruption "worked" then
   self-cancelled or fired randomly): salvaged PR #71083 by @beardedeagle
   (previous commit, kept authorship) — rolling-window VAD floor, 8x
   multiplier, 4000-RMS trigger ceiling, barge_in_grace_seconds (2s)
   before the mic opens, min-floor clamp. barge_in_grace_seconds is now
   documented in DEFAULT_CONFIG.

Desktop spoken barge (renderer mic via voice-barge-in.ts) already covers
both its live-stream and fallback speech paths — verified, no change.
@teknium1

Copy link
Copy Markdown
Contributor

Salvaged into PR #74000 (merged, b6729ba) with your commit authorship preserved in git history. Your rolling-window VAD floor, multiplier tuning, RMS ceiling, and barge grace window were exactly the missing piece that made spoken barge-in reliable — they shipped alongside the fix that armed the barge monitor on the whole-file playback paths, and voice.barge_in_grace_seconds is now a documented config key. Thanks for the careful signal-processing work; live-tested as part of the voice UX polish wave.

@teknium1 teknium1 closed this Jul 29, 2026
randlee pushed a commit to randlee/hermes-agent that referenced this pull request Aug 11, 2026
…or TTS is playing

The continuous-voice no-speech counter (3 strikes -> voice off) counted
every silent capture cycle unconditionally. During a long agent turn
(thinking/tool-calling for minutes) or while TTS is speaking, the user
is CORRECTLY silent — those cycles ended the voice chat under them.

- hermes_cli/voice.py: new set_voice_busy_probe() seam + _voice_activity_held()
  (TTS-playing via the existing _tts_playing Event, agent-busy via the
  registered probe). Both the continuous-loop strike path and the
  force-transcribe single-shot strike path skip counting while held.
  Fail-open: a broken probe counts cycles as before.
- tui_gateway/server.py: registers _any_session_running() as the probe
  on voice.record start (voice is process-global; any running session holds).
- cli.py: classic CLI strike path skips counting while _agent_running
  or TTS playback is in flight.

Stop phrase and barge-in still work during the hold (own paths).
Includes a fixture fix for the NousResearch#71083 cherry-pick: the fake tools.tts_tool
module needs _load_tts_config (main's tts_streaming imports it).
randlee pushed a commit to randlee/hermes-agent that referenced this pull request Aug 11, 2026
…ateway/desktop backends)

Premise check on live main: barge-in machinery EXISTS for the per-turn
STREAMING pipeline only — cli.py chat() arms _voice_barge_in_monitor and
tui_gateway _tts_stream_begin arms _tts_stream_barge_in_monitor. What was
actually broken for spoken interruptions:

1. CLI whole-file fallback (_voice_speak_response_async — used whenever
   streaming TTS cannot start: sounddevice missing, requirement probe
   fails): NO monitor was ever armed, so talking over the reply did
   nothing. Now arms _voice_barge_in_monitor in continuous voice mode.
2. Gateway fallback speak (tts_queue None → speak_text thread) and the
   voice.tts RPC (desktop-triggered speech): speak_text ran bare, and
   its internal streaming dispatch created a PRIVATE stop event nothing
   could reach — uninterruptible even by stop_playback(). New
   _speak_text_with_barge() runs the same barge monitor beside the speak
   thread; hermes_cli.voice.speak_text/_speak_text_streaming accept an
   external stop_event so a barge cuts the streaming pipeline too.
   Stop-phrase handling and voice.transcript submission are inherited
   from the shared monitor (merged NousResearch#73933 behavior preserved).
3. False barge during TTS (the reason interruption "worked" then
   self-cancelled or fired randomly): salvaged PR NousResearch#71083 by @beardedeagle
   (previous commit, kept authorship) — rolling-window VAD floor, 8x
   multiplier, 4000-RMS trigger ceiling, barge_in_grace_seconds (2s)
   before the mic opens, min-floor clamp. barge_in_grace_seconds is now
   documented in DEFAULT_CONFIG.

Desktop spoken barge (renderer mic via voice-barge-in.ts) already covers
both its live-stream and fallback speech paths — verified, no change.
randlee pushed a commit to randlee/hermes-agent that referenced this pull request Aug 11, 2026
33hodl pushed a commit to 33hodl/hermes-agent that referenced this pull request Aug 12, 2026
…or TTS is playing

The continuous-voice no-speech counter (3 strikes -> voice off) counted
every silent capture cycle unconditionally. During a long agent turn
(thinking/tool-calling for minutes) or while TTS is speaking, the user
is CORRECTLY silent — those cycles ended the voice chat under them.

- hermes_cli/voice.py: new set_voice_busy_probe() seam + _voice_activity_held()
  (TTS-playing via the existing _tts_playing Event, agent-busy via the
  registered probe). Both the continuous-loop strike path and the
  force-transcribe single-shot strike path skip counting while held.
  Fail-open: a broken probe counts cycles as before.
- tui_gateway/server.py: registers _any_session_running() as the probe
  on voice.record start (voice is process-global; any running session holds).
- cli.py: classic CLI strike path skips counting while _agent_running
  or TTS playback is in flight.

Stop phrase and barge-in still work during the hold (own paths).
Includes a fixture fix for the NousResearch#71083 cherry-pick: the fake tools.tts_tool
module needs _load_tts_config (main's tts_streaming imports it).
33hodl pushed a commit to 33hodl/hermes-agent that referenced this pull request Aug 12, 2026
…ateway/desktop backends)

Premise check on live main: barge-in machinery EXISTS for the per-turn
STREAMING pipeline only — cli.py chat() arms _voice_barge_in_monitor and
tui_gateway _tts_stream_begin arms _tts_stream_barge_in_monitor. What was
actually broken for spoken interruptions:

1. CLI whole-file fallback (_voice_speak_response_async — used whenever
   streaming TTS cannot start: sounddevice missing, requirement probe
   fails): NO monitor was ever armed, so talking over the reply did
   nothing. Now arms _voice_barge_in_monitor in continuous voice mode.
2. Gateway fallback speak (tts_queue None → speak_text thread) and the
   voice.tts RPC (desktop-triggered speech): speak_text ran bare, and
   its internal streaming dispatch created a PRIVATE stop event nothing
   could reach — uninterruptible even by stop_playback(). New
   _speak_text_with_barge() runs the same barge monitor beside the speak
   thread; hermes_cli.voice.speak_text/_speak_text_streaming accept an
   external stop_event so a barge cuts the streaming pipeline too.
   Stop-phrase handling and voice.transcript submission are inherited
   from the shared monitor (merged NousResearch#73933 behavior preserved).
3. False barge during TTS (the reason interruption "worked" then
   self-cancelled or fired randomly): salvaged PR NousResearch#71083 by @beardedeagle
   (previous commit, kept authorship) — rolling-window VAD floor, 8x
   multiplier, 4000-RMS trigger ceiling, barge_in_grace_seconds (2s)
   before the mic opens, min-floor clamp. barge_in_grace_seconds is now
   documented in DEFAULT_CONFIG.

Desktop spoken barge (renderer mic via voice-barge-in.ts) already covers
both its live-stream and fallback speech paths — verified, no change.
33hodl pushed a commit to 33hodl/hermes-agent that referenced this pull request Aug 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/cli CLI entry point, hermes_cli/, setup wizard P3 Low — cosmetic, nice to have 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.

4 participants