fix(voice): silent cycles never end the chat while the agent is busy 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).
This commit is contained in:
Teknium 2026-07-29 00:34:32 -07:00
parent be424703c4
commit 9e5f1b619c
5 changed files with 265 additions and 6 deletions

24
cli.py
View file

@ -12000,14 +12000,26 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin):
pass
# Track consecutive no-speech cycles to avoid infinite restart loops.
# While the agent is mid-turn or TTS is speaking, the user is
# CORRECTLY silent (waiting/listening) — those cycles must not
# count, or a multi-minute tool run ends the voice chat under
# the user. The stop phrase and barge-in still work during the
# hold (they run on their own paths above).
stop_continuous_restart = False
_activity_hold = bool(
getattr(self, "_agent_running", False)
or not self._voice_tts_done.is_set()
)
if not submitted:
self._no_speech_count = getattr(self, '_no_speech_count', 0) + 1
if self._no_speech_count >= 3:
self._voice_continuous = False
self._no_speech_count = 0
_cprint(f"{_DIM}No speech detected 3 times, continuous mode stopped.{_RST}")
stop_continuous_restart = True
if _activity_hold:
pass # held: keep listening without counting the cycle
else:
self._no_speech_count = getattr(self, '_no_speech_count', 0) + 1
if self._no_speech_count >= 3:
self._voice_continuous = False
self._no_speech_count = 0
_cprint(f"{_DIM}No speech detected 3 times, continuous mode stopped.{_RST}")
stop_continuous_restart = True
else:
self._no_speech_count = 0