From a1241ee9caa168886a7786f9a7e0e5e4aae50c90 Mon Sep 17 00:00:00 2001 From: Artem Akymenko Date: Thu, 23 Jul 2026 07:11:22 +0000 Subject: [PATCH] refactor: word substitution in planner, add planner tests --- abogen/application/conversion_planner.py | 25 +++++++++-- tests/test_conversion_planner.py | 56 +++++++++++++++++++++++- 2 files changed, 76 insertions(+), 5 deletions(-) diff --git a/abogen/application/conversion_planner.py b/abogen/application/conversion_planner.py index 7fb7a1b..140a8f9 100644 --- a/abogen/application/conversion_planner.py +++ b/abogen/application/conversion_planner.py @@ -82,16 +82,33 @@ def _extract_source_text(request: ConversionRequest) -> Optional[str]: from abogen.subtitle_utils import clean_text if request.direct_text: - return clean_text(request.direct_text) - if request.source_path and request.source_path.exists(): + text = clean_text(request.direct_text) + elif request.source_path and request.source_path.exists(): encoding = "utf-8" try: with open(request.source_path, "r", encoding=encoding, errors="replace") as f: text = f.read() except Exception: return None - return clean_text(text) - return None + text = clean_text(text) + else: + return None + + # Apply word substitutions if configured + if request.word_substitution: + from abogen.word_substitution import apply_word_substitutions + + ws = request.word_substitution + text = apply_word_substitutions( + text, + ws.substitutions_list, + ws.case_sensitive, + ws.replace_caps, + ws.replace_numerals, + ws.fix_punctuation, + ) + + return text def _extract_metadata(request: ConversionRequest) -> Dict[str, Any]: diff --git a/tests/test_conversion_planner.py b/tests/test_conversion_planner.py index 275875c..b9ae65d 100644 --- a/tests/test_conversion_planner.py +++ b/tests/test_conversion_planner.py @@ -25,7 +25,7 @@ from abogen.application.conversion_models import ( OutputLayout, SegmentPlan, ) -from abogen.application.conversion_config import ChapterChunkConfig +from abogen.application.conversion_config import ChapterChunkConfig, WordSubstitutionConfig from abogen.application.conversion_planner import build_conversion_plan from abogen.application.conversion_request import ConversionRequest @@ -203,6 +203,60 @@ class TestBuildConversionPlan: assert plan.chapters[0].segments[0].kind == "body" +class TestWordSubstitution: + """Tests for word substitution in the planner.""" + + def test_basic_substitution(self): + """Single word substitution is applied.""" + req = ConversionRequest( + direct_text="The quick brown fox", + voice="M1", + word_substitution=WordSubstitutionConfig( + substitutions_list="fox|cat", + ), + ) + plan = build_conversion_plan(req) + assert "cat" in plan.chapters[0].body_text + assert "fox" not in plan.chapters[0].body_text + + def test_multiple_substitutions(self): + """Multiple word substitutions are applied.""" + req = ConversionRequest( + direct_text="The quick brown fox jumps", + voice="M1", + word_substitution=WordSubstitutionConfig( + substitutions_list="fox|cat\nquick|slow", + ), + ) + plan = build_conversion_plan(req) + text = plan.chapters[0].body_text + assert "cat" in text + assert "slow" in text + + def test_substitution_preserves_chapter_markers(self): + """Chapter markers are preserved during substitution.""" + req = ConversionRequest( + direct_text="<>\nThe quick brown fox", + voice="M1", + word_substitution=WordSubstitutionConfig( + substitutions_list="fox|cat", + ), + ) + plan = build_conversion_plan(req) + assert len(plan.chapters) >= 1 + assert "cat" in plan.chapters[0].body_text + + def test_no_substitution_when_disabled(self): + """No substitution when word_substitution is None.""" + req = ConversionRequest( + direct_text="The quick brown fox", + voice="M1", + word_substitution=None, + ) + plan = build_conversion_plan(req) + assert "fox" in plan.chapters[0].body_text + + class TestPlannerWithFileSource: """Tests using actual file sources (not direct_text)."""