aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md5
-rw-r--r--converter/audio.py17
-rw-r--r--converter/converter.py39
3 files changed, 44 insertions, 17 deletions
diff --git a/README.md b/README.md
index 9c52213..20ee20a 100644
--- a/README.md
+++ b/README.md
@@ -1,9 +1,6 @@
# Qwen3 Audiobook Converter
-Convert TXT, PDF, and EPUB files into audiobooks using the Qwen3-TTS voice model.
-
-Original project: [https://github.com/WhiskeyCoder/Qwen3-Audiobook-Converter](https://github.com/WhiskeyCoder/Qwen3-Audiobook-Converter
-). This repo just has minor fixes, flags, and documentation updates. It also splits the qwen3-tts server into two processes running models on different ports.
+Convert TXT, PDF, and EPUB files into audiobooks using the Qwen3-TTS voice model. This builds upon [WhiskeyCoder/Qwen3-Audiobook-Converter](https://github.com/WhiskeyCoder/Qwen3-Audiobook-Converter) adding m4b support, transcription and speed options, and clearer instructions. It also splits the qwen3-tts server into two processes running models on different ports.
## Overview
diff --git a/converter/audio.py b/converter/audio.py
index f1c508c..5a7ec35 100644
--- a/converter/audio.py
+++ b/converter/audio.py
@@ -210,14 +210,17 @@ def _collect_chunk_files(total_chunks: int,
def combine_chunks(total_chunks: int, output_path: Path,
chunk_results: Optional[Dict[int, Optional[Path]]] = None,
- speed: float = 1.0, output_format: str = "mp3") -> bool:
+ speed: float = 1.0, output_format: str = "mp3",
+ intermediate: bool = False) -> bool:
"""Combine audio chunks into the final audiobook using ffmpeg's concat demuxer.
``chunk_results`` maps chunk numbers to the audio file each chunk produced
(None for failed chunks); failed and missing chunks are skipped. When
``speed`` differs from 1.0, an additional speed-adjusted copy is written
next to the normal-speed file. Chunks are streamed by ffmpeg, so the whole
- book is never held in memory.
+ book is never held in memory. Set ``intermediate`` for scratch chapter
+ audio on the way to a larger output (e.g. a chaptered m4b) so save
+ messages don't present it as the final audiobook.
"""
if shutil.which("ffmpeg") is None or shutil.which("ffprobe") is None:
logger.error("ffmpeg and ffprobe are required to combine audio chunks (install ffmpeg)")
@@ -265,8 +268,14 @@ def combine_chunks(total_chunks: int, output_path: Path,
if not duration_ok:
return False
- logger.info("Audiobook saved: %s (%d/%d chunks)", output_path, len(chunk_files), total_chunks)
- print(f"[INFO] Saved audiobook: {output_path.name} ({len(chunk_files)}/{total_chunks} chunks)")
+ if intermediate:
+ logger.info("Chapter audio saved (intermediate): %s (%d/%d chunks)",
+ output_path, len(chunk_files), total_chunks)
+ print(f"[INFO] Saved chapter audio (intermediate): {output_path.name} "
+ f"({len(chunk_files)}/{total_chunks} chunks)")
+ else:
+ logger.info("Audiobook saved: %s (%d/%d chunks)", output_path, len(chunk_files), total_chunks)
+ print(f"[INFO] Saved audiobook: {output_path.name} ({len(chunk_files)}/{total_chunks} chunks)")
if speed_path is not None:
logger.info("Saved speed-adjusted audiobook (%gx): %s", speed, speed_path)
diff --git a/converter/converter.py b/converter/converter.py
index d5a1096..bc64f38 100644
--- a/converter/converter.py
+++ b/converter/converter.py
@@ -8,7 +8,7 @@ import traceback
from collections import Counter
from datetime import datetime
from pathlib import Path
-from typing import Dict, List, Optional
+from typing import Dict, List, Optional, Tuple
from . import audio, chunking, config, extractors
from .tts import QwenTTSClient
@@ -143,15 +143,22 @@ class AudiobookConverter:
"""
chapter_files = []
titles = []
+ total_chapters = len(sections)
for index, section in enumerate(sections, 1):
chapter_path = config.CHUNKS_FOLDER / f"chapter_{index:04d}.wav"
+ title = (section.title or "").strip() or f"Chapter {index}"
+ print(f"\n{'=' * 50}")
+ print(f"CHAPTER {index}/{total_chapters}: {title}")
+ print(f"{'=' * 50}")
+ logger.info("Converting chapter %d/%d: %s", index, total_chapters, title)
if not self._convert_text(section.text, chapter_path, time.time(),
- speed=1.0, output_format="wav"):
+ speed=1.0, output_format="wav",
+ chapter=(index, total_chapters)):
logger.warning("Skipping chapter %d (%s) due to conversion failure",
- index, section.title)
+ index, title)
continue
chapter_files.append(chapter_path)
- titles.append(section.title or f"Chapter {index}")
+ titles.append(title)
if not chapter_files:
logger.error("No chapters were successfully converted")
@@ -205,8 +212,14 @@ class AudiobookConverter:
def _convert_text(self, text: str, output_path: Path, start_time: float,
speed: Optional[float] = None,
- output_format: Optional[str] = None) -> bool:
- """Chunk, synthesize, and assemble ``text`` into ``output_path``."""
+ output_format: Optional[str] = None,
+ chapter: Optional[Tuple[int, int]] = None) -> bool:
+ """Chunk, synthesize, and assemble ``text`` into ``output_path``.
+
+ When ``chapter`` (a ``(number, total)`` pair) is given, the output is
+ an intermediate per-chapter file and progress messages are phrased
+ accordingly instead of implying the whole book is done.
+ """
if speed is None:
speed = self.speed
if output_format is None:
@@ -246,14 +259,22 @@ 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)
+ speed=speed, output_format=output_format,
+ intermediate=chapter is not None)
if success:
duration = time.time() - start_time
minutes = int(duration // 60)
seconds = int(duration % 60)
- logger.info("Conversion completed in %dm %ds: %s", minutes, seconds, output_path)
- print(f"[SUCCESS] Conversion completed in {minutes}m {seconds}s")
+ if chapter is not None:
+ logger.info("Chapter %d/%d converted in %dm %ds (%d/%d chunks)",
+ chapter[0], chapter[1], minutes, seconds,
+ successful_chunks, total_chunks)
+ print(f"[INFO] Chapter {chapter[0]}/{chapter[1]} converted "
+ f"({successful_chunks}/{total_chunks} chunks)")
+ else:
+ logger.info("Conversion completed in %dm %ds: %s", minutes, seconds, output_path)
+ print(f"[SUCCESS] Conversion completed in {minutes}m {seconds}s")
else:
logger.error("Failed to combine chunks into final audiobook")