feat: EPUB3 finalizer in service, extraction in ConversionPlan

This commit is contained in:
Artem Akymenko
2026-07-24 19:17:37 +03:00
parent 4123cadd87
commit 3311bef2f7
3 changed files with 59 additions and 9 deletions
+2
View File
@@ -21,6 +21,7 @@ from typing import TYPE_CHECKING, Any, Dict, List, Optional
if TYPE_CHECKING: if TYPE_CHECKING:
from abogen.application.conversion_request import ConversionRequest from abogen.application.conversion_request import ConversionRequest
from abogen.text_extractor import ExtractionResult
@dataclass @dataclass
@@ -93,3 +94,4 @@ class ConversionPlan:
intro: Optional[IntroOutroSpec] = None intro: Optional[IntroOutroSpec] = None
outro: Optional[IntroOutroSpec] = None outro: Optional[IntroOutroSpec] = None
output_layout: Optional[OutputLayout] = None output_layout: Optional[OutputLayout] = None
extraction: Optional[ExtractionResult] = None
+13 -6
View File
@@ -50,7 +50,7 @@ def build_conversion_plan(request: ConversionRequest) -> ConversionPlan:
raise ValueError("No text content to convert") raise ValueError("No text content to convert")
# 2. Extract metadata # 2. Extract metadata
metadata = _extract_metadata(request) metadata, extraction = _extract_metadata(request)
# 3. Parse chapters # 3. Parse chapters
raw_chapters = _parse_chapters(source_text, request) raw_chapters = _parse_chapters(source_text, request)
@@ -74,6 +74,7 @@ def build_conversion_plan(request: ConversionRequest) -> ConversionPlan:
intro=intro, intro=intro,
outro=outro, outro=outro,
output_layout=output_layout, output_layout=output_layout,
extraction=extraction,
) )
@@ -111,10 +112,15 @@ def _extract_source_text(request: ConversionRequest) -> Optional[str]:
return text return text
def _extract_metadata(request: ConversionRequest) -> Dict[str, Any]: def _extract_metadata(
"""Extract metadata from source file.""" request: ConversionRequest,
) -> Tuple[Dict[str, Any], Optional[Any]]:
"""Extract metadata from source file.
Returns (metadata, extraction) tuple.
"""
if request.direct_text: if request.direct_text:
return dict(request.metadata_tags) return dict(request.metadata_tags), None
if request.source_path and request.source_path.exists(): if request.source_path and request.source_path.exists():
try: try:
@@ -123,11 +129,12 @@ def _extract_metadata(request: ConversionRequest) -> Dict[str, Any]:
) )
metadata = dict(extraction.metadata) if extraction.metadata else {} metadata = dict(extraction.metadata) if extraction.metadata else {}
except Exception: except Exception:
extraction = None
metadata = {} metadata = {}
metadata = merge_metadata(metadata, request.metadata_tags) metadata = merge_metadata(metadata, request.metadata_tags)
return metadata return metadata, extraction
return dict(request.metadata_tags) return dict(request.metadata_tags), None
def _parse_chapters( def _parse_chapters(
+44 -3
View File
@@ -79,8 +79,8 @@ def run_conversion(
tts_context=tts_context, tts_context=tts_context,
) )
# Stage 4: Finalize (m4b metadata embedding) # Stage 4: Finalize (m4b metadata embedding, EPUB3 generation)
_finalize(request, result, events) _finalize(request, result, plan, events)
events.log("Conversion complete") events.log("Conversion complete")
return result return result
@@ -93,11 +93,13 @@ def run_conversion(
def _finalize( def _finalize(
request: ConversionRequest, request: ConversionRequest,
result: ConversionResult, result: ConversionResult,
plan: ConversionPlan,
events: ConversionEvents, events: ConversionEvents,
) -> None: ) -> None:
"""Post-conversion finalization (m4b metadata embedding, etc.).""" """Post-conversion finalization (m4b metadata embedding, EPUB3 generation, etc.)."""
from abogen.domain.enums import OutputFormat from abogen.domain.enums import OutputFormat
# m4b metadata embedding
if ( if (
result.audio_path result.audio_path
and request.output_format == OutputFormat.M4B and request.output_format == OutputFormat.M4B
@@ -122,6 +124,45 @@ def _finalize(
events.log(f"Failed to embed m4b metadata: {exc}", level="error") events.log(f"Failed to embed m4b metadata: {exc}", level="error")
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_config = request.epub3_export
if epub3_config and plan.extraction:
audio_asset = result.audio_path
if not audio_asset and result.chapter_paths:
audio_asset = result.chapter_paths[0]
if audio_asset:
try:
from pathlib import Path
from abogen.epub3.exporter import build_epub3_package
epub_root = result.project_root or plan.output_layout.parent_dir
from abogen.domain.output_paths import build_output_path
epub_output_path = build_output_path(epub_root, request.original_filename, "epub")
events.log("Generating EPUB 3 package...")
epub_path = build_epub3_package(
output_path=epub_output_path,
book_id=epub3_config.book_id,
extraction=plan.extraction,
metadata_tags=result.metadata or {},
chapter_markers=result.chapter_markers or [],
chunk_markers=result.chunk_markers or [],
chunks=request.chapter_chunk.chunks if request.chapter_chunk else [],
audio_path=audio_asset,
speaker_mode=request.chapter_chunk.speaker_mode if request.chapter_chunk else "single",
cover_image_path=request.cover_image_path,
cover_image_mime=request.cover_image_mime,
)
result.epub_path = epub_path
result.artifacts["epub3"] = epub_path
events.log(f"EPUB 3 package created at {epub_path}")
except Exception as exc:
events.log(f"Failed to generate EPUB 3: {exc}", level="error")
else:
events.log("Skipped EPUB 3 generation: audio output unavailable.", level="warning")
def _prepare_tts_context( def _prepare_tts_context(
request: ConversionRequest, request: ConversionRequest,