{"repo":"NousResearch/hermes-agent","public_id":"26404","display_label":"Edge TTS audio conversion scoped to Telegram only","summary":"A bug was identified where Edge TTS audio was being unnecessarily converted from MP3 to OGG even outside of Telegram voice delivery contexts, breaking local CLI playback on macOS. Fixes aim to restrict the OGG conversion to only when Telegram voice delivery is the target.","subsystem":null,"counts":{"members":4,"open_issues":0,"open_issues_distinct":0,"open_prs":0,"closed_issues":1,"merged_prs":1,"attempt_prs":2},"landed_fix":{"status":"merged_to_main","basis":"structural","pr":28512,"merged_at":"2026-05-19T05:29:46+00:00"},"resolution":{"state":"unresolved","by":null},"freshness":{"state":"verified","last_verified_at":"2026-05-19T05:29:46+00:00","is_stale":false},"grouping":{"backbone":"STRUCTURAL_SPANNING","link_density":0.5},"fix_edges":[{"issue_number":26404,"pr_number":28512,"tier":"verified","basis":"superseded_ref","confidence":0.9,"recoverable_from_timeline":false,"pr_merged_at":"2026-05-19T05:29:46+00:00"}],"members":[{"kind":"issue","number":26404,"title":"[Bug]: Unnecessary MP3→OGG conversion in Edge TTS breaks local CLI playback on macOS","url":null,"state":"closed"},{"kind":"pr","number":26406,"title":"fix(tts): preserve native audio outside Telegram voice delivery","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#28512","number":28512}],"category":"merged_via","implication":"superseded"},"merged":false},{"kind":"pr","number":26748,"title":"fix(tts): gate Edge TTS OGG conversion on Telegram delivery (#26404)","url":null,"state":"closed","close_reason":{"refs":[{"kind":"pr","value":"#26406","number":26406},{"kind":"pr","value":"#26404","number":26404}],"category":"duplicate_of","implication":"duplicate"},"merged":false},{"kind":"pr","number":28512,"title":"fix(tts): preserve native audio outside Telegram voice delivery","url":null,"state":"merged","close_reason":{"category":"merged"},"merged":true,"is_fix_pr":true,"fix_tier":"verified","files_changed":["scripts/release.py","tests/tools/test_tts_opus_routing.py","tools/tts_tool.py"],"fix_hunks":[{"file":"scripts/release.py","hunks":["@@ -174,6 +174,7 @@\n     \"[redacted:email]\": \"Zyrixtrex\",  # PR #26754 salvage (avoid duplicate text after auto-TTS)\n     \"264138787+nftpoetrist@users.noreply.github.com\": \"nftpoetrist\",  # PR #25856 salvage (escape slash-confirm preview)\n     \"197455947+samahn0601@users.noreply.github.com\": \"samahn0601\",  # PR #27887 salvage (retry wrapped connect timeouts)\n+    \"[redacted:email]\": \"aqilaziz\",  # PR #26406 salvage (preserve native audio outside Telegram)\n     \"282919977+eliteworkstation94-ai@users.noreply.github.com\": \"eliteworkstation94-ai\",  # PR #28157 salvage (group reply session splits)\n     \"[redacted:email]\": \"hllqkb\",\n     \"25840394+Bongulielmi@users.noreply.github.com\": \"Bongulielmi\","]},{"file":"tests/tools/test_tts_opus_routing.py","hunks":["@@ -0,0 +1,70 @@\n+import json\n+from pathlib import Path\n+from unittest.mock import Mock\n+\n+import pytest\n+\n+from gateway.session_context import _UNSET, _VAR_MAP\n+from tools import tts_tool\n+\n+\n+def _reset_session_context() -> None:\n+    for var in _VAR_MAP.values():\n+        var.set(_UNSET)\n+\n+\n[redacted:email](autouse=True)\n+def _clean_session_platform(monkeypatch):\n+    _reset_session_context()\n+    monkeypatch.delenv(\"HERMES_SESSION_PLATFORM\", raising=False)\n+    yield\n+    _reset_session_context()\n+\n+\n+async def _write_edge_output(_text: str, output_path: str, _tts_config: dict) -> str:\n+    Path(output_path).write_bytes(b\"mp3\")\n+    return output_path\n+\n+\n+def test_edge_cli_preserves_native_mp3(tmp_path, monkeypatch):\n+    out = tmp_path / \"speech.mp3\"\n+    convert = Mock()\n+\n+    monkeypatch.setattr(tts_tool, \"_load_tts_config\", lambda: {\"provider\": \"edge\"})\n+    monkeypatch.setattr(tts_tool, \"_import_edge_tts\", lambda: object())\n+    monkeypatch.setattr(tts_tool, \"_generate_edge_tts\", _write_edge_output)\n+    monkeypatch.setattr(tts_tool, \"_convert_to_opus\", convert)\n+\n+    result = json.loads(tts_tool.text_to_speech_tool(\"hello\", output_path=str(out)))\n+\n+    assert result[\"success\"] is True\n+    assert result[\"file_path\"] == str(out)\n+    assert result[\"voice_compatible\"] is False\n+    assert result[\"media_tag\"] == f\"MEDIA:{out}\"\n+    convert.assert_not_called()\n+\n+\n+def test_edge_telegram_converts_to_opus_voice(tmp_path, monkeypatch):\n+    out = tmp_path / \"speech.mp3\"\n+    opus = tmp_path / \"speech.ogg\"\n+\n+    def fake_convert(path: str) -> str:\n+        assert path == str(out)\n+        opus.write_bytes(b\"ogg\")\n+        return str(opus)\n+\n+    convert = Mock(side_effect=fake_convert)\n+\n+    monkeypatch.setenv(\"HERMES_SESSION_PLATFORM\", \"telegram\")\n+    monkeypatch.setattr(tts_tool, \"_load_tts_config\", lambda: {\"provider\": \"edge\"})\n+    monkeypatch.setattr(tts_tool, \"_import_edge_tts\", lambda: object())\n+    monkeypatch.setattr(tts_tool, \"_generate_edge_tts\", _write_edge_output)\n+    monkeypatch.setattr(tts_tool, \"_convert_to_opus\", convert)\n+\n+    result = json.loads(tts_tool.text_to_speech_tool(\"hello\", output_path=str(out)))\n+\n+    assert result[\"success\"] is True\n+    assert result[\"file_path\"] == str(opus)\n+    assert result[\"voice_compatible\"] is True\n+    assert result[\"media_tag\"] == f\"[[audio_as_voice]]\\nMEDIA:{opus}\"\n+    convert.assert_called_once_with(str(out))"]},{"file":"tools/tts_tool.py","hunks":["@@ -1830,8 +1830,10 @@ def text_to_speech_tool(\n                 \"error\": f\"TTS generation produced no output (provider: {provider})\"\n             }, ensure_ascii=False)\n \n-        # Try Opus conversion for Telegram compatibility\n-        # Edge TTS outputs MP3, NeuTTS/KittenTTS output WAV — all need ffmpeg conversion\n+        # Try Opus conversion for Telegram compatibility.\n+        # Edge TTS outputs MP3, NeuTTS/KittenTTS output WAV. Keep those native\n+        # formats for local/CLI playback and only convert when the current\n+        # platform actually needs Opus voice delivery.\n         voice_compatible = False\n         if command_provider_config is not None:\n             # Command providers are documents by default. Voice-bubble","@@ -1843,13 +1845,17 @@ def text_to_speech_tool(\n                     if opus_path:\n                         file_str = opus_path\n                 voice_compatible = file_str.endswith(\".ogg\")\n-        elif provider in {\"edge\", \"neutts\", \"minimax\", \"xai\", \"kittentts\", \"piper\"} and not file_str.endswith(\".ogg\"):\n+        elif (\n+            want_opus\n+            and provider in {\"edge\", \"neutts\", \"minimax\", \"xai\", \"kittentts\", \"piper\"}\n+            and not file_str.endswith(\".ogg\")\n+        ):\n             opus_path = _convert_to_opus(file_str)\n             if opus_path:\n                 file_str = opus_path\n                 voice_compatible = True\n         elif provider in {\"elevenlabs\", \"openai\", \"mistral\", \"gemini\"}:\n-            voice_compatible = file_str.endswith(\".ogg\")\n+            voice_compatible = want_opus and file_str.endswith(\".ogg\")\n \n         file_size = os.path.getsize(file_str)\n         logger.info(\"TTS audio saved: %s (%s bytes, provider: %s)\", file_str, f\"{file_size:,}\", provider)\n"]}]}],"trust":{"default_tier":"verified","includes_suggested":false}}