diff --git a/cli.py b/cli.py index 4b315f9b6..18aeb2716 100644 --- a/cli.py +++ b/cli.py @@ -7414,11 +7414,12 @@ class HermesCLI: self._voice_stop_and_transcribe() # Audio cue: single beep BEFORE starting stream (avoid CoreAudio conflict) - try: - from tools.voice_mode import play_beep - play_beep(frequency=880, count=1) - except Exception: - pass + if self._voice_beeps_enabled(): + try: + from tools.voice_mode import play_beep + play_beep(frequency=880, count=1) + except Exception: + pass try: self._voice_recorder.start(on_silence_stop=_on_silence) @@ -7466,11 +7467,12 @@ class HermesCLI: wav_path = self._voice_recorder.stop() # Audio cue: double beep after stream stopped (no CoreAudio conflict) - try: - from tools.voice_mode import play_beep - play_beep(frequency=660, count=2) - except Exception: - pass + if self._voice_beeps_enabled(): + try: + from tools.voice_mode import play_beep + play_beep(frequency=660, count=2) + except Exception: + pass if wav_path is None: _cprint(f"{_DIM}No speech detected.{_RST}") @@ -7621,6 +7623,17 @@ class HermesCLI: _cprint(f"Unknown voice subcommand: {subcommand}") _cprint("Usage: /voice [on|off|tts|status]") + def _voice_beeps_enabled(self) -> bool: + """Return whether CLI voice mode should play record start/stop beeps.""" + try: + from hermes_cli.config import load_config + voice_cfg = load_config().get("voice", {}) + if isinstance(voice_cfg, dict): + return bool(voice_cfg.get("beep_enabled", True)) + except Exception: + pass + return True + def _enable_voice_mode(self): """Enable voice mode after checking requirements.""" if self._voice_mode: diff --git a/hermes_cli/config.py b/hermes_cli/config.py index 5f10f0de2..b1566a2a5 100644 --- a/hermes_cli/config.py +++ b/hermes_cli/config.py @@ -645,6 +645,7 @@ DEFAULT_CONFIG = { "record_key": "ctrl+b", "max_recording_seconds": 120, "auto_tts": False, + "beep_enabled": True, # Play record start/stop beeps in CLI voice mode "silence_threshold": 200, # RMS below this = silence (0-32767) "silence_duration": 3.0, # Seconds of silence before auto-stop }, @@ -849,7 +850,7 @@ DEFAULT_CONFIG = { }, # Config schema version - bump this when adding new required fields - "_config_version": 21, + "_config_version": 22, } # ============================================================================= diff --git a/tests/tools/test_voice_cli_integration.py b/tests/tools/test_voice_cli_integration.py index da500996a..e7d8811e0 100644 --- a/tests/tools/test_voice_cli_integration.py +++ b/tests/tools/test_voice_cli_integration.py @@ -933,6 +933,58 @@ class TestEnableVoiceModeReal: assert cli._voice_mode is True +class TestVoiceBeepConfigReal: + """Tests the CLI voice beep toggle.""" + + @patch("hermes_cli.config.load_config", return_value={"voice": {}}) + def test_beeps_enabled_by_default(self, _cfg): + cli = _make_voice_cli() + assert cli._voice_beeps_enabled() is True + + @patch("hermes_cli.config.load_config", return_value={"voice": {"beep_enabled": False}}) + def test_beeps_can_be_disabled(self, _cfg): + cli = _make_voice_cli() + assert cli._voice_beeps_enabled() is False + + @patch("cli._cprint") + @patch("cli.threading.Thread") + @patch("tools.voice_mode.play_beep") + @patch("tools.voice_mode.create_audio_recorder") + @patch( + "tools.voice_mode.check_voice_requirements", + return_value={ + "available": True, + "audio_available": True, + "stt_available": True, + "details": "OK", + "missing_packages": [], + }, + ) + @patch( + "hermes_cli.config.load_config", + return_value={ + "voice": { + "beep_enabled": False, + "silence_threshold": 200, + "silence_duration": 3.0, + } + }, + ) + def test_start_recording_skips_beep_when_disabled( + self, _cfg, _req, mock_create, mock_beep, mock_thread, _cp + ): + recorder = MagicMock() + recorder.supports_silence_autostop = True + mock_create.return_value = recorder + mock_thread.return_value = MagicMock(start=MagicMock()) + + cli = _make_voice_cli() + cli._voice_start_recording() + + recorder.start.assert_called_once() + mock_beep.assert_not_called() + + class TestDisableVoiceModeReal: """Tests _disable_voice_mode with real CLI instance.""" @@ -1087,6 +1139,16 @@ class TestVoiceStopAndTranscribeReal: cli._voice_stop_and_transcribe() assert cli._pending_input.empty() + @patch("cli._cprint") + @patch("hermes_cli.config.load_config", return_value={"voice": {"beep_enabled": False}}) + @patch("tools.voice_mode.play_beep") + def test_no_speech_detected_skips_beep_when_disabled(self, mock_beep, _cfg, _cp): + recorder = MagicMock() + recorder.stop.return_value = None + cli = _make_voice_cli(_voice_recording=True, _voice_recorder=recorder) + cli._voice_stop_and_transcribe() + mock_beep.assert_not_called() + @patch("cli._cprint") @patch("cli.os.unlink") @patch("cli.os.path.isfile", return_value=True) diff --git a/website/docs/guides/use-voice-mode-with-hermes.md b/website/docs/guides/use-voice-mode-with-hermes.md index 42b335559..d43c0a018 100644 --- a/website/docs/guides/use-voice-mode-with-hermes.md +++ b/website/docs/guides/use-voice-mode-with-hermes.md @@ -164,6 +164,7 @@ voice: record_key: "ctrl+b" max_recording_seconds: 120 auto_tts: false + beep_enabled: true silence_threshold: 200 silence_duration: 3.0 diff --git a/website/docs/user-guide/configuration.md b/website/docs/user-guide/configuration.md index 4eb0c56d9..c6afd8332 100644 --- a/website/docs/user-guide/configuration.md +++ b/website/docs/user-guide/configuration.md @@ -1049,6 +1049,7 @@ voice: record_key: "ctrl+b" # Push-to-talk key inside the CLI max_recording_seconds: 120 # Hard stop for long recordings auto_tts: false # Enable spoken replies automatically when /voice on + beep_enabled: true # Play record start/stop beeps in CLI voice mode silence_threshold: 200 # RMS threshold for speech detection silence_duration: 3.0 # Seconds of silence before auto-stop ``` diff --git a/website/docs/user-guide/features/voice-mode.md b/website/docs/user-guide/features/voice-mode.md index 2befd59e0..b82718cf0 100644 --- a/website/docs/user-guide/features/voice-mode.md +++ b/website/docs/user-guide/features/voice-mode.md @@ -149,7 +149,7 @@ Two-stage algorithm detects when you've finished speaking: If no speech is detected at all for 15 seconds, recording stops automatically. -Both `silence_threshold` and `silence_duration` are configurable in `config.yaml`. +Both `silence_threshold` and `silence_duration` are configurable in `config.yaml`. You can also disable the record start/stop beeps with `voice.beep_enabled: false`. ### Streaming TTS @@ -383,6 +383,7 @@ voice: record_key: "ctrl+b" # Key to start/stop recording max_recording_seconds: 120 # Maximum recording length auto_tts: false # Auto-enable TTS when voice mode starts + beep_enabled: true # Play record start/stop beeps silence_threshold: 200 # RMS level (0-32767) below which counts as silence silence_duration: 3.0 # Seconds of silence before auto-stop