refactor: remove _prepare_tts_context, use build_tts_context directly

- Deleted _prepare_tts_context() (104 lines of duplicated logic)
- Replaced with direct build_tts_context() call in run_conversion()
- Fixed _finalize() to use raw fields (generate_epub3, epub3_book_id)
- Removed _MockJob antipattern
- Updated tests to use raw normalization_overrides field
- ConversionRequest uses raw fields instead of PronunciationConfig/Epub3ExportConfig
This commit is contained in:
Artem Akymenko
2026-07-27 12:19:59 +03:00
parent 0dc491e420
commit 625b6610e2
3 changed files with 32 additions and 131 deletions
+15 -9
View File
@@ -12,12 +12,10 @@ from __future__ import annotations
import dataclasses
from dataclasses import dataclass, field
from pathlib import Path
from typing import Any, Dict, Optional
from typing import Any, Dict, List, Optional
from abogen.application.conversion_config import (
ChapterChunkConfig,
Epub3ExportConfig,
PronunciationConfig,
SubtitleInputConfig,
WordSubstitutionConfig,
)
@@ -45,8 +43,11 @@ class ConversionRequest:
Only contains fields that describe the conversion task itself.
UI-only fields (display, logging, user prompts) stay in adapters.
Feature toggles use config objects: if the object is present,
the feature is enabled. No boolean flags needed.
Feature toggles use config objects or boolean flags:
- word_substitution, subtitle_input, chapter_chunk = config objects (None = disabled)
- generate_epub3 = boolean flag
Pronunciation overrides are raw data (lists of dicts), compiled by app layer.
Validation runs on creation via __post_init__:
- None values → replaced with field default (from declaration)
@@ -95,18 +96,23 @@ class ConversionRequest:
# --- Metadata ---
metadata_tags: Dict[str, Any] = field(default_factory=dict)
# --- Voice profiles (loaded by UI, used by app for voice resolution) ---
speakers: Dict[str, Any] = field(default_factory=dict)
# --- Pronunciation overrides (raw data, compiled by app layer) ---
pronunciation_overrides: List[Dict[str, Any]] = field(default_factory=list)
manual_overrides: List[Dict[str, Any]] = field(default_factory=list)
heteronym_overrides: List[Dict[str, Any]] = field(default_factory=list)
normalization_overrides: Optional[Dict[str, Any]] = None
# --- Artifacts ---
cover_image_path: Optional[Path] = None
cover_image_mime: Optional[str] = None
# --- Feature toggles ---
generate_epub3: bool = False
epub3_book_id: str = ""
# --- Feature configs (None = disabled) ---
word_substitution: Optional[WordSubstitutionConfig] = None
subtitle_input: Optional[SubtitleInputConfig] = None
epub3_export: Optional[Epub3ExportConfig] = None
pronunciation: Optional[PronunciationConfig] = None
chapter_chunk: Optional[ChapterChunkConfig] = None
def __post_init__(self) -> None:
+15 -112
View File
@@ -16,7 +16,7 @@ The service NEVER imports from PyQt or WebUI.
from __future__ import annotations
from collections import defaultdict
from typing import Dict, Optional
from typing import Dict
from abogen.application.conversion_executor import execute_conversion
from abogen.application.conversion_models import ConversionPlan
@@ -28,9 +28,7 @@ from abogen.application.conversion_ports import (
)
from abogen.application.conversion_request import ConversionRequest
from abogen.application.conversion_result import ConversionResult
from abogen.domain.enums import SubtitleMode
from abogen.domain.normalization import TTSContext
from abogen.domain.split_pattern import get_split_pattern
from abogen.domain.normalization import build_tts_context
def run_conversion(
@@ -65,7 +63,16 @@ def run_conversion(
# Stage 1: Prepare TTS context
events.log("Preparing conversion pipeline")
usage_counter: Dict[str, int] = defaultdict(int)
tts_context = _prepare_tts_context(request, events, usage_counter=usage_counter)
tts_context = build_tts_context(
language=request.language,
subtitle_mode=request.subtitle_mode.value if request.subtitle_mode else "Disabled",
pronunciation_overrides=request.pronunciation_overrides,
manual_overrides=request.manual_overrides,
heteronym_overrides=request.heteronym_overrides,
normalization_overrides=request.normalization_overrides,
usage_counter=usage_counter,
log_callback=lambda level, msg: events.log(msg, level=level),
)
# Stage 2: Build conversion plan
events.log("Building conversion plan")
@@ -129,8 +136,7 @@ def _finalize(
raise RuntimeError(f"Failed to embed m4b metadata: {exc}") from exc
# EPUB3 generation
epub3_config = request.epub3_export
if epub3_config and plan.extraction:
if request.generate_epub3 and plan.extraction:
audio_asset = result.audio_path
if not audio_asset and result.chapter_paths:
audio_asset = result.chapter_paths[0]
@@ -147,7 +153,7 @@ def _finalize(
events.log("Generating EPUB 3 package...")
epub_path = build_epub3_package(
output_path=epub_output_path,
book_id=epub3_config.book_id,
book_id=request.epub3_book_id,
extraction=plan.extraction,
metadata_tags=result.metadata or {},
chapter_markers=result.chapter_markers or [],
@@ -177,7 +183,7 @@ def _finalize(
chunk_level=request.chapter_chunk.chunk_level if request.chapter_chunk else None,
speaker_mode=request.chapter_chunk.speaker_mode if request.chapter_chunk else None,
speakers=request.chapter_chunk.speakers if request.chapter_chunk else None,
generate_epub3=bool(request.epub3_export),
generate_epub3=bool(request.generate_epub3),
)
metadata_dir = plan.output_layout.metadata_dir
@@ -200,107 +206,4 @@ def _finalize(
events.log(f"Failed to record override usage: {exc}", level="debug")
def _prepare_tts_context(
request: ConversionRequest,
events: ConversionEvents,
*,
usage_counter: Optional[Dict[str, int]] = None,
) -> TTSContext:
"""Prepare TTSContext with normalization settings.
This compiles pronunciation/heteronym rules and creates the
normalization context used during conversion.
Args:
request: Conversion request with override settings
events: For logging warnings about missing features
Returns:
TTSContext ready for text normalization
"""
from abogen.domain.normalization import (
build_apostrophe_config,
get_runtime_settings,
)
from abogen.normalization_settings import apply_overrides, build_llm_configuration
from abogen.domain.pronunciation import (
compile_heteronym_sentence_rules,
compile_pronunciation_rules,
merge_pronunciation_overrides,
)
# Get runtime normalization settings
normalization_settings = get_runtime_settings()
# Extract pronunciation config early (needed for normalization overrides)
pronunciation = request.pronunciation
# Apply per-job normalization overrides (same as runners)
job_overrides = pronunciation.normalization_overrides if pronunciation else None
if job_overrides:
normalization_settings = apply_overrides(normalization_settings, job_overrides)
# Build apostrophe config
apostrophe_config = build_apostrophe_config(
settings=normalization_settings,
)
# Validate LLM apostrophe mode
apostrophe_mode = str(normalization_settings.get("normalization_apostrophe_mode", "spacy")).lower()
if apostrophe_mode == "llm":
llm_config = build_llm_configuration(normalization_settings)
if not llm_config.is_configured():
raise RuntimeError(
"LLM-based apostrophe normalization is selected, but the LLM configuration is incomplete."
)
# Check for num2words availability
if apostrophe_config.convert_numbers:
try:
import num2words # noqa: F401
except ImportError:
events.log(
"Number normalization is enabled but 'num2words' library is not available. "
"Numbers will NOT be converted to words.",
level="warning",
)
# Compute split pattern
split_pattern = get_split_pattern(
request.language or Language.EN_US,
request.subtitle_mode or SubtitleMode.DISABLED,
)
# Merge pronunciation overrides (manual + pronunciation)
class _MockJob:
def __init__(self, pron):
self.pronunciation_overrides = pron.pronunciation_overrides if pron else []
self.manual_overrides = pron.manual_overrides if pron else []
self.heteronym_overrides = pron.heteronym_overrides if pron else []
merged_overrides = merge_pronunciation_overrides(_MockJob(pronunciation))
# Compile rules
pronunciation_rules = compile_pronunciation_rules(merged_overrides)
heteronym_overrides = pronunciation.heteronym_overrides if pronunciation else []
heteronym_rules = compile_heteronym_sentence_rules(heteronym_overrides)
if heteronym_rules:
events.log(
f"Applying {len(heteronym_rules)} heteronym override(s) during conversion.",
level="debug",
)
if pronunciation_rules:
events.log(
f"Applying {len(pronunciation_rules)} pronunciation override(s) during conversion.",
level="debug",
)
return TTSContext(
split_pattern=split_pattern,
pronunciation_rules=pronunciation_rules,
heteronym_rules=heteronym_rules,
normalization_overrides=pronunciation.normalization_overrides if pronunciation else None,
usage_counter=usage_counter or {},
)
-8
View File
@@ -250,16 +250,12 @@ class TestConversionService:
from abogen.application.conversion_service import run_conversion
with tempfile.TemporaryDirectory() as tmpdir:
from abogen.application.conversion_config import PronunciationConfig
req = ConversionRequest(
direct_text="Hello",
voice="M1",
save_mode="custom_folder",
output_folder=Path(tmpdir),
pronunciation=PronunciationConfig(
normalization_overrides={"normalization_numbers": False},
),
)
events = FakeEvents()
pipeline = FakePipelineProvider()
@@ -273,16 +269,12 @@ class TestConversionService:
from abogen.application.conversion_service import run_conversion
with tempfile.TemporaryDirectory() as tmpdir:
from abogen.application.conversion_config import PronunciationConfig
req = ConversionRequest(
direct_text="Hello",
voice="M1",
save_mode="custom_folder",
output_folder=Path(tmpdir),
pronunciation=PronunciationConfig(
normalization_overrides={"normalization_apostrophe_mode": "llm"},
),
)
events = FakeEvents()
pipeline = FakePipelineProvider()