fix(voice): prefer requested MP3 for playback

This commit is contained in:
YAMAGUCHI Seiji 2026-07-15 03:45:41 +09:00 committed by Teknium
parent 1182efa0a8
commit 89d5785692
4 changed files with 64 additions and 7 deletions

11
cli.py
View file

@ -12005,11 +12005,14 @@ class HermesCLI(CLIAgentSetupMixin, CLICommandsMixin, CLIBillingMixin):
tts_result = json.loads(raw_result) if isinstance(raw_result, str) else {}
except Exception:
tts_result = {}
audio_path = tts_result.get("file_path") or mp3_path
# Play the actual file returned by the TTS provider. Command
# providers may keep native formats such as FLAC/WAV instead of
# writing the requested MP3 path.
# Prefer the requested MP3 when the provider produced it. This
# preserves reliable local playback while still supporting
# providers that write to and return a different path.
audio_path = mp3_path
if not os.path.isfile(mp3_path) or os.path.getsize(mp3_path) == 0:
audio_path = tts_result.get("file_path") or mp3_path
if os.path.isfile(audio_path) and os.path.getsize(audio_path) > 0:
play_audio_file(audio_path)
# Clean up

View file

@ -854,7 +854,13 @@ def speak_text(text: str) -> None:
tts_result = json.loads(raw_result) if isinstance(raw_result, str) else {}
except Exception:
tts_result = {}
audio_path = tts_result.get("file_path") or mp3_path
# Prefer the requested MP3 when the provider produced it. This
# preserves reliable local playback while still supporting providers
# that write to and return a different path.
audio_path = mp3_path
if not os.path.isfile(mp3_path) or os.path.getsize(mp3_path) == 0:
audio_path = tts_result.get("file_path") or mp3_path
if os.path.isfile(audio_path) and os.path.getsize(audio_path) > 0:
_debug(f"speak_text: playing {audio_path} ({os.path.getsize(audio_path)} bytes)")

View file

@ -294,20 +294,44 @@ class TestSpeakTextGuards:
from tools import tts_tool
played = []
returned_path = "/tmp/hermes_voice/actual.flac"
monkeypatch.setattr(
tts_tool,
"text_to_speech_tool",
lambda **_kwargs: '{"success": true, "file_path": "/tmp/hermes_voice/actual.flac"}',
lambda **_kwargs: f'{{"success": true, "file_path": "{returned_path}"}}',
)
monkeypatch.setattr(voice.os, "makedirs", lambda *_args, **_kwargs: None)
monkeypatch.setattr(voice.os.path, "isfile", lambda path: path == returned_path)
monkeypatch.setattr(voice.os.path, "getsize", lambda _path: 1000)
monkeypatch.setattr(voice.os, "unlink", lambda _path: None)
monkeypatch.setattr(voice, "play_audio_file", lambda path: played.append(path))
assert voice.speak_text("Hello world") is None
assert played == [returned_path]
def test_speak_text_prefers_requested_mp3_over_returned_ogg(self, monkeypatch):
import hermes_cli.voice as voice
from tools import tts_tool
played = []
requested_paths = []
def fake_tts(**kwargs):
requested_path = kwargs["output_path"]
requested_paths.append(requested_path)
ogg_path = requested_path.rsplit(".", 1)[0] + ".ogg"
return f'{{"success": true, "file_path": "{ogg_path}"}}'
monkeypatch.setattr(tts_tool, "text_to_speech_tool", fake_tts)
monkeypatch.setattr(voice.os, "makedirs", lambda *_args, **_kwargs: None)
monkeypatch.setattr(voice.os.path, "isfile", lambda _path: True)
monkeypatch.setattr(voice.os.path, "getsize", lambda _path: 1000)
monkeypatch.setattr(voice.os, "unlink", lambda _path: None)
monkeypatch.setattr(voice, "play_audio_file", lambda path: played.append(path))
assert voice.speak_text("Hello world") is None
assert played == ["/tmp/hermes_voice/actual.flac"]
assert played == requested_paths
class TestContinuousAPI:

View file

@ -1128,10 +1128,34 @@ class TestVoiceSpeakResponseReal:
def test_play_audio_uses_returned_tts_file_path(
self, _tts, mock_play, _mkd, _isf, _gsz, _unl, _cp
):
_isf.side_effect = lambda path: path == "/tmp/hermes_voice/actual.flac"
cli = _make_voice_cli(_voice_tts=True)
cli._voice_speak_response("Hello world")
mock_play.assert_called_once_with("/tmp/hermes_voice/actual.flac")
@patch("cli._cprint")
@patch("cli.os.unlink")
@patch("cli.os.path.getsize", return_value=1000)
@patch("cli.os.path.isfile", return_value=True)
@patch("cli.os.makedirs")
@patch("tools.voice_mode.play_audio_file")
@patch("tools.tts_tool.text_to_speech_tool")
def test_play_audio_prefers_requested_mp3_over_returned_ogg(
self, mock_tts, mock_play, _mkd, _isf, _gsz, _unl, _cp
):
def fake_tts(**kwargs):
mp3_path = kwargs["output_path"]
ogg_path = mp3_path.rsplit(".", 1)[0] + ".ogg"
return f'{{"success": true, "file_path": "{ogg_path}"}}'
mock_tts.side_effect = fake_tts
cli = _make_voice_cli(_voice_tts=True)
cli._voice_speak_response("Hello world")
requested_path = mock_tts.call_args.kwargs["output_path"]
mock_play.assert_called_once_with(requested_path)
class TestVoiceStopAndTranscribeReal:
"""Tests _voice_stop_and_transcribe with real CLI instance."""