fix(tools): transcode to OGG/Opus for OpenAI-compatible TTS backends without opus (#54589) - #54597
fix(tools): transcode to OGG/Opus for OpenAI-compatible TTS backends without opus (#54589)#54597PRATHAMESH75 wants to merge 1 commit into
Conversation
…out opus _generate_openai_tts hardcoded response_format="opus" for .ogg targets (Telegram voice bubbles). Real OpenAI encodes opus natively, but many OpenAI-compatible backends (e.g. a self-hosted Speaches/Kokoro endpoint) only support mp3/flac/wav/pcm and reject the request, so no voice bubble is delivered. Try native opus first (unchanged path for real OpenAI -- no ffmpeg dependency, no re-encode), then fall back to synthesizing mp3 and transcoding to OGG/Opus locally via the existing _convert_to_opus() helper, mirroring the Edge provider. If ffmpeg is unavailable, keep the mp3 so the caller still has playable audio. The call site now captures the returned path so the fallback is honored. Fixes NousResearch#54589
8cf6329 to
a8815e9
Compare
|
Heads-up on CI: the only failing check is The required aggregate check ( |
teknium1
left a comment
There was a problem hiding this comment.
Thanks for the targeted fallback and regression coverage. The premise is verified on current main: tools/tts_tool.py:1050-1071 still sends response_format="opus" whenever a target ends in .ogg, despite _convert_to_opus() already existing at tools/tts_tool.py:899.
Problems
- The new test stubs
_resolve_openai_audio_client_config()with two values attests/tools/test_tts_openai_opus.py:33, but current main destructures three values attools/tts_tool.py:1027; the resolver contract changed inb53ba0e188. except Exceptionat proposedtools/tts_tool.py:1062retries as MP3 after any create or file-stream failure. Limit fallback to a confirmed unsupported-opus response, and propagate unrelated authentication, transport, or write errors.
Suggested changes
- Salvage against the current managed-gateway flow and update mocks to return
(api_key, base_url, False). - Add a regression test that an unrelated error does not issue an MP3 retry.
Automated hermes-sweeper review.
|
|
||
| with patch("tools.tts_tool._import_openai_client", return_value=mock_cls), \ | ||
| patch("tools.tts_tool._resolve_openai_audio_client_config", | ||
| return_value=("test-key", None)), \ |
There was a problem hiding this comment.
Current main's resolver now returns (api_key, base_url, is_managed) and _generate_openai_tts destructures all three values (tools/tts_tool.py:1027). Update this and the other new mocks to return a three-tuple so the salvaged tests exercise the current contract.
| try: | ||
| _synthesize("opus", output_path) | ||
| return output_path | ||
| except Exception as e: |
There was a problem hiding this comment.
This catches failures from both the API request and stream_to_file, then issues a second synthesis request even for auth, transport, or local-write errors. Restrict the fallback to a confirmed unsupported-opus format response and propagate unrelated exceptions.
|
Resolved at the class level by PR #73072. Rather than a per-provider transcode, |
What does this PR do?
Fixes a bug where
_generate_openai_tts()intools/tts_tool.pyhardcodedresponse_format="opus"for any.oggtarget (Telegram voice bubbles). Real OpenAI encodes opus natively, but many OpenAI-compatible backends — e.g. a self-hosted Speaches/Kokoro endpoint — only supportmp3/flac/wav/pcmand reject the opus request, so theaudio.speech.create()call errors and no voice bubble is delivered.The fix makes the OpenAI path try native opus first, then fall back to synthesizing
mp3and transcoding to OGG/Opus locally via the existing_convert_to_opus()helper — mirroring how the Edge provider already handles this. I chose try-opus-first over an unconditional mp3+transcode specifically to avoid regressing real-OpenAI users, who would otherwise gain an ffmpeg dependency and a lossy mp3→opus re-encode on every voice reply. If ffmpeg is unavailable, the mp3 is kept so the caller still has playable audio (delivered as a document instead of a voice bubble) rather than failing with "no output".This is the inverse of #14841 (which assumes an opus-capable backend); both share this function. It is complementary to and does not overlap #54488, which transcodes at the Matrix/gateway layer (
gateway/run.py) rather than in the TTS synthesis tool.Related Issue
Fixes #54589
Type of Change
Changes Made
tools/tts_tool.py—_generate_openai_tts(): tryresponse_format="opus"for.oggtargets, and on failure re-synthesize asmp3and transcode to OGG/Opus via_convert_to_opus(); fall back to the mp3 when ffmpeg is absent. Non-.oggtargets are unchanged (direct mp3).tools/tts_tool.py—text_to_speech_tool(): capture the path returned by_generate_openai_tts()(consistent with the command/plugin providers) so the fallback path is honored downstream.tests/tools/test_tts_openai_opus.py— new tests covering native opus (no transcode), opus-rejection → mp3+transcode, no-ffmpeg → keep mp3, and plain.mp3targets.How to Test
scripts/run_tests.sh tests/tools/test_tts_openai_opus.py tests/tools/test_tts_speed.py tests/tools/test_tts_max_text_length.py tests/tools/test_managed_media_gateways.py→
Summary: 4 files, 62 tests passed, 0 failed.tts.openai.base_urlat a Speaches/Kokoro endpoint (no opus encoder) and trigger a Telegram voice reply. Before: backend rejectsopusand no bubble is sent. After: Hermes synthesizes mp3 and transcodes to OGG/Opus locally, delivering a playable voice bubble.Checklist
Code
fix(scope):,feat(scope):, etc.)pytest tests/ -qand all tests passDocumentation & Housekeeping
docs/, docstrings) — or N/Acli-config.yaml.exampleif I added/changed config keys — or N/ACONTRIBUTING.mdorAGENTS.mdif I changed architecture or workflows — or N/AScreenshots / Logs
Backend error before the fix: