aboutsummaryrefslogtreecommitdiff
path: root/converter/converter.py
diff options
context:
space:
mode:
Diffstat (limited to 'converter/converter.py')
-rw-r--r--converter/converter.py44
1 files changed, 34 insertions, 10 deletions
diff --git a/converter/converter.py b/converter/converter.py
index a58fc33..6d09e09 100644
--- a/converter/converter.py
+++ b/converter/converter.py
@@ -11,7 +11,8 @@ from datetime import datetime
from pathlib import Path
from typing import Dict, List, Optional, Tuple
-from . import audio, chunking, config, extractors
+from . import audio, chunking, config, cover, extractors
+from .audio import TrackMeta
from .tts import QwenTTSClient
logger = logging.getLogger(__name__)
@@ -142,31 +143,48 @@ class AudiobookConverter:
audio.cleanup_chunks()
logger.info("Extracting text...")
- sections = extractors.extract_sections(file_path)
+ book = extractors.extract_book(file_path)
+ sections = book.sections
if not sections or all(not s.text.strip() for s in sections):
logger.error("No text extracted")
return False
stem = output_name or file_path.stem
+ # Cover art: generated once per book. Named with the chunk_
+ # prefix so cleanup_chunks() removes it with the other scratch
+ # files at the end of the book.
+ cover_path = cover.generate_cover(
+ book.title, config.CHUNKS_FOLDER / "chunk_cover.png")
+ if cover_path:
+ print(f"[INFO] Generated cover art for '{book.title}'")
+ meta = TrackMeta(title=book.title, artist=book.author, album=book.title)
+
# m4b is always a single file; multi-chapter books get embedded
# chapter markers so listeners can skip between chapters.
if self.output_format == "m4b":
if len(sections) > 1:
- return self._convert_m4b_with_chapters(sections, stem, start_time)
+ return self._convert_m4b_with_chapters(sections, stem, start_time,
+ meta=meta, cover=cover_path)
output_path = config.AUDIOBOOKS_FOLDER / f"{stem}.{self.output_format}"
- return self._convert_text(sections[0].text, output_path, start_time)
+ return self._convert_text(sections[0].text, output_path, start_time,
+ meta=meta, cover=cover_path)
if self.single_file or len(sections) == 1:
text = "\n\n".join(section.text for section in sections)
output_path = config.AUDIOBOOKS_FOLDER / f"{stem}.{self.output_format}"
- return self._convert_text(text, output_path, start_time)
+ return self._convert_text(text, output_path, start_time,
+ meta=meta, cover=cover_path)
success = True
for index, section in enumerate(sections, 1):
chapter_name = f"{stem}_{index:02d}_{self._sanitize_filename(section.title)}"
output_path = config.AUDIOBOOKS_FOLDER / f"{chapter_name}.{self.output_format}"
- success = self._convert_text(section.text, output_path, time.time()) and success
+ track_meta = meta._replace(
+ title=(section.title or "").strip() or f"Chapter {index}",
+ track=index, total_tracks=len(sections))
+ success = self._convert_text(section.text, output_path, time.time(),
+ meta=track_meta, cover=cover_path) and success
return success
except Exception as exc:
@@ -177,7 +195,9 @@ class AudiobookConverter:
# Always cleanup, even on failure or interrupt
audio.cleanup_chunks()
- def _convert_m4b_with_chapters(self, sections, stem: str, start_time: float) -> bool:
+ def _convert_m4b_with_chapters(self, sections, stem: str, start_time: float,
+ meta: Optional[TrackMeta] = None,
+ cover: Optional[Path] = None) -> bool:
"""Convert each chapter to audio, then assemble a single m4b with
embedded chapter markers.
@@ -208,7 +228,8 @@ class AudiobookConverter:
return False
output_path = config.AUDIOBOOKS_FOLDER / f"{stem}.{self.output_format}"
- if not audio.combine_chapters_to_m4b(chapter_files, titles, output_path, speed=self.speed):
+ if not audio.combine_chapters_to_m4b(chapter_files, titles, output_path, speed=self.speed,
+ meta=meta, cover=cover):
return False
duration = time.time() - start_time
logger.info("Conversion completed in %dm %ds: %s",
@@ -256,7 +277,9 @@ class AudiobookConverter:
def _convert_text(self, text: str, output_path: Path, start_time: float,
speed: Optional[float] = None,
output_format: Optional[str] = None,
- chapter: Optional[Tuple[int, int]] = None) -> bool:
+ chapter: Optional[Tuple[int, int]] = None,
+ meta: Optional[TrackMeta] = None,
+ cover: Optional[Path] = None) -> bool:
"""Chunk, synthesize, and assemble ``text`` into ``output_path``.
When ``chapter`` (a ``(number, total)`` pair) is given, the output is
@@ -303,7 +326,8 @@ class AudiobookConverter:
# Combine chunks (only the successful ones)
success = audio.combine_chunks(total_chunks, output_path, chunk_results=results,
speed=speed, output_format=output_format,
- intermediate=chapter is not None)
+ intermediate=chapter is not None,
+ meta=meta, cover=cover)
if success:
duration = time.time() - start_time