mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-30 19:09:28 +00:00
- _FakeProc gains returncode (main's player loop checks proc.returncode) - WSL gate tests clear SSH_* env vars (main hard-warns over SSH without forwarded audio) and accept the merged #37346 forwarded-sound-server notice wording - test_tts_macos_output stubs resolve_streaming_provider so the OutputStream setup path actually runs on main's chunked-streamer code - voice CLI integration tests unwrap the _VoiceInputMessage sentinel - _is_wsl: explicit encoding + drop unreachable return (ruff PLW1514)
118 lines
3.8 KiB
Python
118 lines
3.8 KiB
Python
"""macOS output policy for streaming TTS.
|
|
|
|
On macOS, stream_tts_to_speaker must NOT open a sounddevice OutputStream
|
|
(PortAudio/CoreAudio init triggers a kTCCServiceMediaLibrary prompt). It
|
|
should route audio through the tempfile/afplay fallback instead.
|
|
See PR #62601 / #13291.
|
|
"""
|
|
|
|
import queue
|
|
import threading
|
|
|
|
import pytest
|
|
|
|
|
|
class _FakeStreamer:
|
|
"""Minimal chunked-streamer stand-in so the OutputStream setup path runs."""
|
|
|
|
sample_rate = 24000
|
|
channels = 1
|
|
|
|
def stream(self, text):
|
|
return iter([])
|
|
|
|
|
|
def _run_stream(monkeypatch, system_name):
|
|
"""Drive stream_tts_to_speaker once with a mock client on *system_name*.
|
|
|
|
Returns True if _import_sounddevice was called during the run.
|
|
"""
|
|
import tools.tts_tool as tts
|
|
|
|
monkeypatch.setattr("tools.tts_tool.platform.system", lambda: system_name)
|
|
monkeypatch.setattr("tools.tts_tool.get_env_value",
|
|
lambda name, default=None: "fake-key"
|
|
if name == "ELEVENLABS_API_KEY" else default)
|
|
monkeypatch.setattr("tools.tts_tool._load_tts_config", lambda: {})
|
|
|
|
class _FakeTTS:
|
|
def __init__(self, *a, **k):
|
|
self.text_to_speech = self
|
|
|
|
def convert(self, *a, **k):
|
|
return iter([]) # no audio chunks needed for setup assertion
|
|
|
|
monkeypatch.setattr("tools.tts_tool._import_elevenlabs", lambda: _FakeTTS)
|
|
monkeypatch.setattr(
|
|
"tools.tts_streaming.resolve_streaming_provider",
|
|
lambda cfg, preferred=None: _FakeStreamer(),
|
|
)
|
|
|
|
sd_called = {"hit": False}
|
|
|
|
def _spy_import_sd():
|
|
sd_called["hit"] = True
|
|
raise AssertionError("sounddevice must not be imported for output on macOS")
|
|
|
|
monkeypatch.setattr("tools.tts_tool._import_sounddevice", _spy_import_sd)
|
|
|
|
text_queue: queue.Queue = queue.Queue()
|
|
text_queue.put(None) # end-of-text sentinel: no sentence spoken
|
|
stop_event = threading.Event()
|
|
done_event = threading.Event()
|
|
|
|
tts.stream_tts_to_speaker(text_queue, stop_event, done_event)
|
|
assert done_event.is_set()
|
|
return sd_called["hit"]
|
|
|
|
|
|
def test_streaming_tts_skips_sounddevice_on_macos(monkeypatch):
|
|
assert _run_stream(monkeypatch, "Darwin") is False
|
|
|
|
|
|
def test_streaming_tts_uses_sounddevice_off_macos(monkeypatch):
|
|
# Off macOS the OutputStream setup runs; _import_sounddevice raising here
|
|
# is caught by the function's own guard, so the call itself is what we assert.
|
|
called = _run_stream_offmac(monkeypatch)
|
|
assert called is True
|
|
|
|
|
|
def _run_stream_offmac(monkeypatch):
|
|
"""Like _run_stream but tolerant of the sounddevice import being attempted."""
|
|
import tools.tts_tool as tts
|
|
|
|
monkeypatch.setattr("tools.tts_tool.platform.system", lambda: "Linux")
|
|
monkeypatch.setattr("tools.tts_tool.get_env_value",
|
|
lambda name, default=None: "fake-key"
|
|
if name == "ELEVENLABS_API_KEY" else default)
|
|
monkeypatch.setattr("tools.tts_tool._load_tts_config", lambda: {})
|
|
|
|
class _FakeTTS:
|
|
def __init__(self, *a, **k):
|
|
self.text_to_speech = self
|
|
|
|
def convert(self, *a, **k):
|
|
return iter([])
|
|
|
|
monkeypatch.setattr("tools.tts_tool._import_elevenlabs", lambda: _FakeTTS)
|
|
monkeypatch.setattr(
|
|
"tools.tts_streaming.resolve_streaming_provider",
|
|
lambda cfg, preferred=None: _FakeStreamer(),
|
|
)
|
|
|
|
sd_called = {"hit": False}
|
|
|
|
def _spy_import_sd():
|
|
sd_called["hit"] = True
|
|
raise OSError("no audio device in test") # handled by the function's guard
|
|
|
|
monkeypatch.setattr("tools.tts_tool._import_sounddevice", _spy_import_sd)
|
|
|
|
text_queue: queue.Queue = queue.Queue()
|
|
text_queue.put(None)
|
|
stop_event = threading.Event()
|
|
done_event = threading.Event()
|
|
|
|
tts.stream_tts_to_speaker(text_queue, stop_event, done_event)
|
|
assert done_event.is_set()
|
|
return sd_called["hit"]
|