From 653c79bb5aa0731069dda802879aebbb9de8e83d Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Sun, 18 Oct 2026 03:07:50 -0700 Subject: [PATCH] refactor(gateway): move the voice helpers into the transcription mixin 655aef56d89 moved the transcription methods into GatewayInboundTranscriptionMixin but left _prepend_media_prefix (with its _EMPTY_TEXT_PLACEHOLDER), _untranscribed_audio_note and _transcribe_one_clip in run_inbound.py, though only the mixin calls them. They move unchanged; asyncio is imported for _transcribe_one_clip. No behaviour change: the voice/STT suites pass (14 files, 260 tests). Review finding by @kshitijk4poor on #140973 Refs #134889 (MSG-22) Refs #140719 --- gateway/run_inbound.py | 43 --------------------------- gateway/run_inbound_transcription.py | 44 ++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 43 deletions(-) diff --git a/gateway/run_inbound.py b/gateway/run_inbound.py index e3151b84d3..d9f7ac1704 100644 --- a/gateway/run_inbound.py +++ b/gateway/run_inbound.py @@ -1838,46 +1838,3 @@ class GatewayInboundMixin(GatewayInboundHooksMixin, GatewayInboundTranscriptionM return user_text prefix = "\n\n".join(enriched_parts) return f"{prefix}\n\n{user_text}" if user_text else prefix - - _EMPTY_TEXT_PLACEHOLDER = "(The user sent a message with no text content)" - - @classmethod - def _prepend_media_prefix(cls, prefix: str, user_text: str) -> str: - """``prefix`` + the user's text; the Discord empty-content placeholder is dropped as redundant.""" - if user_text and user_text.strip() != cls._EMPTY_TEXT_PLACEHOLDER: - return f"{prefix}\n\n{user_text}" - return prefix - - @staticmethod - def _untranscribed_audio_note(path: str) -> str: - """One minimal neutral marker for every STT failure. Never mention "no STT provider" or setup - steps — persisted in history they make the model keep volunteering STT-setup advice.""" - from tools.credential_files import to_agent_visible_cache_path - agent_path = to_agent_visible_cache_path(os.path.abspath(path)) - return f"[voice message could not be transcribed automatically; the audio is available at: {agent_path}]" - - async def _transcribe_one_clip(self, path: str, transcribe_audio, transcribe_audio_local_fallback) -> tuple[Optional[str], str]: - """``(transcript_or_None, note)`` for one clip via configured STT with local fallback.""" - result = await asyncio.to_thread(transcribe_audio, path, None, "gateway") - if not result.get("success"): - fallback = await asyncio.to_thread(transcribe_audio_local_fallback, path) - if fallback.get("success"): - logger.info("Configured STT failed for %s; recovered with local STT", path) - result = fallback - if not result["success"]: - logger.info("Voice transcription failed for %s: %s", path, result.get("error", "unknown error")) - return None, self._untranscribed_audio_note(path) - transcript = result["transcript"] - # STT may return success=True with an empty/whitespace transcript (silence, cut-off); - # empty quotes make the agent reply to nothing and can loop, so emit a sentinel note. - # See #41603. - if not (transcript or "").strip(): - return None, ( - "[The user sent a voice message but it came through " - "empty or inaudible — speech-to-text returned no " - "words. Do not guess at the content; ask the user " - "to resend or type it out.]" - ) - # Plain quoted line: a "The user sent a voice message..." wrapper read as a meta-instruction - # and made the LLM comment on voice mode instead. - return transcript, f'"{transcript}"' diff --git a/gateway/run_inbound_transcription.py b/gateway/run_inbound_transcription.py index b29fc18fd1..795590678a 100644 --- a/gateway/run_inbound_transcription.py +++ b/gateway/run_inbound_transcription.py @@ -2,6 +2,7 @@ from __future__ import annotations +import asyncio import logging import os from typing import Optional @@ -141,3 +142,46 @@ class GatewayInboundTranscriptionMixin: except Exception as trans_exc: logger.warning("%s transcription failed: %s", log_context, trans_exc) return text, [] + + _EMPTY_TEXT_PLACEHOLDER = "(The user sent a message with no text content)" + + @classmethod + def _prepend_media_prefix(cls, prefix: str, user_text: str) -> str: + """``prefix`` + the user's text; the Discord empty-content placeholder is dropped as redundant.""" + if user_text and user_text.strip() != cls._EMPTY_TEXT_PLACEHOLDER: + return f"{prefix}\n\n{user_text}" + return prefix + + @staticmethod + def _untranscribed_audio_note(path: str) -> str: + """One minimal neutral marker for every STT failure. Never mention "no STT provider" or setup + steps — persisted in history they make the model keep volunteering STT-setup advice.""" + from tools.credential_files import to_agent_visible_cache_path + agent_path = to_agent_visible_cache_path(os.path.abspath(path)) + return f"[voice message could not be transcribed automatically; the audio is available at: {agent_path}]" + + async def _transcribe_one_clip(self, path: str, transcribe_audio, transcribe_audio_local_fallback) -> tuple[Optional[str], str]: + """``(transcript_or_None, note)`` for one clip via configured STT with local fallback.""" + result = await asyncio.to_thread(transcribe_audio, path, None, "gateway") + if not result.get("success"): + fallback = await asyncio.to_thread(transcribe_audio_local_fallback, path) + if fallback.get("success"): + logger.info("Configured STT failed for %s; recovered with local STT", path) + result = fallback + if not result["success"]: + logger.info("Voice transcription failed for %s: %s", path, result.get("error", "unknown error")) + return None, self._untranscribed_audio_note(path) + transcript = result["transcript"] + # STT may return success=True with an empty/whitespace transcript (silence, cut-off); + # empty quotes make the agent reply to nothing and can loop, so emit a sentinel note. + # See #41603. + if not (transcript or "").strip(): + return None, ( + "[The user sent a voice message but it came through " + "empty or inaudible — speech-to-text returned no " + "words. Do not guess at the content; ask the user " + "to resend or type it out.]" + ) + # Plain quoted line: a "The user sent a voice message..." wrapper read as a meta-instruction + # and made the LLM comment on voice mode instead. + return transcript, f'"{transcript}"'