diff --git a/hermes_cli/cli_voice_mixin.py b/hermes_cli/cli_voice_mixin.py index 8f704f563d..8f2b0ebfb9 100644 --- a/hermes_cli/cli_voice_mixin.py +++ b/hermes_cli/cli_voice_mixin.py @@ -454,7 +454,7 @@ class CLIVoiceMixin: # Fail-closed echo guard: playback-phase capture has no echo cancellation, so # a close match for what Hermes just spoke is speaker bleed, not a user turn. if getattr(self, "_voice_barge_phase", None) == "playback": - from tools.voice_mode import is_tts_echo + from tools.voice_mode_transcript import is_tts_echo if is_tts_echo(transcript, getattr(self, "_voice_last_tts_text", "")): logger.debug( "Dropping playback-phase barge transcript as TTS echo: %r", transcript) @@ -529,7 +529,7 @@ class CLIVoiceMixin: _cprint(f" {_DIM}{self._voice_record_key_label()} to start/stop recording{_RST}") # Spoken-stop hint from voice.stop_phrases (first entry); "" when disabled. try: - from tools.voice_mode import voice_stop_hint + from tools.voice_mode_transcript import voice_stop_hint _stop_hint = voice_stop_hint() except Exception: _stop_hint = "" diff --git a/tests/test_tui_gateway_server.py b/tests/test_tui_gateway_server.py index aebd9bb885..fdab36972d 100644 --- a/tests/test_tui_gateway_server.py +++ b/tests/test_tui_gateway_server.py @@ -1802,10 +1802,12 @@ def test_voice_toggle_on_carries_stop_hint(monkeypatch): monkeypatch.setitem( sys.modules, "tools.voice_mode", - types.SimpleNamespace( - check_voice_requirements=lambda: {"available": True, "details": ""}, - voice_stop_hint=lambda: 'Say "halt" to end the voice chat.', - ), + types.SimpleNamespace(check_voice_requirements=lambda: {"available": True, "details": ""}), + ) + monkeypatch.setitem( + sys.modules, + "tools.voice_mode_transcript", + types.SimpleNamespace(voice_stop_hint=lambda: 'Say "halt" to end the voice chat.'), ) monkeypatch.setenv("HERMES_VOICE", "0") @@ -1817,11 +1819,8 @@ def test_voice_toggle_on_carries_stop_hint(monkeypatch): # Disabled stop phrases → empty hint, clients show nothing. monkeypatch.setitem( sys.modules, - "tools.voice_mode", - types.SimpleNamespace( - check_voice_requirements=lambda: {"available": True, "details": ""}, - voice_stop_hint=lambda: "", - ), + "tools.voice_mode_transcript", + types.SimpleNamespace(voice_stop_hint=lambda: ""), ) on_resp = _dispatch_sync( {"id": "voice-on2", "method": "voice.toggle", "params": {"action": "on"}} diff --git a/tests/tools/test_voice_cli_integration.py b/tests/tools/test_voice_cli_integration.py index 3e9914eb0c..d0ed71d1ed 100644 --- a/tests/tools/test_voice_cli_integration.py +++ b/tests/tools/test_voice_cli_integration.py @@ -630,7 +630,7 @@ class TestTypedVoiceStop: # Hermetic: don't let a dev machine's voice.stop_phrases config # change which utterances count as a stop phrase. monkeypatch.setattr( - "tools.voice_mode._load_voice_stop_phrases", lambda: ("stop",) + "tools.voice_mode_transcript._load_voice_stop_phrases", lambda: ("stop",) ) def test_typed_stop_ends_voice_chat_when_voice_on(self): diff --git a/tests/tools/test_voice_stop_phrase.py b/tests/tools/test_voice_stop_phrase.py index 02af9e7e49..ebd40e60ca 100644 --- a/tests/tools/test_voice_stop_phrase.py +++ b/tests/tools/test_voice_stop_phrase.py @@ -13,7 +13,7 @@ from unittest.mock import patch import pytest -from tools.voice_mode import ( +from tools.voice_mode_transcript import ( DEFAULT_VOICE_STOP_PHRASES, _load_voice_stop_phrases, is_voice_stop_phrase, @@ -25,12 +25,12 @@ class TestVoiceStopHint: """The 'Say "stop" to end the voice chat.' hint shown on voice-mode start.""" def test_default_phrase(self): - with patch("tools.voice_mode._load_voice_stop_phrases", return_value=("stop",)): + with patch("tools.voice_mode_transcript._load_voice_stop_phrases", return_value=("stop",)): assert voice_stop_hint() == 'Say "stop" to end the voice chat.' def test_disabled_phrases_show_no_hint(self): - with patch("tools.voice_mode._load_voice_stop_phrases", return_value=()): + with patch("tools.voice_mode_transcript._load_voice_stop_phrases", return_value=()): assert voice_stop_hint() == "" @@ -43,7 +43,7 @@ class TestIsVoiceStopPhrase: def test_uses_config_when_phrases_omitted(self): - with patch("tools.voice_mode._load_voice_stop_phrases", return_value=("halt",)): + with patch("tools.voice_mode_transcript._load_voice_stop_phrases", return_value=("halt",)): assert is_voice_stop_phrase("halt") is True assert is_voice_stop_phrase("stop") is False @@ -185,9 +185,10 @@ class TestStopPhraseSurvivesHallucinationFilter: def _transcribe(self, text, phrases): import tools.voice_mode as vm + import tools.voice_mode_transcript as vmt with patch.object( - vm, "_load_voice_stop_phrases", return_value=tuple(phrases) + vmt, "_load_voice_stop_phrases", return_value=tuple(phrases) ), patch( "tools.transcription_tools.transcribe_audio", return_value={"success": True, "transcript": text}, diff --git a/tests/tools/test_voice_tts_echo_guard.py b/tests/tools/test_voice_tts_echo_guard.py index 5ae992165b..db7f24feb4 100644 --- a/tests/tools/test_voice_tts_echo_guard.py +++ b/tests/tools/test_voice_tts_echo_guard.py @@ -12,7 +12,7 @@ Contract: instead of queuing it as the next user turn. """ -from tools.voice_mode import is_tts_echo +from tools.voice_mode_transcript import is_tts_echo class TestIsTtsEcho: diff --git a/tools/transcription_common.py b/tools/transcription_common.py index f0f74ccbba..95b491e3ce 100644 --- a/tools/transcription_common.py +++ b/tools/transcription_common.py @@ -4,7 +4,7 @@ from __future__ import annotations import logging import os -import subprocess # noqa: F401 (type annotation only) +import subprocess from typing import Any, Dict from tools.tts_command_provider import _get_provider_section as _get_stt_section diff --git a/tools/voice_mode.py b/tools/voice_mode.py index 1e6ebecaac..8d50615433 100644 --- a/tools/voice_mode.py +++ b/tools/voice_mode.py @@ -23,12 +23,7 @@ from typing import Any, Callable, Dict, List, Optional logger = logging.getLogger(__name__) -from tools.voice_mode_transcript import ( # noqa: F401 - re-exported; tests patch tools.voice_mode. - _voice_config, WHISPER_HALLUCINATIONS, _HALLUCINATION_REPEAT_RE, is_whisper_hallucination, - DEFAULT_VOICE_STOP_PHRASES, _load_voice_stop_phrases, is_voice_stop_phrase, - DEFAULT_TTS_ECHO_SIMILARITY_THRESHOLD, MIN_FRAGMENT_LENGTH_FOR_ECHO, _normalize_for_echo_compare, - is_tts_echo, voice_stop_hint, -) +from tools.voice_mode_transcript import _voice_config, is_voice_stop_phrase, is_whisper_hallucination from hermes_constants import is_termux as _is_termux_environment # ── Recording parameters ── diff --git a/tools/voice_mode_transcript.py b/tools/voice_mode_transcript.py index f9d5319054..ceb98543ef 100644 --- a/tools/voice_mode_transcript.py +++ b/tools/voice_mode_transcript.py @@ -56,19 +56,12 @@ def _load_voice_stop_phrases() -> tuple: return DEFAULT_VOICE_STOP_PHRASES -def _configured_stop_phrases() -> tuple: - """Resolve ``_load_voice_stop_phrases`` through ``tools.voice_mode`` so - ``patch("tools.voice_mode._load_voice_stop_phrases")`` still takes effect.""" - from tools import voice_mode as _vm - return _vm._load_voice_stop_phrases() - - def is_voice_stop_phrase(transcript: str, stop_phrases: Optional[tuple] = None) -> bool: """True when *transcript* is EXACTLY a configured stop phrase. Deliberately strict: the whole utterance — lowercased, surrounding punctuation stripped — must equal a phrase, so "stop doing that and try again" still reaches the agent. ``voice.stop_phrases: []`` disables.""" cleaned = transcript.strip().lower().strip(".,!?;: \t\n\"'") if transcript else "" - return bool(cleaned) and cleaned in (_configured_stop_phrases() if stop_phrases is None else stop_phrases) + return bool(cleaned) and cleaned in (_load_voice_stop_phrases() if stop_phrases is None else stop_phrases) # Similarity ratio (difflib.SequenceMatcher) above which a playback-phase barge transcript @@ -113,5 +106,5 @@ def voice_stop_hint() -> str: """One-line 'Say "stop" to end the voice chat.' hint for voice-mode start, using the first ``voice.stop_phrases`` entry ("" when disabled). Every surface announcing voice-mode start (CLI, TUI, desktop) uses this one owner instead of hardcoding the wording.""" - phrases = _configured_stop_phrases() + phrases = _load_voice_stop_phrases() return f'Say "{phrases[0]}" to end the voice chat.' if phrases else "" diff --git a/tui_gateway/methods_voice.py b/tui_gateway/methods_voice.py index 3278ede98c..d13a6c3c85 100644 --- a/tui_gateway/methods_voice.py +++ b/tui_gateway/methods_voice.py @@ -631,7 +631,7 @@ def _voice_toggle_mode(rid, params: dict) -> dict: if enabled: # Spoken-stop hint for the client; sourced from voice.stop_phrases, empty when disabled. with contextlib.suppress(Exception): - from tools.voice_mode import voice_stop_hint + from tools.voice_mode_transcript import voice_stop_hint stop_hint = voice_stop_hint() # Speech output already on → warm the engine now, not on the first reply. if _voice_tts_enabled():