refactor(domain): add prepare_text_for_tts — unified normalization pipeline

New function chains all three normalization stages:
  1. Heteronym sentence rules (context-dependent pronunciation)
  2. Pronunciation rules (token-level replacements)
  3. Pipeline normalization (apostrophe, LLM)

This is the single entry point that both Web UI and PyQt should call
before TTS synthesis. Currently only Web UI uses it; PyQt has NO
normalization — this unlocks that capability.

Updated conversion_runner.emit_text to use the new function.
1038 tests pass.
This commit is contained in:
Artem Akymenko
2026-07-16 08:53:15 +00:00
parent 832e2c5197
commit 2228f37c06
3 changed files with 221 additions and 14 deletions
+147
View File
@@ -0,0 +1,147 @@
"""Tests for domain/normalization.py — prepare_text_for_tts."""
import pytest
from unittest.mock import patch, MagicMock
from abogen.domain.normalization import prepare_text_for_tts, normalize_text_for_pipeline
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)