fix(tts): class-level .ogg container repair + multi-platform Opus voice detection - #73072
Merged
Conversation
…ce detection Root-cause fix for the 'TTS voice bubble broken' issue family (#57048, #54589, #57213, #58845, #14841, #45557, #57049). Two class-level defects: 1. Several backends silently write MP3/WAV bytes into a .ogg output path (Edge only emits MP3, Piper writes WAV, xAI writes MP3, some OpenAI-compatible servers ignore response_format=opus). Platforms that need real Ogg/Opus render 0-second/broken voice bubbles. Instead of per-provider patches, text_to_speech_tool now sniffs magic bytes once after synthesis (_sniff_audio_container) and repairs the container centrally (_repair_ogg_container): ffmpeg transcode in place, or rename to the honest extension when ffmpeg is unavailable. Covers every current and future provider, including command providers and plugins. 2. want_opus only recognized Telegram, so Matrix/Feishu/WhatsApp/Signal auto-TTS voice replies were synthesized as MP3 and delivered as broken attachments. New OPUS_VOICE_PLATFORMS set covers all voice-bubble platforms. _convert_to_opus refactored onto a shared _ffmpeg_transcode_to_opus that supports safe in-place transcodes (-f ogg forced muxer, temp file + os.replace). Tests: tests/tools/test_tts_container_repair.py (13 tests incl. a live ffmpeg round-trip); full TTS suite green (153 + 187 passed).
Contributor
૮ >ﻌ< ა ci reviewran on ddd7118 ℹ️ InfoDesktop E2E visual evidence · View test artifacts · View job1 visual diff. inline evidence upload failed. Failed to upload diff-665a0833239e-onboarding-overlay-diff.png with gh image (exit code 1): Error uploading /home/runner/work/_temp/e2e-evidence/diff-665a0833239e-onboarding-overlay-diff.png: step 0 (get upload token): uploadToken not found on repo page — do you have write access to NousResearch/hermes-agent? (or, if NousResearch enforces SAML SSO, authorize at https://github.com/orgs/NousResearch/sso) |
This was referenced Jul 28, 2026
Closed
teknium1
pushed a commit
that referenced
this pull request
Jul 28, 2026
…off-loop Salvaged from PR #68063 (@malaiwah). MatrixAdapter.send_voice now transcodes any non-Ogg audio to Ogg/Opus at the adapter boundary (best-effort — the original file is sent unchanged when ffmpeg is unavailable), so MSC3245 voice bubbles render even when a caller hands the adapter MP3/WAV audio. _matrix_voice_metadata_for_file probing now runs via asyncio.to_thread so ffprobe/ffmpeg subprocess timeouts can't stall the adapter event loop. The PR's tools/tts_tool.py want_opus hunk was dropped: main's OPUS_VOICE_PLATFORMS set (PR #73072) already includes matrix; the Matrix opus-routing test is kept. Refs #14841
teknium1
pushed a commit
that referenced
this pull request
Jul 28, 2026
…bbles Salvaged from PR #62040 (@giladbau), simplified per post-#73072 main: the central _repair_ogg_container transcode makes an explicit .ogg output path sufficient — no target_platform plumbing through the TTS tool needed. Root cause (class-level): both gateway auto-TTS delivery call sites relied on the TTS tool reading HERMES_SESSION_PLATFORM to pick Ogg/Opus vs MP3, but that contextvar is cleared by _clear_session_env before the base adapter's post-handler auto-TTS block runs, so want_opus was always False on that path → MP3 → Telegram sent an audio attachment instead of a native voice bubble (#57049, #36685). The runner's _send_voice_reply had the sibling bug: it hardcoded .ogg for Telegram only, leaving Matrix and Feishu runner voice replies as MP3 (#14841, #45557). Fix: new build_auto_tts_output_path(platform) in gateway/platforms/base.py hands an explicit .ogg temp path when the platform is in the TTS tool's OPUS_VOICE_PLATFORMS set (single source of truth — telegram/matrix/feishu/ whatsapp/signal), .mp3 otherwise. Used by BOTH delivery call sites: - BasePlatformAdapter auto-TTS block (also honors the tool's success flag and cleans up requested + returned paths) - GatewayRunner._send_voice_reply (replaces the telegram-only ternary) Fixes #57049 Fixes #36685 Refs #14841 #45557
teknium1
added a commit
that referenced
this pull request
Jul 28, 2026
…iner.py One sniffer owns magic-byte container detection (Teknium's one-concept- one-owner rule): the new tools/audio_container.py is used by - gateway/platforms/base.py _sniff_audio_ext (inbound cache — PR #36166's central sniffer, now covering AAC/ADTS, MP4-brand disambiguation, webm) - gateway/platforms/signal.py _guess_extension (audio/AV branches delegated; RIFF/WAVE fix from PR #50690 and M4A-brand fix from PR #72490 now live centrally) - tools/tts_tool.py _sniff_audio_container (outbound repair, PR #73072) cache_audio_from_url inherits the sniff via cache_audio_from_bytes. Adds tests/tools/test_audio_container.py covering every magic-byte type, wrong-extension repair on the inbound cache, unknown passthrough, the URL path, and Signal's delegation.
teknium1
pushed a commit
that referenced
this pull request
Jul 28, 2026
Salvaged from PR #26233 (@LeonSGP43), rebased onto the current 3-tuple _resolve_openai_audio_client_config (is_managed flag, post-#73072 layout). Same fix independently submitted earlier in PR #26209 (@zccyman) — credit to both. Resolution order now mirrors the STT resolver: tts.openai.api_key/base_url from config.yaml -> VOICE_TOOLS_OPENAI_KEY/OPENAI_API_KEY env (still honoring config base_url) -> managed gateway. _has_openai_audio_backend also counts a config api_key as an available backend. Fixes #26175
teknium1
pushed a commit
that referenced
this pull request
Jul 28, 2026
…off-loop Salvaged from PR #68063 (@malaiwah). MatrixAdapter.send_voice now transcodes any non-Ogg audio to Ogg/Opus at the adapter boundary (best-effort — the original file is sent unchanged when ffmpeg is unavailable), so MSC3245 voice bubbles render even when a caller hands the adapter MP3/WAV audio. _matrix_voice_metadata_for_file probing now runs via asyncio.to_thread so ffprobe/ffmpeg subprocess timeouts can't stall the adapter event loop. The PR's tools/tts_tool.py want_opus hunk was dropped: main's OPUS_VOICE_PLATFORMS set (PR #73072) already includes matrix; the Matrix opus-routing test is kept. Refs #14841
teknium1
pushed a commit
that referenced
this pull request
Jul 28, 2026
…bbles Salvaged from PR #62040 (@giladbau), simplified per post-#73072 main: the central _repair_ogg_container transcode makes an explicit .ogg output path sufficient — no target_platform plumbing through the TTS tool needed. Root cause (class-level): both gateway auto-TTS delivery call sites relied on the TTS tool reading HERMES_SESSION_PLATFORM to pick Ogg/Opus vs MP3, but that contextvar is cleared by _clear_session_env before the base adapter's post-handler auto-TTS block runs, so want_opus was always False on that path → MP3 → Telegram sent an audio attachment instead of a native voice bubble (#57049, #36685). The runner's _send_voice_reply had the sibling bug: it hardcoded .ogg for Telegram only, leaving Matrix and Feishu runner voice replies as MP3 (#14841, #45557). Fix: new build_auto_tts_output_path(platform) in gateway/platforms/base.py hands an explicit .ogg temp path when the platform is in the TTS tool's OPUS_VOICE_PLATFORMS set (single source of truth — telegram/matrix/feishu/ whatsapp/signal), .mp3 otherwise. Used by BOTH delivery call sites: - BasePlatformAdapter auto-TTS block (also honors the tool's success flag and cleans up requested + returned paths) - GatewayRunner._send_voice_reply (replaces the telegram-only ternary) Fixes #57049 Fixes #36685 Refs #14841 #45557
2 tasks
randlee
pushed a commit
to randlee/hermes-agent
that referenced
this pull request
Aug 11, 2026
…iner.py One sniffer owns magic-byte container detection (Teknium's one-concept- one-owner rule): the new tools/audio_container.py is used by - gateway/platforms/base.py _sniff_audio_ext (inbound cache — PR NousResearch#36166's central sniffer, now covering AAC/ADTS, MP4-brand disambiguation, webm) - gateway/platforms/signal.py _guess_extension (audio/AV branches delegated; RIFF/WAVE fix from PR NousResearch#50690 and M4A-brand fix from PR NousResearch#72490 now live centrally) - tools/tts_tool.py _sniff_audio_container (outbound repair, PR NousResearch#73072) cache_audio_from_url inherits the sniff via cache_audio_from_bytes. Adds tests/tools/test_audio_container.py covering every magic-byte type, wrong-extension repair on the inbound cache, unknown passthrough, the URL path, and Signal's delegation.
randlee
pushed a commit
to randlee/hermes-agent
that referenced
this pull request
Aug 11, 2026
Salvaged from PR NousResearch#26233 (@LeonSGP43), rebased onto the current 3-tuple _resolve_openai_audio_client_config (is_managed flag, post-NousResearch#73072 layout). Same fix independently submitted earlier in PR NousResearch#26209 (@zccyman) — credit to both. Resolution order now mirrors the STT resolver: tts.openai.api_key/base_url from config.yaml -> VOICE_TOOLS_OPENAI_KEY/OPENAI_API_KEY env (still honoring config base_url) -> managed gateway. _has_openai_audio_backend also counts a config api_key as an available backend. Fixes NousResearch#26175
randlee
pushed a commit
to randlee/hermes-agent
that referenced
this pull request
Aug 11, 2026
…off-loop Salvaged from PR NousResearch#68063 (@malaiwah). MatrixAdapter.send_voice now transcodes any non-Ogg audio to Ogg/Opus at the adapter boundary (best-effort — the original file is sent unchanged when ffmpeg is unavailable), so MSC3245 voice bubbles render even when a caller hands the adapter MP3/WAV audio. _matrix_voice_metadata_for_file probing now runs via asyncio.to_thread so ffprobe/ffmpeg subprocess timeouts can't stall the adapter event loop. The PR's tools/tts_tool.py want_opus hunk was dropped: main's OPUS_VOICE_PLATFORMS set (PR NousResearch#73072) already includes matrix; the Matrix opus-routing test is kept. Refs NousResearch#14841
randlee
pushed a commit
to randlee/hermes-agent
that referenced
this pull request
Aug 11, 2026
…bbles Salvaged from PR NousResearch#62040 (@giladbau), simplified per post-NousResearch#73072 main: the central _repair_ogg_container transcode makes an explicit .ogg output path sufficient — no target_platform plumbing through the TTS tool needed. Root cause (class-level): both gateway auto-TTS delivery call sites relied on the TTS tool reading HERMES_SESSION_PLATFORM to pick Ogg/Opus vs MP3, but that contextvar is cleared by _clear_session_env before the base adapter's post-handler auto-TTS block runs, so want_opus was always False on that path → MP3 → Telegram sent an audio attachment instead of a native voice bubble (NousResearch#57049, NousResearch#36685). The runner's _send_voice_reply had the sibling bug: it hardcoded .ogg for Telegram only, leaving Matrix and Feishu runner voice replies as MP3 (NousResearch#14841, NousResearch#45557). Fix: new build_auto_tts_output_path(platform) in gateway/platforms/base.py hands an explicit .ogg temp path when the platform is in the TTS tool's OPUS_VOICE_PLATFORMS set (single source of truth — telegram/matrix/feishu/ whatsapp/signal), .mp3 otherwise. Used by BOTH delivery call sites: - BasePlatformAdapter auto-TTS block (also honors the tool's success flag and cleans up requested + returned paths) - GatewayRunner._send_voice_reply (replaces the telegram-only ternary) Fixes NousResearch#57049 Fixes NousResearch#36685 Refs NousResearch#14841 NousResearch#45557
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Broken TTS voice bubbles are fixed at the class level:
text_to_speech_toolnow sniffs the audio file's magic bytes once after synthesis and repairs any "MP3/WAV bytes in a.oggpath" centrally — covering every current and future provider — andwant_opusnow recognizes all Ogg/Opus voice-bubble platforms, not just Telegram.Root cause (two class defects, ~15 open one-off PRs each patching one provider or one platform):
response_format="opus". The bytes land in a.oggpath and Telegram/Matrix/Feishu render a broken 0-second voice bubble.want_opus = (platform == "telegram")— Matrix, Feishu, WhatsApp and Signal auto-TTS replies were synthesized as MP3 and delivered as broken attachments.Addresses #57048, #54589, #57213, #58845, #14841, #45557 (the container half), #57049 (the platform half).
Changes
tools/tts_tool.py:_sniff_audio_container()— magic-byte detection (ogg/wav/mp3/flac)._repair_ogg_container()— central post-synthesis check in the dispatcher: ffmpeg transcode in place to real Ogg/Opus; if ffmpeg is unavailable, rename to the honest extension so platforms deliver a working file instead of a dead bubble._ffmpeg_transcode_to_opus()— shared transcode helper (safe in-place via temp file +os.replace, forced-f oggmuxer);_convert_to_opusnow delegates to it.OPUS_VOICE_PLATFORMS= {telegram, matrix, feishu, whatsapp, signal} replaces the Telegram-only check.Validation
tests/tools/test_tts_container_repair.py(new, 13 tests).ogg→ sniffedmp3→ repaired → sniffedoggv.ogg(MP3 bytes) →v.mp3Infographic