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
|
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:
|
||||||
|
|||||||
@@ -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 {},
|
|
||||||
)
|
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user