From fd213a82f44482ebe1194c0c62a178d87dc82de9 Mon Sep 17 00:00:00 2001 From: Solitud1nem <76743883+Solitud1nem@users.noreply.github.com> Date: Fri, 29 May 2026 09:40:41 +0000 Subject: [PATCH] fix(voice): enforce voice.max_recording_seconds (was dead config) --- cli.py | 11 ++++++++ tests/test_voice_max_recording_seconds.py | 32 +++++++++++++++++++++++ tools/voice_mode.py | 22 ++++++++++++++++ 3 files changed, 65 insertions(+) create mode 100644 tests/test_voice_max_recording_seconds.py diff --git a/cli.py b/cli.py index 899b1f5f6561..084bf0a93adc 100644 --- a/cli.py +++ b/cli.py @@ -11766,6 +11766,17 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin): self._voice_recorder._silence_duration = ( _duration if isinstance(_duration, (int, float)) and not isinstance(_duration, bool) else 3.0 ) + # voice.max_recording_seconds — hard cap on a single recording's length. + # Same numeric guard as the silence params (bool excluded: a hand-edited + # ``max_recording_seconds: true`` must not become ``1``). <= 0 disables + # the cap. Previously this documented key was never read, so the setting + # had no effect (dead config); wiring it here makes it take effect. + _max_rec = voice_cfg.get("max_recording_seconds") + self._voice_recorder._max_recording_seconds = ( + _max_rec + if isinstance(_max_rec, (int, float)) and not isinstance(_max_rec, bool) and _max_rec > 0 + else 0.0 + ) def _on_silence(): """Called by AudioRecorder when silence is detected after speech.""" diff --git a/tests/test_voice_max_recording_seconds.py b/tests/test_voice_max_recording_seconds.py new file mode 100644 index 000000000000..394c9bfcd563 --- /dev/null +++ b/tests/test_voice_max_recording_seconds.py @@ -0,0 +1,32 @@ +"""Regression test: voice.max_recording_seconds is actually enforced. + +Before this fix, ``voice.max_recording_seconds`` was defined in the default +config (hermes_cli/config.py) and documented, but no code read it — the +AudioRecorder had no total-length cap, so the setting was dead config. This +test pins the enforcement contract on the recorder: + +* cap disabled (0 / unset) → never auto-stops on duration (previous behaviour), +* cap set to N>0 → auto-stop condition becomes true once elapsed >= N. +""" +from tools.voice_mode import AudioRecorder + + +def test_cap_disabled_by_default(): + r = AudioRecorder() + assert r._max_recording_seconds == 0.0 + # No cap → duration trigger never fires, even at absurd elapsed times. + assert r._max_duration_reached(10_000.0) is False + + +def test_cap_enforced_when_set(): + r = AudioRecorder() + r._max_recording_seconds = 120 + assert r._max_duration_reached(119.9) is False + assert r._max_duration_reached(120.0) is True + assert r._max_duration_reached(300.0) is True + + +def test_zero_means_unlimited(): + r = AudioRecorder() + r._max_recording_seconds = 0 + assert r._max_duration_reached(99_999.0) is False diff --git a/tools/voice_mode.py b/tools/voice_mode.py index 74f688875a5a..34020d54d0ac 100644 --- a/tools/voice_mode.py +++ b/tools/voice_mode.py @@ -497,11 +497,25 @@ class AudioRecorder: self._silence_threshold: int = SILENCE_RMS_THRESHOLD self._silence_duration: float = SILENCE_DURATION_SECONDS self._max_wait: float = 15.0 # Max seconds to wait for speech before auto-stop + # Hard cap on total recording length, wired from voice.max_recording_seconds + # by the CLI before each recording. 0 (or unset) = no cap (previous behaviour). + self._max_recording_seconds: float = 0.0 # Peak RMS seen during recording (for speech presence check in stop()) self._peak_rms: int = 0 # Live audio level (read by UI for visual feedback) self._current_rms: int = 0 + def _max_duration_reached(self, elapsed: float) -> bool: + """Whether the configured hard recording-length cap has elapsed. + + ``voice.max_recording_seconds`` is applied by the CLI before each + recording (see ``HermesCLI._voice_start_recording``). A value <= 0 + (or unset) disables the cap, preserving the previous unbounded + behaviour. + """ + cap = self._max_recording_seconds + return bool(cap and cap > 0 and elapsed >= cap) + # -- public properties --------------------------------------------------- @property @@ -618,6 +632,14 @@ class AudioRecorder: self._max_wait) should_fire = True + # 3. Hard cap on total recording length (voice.max_recording_seconds). + # Independent of speech/silence so a continuous speaker past the + # configured limit still auto-stops instead of recording forever. + if not should_fire and self._max_duration_reached(elapsed): + logger.info("Max recording length reached (%.0fs), auto-stopping", + self._max_recording_seconds) + should_fire = True + if should_fire: with self._lock: cb = self._on_silence_stop