Skip to content

fix(gateway): deliver native-Opus TTS replies as Telegram voice notes - #32539

Closed
marnelram wants to merge 1 commit into
NousResearch:mainfrom
marnelram:fix/native-opus-tts-voice-note
Closed

fix(gateway): deliver native-Opus TTS replies as Telegram voice notes#32539
marnelram wants to merge 1 commit into
NousResearch:mainfrom
marnelram:fix/native-opus-tts-voice-note

Conversation

@marnelram

Copy link
Copy Markdown

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:

  1. gateway/platforms/base.py auto-TTS path — the gateway runner sets HERMES_SESSION_PLATFORM=telegram while the agent runs and then clear_session_vars resets it to "" (intentional — to distinguish "explicitly cleared" from "never set"). The base adapter's auto-TTS step at _process_message_background runs after that clear, so text_to_speech_tool sees platform=""want_opus=False → falls through to the .mp3 branch.

  2. gateway/run.py _send_voice_reply — independently, this helper hardcodes the temp path extension to .mp3 with 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 affects voice_mode=all text-input → voice-reply flows.

In both cases, adapter.send_voice then sees a .mp3 file and routes it through Telegram's sendAudio (audio-file card UI) instead of sendVoice (waveform bubble).

Changes

  • gateway/platforms/base.py: set HERMES_SESSION_PLATFORM via the contextvar (token-scoped, restored in finally) 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 — .ogg for {openai, elevenlabs, mistral, gemini, inworld}, .mp3 for everything else (preserving the existing _convert_to_opus path 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)

  1. Set tts.provider: inworld (or openai, elevenlabs, mistral, gemini) in ~/.hermes/config.yaml.
  2. Enable voice mode on a Telegram chat: /voice on.
  3. Send Nellie a voice note.
  4. Observe the reply renders as an audio-file card, not a waveform voice note. Confirm via journalctl -u hermes-gateway — the TTS audio saved: line will show provider=inworld, want_opus=False, file ending in .mp3.

Test plan

  • pytest tests/gateway/test_send_voice_reply_native_opus_ext.py — 10 new tests pass
  • pytest tests/gateway/test_send_voice_reply_notify.py — existing notify-flag regressions still pass
  • pytest tests/tools/test_tts_opus_routing.py — opus-routing tests still pass
  • Manual: switch tts.provider: inworld (model inworld-tts-2, voice Selene, 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

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>
@alt-glitch alt-glitch added P2 Medium — degraded but workaround exists type/bug Something isn't working comp/gateway Gateway runner, session dispatch, delivery platform/telegram Telegram bot adapter tool/tts Text-to-speech and transcription labels May 26, 2026
@alt-glitch

Copy link
Copy Markdown
Collaborator

Related to #20182 (OGG for Telegram auto-TTS) and #20878 (duplicate of #20182). This PR is more comprehensive — it also fixes the HERMES_SESSION_PLATFORM contextvar timing issue in base.py and handles native-Opus provider detection, addressing the root cause described in #27970.

@teknium1 teknium1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:11397 now 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 at gateway/run.py:13153-13158.
  • The new tests cover only _send_voice_reply, but current main already has equivalent Telegram/non-Telegram coverage in tests/gateway/test_auto_voice_reply_format.py:17-72, introduced by ae82eed2b194a5708bfecbc153637e434fc15ddb.

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.

Comment thread gateway/run.py
_active_provider = ""
_voice_reply_ext = (
".ogg"
if _active_provider in {"openai", "elevenlabs", "mistral", "gemini", "inworld"}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@teknium1 teknium1 added sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform labels Jul 13, 2026
@teknium1

Copy link
Copy Markdown
Contributor

The root cause here (gateway auto-TTS choosing MP3 vs Ogg/Opus via the cleared HERMES_SESSION_PLATFORM contextvar, so opus platforms got audio attachments instead of native voice bubbles) was fixed class-wide in #73508: platform-awareness now comes from the caller via build_auto_tts_output_path(platform) keyed off OPUS_VOICE_PLATFORMS (based on @giladbau's #62040), with the central container repair guaranteeing real Ogg/Opus bytes.

(Landed via #73508, merge f440a44753.) Closing.

@teknium1 teknium1 closed this Jul 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

comp/gateway Gateway runner, session dispatch, delivery P2 Medium — degraded but workaround exists platform/telegram Telegram bot adapter sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages sweeper:risk-session-state Sweeper risk: may lose/corrupt/mis-associate session or context state tool/tts Text-to-speech and transcription type/bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants