Skip to content

fix: Matrix voice as Ogg/Opus — direct TTS path + non-blocking metadata (completes #54488) - #68063

Closed
malaiwah wants to merge 2 commits into
NousResearch:mainfrom
malaiwah:fix/matrix-voice-opus-complete
Closed

fix: Matrix voice as Ogg/Opus — direct TTS path + non-blocking metadata (completes #54488)#68063
malaiwah wants to merge 2 commits into
NousResearch:mainfrom
malaiwah:fix/matrix-voice-opus-complete

Conversation

@malaiwah

@malaiwah malaiwah commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

Summary

Builds directly on #54488 by @ciabata-git (that commit is included unchanged — please credit it there / land that PR and treat this as the follow-up, whichever is easier to review) and resolves the two blockers from its automated review:

1. Direct text_to_speech delivery now covered

tools/tts_tool.py's want_opus now includes Matrix, so a model-invoked TTS call on a Matrix session emits Ogg/Opus like Telegram does — native-Opus providers (openai/elevenlabs/mistral/gemini) directly, MP3-native providers via the existing tool-level _convert_to_opus step (which writes to a distinct .ogg path, so no Edge in/out collision — that only occurs when a .ogg output path is passed into Edge, which nothing here does).

As a defense in depth, MatrixAdapter.send_voice now enforces the MSC3245 codec at the adapter boundary: any non-Ogg audio reaching it (any current or future caller) is transcoded to Ogg/Opus before upload. Best-effort — if ffmpeg is unavailable or fails, the original file is sent unchanged, preserving today's behaviour.

2. Metadata probing no longer blocks the event loop

_matrix_voice_metadata_for_file() is now awaited via asyncio.to_thread(...) from _send_local_file, so its ffprobe/ffmpeg subprocess timeouts (10s/15s) can't stall the Matrix adapter. The new boundary transcode runs off-loop the same way.

Testing

  • pytest tests/tools/test_tts_opus_routing.py "tests/gateway/test_matrix_voice.py::TestMatrixSendVoiceMSC3245" "tests/gateway/test_voice_command.py::TestSendVoiceReply" -q -o addopts=13 passed (includes the new Matrix tool-routing case mirroring the Telegram one, adapter-boundary transcode case, and skip-when-already-Ogg case)
  • ruff==0.15.10 check on the four changed files → All checks passed
  • python -m py_compile on changed files → passed
  • Downstream validation: this combination (Opus container + info.duration + org.matrix.msc1767.audio duration/waveform) is running in production on our Matrix homeserver — voice bubbles render and play correctly in Element X iOS, verified on-device against a field-by-field diff with an Element-recorded voice message (see Matrix voice replies sent as mp3 instead of ogg/opus — render as broken attachments #14841).

Closes #14841 (together with the #54488 base it includes).

🤖 Generated with Claude Code

hermes-agent and others added 2 commits July 20, 2026 13:51
Builds on NousResearch#54488 (kept as-is) and addresses its two review blockers:

1. Direct text_to_speech coverage — tools/tts_tool.py want_opus now
   includes Matrix, so model-invoked TTS on a Matrix session emits
   Ogg/Opus (native-Opus providers directly; MP3 providers via the
   existing tool-level _convert_to_opus step, which converts to a
   distinct path — no Edge in/out collision). As a safety net,
   MatrixAdapter.send_voice now enforces the MSC3245 codec at the
   adapter boundary: any non-Ogg audio reaching it is transcoded to
   Ogg/Opus (best-effort; original sent unchanged if ffmpeg is
   unavailable, preserving previous behaviour).

2. Non-blocking metadata — _matrix_voice_metadata_for_file() is now
   awaited via asyncio.to_thread from _send_local_file, so its
   ffprobe/ffmpeg subprocess timeouts can no longer stall the Matrix
   adapter event loop; the boundary transcode runs off-loop the same
   way.

Tests: Matrix case added to tests/tools/test_tts_opus_routing.py
(mirrors the Telegram case); adapter-boundary transcode + skip-when-Ogg
cases added to tests/gateway/test_matrix_voice.py.

Refs NousResearch#14841

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@malaiwah
malaiwah force-pushed the fix/matrix-voice-opus-complete branch from dd603d9 to ff0c58a Compare July 20, 2026 13:52
@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists comp/gateway Gateway runner, session dispatch, delivery comp/plugins Plugin system and bundled plugins tool/tts Text-to-speech and transcription platform/matrix Matrix adapter (E2EE) sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages labels Jul 20, 2026
@alt-glitch

Copy link
Copy Markdown
Collaborator

This was generated by AI during triage.

Related: #54488, #63859, and #14900 address the Matrix voice/Opus cluster. This PR extends the direct TTS path and adds adapter-boundary conversion plus non-blocking metadata; reviewers should choose or consolidate the overlapping approaches.

@Bryntly Bryntly left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The implementation looks great overall!

However, CI is failing on tests/gateway/test_auto_voice_reply_format.py. This is because gateway/run.py was correctly updated to unconditionally generate an .mp3 intermediate path, but the test was still expecting .ogg to be passed to text_to_speech_tool.

Could you please apply the following diff to fix the test?

diff --git a/tests/gateway/test_auto_voice_reply_format.py b/tests/gateway/test_auto_voice_reply_format.py
index eeb39ab60..36a7f626f 100644
--- a/tests/gateway/test_auto_voice_reply_format.py
+++ b/tests/gateway/test_auto_voice_reply_format.py
@@ -15,7 +15,7 @@ from gateway.session import SessionSource
 class TestAutoVoiceReplyFormat:
     @pytest.mark.asyncio
     async def test_telegram_auto_voice_reply_requests_ogg_for_native_voice_bubble(self):
-        """Telegram auto-TTS should request OGG/Opus so send_voice sends a voice bubble."""
+        """Telegram auto-TTS should request MP3 then transcode to OGG/Opus so send_voice sends a voice bubble."""
         runner = _make_runner()
         adapter = _make_adapter(Platform.TELEGRAM)
         runner.adapters[Platform.TELEGRAM] = adapter
@@ -24,9 +24,9 @@ class TestAutoVoiceReplyFormat:
 
         def fake_tts(*, text, output_path):
             requested_paths.append(output_path)
-            assert output_path.endswith(".ogg")
+            assert output_path.endswith(".mp3")
             Path(output_path).parent.mkdir(parents=True, exist_ok=True)
-            Path(output_path).write_bytes(b"fake ogg opus")
+            Path(output_path).write_bytes(b"fake mp3")
             return json.dumps({
                 "success": True,
                 "file_path": output_path,
@@ -34,11 +34,17 @@ class TestAutoVoiceReplyFormat:
                 "voice_compatible": True,
             })
 
-        with patch("tools.tts_tool.text_to_speech_tool", side_effect=fake_tts):
+        def fake_convert(path):
+            ogg_path = str(Path(path).with_suffix(".ogg"))
+            Path(ogg_path).write_bytes(b"fake ogg opus")
+            return ogg_path
+
+        with patch("tools.tts_tool.text_to_speech_tool", side_effect=fake_tts), \
+             patch("tools.tts_tool._convert_to_opus", side_effect=fake_convert):
             await runner._send_voice_reply(event, "hello from auto tts")
 
         assert requested_paths
-        assert requested_paths[0].endswith(".ogg")
+        assert requested_paths[0].endswith(".mp3")
         adapter.send_voice.assert_awaited_once()
         assert adapter.send_voice.await_args.kwargs["audio_path"].endswith(".ogg")

teknium1 pushed a commit that referenced this pull request Jul 28, 2026
…e sends

Salvaged from PR #68063 (base commit, runner-path hunks superseded by the
platform-aware OPUS_VOICE_PLATFORMS fix in this branch). Element and other
Matrix clients render voice bubbles more reliably when m.audio events carry
duration and waveform metadata; probe both best-effort via ffprobe/ffmpeg.
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
…e sends

Salvaged from PR #68063 (base commit, runner-path hunks superseded by the
platform-aware OPUS_VOICE_PLATFORMS fix in this branch). Element and other
Matrix clients render voice bubbles more reliably when m.audio events carry
duration and waveform metadata; probe both best-effort via ffprobe/ffmpeg.
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

Copy link
Copy Markdown
Contributor

Merged into main via consolidated salvage PR #73508 (merge f440a44753). Your Matrix MSC3245 duration/waveform metadata and adapter-boundary Ogg/Opus enforcement were cherry-picked (2 commits) with your authorship; your Matrix opus-routing test was kept.

Your contribution is credited to you in git history. Thank you! Closing this PR as merged-via-salvage.

@teknium1 teknium1 closed this Jul 29, 2026
randlee pushed a commit to randlee/hermes-agent that referenced this pull request Aug 11, 2026
…e sends

Salvaged from PR NousResearch#68063 (base commit, runner-path hunks superseded by the
platform-aware OPUS_VOICE_PLATFORMS fix in this branch). Element and other
Matrix clients render voice bubbles more reliably when m.audio events carry
duration and waveform metadata; probe both best-effort via ffprobe/ffmpeg.
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
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 comp/plugins Plugin system and bundled plugins P2 Medium — degraded but workaround exists platform/matrix Matrix adapter (E2EE) sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages 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.

Matrix voice replies sent as mp3 instead of ogg/opus — render as broken attachments

5 participants