mirror of
https://github.com/denizsafak/abogen.git
synced 2026-09-20 11:40:57 +02:00
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:
@@ -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:
|
||||
|
||||
@@ -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 {},
|
||||
)
|
||||
|
||||
@@ -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},
|
||||
),
|
||||
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"},
|
||||
),
|
||||
normalization_overrides={"normalization_apostrophe_mode": "llm"},
|
||||
)
|
||||
events = FakeEvents()
|
||||
pipeline = FakePipelineProvider()
|
||||
|
||||
Reference in New Issue
Block a user