fix(gateway): deliver native-Opus TTS replies as Telegram voice notes - #32539
fix(gateway): deliver native-Opus TTS replies as Telegram voice notes#32539marnelram wants to merge 1 commit into
Conversation
The auto-TTS step in BasePlatformAdapter._process_message_background runs
*after* the gateway runner clears HERMES_SESSION_PLATFORM (to ""), so
text_to_speech_tool sees an empty platform and skips the Telegram-aware
output-format branch (want_opus=False). The result is a .mp3 file, which
adapter.send_voice then routes through Telegram's sendAudio (audio-file
card) instead of sendVoice (waveform bubble).
GatewayRunner._send_voice_reply has the same effective bug for a
different reason: it hardcoded the temp path's extension to .mp3.
Providers in the native-Opus set ({openai, elevenlabs, mistral, gemini,
inworld}) honor the supplied extension, so handing them .mp3 produces
MP3 bytes — same audio-file-card outcome.
Fixes:
- gateway/platforms/base.py: set HERMES_SESSION_PLATFORM via the
contextvar around the auto-TTS call so text_to_speech_tool sees the
right platform and picks .ogg for native-Opus providers.
- gateway/run.py: pick the temp extension based on the configured
provider — .ogg for native-Opus providers, .mp3 for the rest (which
still rely on the downstream _convert_to_opus step).
Adds tests/gateway/test_send_voice_reply_native_opus_ext.py covering
both branches across all five native-Opus providers and five
non-native providers.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
teknium1
left a comment
There was a problem hiding this comment.
Thanks for tracing the ContextVar timing issue. The gateway/platforms/base.py portion still addresses a current-main defect: GatewayRunner clears session context at gateway/run.py:12303, while the base adapter invokes auto-TTS later at gateway/platforms/base.py:4991; text_to_speech_tool reads that platform at tools/tts_tool.py:2200-2202.
Problems
gateway/run.py:11397now chooses OGG from provider alone. This drops the platform gate: a non-Telegram reply using one of those providers would receive OGG, whereas current main intentionally uses OGG only for Telegram atgateway/run.py:13153-13158.- The new tests cover only
_send_voice_reply, but current main already has equivalent Telegram/non-Telegram coverage intests/gateway/test_auto_voice_reply_format.py:17-72, introduced byae82eed2b194a5708bfecbc153637e434fc15ddb.
Suggested changes
- Keep the scoped platform binding in
gateway/platforms/base.py, remove the redundant runner extension-selection change, and replace its tests with a direct base-adapter regression proving auto-TTS sees Telegram after the handler clears its session context.
Automated hermes-sweeper review.
| _active_provider = "" | ||
| _voice_reply_ext = ( | ||
| ".ogg" | ||
| if _active_provider in {"openai", "elevenlabs", "mistral", "gemini", "inworld"} |
There was a problem hiding this comment.
This selection no longer considers event.source.platform, so a non-Telegram reply with one of these providers now gets OGG. Current main deliberately gates OGG on Telegram at gateway/run.py:13153-13158; please retain that behavior and remove this now-redundant provider matrix.
|
The root cause here (gateway auto-TTS choosing MP3 vs Ogg/Opus via the cleared (Landed via #73508, merge |
Summary
Auto-TTS replies from Inworld (and any other native-Opus provider — OpenAI, ElevenLabs, Mistral, Gemini) currently arrive in Telegram as audio-file cards (with title + play button) instead of native voice notes (waveform bubble). Two separate but related bugs cause this:
gateway/platforms/base.pyauto-TTS path — the gateway runner setsHERMES_SESSION_PLATFORM=telegramwhile the agent runs and thenclear_session_varsresets it to""(intentional — to distinguish "explicitly cleared" from "never set"). The base adapter's auto-TTS step at_process_message_backgroundruns after that clear, sotext_to_speech_toolseesplatform=""→want_opus=False→ falls through to the.mp3branch.gateway/run.py_send_voice_reply— independently, this helper hardcodes the temp path extension to.mp3with a comment claiming "the TTS tool may convert to .ogg." That's true for Edge/NeuTTS/Piper (they're MP3/WAV-native and get_convert_to_opus'd), but for native-Opus providers the TTS tool honors the supplied extension — so it produces MP3 bytes too. This also affectsvoice_mode=alltext-input → voice-reply flows.In both cases,
adapter.send_voicethen sees a.mp3file and routes it through Telegram'ssendAudio(audio-file card UI) instead ofsendVoice(waveform bubble).Changes
gateway/platforms/base.py: setHERMES_SESSION_PLATFORMvia the contextvar (token-scoped, restored infinally) around the auto-TTS call so the TTS tool sees the live platform.gateway/run.py: pick the temp extension based on the configured provider —.oggfor{openai, elevenlabs, mistral, gemini, inworld},.mp3for everything else (preserving the existing_convert_to_opuspath for Edge TTS et al.).tests/gateway/test_send_voice_reply_native_opus_ext.py: parametrized over all five native-Opus providers and five non-native providers, asserting the right extension is handed to the TTS tool.Reproduction (pre-fix)
tts.provider: inworld(oropenai,elevenlabs,mistral,gemini) in~/.hermes/config.yaml./voice on.journalctl -u hermes-gateway— theTTS audio saved:line will showprovider=inworld, want_opus=False, file ending in.mp3.Test plan
pytest tests/gateway/test_send_voice_reply_native_opus_ext.py— 10 new tests passpytest tests/gateway/test_send_voice_reply_notify.py— existing notify-flag regressions still passpytest tests/tools/test_tts_opus_routing.py— opus-routing tests still passtts.provider: inworld(modelinworld-tts-2, voiceSelene,delivery_mode: BALANCED),/voice on, send a Telegram voice note → reply now renders as the native waveform bubble with no extra latency from Opus conversion🤖 Generated with Claude Code