fbd6602814
---ci--- phase: 0 milestone: v0.1 status: complete ---/ci---
101 lines
3.0 KiB
Python
101 lines
3.0 KiB
Python
"""Unit tests for the TTS adapters (TASK-02-02).
|
|
|
|
Both adapters must pass with a mock stream and degrade gracefully when keys /
|
|
voice models are absent. PRAXIS_TTS selection must route to the right adapter.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
|
|
from server.services.base import TTSProvider, TTSResult
|
|
from server.tts.cartesia_tts import CartesiaTTS
|
|
from server.tts.piper_tts import PiperTTS
|
|
|
|
|
|
def test_cartesia_selectable_via_env(monkeypatch):
|
|
"""PRAXIS_TTS=cartesia selects CartesiaTTS."""
|
|
from server.services.registry import get_tts
|
|
|
|
monkeypatch.setenv("PRAXIS_TTS", "cartesia")
|
|
monkeypatch.setenv("CARTESIA_API_KEY", "test-key")
|
|
get_tts.cache_clear()
|
|
tts = get_tts()
|
|
assert isinstance(tts, CartesiaTTS)
|
|
assert tts.name == "cartesia"
|
|
assert tts.voice_id # has a default voice id
|
|
|
|
|
|
def test_piper_selectable_via_env(monkeypatch):
|
|
"""PRAXIS_TTS=piper selects PiperTTS."""
|
|
from server.services.registry import get_tts
|
|
|
|
monkeypatch.setenv("PRAXIS_TTS", "piper")
|
|
get_tts.cache_clear()
|
|
tts = get_tts()
|
|
assert isinstance(tts, PiperTTS)
|
|
assert tts.name == "piper"
|
|
|
|
|
|
def test_cartesia_missing_key_no_audio():
|
|
"""Cartesia with no API key yields no audio but doesn't crash (graceful)."""
|
|
tts = CartesiaTTS(api_key="")
|
|
|
|
async def _run():
|
|
chunks = [c async for c in tts.synthesize("hello")]
|
|
return chunks
|
|
|
|
chunks = asyncio.run(_run())
|
|
assert chunks == []
|
|
|
|
|
|
def test_cartesia_synthesize_all_with_mock(monkeypatch):
|
|
"""Cartesia.synthesize_all returns audio + TTSResult with a mocked stream."""
|
|
tts = CartesiaTTS(api_key="test-key", voice_id="v1")
|
|
|
|
async def _fake_stream(text):
|
|
yield b"\x00\x01"
|
|
yield b"\x02\x03"
|
|
|
|
monkeypatch.setattr(tts, "synthesize", _fake_stream)
|
|
audio, result = asyncio.run(tts.synthesize_all("hi"))
|
|
assert audio == b"\x00\x01\x02\x03"
|
|
assert result.chars == 2
|
|
assert result.voice_id == "v1"
|
|
assert result.first_audio_ms is not None
|
|
|
|
|
|
def test_piper_missing_model_no_audio():
|
|
"""Piper with no voice model yields no audio but doesn't crash (graceful)."""
|
|
tts = PiperTTS(voice_model="")
|
|
|
|
async def _run():
|
|
chunks = [c async for c in tts.synthesize("hello")]
|
|
return chunks
|
|
|
|
chunks = asyncio.run(_run())
|
|
assert chunks == []
|
|
|
|
|
|
def test_piper_synthesize_all_with_mock(monkeypatch):
|
|
"""Piper.synthesize_all returns audio + TTSResult with a mocked stream."""
|
|
tts = PiperTTS(voice_model="/nonexistent", voice_id="p1")
|
|
|
|
async def _fake_stream(text):
|
|
yield b"\x10\x20"
|
|
yield b"\x30\x40"
|
|
|
|
monkeypatch.setattr(tts, "synthesize", _fake_stream)
|
|
audio, result = asyncio.run(tts.synthesize_all("hi"))
|
|
assert audio == b"\x10\x20\x30\x40"
|
|
assert result.chars == 2
|
|
assert result.voice_id == "p1"
|
|
|
|
|
|
def test_both_adapters_are_ttsprovider():
|
|
"""Both adapters satisfy the TTSProvider ABC."""
|
|
assert isinstance(CartesiaTTS(api_key="k"), TTSProvider)
|
|
assert isinstance(PiperTTS(), TTSProvider) |