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 import dataclasses
from dataclasses import dataclass, field from dataclasses import dataclass, field
from pathlib import Path from pathlib import Path
from typing import Any, Dict, Optional from typing import Any, Dict, List, Optional
from abogen.application.conversion_config import ( from abogen.application.conversion_config import (
ChapterChunkConfig, ChapterChunkConfig,
Epub3ExportConfig,
PronunciationConfig,
SubtitleInputConfig, SubtitleInputConfig,
WordSubstitutionConfig, WordSubstitutionConfig,
) )
@@ -45,8 +43,11 @@ class ConversionRequest:
Only contains fields that describe the conversion task itself. Only contains fields that describe the conversion task itself.
UI-only fields (display, logging, user prompts) stay in adapters. UI-only fields (display, logging, user prompts) stay in adapters.
Feature toggles use config objects: if the object is present, Feature toggles use config objects or boolean flags:
the feature is enabled. No boolean flags needed. - 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__: Validation runs on creation via __post_init__:
- None values → replaced with field default (from declaration) - None values → replaced with field default (from declaration)
@@ -95,18 +96,23 @@ class ConversionRequest:
# --- Metadata --- # --- Metadata ---
metadata_tags: Dict[str, Any] = field(default_factory=dict) metadata_tags: Dict[str, Any] = field(default_factory=dict)
# --- Voice profiles (loaded by UI, used by app for voice resolution) --- # --- Pronunciation overrides (raw data, compiled by app layer) ---
speakers: Dict[str, Any] = field(default_factory=dict) 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 --- # --- Artifacts ---
cover_image_path: Optional[Path] = None cover_image_path: Optional[Path] = None
cover_image_mime: Optional[str] = None cover_image_mime: Optional[str] = None
# --- Feature toggles ---
generate_epub3: bool = False
epub3_book_id: str = ""
# --- Feature configs (None = disabled) --- # --- Feature configs (None = disabled) ---
word_substitution: Optional[WordSubstitutionConfig] = None word_substitution: Optional[WordSubstitutionConfig] = None
subtitle_input: Optional[SubtitleInputConfig] = None subtitle_input: Optional[SubtitleInputConfig] = None
epub3_export: Optional[Epub3ExportConfig] = None
pronunciation: Optional[PronunciationConfig] = None
chapter_chunk: Optional[ChapterChunkConfig] = None chapter_chunk: Optional[ChapterChunkConfig] = None
def __post_init__(self) -> 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 __future__ import annotations
from collections import defaultdict 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_executor import execute_conversion
from abogen.application.conversion_models import ConversionPlan 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_request import ConversionRequest
from abogen.application.conversion_result import ConversionResult from abogen.application.conversion_result import ConversionResult
from abogen.domain.enums import SubtitleMode from abogen.domain.normalization import build_tts_context
from abogen.domain.normalization import TTSContext
from abogen.domain.split_pattern import get_split_pattern
def run_conversion( def run_conversion(
@@ -65,7 +63,16 @@ def run_conversion(
# Stage 1: Prepare TTS context # Stage 1: Prepare TTS context
events.log("Preparing conversion pipeline") events.log("Preparing conversion pipeline")
usage_counter: Dict[str, int] = defaultdict(int) 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 # Stage 2: Build conversion plan
events.log("Building conversion plan") events.log("Building conversion plan")
@@ -129,8 +136,7 @@ def _finalize(
raise RuntimeError(f"Failed to embed m4b metadata: {exc}") from exc raise RuntimeError(f"Failed to embed m4b metadata: {exc}") from exc
# EPUB3 generation # EPUB3 generation
epub3_config = request.epub3_export if request.generate_epub3 and plan.extraction:
if epub3_config and plan.extraction:
audio_asset = result.audio_path audio_asset = result.audio_path
if not audio_asset and result.chapter_paths: if not audio_asset and result.chapter_paths:
audio_asset = result.chapter_paths[0] audio_asset = result.chapter_paths[0]
@@ -147,7 +153,7 @@ def _finalize(
events.log("Generating EPUB 3 package...") events.log("Generating EPUB 3 package...")
epub_path = build_epub3_package( epub_path = build_epub3_package(
output_path=epub_output_path, output_path=epub_output_path,
book_id=epub3_config.book_id, book_id=request.epub3_book_id,
extraction=plan.extraction, extraction=plan.extraction,
metadata_tags=result.metadata or {}, metadata_tags=result.metadata or {},
chapter_markers=result.chapter_markers 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, 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, speaker_mode=request.chapter_chunk.speaker_mode if request.chapter_chunk else None,
speakers=request.chapter_chunk.speakers 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 metadata_dir = plan.output_layout.metadata_dir
@@ -200,107 +206,4 @@ def _finalize(
events.log(f"Failed to record override usage: {exc}", level="debug") 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 from abogen.application.conversion_service import run_conversion
with tempfile.TemporaryDirectory() as tmpdir: with tempfile.TemporaryDirectory() as tmpdir:
from abogen.application.conversion_config import PronunciationConfig
req = ConversionRequest( req = ConversionRequest(
direct_text="Hello", direct_text="Hello",
voice="M1", voice="M1",
save_mode="custom_folder", save_mode="custom_folder",
output_folder=Path(tmpdir), output_folder=Path(tmpdir),
pronunciation=PronunciationConfig(
normalization_overrides={"normalization_numbers": False}, normalization_overrides={"normalization_numbers": False},
),
) )
events = FakeEvents() events = FakeEvents()
pipeline = FakePipelineProvider() pipeline = FakePipelineProvider()
@@ -273,16 +269,12 @@ class TestConversionService:
from abogen.application.conversion_service import run_conversion from abogen.application.conversion_service import run_conversion
with tempfile.TemporaryDirectory() as tmpdir: with tempfile.TemporaryDirectory() as tmpdir:
from abogen.application.conversion_config import PronunciationConfig
req = ConversionRequest( req = ConversionRequest(
direct_text="Hello", direct_text="Hello",
voice="M1", voice="M1",
save_mode="custom_folder", save_mode="custom_folder",
output_folder=Path(tmpdir), output_folder=Path(tmpdir),
pronunciation=PronunciationConfig(
normalization_overrides={"normalization_apostrophe_mode": "llm"}, normalization_overrides={"normalization_apostrophe_mode": "llm"},
),
) )
events = FakeEvents() events = FakeEvents()
pipeline = FakePipelineProvider() pipeline = FakePipelineProvider()