diff --git a/gateway/platforms/base.py b/gateway/platforms/base.py index 1025964dc43b..caa429462c22 100644 --- a/gateway/platforms/base.py +++ b/gateway/platforms/base.py @@ -13,6 +13,7 @@ import random import re import socket as _socket +import tempfile import subprocess import sys import time @@ -4965,8 +4966,21 @@ async def _stop_typing_task() -> None: speech_text = self.prepare_tts_text(text_content) if not speech_text: raise ValueError("Empty text after markdown cleanup") + # Pass an explicit output_path with the correct + # extension for the current platform. The + # session contextvar (HERMES_SESSION_PLATFORM) is + # not populated at this call site — it is set by + # _set_session_env() inside _handle_message_with_agent + # and cleared on return. Without an explicit path + # text_to_speech_tool falls back to .mp3, which + # causes Telegram to render sendAudio instead of + # sendVoice. + _tts_ext = "ogg" if self.platform == Platform.TELEGRAM else "mp3" + _ts = datetime.now().strftime("%Y%m%d_%H%M%S") + _tts_out = Path(tempfile.gettempdir()) / f"auto_tts_{_ts}.{_tts_ext}" tts_result_str = await asyncio.to_thread( - text_to_speech_tool, text=speech_text + text_to_speech_tool, text=speech_text, + output_path=str(_tts_out), ) tts_data = _json.loads(tts_result_str) _tts_path = tts_data.get("file_path") diff --git a/tests/gateway/test_base_topic_sessions.py b/tests/gateway/test_base_topic_sessions.py index 4de540b49d1d..03d5f964166a 100644 --- a/tests/gateway/test_base_topic_sessions.py +++ b/tests/gateway/test_base_topic_sessions.py @@ -353,3 +353,40 @@ async def test_telegram_auto_tts_send_failure_keeps_followup_text(self, tmp_path "metadata": {"thread_id": "17585", "notify": True}, } ] + + @pytest.mark.asyncio + async def test_telegram_auto_tts_passes_explicit_ogg_output_path(self, tmp_path): + """Auto-TTS must pass an explicit .ogg output_path on Telegram. + + Regression test for #57049: the session contextvar + (HERMES_SESSION_PLATFORM) is not populated at the auto-TTS call + site in base.py, so text_to_speech_tool falls back to .mp3. + The fix passes an explicit output_path with the correct + extension based on self.platform. + """ + adapter = DummyTelegramAdapter() + adapter._keep_typing = self._hold_typing() + adapter._should_auto_tts_for_chat = lambda _chat_id: True + adapter.play_tts = AsyncMock(return_value=SendResult(success=True, message_id="tts-1")) + adapter.set_message_handler(lambda _event: asyncio.sleep(0, result="Short reply")) + + tts_path = tmp_path / "reply.ogg" + tts_path.write_text("audio", encoding="utf-8") + event = self._make_voice_event() + + captured_kwargs = {} + + def capture_tts(*args, **kwargs): + captured_kwargs.update(kwargs) + return json.dumps({"file_path": str(tts_path)}) + + with patch("tools.tts_tool.check_tts_requirements", return_value=True), patch( + "tools.tts_tool.text_to_speech_tool", + side_effect=capture_tts, + ): + await adapter._process_message_background(event, build_session_key(event.source)) + + assert "output_path" in captured_kwargs, "text_to_speech_tool should receive explicit output_path" + assert captured_kwargs["output_path"].endswith(".ogg"), ( + f"Telegram auto-TTS must use .ogg extension, got: {captured_kwargs['output_path']}" + )