refactor: heading transforms + dedup moved to shared layer

- conversion_planner.py: caps normalization in _build_chapters()
- conversion_executor.py: heading dedup + state machine via headings_equivalent()
- Fixed seg_start_time → chapter_body_start bug in executor
- Removed getattr fallback defaults in both adapters
- Added 4 tests for caps normalization and heading dedup
- Updated ARCHITECTURE_REFACTOR_PLAN.md with deferred WebUI cleanup
This commit is contained in:
Artem Akymenko
2026-07-26 11:41:59 +03:00
parent f516cf1985
commit 0b953d48e8
6 changed files with 180 additions and 4 deletions
+111
View File
@@ -511,3 +511,114 @@ class TestExecuteConversion:
assert result.metadata["title"] == "Test Book"
assert result.metadata["author"] == "Author"
class TestHeadingDedup:
"""Tests for heading dedup in executor."""
def test_heading_dedup_strips_matching_first_line(self):
"""When first segment matches heading, it should be stripped."""
with tempfile.TemporaryDirectory() as tmpdir:
req = ConversionRequest(
direct_text="Hello",
voice="M1",
auto_prefix_chapter_titles=True,
)
# Simulate: heading = "Chapter 1", first segment = "Chapter 1: The Beginning"
# headings_equivalent should match these
plan = ConversionPlan(
request=req,
metadata={},
chapters=[
ChapterPlan(
index=1,
title="Chapter 1",
original_title="Chapter 1",
body_text="Chapter 1: The Beginning\nBody text here",
segments=[
SegmentPlan(
text="Chapter 1: The Beginning",
voice_spec="M1",
kind="body",
source="chapter",
),
SegmentPlan(
text="Body text here",
voice_spec="M1",
kind="body",
source="chapter",
),
],
voice_spec="M1",
)
],
output_layout=OutputLayout(
parent_dir=Path(tmpdir),
audio_dir=Path(tmpdir),
),
)
events = FakeEvents()
pipeline = FakePipelineProvider()
resolver = FakeVoiceResolver()
tts_context = TTSContext()
result = execute_conversion(
plan, events, pipeline, resolver, tts_context
)
# The executor should have logged the heading
log_messages = [m for m, _ in events.logs if "Title:" in m]
assert len(log_messages) >= 1
def test_heading_dedup_no_match_preserves_all(self):
"""When first segment doesn't match heading, nothing is stripped."""
with tempfile.TemporaryDirectory() as tmpdir:
req = ConversionRequest(
direct_text="Hello",
voice="M1",
auto_prefix_chapter_titles=True,
)
plan = ConversionPlan(
request=req,
metadata={},
chapters=[
ChapterPlan(
index=1,
title="Chapter 1",
original_title="Chapter 1",
body_text="Completely different text\nMore text",
segments=[
SegmentPlan(
text="Completely different text",
voice_spec="M1",
kind="body",
source="chapter",
),
SegmentPlan(
text="More text",
voice_spec="M1",
kind="body",
source="chapter",
),
],
voice_spec="M1",
)
],
output_layout=OutputLayout(
parent_dir=Path(tmpdir),
audio_dir=Path(tmpdir),
),
)
events = FakeEvents()
pipeline = FakePipelineProvider()
resolver = FakeVoiceResolver()
tts_context = TTSContext()
result = execute_conversion(
plan, events, pipeline, resolver, tts_context
)
# Both segments should be synthesized (heading + 2 body segments)
assert result.total_segments >= 2
+28
View File
@@ -641,3 +641,31 @@ class TestFeatureParity:
if output_format.lower() == "m4b":
merge_chapters_at_end = True
assert merge_chapters_at_end is True
class TestCapsNormalization:
"""Tests for caps normalization in planner."""
def test_caps_normalization_applied_when_enabled(self):
"""When normalize_chapter_opening_caps=True, body text is normalized."""
req = ConversionRequest(
direct_text="<<CHAPTER_MARKER:Chapter 1>>\nALL CAPS OPENING TEXT here",
voice="M1",
normalize_chapter_opening_caps=True,
)
plan = build_conversion_plan(req)
body = plan.chapters[0].body_text
# ALL CAPS should be normalized to Title Case
assert body != "ALL CAPS OPENING TEXT here"
assert "ALL CAPS" not in body
def test_caps_normalization_skipped_when_disabled(self):
"""When normalize_chapter_opening_caps=False, body text is unchanged."""
req = ConversionRequest(
direct_text="<<CHAPTER_MARKER:Chapter 1>>\nALL CAPS OPENING TEXT here",
voice="M1",
normalize_chapter_opening_caps=False,
)
plan = build_conversion_plan(req)
body = plan.chapters[0].body_text
assert "ALL CAPS OPENING TEXT" in body