Files
gianni-dalerta fe337abc7b feat: add ElevenLabs TTS support
- ElevenLabs as primary TTS when API key available
- Falls back to Chatterbox/XTTS/mock
- Uses Jessica voice (cgSgspJ2msm6clMCkdW9)
- 24kHz PCM output to match server expectation
- Updated tests to recognize elevenlabs backend

All 30 tests passing.
2026-02-01 14:01:21 -05:00

163 lines
5.2 KiB
Python

"""
Unit tests for OpenClaw Voice modules.
"""
import pytest
import numpy as np
import asyncio
import os
import sys
# Add src to path
sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..'))
from src.server.stt import WhisperSTT
from src.server.tts import ChatterboxTTS
from src.server.backend import AIBackend
from src.server.vad import VoiceActivityDetector
class TestWhisperSTT:
"""Tests for Speech-to-Text module."""
def test_init_loads_model(self):
"""Test that STT initializes (may be mock or real)."""
stt = WhisperSTT(model_name="tiny", device="cpu")
assert stt is not None
assert stt._backend in ["faster-whisper", "openai-whisper", "mock"]
@pytest.mark.asyncio
async def test_transcribe_returns_string(self):
"""Test that transcribe returns a string."""
stt = WhisperSTT(model_name="tiny", device="cpu")
# Create 1 second of silence at 16kHz
audio = np.zeros(16000, dtype=np.float32)
result = await stt.transcribe(audio)
assert isinstance(result, str)
@pytest.mark.asyncio
async def test_transcribe_with_noise(self):
"""Test transcription with random noise (should return something)."""
stt = WhisperSTT(model_name="tiny", device="cpu")
# Random noise
audio = np.random.randn(16000).astype(np.float32) * 0.1
result = await stt.transcribe(audio)
assert isinstance(result, str)
class TestChatterboxTTS:
"""Tests for Text-to-Speech module."""
def test_init_loads_model(self):
"""Test that TTS initializes (may be mock or real)."""
tts = ChatterboxTTS()
assert tts is not None
assert tts._backend in ["elevenlabs", "chatterbox", "xtts", "pyttsx3", "mock"]
@pytest.mark.asyncio
async def test_synthesize_returns_audio(self):
"""Test that synthesize returns numpy array."""
tts = ChatterboxTTS()
result = await tts.synthesize("Hello world")
assert isinstance(result, np.ndarray)
assert result.dtype == np.float32
assert len(result) > 0
class TestAIBackend:
"""Tests for AI Backend module."""
def test_init_creates_client(self):
"""Test backend initialization."""
backend = AIBackend(
backend_type="openai",
model="gpt-4o-mini",
)
assert backend is not None
assert backend.backend_type == "openai"
def test_system_prompt_default(self):
"""Test default system prompt is set."""
backend = AIBackend()
assert backend.system_prompt is not None
assert "voice assistant" in backend.system_prompt.lower()
def test_clear_history(self):
"""Test conversation history can be cleared."""
backend = AIBackend()
backend.conversation_history = [{"role": "user", "content": "test"}]
backend.clear_history()
assert len(backend.conversation_history) == 0
@pytest.mark.asyncio
@pytest.mark.skipif(
not os.getenv("OPENAI_API_KEY"),
reason="OPENAI_API_KEY not set"
)
async def test_chat_returns_response(self):
"""Test actual API call (requires API key)."""
backend = AIBackend(
backend_type="openai",
model="gpt-4o-mini",
api_key=os.getenv("OPENAI_API_KEY"),
)
result = await backend.chat("Say 'test' and nothing else.")
assert isinstance(result, str)
assert len(result) > 0
class TestVAD:
"""Tests for Voice Activity Detection module."""
def test_init(self):
"""Test VAD initialization."""
vad = VoiceActivityDetector()
assert vad is not None
def test_is_speech_silence(self):
"""Test that silence is not detected as speech."""
vad = VoiceActivityDetector()
silence = np.zeros(16000, dtype=np.float32)
# Should return True if no VAD model (assumes speech)
# or False if VAD model is loaded and detects no speech
result = vad.is_speech(silence)
assert isinstance(result, bool)
def test_is_speech_noise(self):
"""Test with random noise."""
vad = VoiceActivityDetector()
noise = np.random.randn(16000).astype(np.float32)
result = vad.is_speech(noise)
assert isinstance(result, bool)
class TestIntegration:
"""Integration tests for the full pipeline."""
@pytest.mark.asyncio
async def test_stt_tts_round_trip(self):
"""Test STT → TTS round trip (mock mode OK)."""
stt = WhisperSTT(model_name="tiny", device="cpu")
tts = ChatterboxTTS()
# Generate some audio (silence)
input_audio = np.zeros(16000, dtype=np.float32)
# Transcribe
text = await stt.transcribe(input_audio)
assert isinstance(text, str)
# Synthesize (even empty text should work)
if text.strip():
output_audio = await tts.synthesize(text)
else:
output_audio = await tts.synthesize("Hello")
assert isinstance(output_audio, np.ndarray)
assert len(output_audio) > 0
# Run tests with: pytest tests/ -v
if __name__ == "__main__":
pytest.main([__file__, "-v"])