- Created TTSService class with pipeline caching per language - Implemented text segmentation to identify role markers (colon and bracket patterns) - Implemented segment synthesis with voice assignment - Implemented audio concatenation at 24 kHz sample rate - Added fallback to default voice (af_bella) for unmapped roles - Supports language codes: a, b, f, j - Handles long text by chunking on sentence boundaries - Added comprehensive unit tests (13 tests, all passing) Requirements: 10.1, 10.3, 10.4, 6.2, 6.3, 6.4, 6.5, 6.6
164 lines
5.0 KiB
Python
164 lines
5.0 KiB
Python
"""
|
|
Basic tests for TTS service functionality.
|
|
"""
|
|
import pytest
|
|
import numpy as np
|
|
from services.tts_service import TTSService
|
|
|
|
|
|
def test_tts_service_initialization():
|
|
"""Test that TTSService initializes correctly."""
|
|
service = TTSService()
|
|
assert service is not None
|
|
assert service.pipelines == {}
|
|
assert service.DEFAULT_VOICE == "af_bella"
|
|
assert service.SAMPLE_RATE == 24000
|
|
|
|
|
|
def test_get_pipeline_caching():
|
|
"""Test that pipelines are cached per language."""
|
|
service = TTSService()
|
|
|
|
# Get pipeline for American English
|
|
pipeline1 = service.get_pipeline('a')
|
|
assert pipeline1 is not None
|
|
assert 'a' in service.pipelines
|
|
|
|
# Get same pipeline again - should return cached version
|
|
pipeline2 = service.get_pipeline('a')
|
|
assert pipeline1 is pipeline2
|
|
|
|
|
|
def test_get_pipeline_invalid_language():
|
|
"""Test that invalid language codes raise ValueError."""
|
|
service = TTSService()
|
|
|
|
with pytest.raises(ValueError, match="Unsupported language code"):
|
|
service.get_pipeline('x')
|
|
|
|
|
|
def test_segment_text_colon_pattern():
|
|
"""Test text segmentation with colon pattern."""
|
|
service = TTSService()
|
|
text = "Alice: Hello there\nBob: Hi Alice"
|
|
role_voice_map = {"Alice": "af_bella", "Bob": "am_adam"}
|
|
|
|
segments = service.segment_text(text, role_voice_map)
|
|
|
|
assert len(segments) == 2
|
|
assert segments[0] == ("Alice", "af_bella", "Hello there")
|
|
assert segments[1] == ("Bob", "am_adam", "Hi Alice")
|
|
|
|
|
|
def test_segment_text_bracket_pattern():
|
|
"""Test text segmentation with bracket pattern."""
|
|
service = TTSService()
|
|
text = "[Narrator] The story begins\n[Alice] Hello world"
|
|
role_voice_map = {"Narrator": "af_sarah", "Alice": "af_bella"}
|
|
|
|
segments = service.segment_text(text, role_voice_map)
|
|
|
|
assert len(segments) == 2
|
|
assert segments[0] == ("Narrator", "af_sarah", "The story begins")
|
|
assert segments[1] == ("Alice", "af_bella", "Hello world")
|
|
|
|
|
|
def test_segment_text_default_voice_fallback():
|
|
"""Test that unmapped roles fall back to default voice."""
|
|
service = TTSService()
|
|
text = "Alice: Hello\nBob: Hi"
|
|
role_voice_map = {"Alice": "af_bella"} # Bob not mapped
|
|
|
|
segments = service.segment_text(text, role_voice_map)
|
|
|
|
assert len(segments) == 2
|
|
assert segments[0] == ("Alice", "af_bella", "Hello")
|
|
assert segments[1] == ("Bob", "af_bella", "Hi") # Falls back to default
|
|
|
|
|
|
def test_segment_text_no_markers():
|
|
"""Test text without role markers uses Narrator."""
|
|
service = TTSService()
|
|
text = "This is plain text without markers"
|
|
role_voice_map = {"Narrator": "af_sarah"}
|
|
|
|
segments = service.segment_text(text, role_voice_map)
|
|
|
|
assert len(segments) == 1
|
|
assert segments[0] == ("Narrator", "af_sarah", "This is plain text without markers")
|
|
|
|
|
|
def test_chunk_text_short():
|
|
"""Test that short text is not chunked."""
|
|
service = TTSService()
|
|
text = "This is a short sentence."
|
|
|
|
chunks = service._chunk_text(text, max_length=500)
|
|
|
|
assert len(chunks) == 1
|
|
assert chunks[0] == text
|
|
|
|
|
|
def test_chunk_text_long():
|
|
"""Test that long text is chunked on sentence boundaries."""
|
|
service = TTSService()
|
|
text = "First sentence. Second sentence. Third sentence. Fourth sentence."
|
|
|
|
chunks = service._chunk_text(text, max_length=30)
|
|
|
|
assert len(chunks) > 1
|
|
# Each chunk should contain complete sentences
|
|
for chunk in chunks:
|
|
assert chunk.strip()
|
|
|
|
|
|
def test_concatenate_audio_empty():
|
|
"""Test concatenating empty list returns empty array."""
|
|
service = TTSService()
|
|
|
|
result = service.concatenate_audio([])
|
|
|
|
assert isinstance(result, np.ndarray)
|
|
assert len(result) == 0
|
|
|
|
|
|
def test_concatenate_audio_single():
|
|
"""Test concatenating single segment."""
|
|
service = TTSService()
|
|
audio = np.array([0.1, 0.2, 0.3], dtype=np.float32)
|
|
|
|
result = service.concatenate_audio([audio])
|
|
|
|
assert isinstance(result, np.ndarray)
|
|
assert len(result) == 3
|
|
np.testing.assert_array_equal(result, audio)
|
|
|
|
|
|
def test_concatenate_audio_multiple():
|
|
"""Test concatenating multiple segments."""
|
|
service = TTSService()
|
|
audio1 = np.array([0.1, 0.2], dtype=np.float32)
|
|
audio2 = np.array([0.3, 0.4], dtype=np.float32)
|
|
audio3 = np.array([0.5, 0.6], dtype=np.float32)
|
|
|
|
result = service.concatenate_audio([audio1, audio2, audio3])
|
|
|
|
assert isinstance(result, np.ndarray)
|
|
assert len(result) == 6
|
|
expected = np.array([0.1, 0.2, 0.3, 0.4, 0.5, 0.6], dtype=np.float32)
|
|
np.testing.assert_array_almost_equal(result, expected)
|
|
|
|
|
|
def test_concatenate_audio_filters_empty():
|
|
"""Test that empty segments are filtered out."""
|
|
service = TTSService()
|
|
audio1 = np.array([0.1, 0.2], dtype=np.float32)
|
|
audio2 = np.array([], dtype=np.float32)
|
|
audio3 = np.array([0.3, 0.4], dtype=np.float32)
|
|
|
|
result = service.concatenate_audio([audio1, audio2, audio3])
|
|
|
|
assert len(result) == 4
|
|
expected = np.array([0.1, 0.2, 0.3, 0.4], dtype=np.float32)
|
|
np.testing.assert_array_almost_equal(result, expected)
|