Files
VoiceStudio/tests/test_pronunciation_inert_1949.py
Palash DebnathandClaude Opus 5 de962d164a fix(pronunciation): say when an IPA/CMU entry is stored but not applied
Closes #1949.

Settings offers three notations. Only Respelling substitutes text today; IPA
and CMU rows save cleanly, are validated, get a badge and can be toggled on,
then get dropped before term matching and are never read again.

That much is Phase 1 behaving as designed. The defect is that it was INVISIBLE:
"Test a sentence" answered "No entries match — spoken as written" for a term
that does match. Not a degraded answer, a wrong one — and it sent the user off
to re-type an entry that was already correct, or to convert it to Respelling,
where a phoneme string is then read as graphemes.

docs/specs/01-expressive-tts.md asked for exactly the opposite: such entries
"passed through and flagged 'phoneme not honored on this engine' (parity-rule:
visible degradation)". That flag was never implemented. This is it.

The dry run reports inert entries separately, and the panel names them. The
substitution path is deliberately untouched — this does NOT start feeding raw
phoneme strings into the grapheme stream, which is the thing Phase 1 refuses on
purpose, and a test pins that it still refuses.

Not Phase 2. Lowering IPA/CMU to engine markup is a real feature per engine and
stays open; what changes here is that the gap is now honest rather than silent.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017ypcgSsh5j2PEonSJiAU1S
2026-09-09 22:11:05 -07:00

78 lines
2.9 KiB
Python

"""#1949 — an entry that cannot be applied must say so, not look like no match.
Settings offers three notations: Respelling, IPA, CMU. Only respelling
substitutes text today. IPA and CMU rows save cleanly, are validated, get a
badge and can be toggled on — then get dropped before term matching, and
nothing downstream reads them.
That is Phase 1 behaving as designed. The defect is that it was INVISIBLE:
"Test a sentence" answered "No entries match; spoken as written" for a term
that does match. Not a degraded answer, a wrong one — and it sent the user off
to re-type an entry that was already correct.
docs/specs/01-expressive-tts.md asked for the opposite: such entries "passed
through and flagged 'phoneme not honored on this engine' (parity-rule: visible
degradation)". These tests pin that flag.
"""
import pytest
from services.pronunciation import (
apply_pronunciation,
entries_for_language,
inert_entries_for_language,
)
def _row(term, replacement, etype, language="*", enabled=1):
return {
"term": term,
"replacement": replacement,
"type": etype,
"language": language,
"enabled": enabled,
}
def test_a_cmu_entry_is_reported_as_inert():
rows = [_row("Alphan", "AA0 L P HH AA0 N", "cmu")]
assert entries_for_language(rows, "tr") == {}
assert inert_entries_for_language(rows, "tr") == [{"term": "Alphan", "type": "cmu"}]
def test_an_ipa_entry_is_reported_as_inert():
rows = [_row("gif", "dʒɪf", "ipa")]
assert inert_entries_for_language(rows, "en") == [{"term": "gif", "type": "ipa"}]
def test_respelling_is_not_reported_because_it_works():
rows = [_row("gif", "jiff", "respelling")]
assert inert_entries_for_language(rows, "en") == []
assert apply_pronunciation("a gif", rows, "en") == "a jiff"
def test_a_disabled_entry_is_not_reported():
# The user turned it off; saying it was ignored would be noise.
rows = [_row("gif", "dʒɪf", "ipa", enabled=0)]
assert inert_entries_for_language(rows, "en") == []
def test_language_scoping_matches_the_applied_path():
# An entry scoped to another language is not "ignored", it simply does not
# apply here — reporting it would be misleading in the other direction.
rows = [_row("gif", "dʒɪf", "ipa", language="de")]
assert inert_entries_for_language(rows, "en") == []
assert inert_entries_for_language(rows, "de") == [{"term": "gif", "type": "ipa"}]
def test_the_substitution_itself_is_unchanged():
# This reports; it must not start feeding raw phoneme strings into the
# grapheme stream, which is the thing Phase 1 deliberately refuses.
rows = [_row("Alphan", "AA0 L P HH AA0 N", "cmu")]
assert apply_pronunciation("Merhaba, ben Alphan.", rows, "tr") == "Merhaba, ben Alphan."
@pytest.mark.parametrize("etype", ["CMU", "Ipa", " ipa "])
def test_type_matching_tolerates_casing_and_padding(etype):
rows = [_row("x", "y", etype)]
assert len(inert_entries_for_language(rows, "en")) == 1