mirror of
https://github.com/denizsafak/abogen.git
synced 2026-09-20 19:50:59 +02:00
- EngineConfig.language: Language (was lang_code: str = 'a') - Engine owns _KOKORO_LANG_MAP, engine_language(), supported_languages() - Engine provides language_for_voice_id() for voice catalog - Plugins/kokoro/__init__.py calls engine_language() internally - create_pipeline(plugin_id, language=Language) — no kokoro codes - pipeline_factory.py clean of kokoro-specific code - Domain functions raise TypeError if non-enum passed - WebUI api.py: _parse_language() helper at API boundary - Voice catalog returns ISO codes (lang.value) - Constants: LANGUAGE_DESCRIPTIONS keyed by Language enum - All tests updated for Language enum - 1414 tests pass
300 lines
10 KiB
Python
300 lines
10 KiB
Python
"""Tests for domain/normalization.py — prepare_text_for_tts, build_tts_context."""
|
|
|
|
import pytest
|
|
from unittest.mock import patch, MagicMock
|
|
from abogen.domain.enums import Language
|
|
from abogen.domain.normalization import prepare_text_for_tts, normalize_text_for_pipeline, build_tts_context, TTSContext
|
|
|
|
|
|
class TestPrepareTextForTts:
|
|
"""Tests for the comprehensive TTS text preparation pipeline."""
|
|
|
|
def test_empty_text(self):
|
|
result = prepare_text_for_tts("")
|
|
assert result == ""
|
|
|
|
def test_none_text(self):
|
|
result = prepare_text_for_tts(None)
|
|
assert result == ""
|
|
|
|
def test_passthrough_no_rules(self):
|
|
result = prepare_text_for_tts("Hello world")
|
|
assert isinstance(result, str)
|
|
assert len(result) > 0
|
|
|
|
def test_heteronym_rules_applied(self):
|
|
from abogen.domain.pronunciation import compile_heteronym_sentence_rules
|
|
|
|
overrides = [
|
|
{
|
|
"token": "read",
|
|
"pronunciation": "red",
|
|
"context": "past tense",
|
|
}
|
|
]
|
|
rules = compile_heteronym_sentence_rules(overrides)
|
|
if rules:
|
|
result = prepare_text_for_tts("I will read the book", heteronym_rules=rules)
|
|
assert isinstance(result, str)
|
|
|
|
def test_pronunciation_rules_applied(self):
|
|
from abogen.domain.pronunciation import compile_pronunciation_rules
|
|
|
|
overrides = [
|
|
{
|
|
"token": "epub",
|
|
"pronunciation": "ee-pub",
|
|
"normalized": "epub",
|
|
}
|
|
]
|
|
rules = compile_pronunciation_rules(overrides)
|
|
result = prepare_text_for_tts(
|
|
"This is an epub file",
|
|
pronunciation_rules=rules,
|
|
)
|
|
assert "ee-pub" in result
|
|
|
|
def test_usage_counter_tracks_pronunciation(self):
|
|
from abogen.domain.pronunciation import compile_pronunciation_rules
|
|
|
|
overrides = [
|
|
{
|
|
"token": "data",
|
|
"pronunciation": "day-ta",
|
|
"normalized": "data",
|
|
}
|
|
]
|
|
rules = compile_pronunciation_rules(overrides)
|
|
counter = {}
|
|
prepare_text_for_tts(
|
|
"The data is here and the data is there",
|
|
pronunciation_rules=rules,
|
|
usage_counter=counter,
|
|
)
|
|
assert counter.get("data", 0) >= 1
|
|
|
|
def test_combined_heteronym_and_pronunciation(self):
|
|
from abogen.domain.pronunciation import (
|
|
compile_heteronym_sentence_rules,
|
|
compile_pronunciation_rules,
|
|
)
|
|
|
|
heteronym_overrides = [
|
|
{
|
|
"token": "lead",
|
|
"pronunciation": "led",
|
|
"context": "metal",
|
|
}
|
|
]
|
|
pronunciation_overrides = [
|
|
{
|
|
"token": "gif",
|
|
"pronunciation": "jif",
|
|
"normalized": "gif",
|
|
}
|
|
]
|
|
h_rules = compile_heteronym_sentence_rules(heteronym_overrides)
|
|
p_rules = compile_pronunciation_rules(pronunciation_overrides)
|
|
|
|
result = prepare_text_for_tts(
|
|
"A lead gif",
|
|
heteronym_rules=h_rules if h_rules else None,
|
|
pronunciation_rules=p_rules,
|
|
)
|
|
assert isinstance(result, str)
|
|
|
|
@patch("abogen.domain.normalization.get_runtime_settings")
|
|
def test_normalization_overrides_passed_through(self, mock_settings):
|
|
mock_settings.return_value = {
|
|
"normalization_apostrophe_mode": "spacy",
|
|
"normalization_enabled": True,
|
|
}
|
|
result = prepare_text_for_tts(
|
|
"It's a test",
|
|
normalization_overrides={"normalization_enabled": False},
|
|
)
|
|
assert isinstance(result, str)
|
|
|
|
def test_pronunciation_rules_empty(self):
|
|
result = prepare_text_for_tts("Hello", pronunciation_rules=[])
|
|
assert isinstance(result, str)
|
|
|
|
def test_heteronym_rules_empty(self):
|
|
result = prepare_text_for_tts("Hello", heteronym_rules=[])
|
|
assert isinstance(result, str)
|
|
|
|
|
|
class TestNormalizeTextForPipeline:
|
|
"""Tests for the simpler normalization function."""
|
|
|
|
def test_basic_normalization(self):
|
|
result = normalize_text_for_pipeline("It's a test")
|
|
assert isinstance(result, str)
|
|
assert len(result) > 0
|
|
|
|
def test_empty_text(self):
|
|
result = normalize_text_for_pipeline("")
|
|
assert result == ""
|
|
|
|
@patch("abogen.domain.normalization.get_runtime_settings")
|
|
def test_with_overrides(self, mock_settings):
|
|
mock_settings.return_value = {
|
|
"normalization_apostrophe_mode": "spacy",
|
|
}
|
|
result = normalize_text_for_pipeline(
|
|
"It's a test",
|
|
normalization_overrides={"normalization_apostrophe_mode": "spacy"},
|
|
)
|
|
assert isinstance(result, str)
|
|
|
|
|
|
class TestBuildTtsContext:
|
|
"""Tests for the build_tts_context factory."""
|
|
|
|
def test_returns_tts_context(self):
|
|
ctx = build_tts_context(language=Language.EN_US)
|
|
assert isinstance(ctx, TTSContext)
|
|
|
|
def test_default_split_pattern(self):
|
|
ctx = build_tts_context(language=Language.EN_US, subtitle_mode="Disabled")
|
|
assert isinstance(ctx.split_pattern, str)
|
|
assert len(ctx.split_pattern) > 0
|
|
|
|
def test_english_uses_newline_split(self):
|
|
ctx = build_tts_context(language=Language.EN_US, subtitle_mode="Disabled")
|
|
assert ctx.split_pattern == "\n"
|
|
|
|
def test_cjk_uses_punctuation_split(self):
|
|
ctx = build_tts_context(language=Language.JA, subtitle_mode="Disabled")
|
|
assert r"\n" in ctx.split_pattern
|
|
|
|
def test_pronunciation_overrides_compiled(self):
|
|
overrides = [
|
|
{
|
|
"token": "epub",
|
|
"pronunciation": "ee-pub",
|
|
"normalized": "epub",
|
|
}
|
|
]
|
|
ctx = build_tts_context(
|
|
language=Language.EN_US,
|
|
pronunciation_overrides=overrides,
|
|
)
|
|
assert ctx.pronunciation_rules is not None
|
|
assert len(ctx.pronunciation_rules) >= 1
|
|
|
|
def test_manual_overrides_included(self):
|
|
overrides = [
|
|
{
|
|
"token": "gif",
|
|
"pronunciation": "jif",
|
|
"normalized": "gif",
|
|
}
|
|
]
|
|
ctx = build_tts_context(
|
|
language=Language.EN_US,
|
|
manual_overrides=overrides,
|
|
)
|
|
assert ctx.pronunciation_rules is not None
|
|
assert len(ctx.pronunciation_rules) >= 1
|
|
|
|
def test_manual_overrides_win_over_pronunciation(self):
|
|
pronunciation = [
|
|
{"token": "x", "pronunciation": "WRONG", "normalized": "x"}
|
|
]
|
|
manual = [
|
|
{"token": "x", "pronunciation": "RIGHT", "normalized": "x"}
|
|
]
|
|
ctx = build_tts_context(
|
|
language=Language.EN_US,
|
|
pronunciation_overrides=pronunciation,
|
|
manual_overrides=manual,
|
|
)
|
|
found_right = any(
|
|
r.get("replacement") == "RIGHT" for r in ctx.pronunciation_rules
|
|
)
|
|
found_wrong = any(
|
|
r.get("replacement") == "WRONG" for r in ctx.pronunciation_rules
|
|
)
|
|
assert found_right
|
|
assert not found_wrong
|
|
|
|
def test_heteronym_overrides_compiled(self):
|
|
overrides = [
|
|
{
|
|
"token": "read",
|
|
"pronunciation": "red",
|
|
"context": "past tense",
|
|
}
|
|
]
|
|
ctx = build_tts_context(
|
|
language=Language.EN_US,
|
|
heteronym_overrides=overrides,
|
|
)
|
|
assert ctx.heteronym_rules is not None
|
|
|
|
def test_usage_counter_passed_through(self):
|
|
counter = {}
|
|
ctx = build_tts_context(language=Language.EN_US, usage_counter=counter)
|
|
assert ctx.usage_counter is counter
|
|
|
|
def test_usage_counter_default_empty(self):
|
|
ctx = build_tts_context(language=Language.EN_US)
|
|
assert ctx.usage_counter == {}
|
|
|
|
def test_normalization_overrides_stored(self):
|
|
overrides = {"normalization_numbers": False}
|
|
ctx = build_tts_context(language=Language.EN_US, normalization_overrides=overrides)
|
|
assert ctx.normalization_overrides is overrides
|
|
|
|
def test_speakers_used_for_pronunciation(self):
|
|
speakers = {
|
|
"narrator": {
|
|
"token": "route",
|
|
"pronunciation": "root",
|
|
"resolved_voice": "M1",
|
|
}
|
|
}
|
|
ctx = build_tts_context(language=Language.EN_US, speakers=speakers)
|
|
assert ctx.pronunciation_rules is not None
|
|
assert len(ctx.pronunciation_rules) >= 1
|
|
|
|
def test_log_callback_called_on_num2words_missing(self):
|
|
logs = []
|
|
with patch("abogen.domain.normalization.get_runtime_settings", return_value={
|
|
"normalization_apostrophe_mode": "spacy",
|
|
"normalization_enabled": True,
|
|
"normalization_numbers": True,
|
|
}):
|
|
with patch("abogen.normalization_settings.build_apostrophe_config") as mock_cfg:
|
|
mock_cfg.return_value = MagicMock(convert_numbers=True)
|
|
with patch("builtins.__import__", side_effect=ImportError):
|
|
try:
|
|
build_tts_context(language=Language.EN_US, log_callback=lambda lvl, msg: logs.append((lvl, msg)))
|
|
except ImportError:
|
|
pass
|
|
# If num2words is missing and convert_numbers is True, a warning should be logged
|
|
# (depends on mock behavior, so just check no crash)
|
|
|
|
def test_llm_mode_raises_if_not_configured(self):
|
|
with patch("abogen.domain.normalization.get_runtime_settings", return_value={
|
|
"normalization_apostrophe_mode": "llm",
|
|
}):
|
|
with pytest.raises(RuntimeError, match="LLM"):
|
|
build_tts_context(language=Language.EN_US)
|
|
|
|
def test_dict_source_accepted(self):
|
|
"""merge_pronunciation_overrides should accept a dict."""
|
|
source = {
|
|
"pronunciation_overrides": [
|
|
{"token": "test", "pronunciation": "test-est", "normalized": "test"}
|
|
],
|
|
"manual_overrides": [],
|
|
"speakers": {},
|
|
"language": "a",
|
|
}
|
|
from abogen.domain.pronunciation import merge_pronunciation_overrides
|
|
result = merge_pronunciation_overrides(source)
|
|
assert isinstance(result, list)
|
|
assert len(result) >= 1
|