diff --git a/abogen/application/conversion_models.py b/abogen/application/conversion_models.py index b0f9eae..ef46799 100644 --- a/abogen/application/conversion_models.py +++ b/abogen/application/conversion_models.py @@ -21,6 +21,7 @@ from typing import TYPE_CHECKING, Any, Dict, List, Optional if TYPE_CHECKING: from abogen.application.conversion_request import ConversionRequest + from abogen.text_extractor import ExtractionResult @dataclass @@ -93,3 +94,4 @@ class ConversionPlan: intro: Optional[IntroOutroSpec] = None outro: Optional[IntroOutroSpec] = None output_layout: Optional[OutputLayout] = None + extraction: Optional[ExtractionResult] = None diff --git a/abogen/application/conversion_planner.py b/abogen/application/conversion_planner.py index 140a8f9..ca6a098 100644 --- a/abogen/application/conversion_planner.py +++ b/abogen/application/conversion_planner.py @@ -50,7 +50,7 @@ def build_conversion_plan(request: ConversionRequest) -> ConversionPlan: raise ValueError("No text content to convert") # 2. Extract metadata - metadata = _extract_metadata(request) + metadata, extraction = _extract_metadata(request) # 3. Parse chapters raw_chapters = _parse_chapters(source_text, request) @@ -74,6 +74,7 @@ def build_conversion_plan(request: ConversionRequest) -> ConversionPlan: intro=intro, outro=outro, output_layout=output_layout, + extraction=extraction, ) @@ -111,10 +112,15 @@ def _extract_source_text(request: ConversionRequest) -> Optional[str]: return text -def _extract_metadata(request: ConversionRequest) -> Dict[str, Any]: - """Extract metadata from source file.""" +def _extract_metadata( + request: ConversionRequest, +) -> Tuple[Dict[str, Any], Optional[Any]]: + """Extract metadata from source file. + + Returns (metadata, extraction) tuple. + """ if request.direct_text: - return dict(request.metadata_tags) + return dict(request.metadata_tags), None if request.source_path and request.source_path.exists(): try: @@ -123,11 +129,12 @@ def _extract_metadata(request: ConversionRequest) -> Dict[str, Any]: ) metadata = dict(extraction.metadata) if extraction.metadata else {} except Exception: + extraction = None metadata = {} 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( diff --git a/abogen/application/conversion_service.py b/abogen/application/conversion_service.py index 12465f9..d77bf38 100644 --- a/abogen/application/conversion_service.py +++ b/abogen/application/conversion_service.py @@ -79,8 +79,8 @@ def run_conversion( tts_context=tts_context, ) - # Stage 4: Finalize (m4b metadata embedding) - _finalize(request, result, events) + # Stage 4: Finalize (m4b metadata embedding, EPUB3 generation) + _finalize(request, result, plan, events) events.log("Conversion complete") return result @@ -93,11 +93,13 @@ def run_conversion( def _finalize( request: ConversionRequest, result: ConversionResult, + plan: ConversionPlan, events: ConversionEvents, ) -> None: - """Post-conversion finalization (m4b metadata embedding, etc.).""" + """Post-conversion finalization (m4b metadata embedding, EPUB3 generation, etc.).""" from abogen.domain.enums import OutputFormat + # m4b metadata embedding if ( result.audio_path and request.output_format == OutputFormat.M4B @@ -122,6 +124,45 @@ def _finalize( events.log(f"Failed to embed m4b metadata: {exc}", level="error") 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( request: ConversionRequest,