From bfc5262725a3a12b0f1c6d4b9e051d27ef4ceede Mon Sep 17 00:00:00 2001 From: devatnull Date: Sun, 28 Jun 2026 16:03:50 +0300 Subject: [PATCH] feat: add STT transcript echo toggle --- gateway/config.py | 10 +++++ gateway/run.py | 19 +++++---- hermes_cli/config.py | 4 ++ .../test_stt_transcript_echo_config.py | 41 +++++++++++++++++++ 4 files changed, 67 insertions(+), 7 deletions(-) create mode 100644 tests/gateway/test_stt_transcript_echo_config.py diff --git a/gateway/config.py b/gateway/config.py index 7693cef1c27..0103f891f2c 100644 --- a/gateway/config.py +++ b/gateway/config.py @@ -603,6 +603,7 @@ class GatewayConfig: # STT settings stt_enabled: bool = True # Whether to auto-transcribe inbound voice messages + stt_echo_transcripts: bool = True # Whether to echo raw STT transcripts back to the user # Session isolation in shared chats group_sessions_per_user: bool = True # Isolate group/channel sessions per participant when user IDs are available @@ -726,6 +727,7 @@ class GatewayConfig: "always_log_local": self.always_log_local, "filter_silence_narration": self.filter_silence_narration, "stt_enabled": self.stt_enabled, + "stt_echo_transcripts": self.stt_echo_transcripts, "group_sessions_per_user": self.group_sessions_per_user, "thread_sessions_per_user": self.thread_sessions_per_user, "max_concurrent_sessions": self.max_concurrent_sessions, @@ -772,6 +774,13 @@ class GatewayConfig: stt_enabled = data.get("stt_enabled") if stt_enabled is None: stt_enabled = data.get("stt", {}).get("enabled") if isinstance(data.get("stt"), dict) else None + stt_echo_transcripts = data.get("stt_echo_transcripts") + if stt_echo_transcripts is None: + stt_echo_transcripts = ( + data.get("stt", {}).get("echo_transcripts") + if isinstance(data.get("stt"), dict) + else None + ) group_sessions_per_user = data.get("group_sessions_per_user") thread_sessions_per_user = data.get("thread_sessions_per_user") @@ -815,6 +824,7 @@ class GatewayConfig: data.get("filter_silence_narration"), True ), stt_enabled=_coerce_bool(stt_enabled, True), + stt_echo_transcripts=_coerce_bool(stt_echo_transcripts, True), group_sessions_per_user=_coerce_bool(group_sessions_per_user, True), thread_sessions_per_user=_coerce_bool(thread_sessions_per_user, False), multiplex_profiles=_coerce_bool(multiplex_profiles, False), diff --git a/gateway/run.py b/gateway/run.py index 7e50f73638c..e94d46971bf 100644 --- a/gateway/run.py +++ b/gateway/run.py @@ -10147,10 +10147,11 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew message_text, audio_paths, ) - # Echo each successful transcript back to the user immediately, - # before the agent loop runs. Lets the user verify STT quality - # in real-time and see the raw whisper output verbatim. - if _successful_transcripts: + # Echo each successful transcript back to the user immediately + # when configured. Lets users verify STT quality in real-time, + # while allowing quiet STT for users who only want the agent to + # receive the transcription. + if _successful_transcripts and self._should_echo_stt_transcripts(): _echo_adapter = self.adapters.get(source.platform) _echo_meta = self._thread_metadata_for_source(source, self._reply_anchor_for_event(event)) if _echo_adapter: @@ -12638,6 +12639,10 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew return True + def _should_echo_stt_transcripts(self) -> bool: + """Return whether inbound voice/STT transcripts should be echoed to chat.""" + return bool(getattr(self.config, "stt_echo_transcripts", True)) + async def _send_voice_reply(self, event: MessageEvent, text: str) -> None: """Generate TTS audio and send as a voice message before the text reply.""" import uuid as _uuid @@ -14738,9 +14743,9 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew enriched_text, successful_transcripts = await self._enrich_message_with_transcription( text, audio_paths, ) - # Echo raw transcripts back to the user so voice interrupts - # feel identical to fresh voice messages. - if successful_transcripts: + # Echo raw transcripts back to the user when configured so voice + # interrupts feel identical to fresh voice messages. + if successful_transcripts and self._should_echo_stt_transcripts(): echo_adapter = self.adapters.get(source.platform) echo_meta = {"thread_id": source.thread_id} if source.thread_id else None if echo_adapter: diff --git a/hermes_cli/config.py b/hermes_cli/config.py index 3cd901c803e..f7fb4c51b12 100644 --- a/hermes_cli/config.py +++ b/hermes_cli/config.py @@ -2017,6 +2017,10 @@ DEFAULT_CONFIG = { "stt": { "enabled": True, + # When true, gateway voice messages are transcribed for the agent and + # the raw transcript is also echoed back to the user as a 🎙️ message. + # Set false to keep STT for the agent while suppressing that user-facing echo. + "echo_transcripts": True, "provider": "local", # "local" (free, faster-whisper) | "groq" | "openai" (Whisper API) | "mistral" (Voxtral Transcribe) | "elevenlabs" (Scribe) "local": { "model": "base", # tiny, base, small, medium, large-v3 diff --git a/tests/gateway/test_stt_transcript_echo_config.py b/tests/gateway/test_stt_transcript_echo_config.py new file mode 100644 index 00000000000..b65fcb2d4c2 --- /dev/null +++ b/tests/gateway/test_stt_transcript_echo_config.py @@ -0,0 +1,41 @@ +from types import SimpleNamespace + +from gateway.config import GatewayConfig +from gateway.run import GatewayRunner + + +def test_stt_echo_transcripts_defaults_on_for_backwards_compatibility(): + cfg = GatewayConfig.from_dict({}) + + assert cfg.stt_enabled is True + assert cfg.stt_echo_transcripts is True + assert cfg.to_dict()["stt_echo_transcripts"] is True + + +def test_stt_echo_transcripts_can_be_disabled_in_stt_section(): + cfg = GatewayConfig.from_dict({"stt": {"enabled": True, "echo_transcripts": False}}) + + assert cfg.stt_enabled is True + assert cfg.stt_echo_transcripts is False + + +def test_top_level_stt_echo_transcripts_takes_precedence(): + cfg = GatewayConfig.from_dict({ + "stt_echo_transcripts": False, + "stt": {"echo_transcripts": True}, + }) + + assert cfg.stt_echo_transcripts is False + + +def test_gateway_runner_uses_stt_echo_transcripts_flag(): + runner = GatewayRunner.__new__(GatewayRunner) + + runner.config = SimpleNamespace(stt_echo_transcripts=False) + assert runner._should_echo_stt_transcripts() is False + + runner.config = SimpleNamespace(stt_echo_transcripts=True) + assert runner._should_echo_stt_transcripts() is True + + runner.config = SimpleNamespace() + assert runner._should_echo_stt_transcripts() is True