Files
abogen/tests/test_pronunciation.py
T
Artem Akymenko b7a48e3204 refactor: extract pronunciation rules to domain/pronunciation.py
- Extract compile_pronunciation_rules, compile_heteronym_sentence_rules
- Extract apply_pronunciation_rules, apply_heteronym_sentence_rules
- Extract merge_pronunciation_overrides
- Add tests/test_pronunciation.py (31 tests)
- All tests pass
2026-07-14 18:27:22 +00:00

335 lines
12 KiB
Python

"""Tests for abogen.domain.pronunciation — compile/apply pronunciation rules."""
from __future__ import annotations
import re
import pytest
# ---------------------------------------------------------------------------
# We import the domain functions. The module must be created first.
# For now the tests are written against the expected public API so they can
# serve as the contract during extraction.
# ---------------------------------------------------------------------------
class TestCompilePronunciationRules:
"""compile_pronunciation_rules turns override dicts into regex-based rules."""
def test_empty_input(self):
from abogen.domain.pronunciation import compile_pronunciation_rules
assert compile_pronunciation_rules(None) == []
assert compile_pronunciation_rules([]) == []
def test_single_entry(self):
from abogen.domain.pronunciation import compile_pronunciation_rules
overrides = [{"token": "albeit", "pronunciation": "all be it"}]
rules = compile_pronunciation_rules(overrides)
assert len(rules) == 1
assert rules[0]["replacement"] == "all be it"
assert rules[0]["pattern"].search("albeit")
def test_skips_entries_without_pronunciation(self):
from abogen.domain.pronunciation import compile_pronunciation_rules
overrides = [{"token": "hello"}]
assert compile_pronunciation_rules(overrides) == []
def test_skips_entries_without_token(self):
from abogen.domain.pronunciation import compile_pronunciation_rules
overrides = [{"pronunciation": "foo"}]
assert compile_pronunciation_rules(overrides) == []
def test_deduplication_by_casefold(self):
from abogen.domain.pronunciation import compile_pronunciation_rules
overrides = [
{"token": "Albeit", "pronunciation": "all be it"},
{"token": "ALBEIT", "pronunciation": "all be it"},
]
rules = compile_pronunciation_rules(overrides)
assert len(rules) == 1
def test_longer_token_sorted_first(self):
from abogen.domain.pronunciation import compile_pronunciation_rules
overrides = [
{"token": "ice cream", "pronunciation": "ice cream"},
{"token": "ice", "pronunciation": "ais"},
]
rules = compile_pronunciation_rules(overrides)
assert len(rules) == 2
assert len(rules[0]["token"]) >= len(rules[1]["token"])
def test_normalized_fallback_to_entity_token(self):
from abogen.domain.pronunciation import compile_pronunciation_rules
overrides = [{"normalized": "USA", "pronunciation": "you ess ay"}]
rules = compile_pronunciation_rules(overrides)
assert len(rules) == 1
def test_pattern_is_case_insensitive(self):
from abogen.domain.pronunciation import compile_pronunciation_rules
overrides = [{"token": "hello", "pronunciation": "hi"}]
rules = compile_pronunciation_rules(overrides)
assert rules[0]["pattern"].search("Hello")
assert rules[0]["pattern"].search("HELLO")
def test_non_mapping_items_skipped(self):
from abogen.domain.pronunciation import compile_pronunciation_rules
overrides = ["bad", None, 42]
assert compile_pronunciation_rules(overrides) == []
class TestCompileHeteronymSentenceRules:
"""compile_heteronym_sentence_rules builds sentence-level replacements."""
def test_empty_input(self):
from abogen.domain.pronunciation import compile_heteronym_sentence_rules
assert compile_heteronym_sentence_rules(None) == []
assert compile_heteronym_sentence_rules([]) == []
def test_basic_replacement(self):
from abogen.domain.pronunciation import compile_heteronym_sentence_rules
overrides = [
{
"sentence": "I read the book",
"choice": "past",
"options": [
{"key": "present", "replacement_sentence": "I read the book"},
{"key": "past", "replacement_sentence": "I read the book"},
],
}
]
rules = compile_heteronym_sentence_rules(overrides)
assert len(rules) == 1
assert rules[0]["replacement"] == "I read the book"
def test_skips_without_sentence(self):
from abogen.domain.pronunciation import compile_heteronym_sentence_rules
overrides = [{"choice": "a", "options": []}]
assert compile_heteronym_sentence_rules(overrides) == []
def test_skips_without_choice(self):
from abogen.domain.pronunciation import compile_heteronym_sentence_rules
overrides = [{"sentence": "hello", "options": []}]
assert compile_heteronym_sentence_rules(overrides) == []
def test_skips_when_no_matching_option(self):
from abogen.domain.pronunciation import compile_heteronym_sentence_rules
overrides = [
{
"sentence": "I read the book",
"choice": "past",
"options": [{"key": "present", "replacement_sentence": "I read the book"}],
}
]
assert compile_heteronym_sentence_rules(overrides) == []
def test_deduplication(self):
from abogen.domain.pronunciation import compile_heteronym_sentence_rules
entry = {
"sentence": "I read the book",
"choice": "past",
"options": [{"key": "past", "replacement_sentence": "I red the book"}],
}
rules = compile_heteronym_sentence_rules([entry, entry])
assert len(rules) == 1
def test_longer_sentence_sorted_first(self):
from abogen.domain.pronunciation import compile_heteronym_sentence_rules
overrides = [
{
"sentence": "short",
"choice": "a",
"options": [{"key": "a", "replacement_sentence": "s"}],
},
{
"sentence": "a longer sentence here",
"choice": "b",
"options": [{"key": "b", "replacement_sentence": "l"}],
},
]
rules = compile_heteronym_sentence_rules(overrides)
assert len(rules[0]["pattern"].pattern) >= len(rules[1]["pattern"].pattern)
class TestApplyPronunciationRules:
"""apply_pronunciation_rules applies compiled token-level rules."""
def test_empty_text(self):
from abogen.domain.pronunciation import apply_pronunciation_rules
assert apply_pronunciation_rules("", []) == ""
def test_no_rules(self):
from abogen.domain.pronunciation import apply_pronunciation_rules
assert apply_pronunciation_rules("hello", []) == "hello"
def test_basic_replacement(self):
from abogen.domain.pronunciation import compile_pronunciation_rules, apply_pronunciation_rules
rules = compile_pronunciation_rules([{"token": "albeit", "pronunciation": "all be it"}])
result = apply_pronunciation_rules("albeit it was raining", rules)
assert result == "all be it it was raining"
def test_possessive_preserved(self):
from abogen.domain.pronunciation import compile_pronunciation_rules, apply_pronunciation_rules
rules = compile_pronunciation_rules([{"token": "dog", "pronunciation": "dawg"}])
result = apply_pronunciation_rules("the dog's bone", rules)
assert result == "the dawg's bone"
def test_usage_counter_increments(self):
from abogen.domain.pronunciation import compile_pronunciation_rules, apply_pronunciation_rules
rules = compile_pronunciation_rules([{"token": "hello", "pronunciation": "hi"}])
counter: dict[str, int] = {}
apply_pronunciation_rules("hello hello", rules, usage_counter=counter)
assert counter.get("hello", 0) == 2
def test_case_insensitive_match(self):
from abogen.domain.pronunciation import compile_pronunciation_rules, apply_pronunciation_rules
rules = compile_pronunciation_rules([{"token": "test", "pronunciation": "tst"}])
result = apply_pronunciation_rules("This is a Test", rules)
assert "tst" in result.lower()
class TestApplyHeteronymSentenceRules:
"""apply_heteronym_sentence_rules applies sentence-level replacements."""
def test_empty_text(self):
from abogen.domain.pronunciation import apply_heteronym_sentence_rules
assert apply_heteronym_sentence_rules("", []) == ""
def test_no_rules(self):
from abogen.domain.pronunciation import apply_heteronym_sentence_rules
assert apply_heteronym_sentence_rules("hello", []) == "hello"
def test_basic_replacement(self):
from abogen.domain.pronunciation import (
compile_heteronym_sentence_rules,
apply_heteronym_sentence_rules,
)
rules = compile_heteronym_sentence_rules(
[
{
"sentence": "I read the book",
"choice": "past",
"options": [{"key": "past", "replacement_sentence": "I read the book"}],
}
]
)
result = apply_heteronym_sentence_rules("I read the book.", rules)
assert result == "I read the book."
def test_no_match_left_unchanged(self):
from abogen.domain.pronunciation import (
compile_heteronym_sentence_rules,
apply_heteronym_sentence_rules,
)
rules = compile_heteronym_sentence_rules(
[
{
"sentence": "I read the book",
"choice": "past",
"options": [{"key": "past", "replacement_sentence": "I red the book"}],
}
]
)
result = apply_heteronym_sentence_rules("something else entirely", rules)
assert result == "something else entirely"
class TestMergePronunciationOverrides:
"""merge_pronunciation_overrides consolidates override sources."""
def test_empty_job(self):
from abogen.domain.pronunciation import merge_pronunciation_overrides
class FakeJob:
pronunciation_overrides = None
speakers = None
manual_overrides = None
language = "en"
result = merge_pronunciation_overrides(FakeJob())
assert result == []
def test_pronunciation_overrides_included(self):
from abogen.domain.pronunciation import merge_pronunciation_overrides
class FakeJob:
pronunciation_overrides = [
{"token": "hello", "pronunciation": "hi", "normalized": "hello"}
]
speakers = None
manual_overrides = None
language = "en"
result = merge_pronunciation_overrides(FakeJob())
assert len(result) == 1
assert result[0]["token"] == "hello"
assert result[0]["source"] == "pronunciation"
def test_manual_overrides_win(self):
from abogen.domain.pronunciation import merge_pronunciation_overrides
class FakeJob:
pronunciation_overrides = [
{"token": "hello", "pronunciation": "hi old", "normalized": "hello"}
]
speakers = None
manual_overrides = [
{"token": "hello", "pronunciation": "hi new", "normalized": "hello"}
]
language = "en"
result = merge_pronunciation_overrides(FakeJob())
assert len(result) == 1
assert result[0]["pronunciation"] == "hi new"
assert result[0]["source"] == "manual"
def test_speaker_entries_included(self):
from abogen.domain.pronunciation import merge_pronunciation_overrides
class FakeJob:
pronunciation_overrides = None
speakers = {"narrator": {"token": "war", "pronunciation": "wɔːr"}}
manual_overrides = None
language = "en"
result = merge_pronunciation_overrides(FakeJob())
assert len(result) == 1
assert result[0]["source"] == "speaker"
def test_skips_empty_tokens(self):
from abogen.domain.pronunciation import merge_pronunciation_overrides
class FakeJob:
pronunciation_overrides = [{"token": "", "pronunciation": "foo"}]
speakers = None
manual_overrides = None
language = "en"
result = merge_pronunciation_overrides(FakeJob())
assert result == []