mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-31 19:16:29 +00:00
fix(gateway): platform-aware auto-TTS output path for native voice bubbles
Salvaged from PR #62040 (@giladbau), simplified per post-#73072 main: the central _repair_ogg_container transcode makes an explicit .ogg output path sufficient — no target_platform plumbing through the TTS tool needed. Root cause (class-level): both gateway auto-TTS delivery call sites relied on the TTS tool reading HERMES_SESSION_PLATFORM to pick Ogg/Opus vs MP3, but that contextvar is cleared by _clear_session_env before the base adapter's post-handler auto-TTS block runs, so want_opus was always False on that path → MP3 → Telegram sent an audio attachment instead of a native voice bubble (#57049, #36685). The runner's _send_voice_reply had the sibling bug: it hardcoded .ogg for Telegram only, leaving Matrix and Feishu runner voice replies as MP3 (#14841, #45557). Fix: new build_auto_tts_output_path(platform) in gateway/platforms/base.py hands an explicit .ogg temp path when the platform is in the TTS tool's OPUS_VOICE_PLATFORMS set (single source of truth — telegram/matrix/feishu/ whatsapp/signal), .mp3 otherwise. Used by BOTH delivery call sites: - BasePlatformAdapter auto-TTS block (also honors the tool's success flag and cleans up requested + returned paths) - GatewayRunner._send_voice_reply (replaces the telegram-only ternary) Fixes #57049 Fixes #36685 Refs #14841 #45557
This commit is contained in:
parent
28adb86891
commit
1753369f7c
2 changed files with 58 additions and 12 deletions
|
|
@ -15,6 +15,7 @@ import re
|
|||
import socket as _socket
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
import uuid
|
||||
import weakref
|
||||
|
|
@ -160,6 +161,32 @@ def should_send_media_as_audio(platform, ext: str, is_voice: bool = False) -> bo
|
|||
return True
|
||||
|
||||
|
||||
def build_auto_tts_output_path(platform) -> str:
|
||||
"""Return a unique temp output path for gateway auto-TTS synthesis.
|
||||
|
||||
Platform-awareness lives HERE (the caller knows its platform), not in the
|
||||
TTS tool's ``HERMES_SESSION_PLATFORM`` contextvar — that contextvar is
|
||||
cleared by ``_clear_session_env`` before the post-handler auto-TTS block
|
||||
in ``BasePlatformAdapter`` runs, so relying on it always produced MP3
|
||||
(#57049, #36685). Platforms whose native voice bubbles require Ogg/Opus
|
||||
(``tools.tts_tool.OPUS_VOICE_PLATFORMS`` — the single source of truth)
|
||||
get an explicit ``.ogg`` path; the tool's central container repair
|
||||
(``_repair_ogg_container``) then guarantees real Ogg/Opus bytes for every
|
||||
provider, including MP3-only backends like Edge TTS. Everything else
|
||||
keeps the MP3 default.
|
||||
"""
|
||||
from tools.tts_tool import OPUS_VOICE_PLATFORMS
|
||||
|
||||
ext = "ogg" if _platform_name(platform) in OPUS_VOICE_PLATFORMS else "mp3"
|
||||
audio_path = os.path.join(
|
||||
tempfile.gettempdir(),
|
||||
"hermes_voice",
|
||||
f"tts_reply_{uuid.uuid4().hex[:12]}.{ext}",
|
||||
)
|
||||
os.makedirs(os.path.dirname(audio_path), exist_ok=True)
|
||||
return audio_path
|
||||
|
||||
|
||||
def utf16_len(s: str) -> int:
|
||||
"""Count UTF-16 code units in *s*.
|
||||
|
||||
|
|
@ -5547,6 +5574,7 @@ class BasePlatformAdapter(ABC):
|
|||
# an explicit ``/voice on|tts`` opt-in OR when ``voice.auto_tts`` is
|
||||
# True globally and no ``/voice off`` has been issued.
|
||||
_tts_path = None
|
||||
_tts_requested_path = None
|
||||
if (self._should_auto_tts_for_chat(event.source.chat_id)
|
||||
and event.message_type == MessageType.VOICE
|
||||
and text_content
|
||||
|
|
@ -5558,16 +5586,29 @@ class BasePlatformAdapter(ABC):
|
|||
speech_text = self.prepare_tts_text(text_content)
|
||||
if not speech_text:
|
||||
raise ValueError("Empty text after markdown cleanup")
|
||||
# Pass an explicit platform-aware output path: the
|
||||
# HERMES_SESSION_PLATFORM contextvar the tool would
|
||||
# otherwise consult is already cleared by the time
|
||||
# this post-handler block runs, which silently
|
||||
# produced MP3 (audio attachment, not a native
|
||||
# voice bubble) on Opus platforms (#57049, #36685).
|
||||
_tts_requested_path = build_auto_tts_output_path(
|
||||
self.platform
|
||||
)
|
||||
tts_result_str = await asyncio.to_thread(
|
||||
text_to_speech_tool, text=speech_text
|
||||
text_to_speech_tool,
|
||||
text=speech_text,
|
||||
output_path=_tts_requested_path,
|
||||
)
|
||||
tts_data = _json.loads(tts_result_str)
|
||||
_tts_path = tts_data.get("file_path")
|
||||
if tts_data.get("success", True):
|
||||
_tts_path = tts_data.get("file_path") or _tts_requested_path
|
||||
except Exception as tts_err:
|
||||
logger.warning("[%s] Auto-TTS failed: %s", self.name, tts_err)
|
||||
|
||||
# Play TTS audio before text (voice-first experience)
|
||||
_tts_caption_delivered = False
|
||||
_tts_cleanup_paths = {_tts_requested_path, _tts_path} - {None}
|
||||
if _tts_path and Path(_tts_path).exists():
|
||||
try:
|
||||
# Caption eligibility and payload stay on the ORIGINAL
|
||||
|
|
@ -5593,8 +5634,15 @@ class BasePlatformAdapter(ABC):
|
|||
telegram_tts_caption and getattr(tts_result, "success", False)
|
||||
)
|
||||
finally:
|
||||
for _cleanup_path in _tts_cleanup_paths:
|
||||
try:
|
||||
os.remove(_cleanup_path)
|
||||
except OSError:
|
||||
pass
|
||||
elif _tts_cleanup_paths:
|
||||
for _cleanup_path in _tts_cleanup_paths:
|
||||
try:
|
||||
os.remove(_tts_path)
|
||||
os.remove(_cleanup_path)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
|
|
|||
|
|
@ -2207,6 +2207,7 @@ from gateway.platforms.base import (
|
|||
MessageType,
|
||||
_prefix_within_utf16_limit,
|
||||
_reply_anchor_for_event,
|
||||
build_auto_tts_output_path,
|
||||
merge_pending_message_event,
|
||||
utf16_len,
|
||||
)
|
||||
|
|
@ -15835,7 +15836,6 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew
|
|||
|
||||
async def _send_voice_reply(self, event: MessageEvent, text: str) -> None:
|
||||
"""Generate TTS audio and send as a voice message before the text reply."""
|
||||
import uuid as _uuid
|
||||
audio_path = None
|
||||
actual_path = None
|
||||
try:
|
||||
|
|
@ -15845,14 +15845,12 @@ class GatewayRunner(GatewayAuthorizationMixin, GatewayKanbanWatchersMixin, Gatew
|
|||
if not tts_text:
|
||||
return
|
||||
|
||||
# Telegram's adapter only sends native voice bubbles for OGG/Opus.
|
||||
# Other platforms keep the existing MP3 default.
|
||||
audio_ext = "ogg" if event.source.platform == Platform.TELEGRAM else "mp3"
|
||||
audio_path = os.path.join(
|
||||
tempfile.gettempdir(), "hermes_voice",
|
||||
f"tts_reply_{_uuid.uuid4().hex[:12]}.{audio_ext}",
|
||||
)
|
||||
os.makedirs(os.path.dirname(audio_path), exist_ok=True)
|
||||
# Platform-aware output path: platforms whose native voice
|
||||
# bubbles require Ogg/Opus (OPUS_VOICE_PLATFORMS — Telegram,
|
||||
# Matrix, Feishu, WhatsApp, Signal) get an explicit .ogg path;
|
||||
# the TTS tool's central container repair guarantees real
|
||||
# Ogg/Opus bytes for every provider. Others keep MP3.
|
||||
audio_path = build_auto_tts_output_path(event.source.platform)
|
||||
|
||||
result_json = await asyncio.to_thread(
|
||||
text_to_speech_tool, text=tts_text, output_path=audio_path
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue