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
This commit is contained in:
Teknium committed 2026-10-18 03:15:08 -07:00
1 parent 7cf9ce7d80
commit 653c79bb5a
2 files changed
+44 -43

No files matched your search

-43
View File
@@ -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}"'
+44
View File
@@ -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}"'