mirror of
https://github.com/denizsafak/abogen.git
synced 2026-09-20 11:40:57 +02:00
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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user