feat: Add stubs for soundfile and static_ffmpeg modules to facilitate testing

This commit is contained in:
JB
2025-11-11 17:00:58 -08:00
parent 504b5ab5e5
commit 028384e6ee
9 changed files with 424 additions and 54 deletions
+46
View File
@@ -0,0 +1,46 @@
"""Test package initialization.
Provides lightweight fallbacks for optional dependencies so unit tests can run
without the full runtime stack.
"""
from __future__ import annotations
import sys
from types import ModuleType
def _soundfile_write_stub(file_obj, data, samplerate, format="WAV", **_kwargs): # pragma: no cover - stub
"""Minimal stand-in for soundfile.write used in tests.
The real library streams waveform data to disk. Our tests don't exercise
audio synthesis, so it's safe to accept the call and write nothing.
"""
if hasattr(file_obj, "write"):
try:
file_obj.write(b"")
except Exception:
# Ignore errors from exotic buffers; the real implementation would
# write binary samples, so a no-op keeps behavior predictable.
pass
if "soundfile" not in sys.modules: # pragma: no cover - import guard
stub = ModuleType("soundfile")
stub.write = _soundfile_write_stub # type: ignore[attr-defined]
sys.modules["soundfile"] = stub
def _static_ffmpeg_add_paths_stub(*_args, **_kwargs) -> None: # pragma: no cover - stub
"""Placeholder for static_ffmpeg.add_paths used in tests."""
if "static_ffmpeg" not in sys.modules: # pragma: no cover - import guard
ffmpeg_module = ModuleType("static_ffmpeg")
ffmpeg_module.add_paths = _static_ffmpeg_add_paths_stub # type: ignore[attr-defined]
ffmpeg_run = ModuleType("static_ffmpeg.run")
ffmpeg_run.LOCK_FILE = "" # type: ignore[attr-defined]
ffmpeg_module.run = ffmpeg_run # type: ignore[attr-defined]
sys.modules["static_ffmpeg"] = ffmpeg_module
sys.modules["static_ffmpeg.run"] = ffmpeg_run
+75 -29
View File
@@ -2,17 +2,33 @@ from __future__ import annotations
import pytest
from abogen.kokoro_text_normalization import normalize_roman_numeral_titles
from abogen.web.conversion_runner import _normalize_for_pipeline
from abogen.kokoro_text_normalization import (
DEFAULT_APOSTROPHE_CONFIG,
normalize_for_pipeline,
normalize_roman_numeral_titles,
)
from abogen.normalization_settings import (
apply_overrides as apply_normalization_overrides,
build_apostrophe_config,
get_runtime_settings,
)
from abogen.spacy_contraction_resolver import resolve_ambiguous_contractions
SPACY_RESOLVER_AVAILABLE = bool(resolve_ambiguous_contractions("It's been a long time."))
def _normalize_text(text: str, *, normalization_overrides: dict[str, object] | None = None) -> str:
runtime_settings = get_runtime_settings()
if normalization_overrides:
runtime_settings = apply_normalization_overrides(runtime_settings, normalization_overrides)
config = build_apostrophe_config(settings=runtime_settings, base=DEFAULT_APOSTROPHE_CONFIG)
return normalize_for_pipeline(text, config=config, settings=runtime_settings)
def test_title_abbreviations_are_expanded():
text = "Dr. Watson met Mr. Holmes and Ms. Hudson."
normalized = _normalize_for_pipeline(text)
normalized = _normalize_text(text)
assert "Doctor" in normalized
assert "Mister" in normalized
assert "Miz" in normalized
@@ -20,25 +36,25 @@ def test_title_abbreviations_are_expanded():
def test_suffix_abbreviations_are_expanded_with_case_preserved():
text = "John Doe Jr. spoke to JANE DOE SR. about the estate."
normalized = _normalize_for_pipeline(text)
normalized = _normalize_text(text)
assert "John Doe Junior" in normalized
assert "JANE DOE SENIOR" in normalized
def test_missing_terminal_punctuation_is_added():
normalized = _normalize_for_pipeline("Chapter 1")
normalized = _normalize_text("Chapter 1")
assert normalized.endswith(".")
def test_terminal_punctuation_respects_closing_quotes():
normalized = _normalize_for_pipeline('"Chapter 1"')
normalized = _normalize_text('"Chapter 1"')
compact = normalized.replace(" ", "")
assert compact.endswith('."')
def test_normalization_preserves_spacing_around_quotes_and_hyphen():
sample = "“Still,” said Château-Renaud, “Dr. dAvrigny, who attends my mother, declares he is in despair about it."
normalized = _normalize_for_pipeline(sample)
normalized = _normalize_text(sample)
assert normalized.startswith(
"“Still,” said Château-Renaud, “Doctor d'Avrigny, who attends my mother, declares he is in despair about it."
@@ -72,70 +88,100 @@ def test_normalize_roman_titles_preserves_separators() -> None:
def test_grouped_numbers_are_spelled_out() -> None:
normalized = _normalize_for_pipeline("The vault holds 35,000 credits")
normalized = _normalize_text("The vault holds 35,000 credits")
assert "thirty-five thousand" in normalized.lower()
def test_numeric_ranges_are_spoken_with_to() -> None:
normalized = _normalize_for_pipeline("Chapters 1-3")
normalized = _normalize_text("Chapters 1-3")
assert "one to three" in normalized.lower()
def test_simple_fractions_are_spoken() -> None:
normalized = _normalize_for_pipeline("Add 1/2 cup of sugar")
normalized = _normalize_text("Add 1/2 cup of sugar")
assert "one half" in normalized.lower()
def test_plain_numbers_are_spelled_out() -> None:
normalized = _normalize_for_pipeline("He rolled a 42.")
normalized = _normalize_text("He rolled a 42.")
assert "forty-two" in normalized.lower()
def test_decimal_numbers_include_point() -> None:
normalized = _normalize_for_pipeline("Book 4.5 of the series.")
normalized = _normalize_text("Book 4.5 of the series.")
assert "four point five" in normalized.lower()
def test_space_separated_numbers_become_ranges() -> None:
normalized = _normalize_for_pipeline("Read pages 12 14 tonight.")
normalized = _normalize_text("Read pages 12 14 tonight.")
assert "pages twelve to fourteen" in normalized.lower()
def test_year_like_numbers_use_common_pronunciation() -> None:
normalized = _normalize_for_pipeline("In 1924 the journey began")
normalized = _normalize_text("In 1924 the journey began")
folded = normalized.lower().replace("-", " ")
assert "nineteen twenty four" in folded
def test_early_century_years_use_hundred_format() -> None:
normalized = _normalize_for_pipeline("In 1204 the city fell")
normalized = _normalize_text("In 1204 the city fell")
assert "twelve hundred" in normalized.lower()
assert "oh four" in normalized.lower()
def test_roman_numerals_in_titles_are_converted() -> None:
normalized = _normalize_for_pipeline("Chapter IV begins now")
normalized = _normalize_text("Chapter IV begins now")
assert "chapter four" in normalized.lower()
def test_roman_numeral_suffixes_use_ordinals() -> None:
normalized = _normalize_for_pipeline("Bob Smith II arrived late")
normalized = _normalize_text("Bob Smith II arrived late")
assert "bob smith the second" in normalized.lower()
def test_lowercase_roman_after_part_converts_to_cardinal() -> None:
normalized = _normalize_text("We studied part iii of the manuscript.")
assert "part three" in normalized.lower()
def test_hyphenated_phase_with_roman_is_converted() -> None:
normalized = _normalize_text("They executed phase-IV without delay.")
assert "phase four" in normalized.lower()
def test_all_caps_quotes_are_sentence_cased() -> None:
normalized = _normalize_text('"THIS IS A TEST."')
cleaned = normalized.replace('" ', '"')
assert '"This is a test."' in cleaned
def test_caps_quote_preserves_acronyms() -> None:
normalized = _normalize_text("“THE NASA TEAM ARRIVED.”")
assert "“The NASA team arrived.”" in normalized
def test_caps_quote_normalization_respects_override() -> None:
normalized = _normalize_text(
'"KEEP SHOUTING."',
normalization_overrides={"normalization_caps_quotes": False},
)
cleaned = normalized.replace('" ', '"')
assert '"KEEP SHOUTING."' in cleaned
def test_recent_years_split_twenty_style() -> None:
normalized = _normalize_for_pipeline("In 2025 we planned ahead")
normalized = _normalize_text("In 2025 we planned ahead")
folded = normalized.lower().replace("-", " ")
assert "twenty twenty five" in folded
def test_two_thousands_use_two_thousand_prefix() -> None:
normalized = _normalize_for_pipeline("In 2005 we celebrated")
normalized = _normalize_text("In 2005 we celebrated")
assert "two thousand five" in normalized.lower()
def test_year_style_can_be_disabled() -> None:
normalized = _normalize_for_pipeline(
normalized = _normalize_text(
"In 2025 we planned ahead",
normalization_overrides={"normalization_numbers_year_style": "off"},
)
@@ -144,7 +190,7 @@ def test_year_style_can_be_disabled() -> None:
def test_contractions_can_be_kept_when_override_disabled() -> None:
normalized = _normalize_for_pipeline(
normalized = _normalize_text(
"It's a good day.",
normalization_overrides={"normalization_apostrophes_contractions": False},
)
@@ -152,7 +198,7 @@ def test_contractions_can_be_kept_when_override_disabled() -> None:
def test_sibilant_possessives_remain_when_marking_disabled() -> None:
normalized = _normalize_for_pipeline(
normalized = _normalize_text(
"The boss's chair wobbled.",
normalization_overrides={"normalization_apostrophes_sibilant_possessives": False},
)
@@ -161,7 +207,7 @@ def test_sibilant_possessives_remain_when_marking_disabled() -> None:
def test_decades_can_skip_expansion_when_disabled() -> None:
normalized = _normalize_for_pipeline(
normalized = _normalize_text(
"Classic hits from the '90s filled the hall.",
normalization_overrides={"normalization_apostrophes_decades": False},
)
@@ -170,32 +216,32 @@ def test_decades_can_skip_expansion_when_disabled() -> None:
@pytest.mark.skipif(not SPACY_RESOLVER_AVAILABLE, reason="spaCy model unavailable")
def test_spacy_disambiguates_it_has_from_context() -> None:
normalized = _normalize_for_pipeline("It's been a long time.")
normalized = _normalize_text("It's been a long time.")
assert "It has been a long time." == normalized
@pytest.mark.skipif(not SPACY_RESOLVER_AVAILABLE, reason="spaCy model unavailable")
def test_spacy_disambiguates_it_is_from_context() -> None:
normalized = _normalize_for_pipeline("It's cold outside.")
normalized = _normalize_text("It's cold outside.")
assert "It is cold outside." == normalized
@pytest.mark.skipif(not SPACY_RESOLVER_AVAILABLE, reason="spaCy model unavailable")
def test_spacy_disambiguates_she_had() -> None:
normalized = _normalize_for_pipeline("She'd left before dawn.")
normalized = _normalize_text("She'd left before dawn.")
assert "She had left before dawn." == normalized
@pytest.mark.skipif(not SPACY_RESOLVER_AVAILABLE, reason="spaCy model unavailable")
def test_spacy_disambiguates_she_would() -> None:
normalized = _normalize_for_pipeline("She'd go if invited.")
normalized = _normalize_text("She'd go if invited.")
assert "She would go if invited." == normalized
@pytest.mark.skipif(not SPACY_RESOLVER_AVAILABLE, reason="spaCy model unavailable")
def test_sample_sentence_handles_complex_contractions() -> None:
sample = "I've heard the captain'll arrive by dusk, but they'd said the same yesterday."
normalized = _normalize_for_pipeline(sample)
normalized = _normalize_text(sample)
assert (
"I have heard the captain will arrive by dusk, but they had said the same yesterday." == normalized
)
@@ -203,7 +249,7 @@ def test_sample_sentence_handles_complex_contractions() -> None:
def test_modal_will_contractions_can_be_disabled() -> None:
sample = "The captain'll arrive at dawn."
normalized = _normalize_for_pipeline(
normalized = _normalize_text(
sample,
normalization_overrides={"normalization_contraction_modal_will": False},
)