Skip to content

fix(tts): convert Edge TTS MP3 to Opus when caller supplies .ogg path - #57069

Closed
liuhao1024 wants to merge 2 commits into
NousResearch:mainfrom
liuhao1024:liuhao/cron-bugfix-57048-edge-tts-ogg
Closed

fix(tts): convert Edge TTS MP3 to Opus when caller supplies .ogg path#57069
liuhao1024 wants to merge 2 commits into
NousResearch:mainfrom
liuhao1024:liuhao/cron-bugfix-57048-edge-tts-ogg

Conversation

@liuhao1024

Copy link
Copy Markdown
Contributor

What does this PR do?

Fixes a bug where Edge TTS (and similar MP3-only providers like minimax/xai) silently writes MP3 bytes under a .ogg file extension when the caller supplies an explicit .ogg output path. The resulting file fails to render as a native Telegram voice bubble because it's MP3 data, not Opus.

The root cause: _convert_to_opus() was gated by not file_str.endswith(".ogg"), but Edge TTS always writes MP3 regardless of the target filename. When a caller (like gateway/run.py's _send_voice_reply for Telegram) passes an explicit .ogg path, the conversion is skipped because the file already "looks like" .ogg — even though it's MP3 content.

Related Issue

Fixes #57048

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)

Changes Made

  • tools/tts_tool.py: When want_opus is true and the provider is edge/minimax/xai, detect .ogg output paths that actually contain MP3 data (from caller-supplied paths), rename them to .mp3, and then run _convert_to_opus() as normal. This ensures the conversion pipeline always gets real MP3 input.
  • tests/tools/test_tts_opus_routing.py: Added regression test test_edge_telegram_explicit_ogg_path_converts verifying that Edge TTS with an explicit .ogg output_path correctly converts to real Opus.

How to Test

  1. Run python -m pytest tests/tools/test_tts_opus_routing.py -q — all 3 tests should pass
  2. Run python -m pytest tests/tools/test_tts*.py -q — all 244+ TTS tests should pass (no regressions)
  3. The new test specifically validates: when HERMES_SESSION_PLATFORM=telegram and output_path ends in .ogg, Edge TTS writes MP3 bytes → code renames to .mp3_convert_to_opus is called → result is real Opus with voice_compatible: true

Checklist

Code

  • I've read the Contributing Guide
  • My commit messages follow Conventional Commits (fix(scope):, feat(scope):, etc.)
  • I searched for existing PRs to make sure this isn't a duplicate
  • My PR contains only changes related to this fix/feature (no unrelated commits)
  • I've run pytest tests/tools/test_tts*.py -q and all 244 tests pass
  • I've added tests for my changes (required for bug fixes, strongly encouraged for features)
  • I've tested on my platform: macOS 26.4.1

Documentation & Housekeeping

  • I've updated relevant documentation (README, docs/, docstrings) — or N/A
  • I've updated cli-config.yaml.example if I added/changed config keys — or N/A
  • I've updated CONTRIBUTING.md or AGENTS.md if I changed architecture or workflows — or N/A
  • I've considered cross-platform impact (Windows, macOS) per the compatibility guide — or N/A
  • I've updated tool descriptions/schemas if I changed tool behavior — or N/A

Screenshots / Logs

The fix is self-contained in the opus conversion logic. The regression test directly validates the bug scenario (Edge TTS + explicit .ogg path + Telegram platform = real Opus output).

Edge TTS always writes MP3 bytes regardless of the target filename.
When a caller (e.g. gateway/run.py's _send_voice_reply for Telegram)
supplies an explicit .ogg output_path, the old code skipped
_convert_to_opus() because the file already ended in .ogg — even
though the content was MP3.  The resulting file failed to render as
a native Telegram voice bubble.

Fix: for edge/minimax/xai providers, detect .ogg paths that actually
contain MP3 data, rename to .mp3, then run the existing opus
conversion pipeline.  neutts/kittentts/piper are unaffected — they
handle .ogg natively in their own generation functions.

Regression test included.

Fixes NousResearch#57048
@alt-glitch alt-glitch added type/bug Something isn't working P2 Medium — degraded but workaround exists tool/tts Text-to-speech and transcription duplicate This issue or pull request already exists labels Jul 2, 2026
@alt-glitch

Copy link
Copy Markdown
Collaborator

This was generated by AI during triage.

Duplicate of #20882 (open, earlier) — that PR already makes the identical tools/tts_tool.py change: it drops the not file_str.endswith('.ogg') guard and renames .ogg->.mp3 before _convert_to_opus for the same mp3-only provider set (edge/minimax/xai). Same file, same code site, same mechanism. #20882 additionally bundles a gateway/run.py _send_voice_reply hunk. Flagging for the maintainer to pick one; the tts_tool.py fix here is the cleaner, focused version. Also related to the spec issue #57048.

@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 isolating the explicit-.ogg Edge TTS path. The premise is confirmed on current main: gateway/run.py:13300-13311 supplies .ogg for Telegram, Edge saves to that supplied path at tools/tts_tool.py:947-971, and tools/tts_tool.py:2562-2570 skips conversion solely because the filename ends in .ogg.

Problems

  • tools/tts_tool.py:2417 renames into a deterministic <stem>.mp3. On POSIX this can replace an existing caller-owned sibling; if rename fails, tools/tts_tool.py:2419-2420 silently retains the invalid .ogg instead of converting or failing.
  • The new intermediate leaks after a successful conversion: _convert_to_opus() retains its input (tools/tts_tool.py:920-934), while gateway cleanup removes only audio_path and actual_path (gateway/run.py:13357-13362), both .ogg in this route.

Suggested changes

  • Use a collision-free intermediate and clean it up in finally; preserve any pre-existing same-stem MP3 and surface preparation/conversion failure safely.
  • Add coverage for sibling preservation and intermediate cleanup.

Automated hermes-sweeper review.

Comment thread tools/tts_tool.py
}:
mp3_sibling = file_str[:-4] + ".mp3"
try:
os.rename(file_str, mp3_sibling)

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 deterministic sibling can overwrite an existing caller-owned <stem>.mp3 on POSIX; on rename failure the except OSError: pass path then returns the mislabeled .ogg without conversion. Please use a collision-free intermediate and clean it up after _convert_to_opus().

@teknium1 teknium1 added 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:risk-platform-windows Sweeper risk: may break or behave differently on native Windows sweeper:blast-contained Sweeper blast radius: contained — one narrow path / opt-in / few users labels Jul 15, 2026
@teknium1

Copy link
Copy Markdown
Contributor

Resolved at the class level by PR #73072. Rather than a per-provider transcode, text_to_speech_tool now sniffs the audio magic bytes once after every synthesis and repairs any MP3/WAV-bytes-in-.ogg centrally (in-place ffmpeg transcode to real Ogg/Opus, honest-extension rename fallback when ffmpeg is missing) — covering this provider and all others, including command providers and plugins. Your diagnosis of the container mismatch was correct and helped shape the central fix — thanks for the contribution.

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

Labels

duplicate This issue or pull request already exists P2 Medium — degraded but workaround exists sweeper:blast-contained Sweeper blast radius: contained — one narrow path / opt-in / few users 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-platform-windows Sweeper risk: may break or behave differently on native Windows 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.

Edge TTS provider silently saves MP3 bytes under a .ogg extension when given an explicit .ogg output_path (breaks Telegram voice bubbles)

3 participants