mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-27 01:11:40 +00:00
tui_gateway/server.py:3486/3491/3509 imports start_recording, stop_and_transcribe, and speak_text from hermes_cli.voice, but the module never existed (not in git history — never shipped, never deleted). Every voice.record / voice.tts RPC call hit the ImportError branch and the TUI surfaced it as "voice module not available — install audio dependencies" even on boxes with sounddevice / faster-whisper / numpy installed. Adds a thin wrapper on top of tools.voice_mode (recording + transcription) and tools.tts_tool (text-to-speech): - start_recording() — idempotent; stores the active AudioRecorder in a module-global guarded by a Lock so repeat Ctrl+B presses don't fight over the mic. - stop_and_transcribe() — returns None for no-op / no-speech / Whisper-hallucination cases so the TUI's existing "no speech detected" path keeps working unchanged. - speak_text(text) — lazily imports tts_tool (optional provider SDKs stay unloaded until the first /voice tts call), parses the tool's JSON result, and plays the audio via play_audio_file. Paired with the Ctrl+B keybinding fix in the prior commit, the TUI voice pipeline now works end-to-end for the first time.
53 lines
1.8 KiB
Python
53 lines
1.8 KiB
Python
"""Tests for ``hermes_cli.voice`` — the TUI gateway's voice wrapper.
|
|
|
|
The module is imported *lazily* by ``tui_gateway/server.py`` so that a
|
|
box with missing audio deps fails at call time (returning a clean RPC
|
|
error) rather than at gateway startup. These tests therefore only
|
|
assert the public contract the gateway depends on: the three symbols
|
|
exist, ``stop_and_transcribe`` is a no-op when nothing is recording,
|
|
and ``speak_text`` tolerates empty input without touching the provider
|
|
stack.
|
|
"""
|
|
|
|
import os
|
|
import sys
|
|
|
|
import pytest
|
|
|
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
|
|
|
|
|
|
class TestPublicAPI:
|
|
def test_gateway_symbols_importable(self):
|
|
"""Match the exact import shape tui_gateway/server.py uses."""
|
|
from hermes_cli.voice import (
|
|
speak_text,
|
|
start_recording,
|
|
stop_and_transcribe,
|
|
)
|
|
|
|
assert callable(start_recording)
|
|
assert callable(stop_and_transcribe)
|
|
assert callable(speak_text)
|
|
|
|
|
|
class TestStopWithoutStart:
|
|
def test_returns_none_when_no_recording_active(self, monkeypatch):
|
|
"""Idempotent no-op: stop before start must not raise or touch state."""
|
|
import hermes_cli.voice as voice
|
|
|
|
monkeypatch.setattr(voice, "_recorder", None)
|
|
|
|
assert voice.stop_and_transcribe() is None
|
|
|
|
|
|
class TestSpeakTextGuards:
|
|
@pytest.mark.parametrize("text", ["", " ", "\n\t "])
|
|
def test_empty_text_is_noop(self, text):
|
|
"""Empty / whitespace-only text must return without importing tts_tool
|
|
(the gateway spawns a thread per call, so a no-op on empty input
|
|
keeps the thread pool from churning on trivial inputs)."""
|
|
from hermes_cli.voice import speak_text
|
|
|
|
# Should simply return None without raising.
|
|
assert speak_text(text) is None
|