Files
abogen/tests/test_domain_conversion_pipeline.py
T
Artem Akymenko f802fb2af6 refactor: centralize integration config in domain layer, fix OutputFormat enum serialization
- Move stored_integration_config(), build_audiobookshelf_config() to domain/settings_core.py
- Remove legacy fallback from stored_integration_config() (only config[integrations])
- Add load_audiobookshelf_config() as combined entry point
- PostConversionHooks reads config directly via stored_integration_config()
- WebUI imports from domain/settings_core instead of webui/routes/utils/settings
- Remove duplicate _build_abs_config() from PostConversionHooks
- Remove dead audiobookshelf code from infrastructure/exporters.py
- Remove duplicate audiobookshelf functions from webui/service.py
- Fix OutputFormat enum serialization in output_layout_service.py and conversion_executor.py
  (f'{enum}' gave 'OutputFormat.WAV' instead of '.wav')
- Mock spaCy in test_returns_at_least_one_segment instead of loading real model
- Add 28 tests for PostConversionHooks and build_audiobookshelf_config
2026-07-29 13:11:56 +00:00

266 lines
8.5 KiB
Python

"""Tests for domain/conversion_pipeline.py — tts_segments, emit_text_segments."""
from dataclasses import dataclass, field
from typing import Any, List, Optional
from unittest.mock import MagicMock
import numpy as np
import pytest
from abogen.domain.conversion_pipeline import tts_segments, emit_text_segments, SegmentResult
@dataclass
class FakeSegment:
graphemes: str
audio: Any
tokens: list = field(default_factory=list)
@dataclass
class FakeTokenObj:
text: str
start_ts: float
end_ts: float
whitespace: str = ""
def make_backend(segments):
"""Create a mock TTS backend that yields FakeSegments."""
def backend(text, voice=None, speed=1.0, split_pattern=None):
for seg in segments:
yield seg
return backend
class TestEmitTextSegments:
def test_yields_segments(self):
audio = np.ones(24000, dtype="float32")
segments = [FakeSegment("Hello", audio)]
results = list(emit_text_segments(
"Hello world",
backend=make_backend(segments),
voice="A",
speed=1.0,
split_pattern=r"\s+",
))
assert len(results) == 1
assert results[0].graphemes == "Hello"
assert results[0].duration == 1.0
def test_skips_empty_audio(self):
segments = [
FakeSegment("Hello", np.ones(24000, dtype="float32")),
FakeSegment("", np.array([], dtype="float32")),
FakeSegment("World", np.ones(12000, dtype="float32")),
]
results = list(emit_text_segments(
"test",
backend=make_backend(segments),
voice="A",
speed=1.0,
split_pattern=r"\s+",
))
assert len(results) == 2
assert results[0].graphemes == "Hello"
assert results[1].graphemes == "World"
def test_chunk_start_increments(self):
audio = np.ones(24000, dtype="float32")
segments = [FakeSegment("A", audio), FakeSegment("B", audio)]
results = list(emit_text_segments(
"test",
backend=make_backend(segments),
voice="A",
speed=1.0,
split_pattern=r"\s+",
current_time=5.0,
))
assert results[0].chunk_start == 5.0
assert results[1].chunk_start == 6.0
def test_tokens_extracted(self):
token = FakeTokenObj("Hello", 0.0, 0.5, " ")
audio = np.ones(24000, dtype="float32")
segments = [FakeSegment("Hello", audio, [token])]
results = list(emit_text_segments(
"test",
backend=make_backend(segments),
voice="A",
speed=1.0,
split_pattern=r"\s+",
current_time=2.0,
))
assert len(results[0].tokens) == 1
assert results[0].tokens[0]["start"] == 2.0
assert results[0].tokens[0]["end"] == 2.5
assert results[0].tokens[0]["text"] == "Hello"
def test_fake_token_fallback(self):
"""When no tokens provided, creates a single FakeToken for the segment."""
audio = np.ones(24000, dtype="float32")
segments = [FakeSegment("Hello", audio)] # No tokens
results = list(emit_text_segments(
"test",
backend=make_backend(segments),
voice="A",
speed=1.0,
split_pattern=r"\s+",
))
assert len(results[0].tokens) == 1
assert results[0].tokens[0]["text"] == "Hello"
def test_empty_text(self):
results = list(emit_text_segments(
"",
backend=make_backend([]),
voice="A",
speed=1.0,
split_pattern=r"\s+",
))
assert len(results) == 0
def test_segment_result_fields(self):
audio = np.ones(48000, dtype="float32")
segments = [FakeSegment("Test", audio)]
results = list(emit_text_segments(
"test",
backend=make_backend(segments),
voice="A",
speed=1.0,
split_pattern=r"\s+",
))
seg = results[0]
assert isinstance(seg, SegmentResult)
assert seg.graphemes == "Test"
assert seg.duration == 2.0
assert len(seg.audio) == 48000
class TestTtsSegments:
def test_yields_segments_from_normalized_text(self):
audio = np.ones(24000, dtype="float32")
segments = [FakeSegment("Hello", audio)]
results = list(tts_segments(
"Already normalized text",
backend=make_backend(segments),
voice="A",
speed=1.0,
split_pattern=r"\s+",
))
assert len(results) == 1
assert results[0].graphemes == "Hello"
def test_no_normalization_performed(self):
"""tts_segments should NOT normalize — it passes text directly to backend."""
received_texts = []
def capture_backend(text, voice=None, speed=1.0, split_pattern=None):
received_texts.append(text)
yield FakeSegment("ok", np.ones(24000, dtype="float32"))
list(tts_segments(
"Raw unnormalized text",
backend=capture_backend,
voice="A",
speed=1.0,
split_pattern=r"\s+",
))
assert received_texts[0] == "Raw unnormalized text"
def test_chunk_start_increments(self):
audio = np.ones(24000, dtype="float32")
segments = [FakeSegment("A", audio), FakeSegment("B", audio)]
results = list(tts_segments(
"test",
backend=make_backend(segments),
voice="A",
speed=1.0,
split_pattern=r"\s+",
current_time=10.0,
))
assert results[0].chunk_start == 10.0
assert results[1].chunk_start == 11.0
class TestSpacyPreTtsSegmentation:
"""Tests for spacy_pre_tts_segmentation()."""
def test_disabled_when_use_spacy_false(self):
from abogen.domain.conversion_pipeline import spacy_pre_tts_segmentation
from abogen.domain.enums import Language
segments, split = spacy_pre_tts_segmentation(
"Hello world",
Language.EN_US,
"Disabled",
use_spacy_segmentation=False,
)
assert segments == ["Hello world"]
assert isinstance(split, str)
def test_disabled_for_disabled_subtitle_mode(self):
from abogen.domain.conversion_pipeline import spacy_pre_tts_segmentation
from abogen.domain.enums import Language
segments, split = spacy_pre_tts_segmentation(
"Hello world",
Language.FR,
"Disabled",
use_spacy_segmentation=True,
)
assert segments == ["Hello world"]
def test_disabled_for_line_subtitle_mode(self):
from abogen.domain.conversion_pipeline import spacy_pre_tts_segmentation
from abogen.domain.enums import Language
segments, split = spacy_pre_tts_segmentation(
"Hello world",
Language.ES,
"Line",
use_spacy_segmentation=True,
)
assert segments == ["Hello world"]
def test_disabled_for_subtitle_input(self):
from abogen.domain.conversion_pipeline import spacy_pre_tts_segmentation
from abogen.domain.enums import Language
segments, split = spacy_pre_tts_segmentation(
"Hello world",
Language.FR,
"Sentence",
is_subtitle_input=True,
use_spacy_segmentation=True,
)
assert segments == ["Hello world"]
def test_english_excluded_from_pre_tts(self):
"""English uses spaCy only for post-TTS subtitles, not pre-TTS."""
from abogen.domain.conversion_pipeline import spacy_pre_tts_segmentation
from abogen.domain.enums import Language
segments, split = spacy_pre_tts_segmentation(
"Hello world. How are you?",
Language.EN_US,
"Sentence",
use_spacy_segmentation=True,
)
# English should return single segment (no pre-TTS segmentation)
assert len(segments) == 1
def test_returns_at_least_one_segment(self):
from unittest.mock import patch
from abogen.domain.conversion_pipeline import spacy_pre_tts_segmentation
from abogen.domain.enums import Language
with patch("abogen.spacy_utils.segment_sentences", return_value=None):
segments, split = spacy_pre_tts_segmentation(
"",
Language.FR,
"Sentence",
use_spacy_segmentation=True,
)
assert len(segments) >= 1