aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorhistoria <historiavg@proton.me>2026-08-17 19:39:31 -0400
committerhistoria <historiavg@proton.me>2026-08-17 19:39:31 -0400
commit68ee76514a98169a5e7a075648b70ab40417fbdf (patch)
tree906f9fb856eb1e66a26af0b57f5d65462b7977b1
parentb80fa9db6bab6cdb2856874b606a93149cfc1af2 (diff)
downloadtts-audiobook-generator-68ee76514a98169a5e7a075648b70ab40417fbdf.tar.gz
fix(converter): make m4b a single file with embedded chapters
-rw-r--r--README.md51
-rw-r--r--audiobook_converter.py5
-rw-r--r--converter/converter.py16
3 files changed, 26 insertions, 46 deletions
diff --git a/README.md b/README.md
index 17568eb..0f0d090 100644
--- a/README.md
+++ b/README.md
@@ -9,8 +9,9 @@ Original project: [https://github.com/WhiskeyCoder/Qwen3-Audiobook-Converter](ht
The converter sends text extracted from your books to a locally running Qwen3-TTS server and assembles the returned audio into a single audiobook file.
-- Supported input: `.txt`, `.pdf`, `.epub`
+- Input: `.txt`, `.pdf`, or `.epub`
- Output: `.mp3` or `.m4b`
+- Output a single mp3 or one per chapter
- Two voice modes:
- Custom voice: pre-built speakers
- Voice clone: clone a voice from a `.wav` reference audio file
@@ -89,47 +90,21 @@ CUSTOM_VOICE_INSTRUCT = "Speak naturally and clearly, as if reading a dramatic b
python audiobook_converter.py --voice-clone --voice-sample path/to/reference.wav
```
-The reference .wav should be ~10-15 seconds with a minimum of 3 seconds and maximum of 60 seconds. Longer is not better. ~15 seconds is ideal.
+The reference `.wav` should be ~10-15 seconds (3 second minimum, 60 second maximum; ~15 seconds is ideal).
-Whisper will be used automatically to transcribe the reference audio (`faster_whisper` or `whisper`). If no Whisper backend is installed, it falls back to x-vector-only cloning.
+Whisper (`faster_whisper` or `whisper`) is used automatically to transcribe the reference audio; without a Whisper backend it falls back to x-vector-only cloning. Override with `--voice-sample-text "..."` or skip transcription with `--no-transcription`.
-To skip automatic transcription explicitly, pass `--no-transcription`. This should be worse, but in my experience may give a preferable flatter tone to certain voices.
+### Options
-You can override whisper by passing your own transcription with `--voice-sample-text "What the reference audio says"`
+| Flag | Description |
+| --------------------------- | ----------------------------------------------------------------------------------------- |
+| `--speed <n>` | Playback speed, pitch-preserving (`1.0` = normal). A normal-speed copy is also output. |
+| `--format {mp3,m4b}` | Output format (default `mp3`). `m4b` uses AAC audio and is always a single file. |
+| `--single-file` | Merge all chapters into a single mp3 (default: one mp3 per chapter). |
+| `--voice-sample-text "..."` | Transcript of the reference audio (voice clone only). |
+| `--no-transcription` | Skip auto-transcription of the reference audio (voice clone only). |
-### Playback speed
-
-Adjust the speed of the final audiobook without changing pitch (uses ffmpeg `atempo` before encoding). The normal-speed audiobook file is also preserved in the output directory.
-
-```bash
-python audiobook_converter.py --speed 0.9
-```
-
-### Output format (mp3 / m4b)
-
-Use `--format` to choose the output container. The default is `mp3`; `m4b` uses AAC audio (ffmpeg `aac`).
-
-```bash
-python audiobook_converter.py --format m4b
-```
-
-### Chapters (EPUB)
-
-Books with chapters (e.g. EPUB) are converted to **one file per chapter** by default. Files are named `output/<Book>_01_<Chapter>.mp3`, `output/<Book>_02_<Chapter>.mp3`, and so on.
-
-To merge all chapters into a single file instead, pass `--single-file`:
-
-```bash
-python audiobook_converter.py --single-file
-```
-
-When the source has chapters and the output is a single `m4b`, chapter markers are embedded so listeners can skip between chapters:
-
-```bash
-python audiobook_converter.py --format m4b --single-file
-```
-
-TXT and PDF files have no chapter structure and always produce a single file.
+Books with chapters (e.g. EPUB) are converted to **one mp3 per chapter** by default, named `output/<Book>_01_<Chapter>.mp3`, `output/<Book>_02_<Chapter>.mp3`, etc.; use `--single-file` to merge them. `m4b` output is always a single file with chapter markers embedded so listeners can skip between chapters. TXT and PDF files have no chapter structure and always produce a single file.
The `chunks/` folder is scratch space for the current book only — it is emptied before and after every conversion, so an interrupted run never affects the next one.
diff --git a/audiobook_converter.py b/audiobook_converter.py
index 783a3ce..36bd5c2 100644
--- a/audiobook_converter.py
+++ b/audiobook_converter.py
@@ -86,8 +86,9 @@ Examples:
parser.add_argument(
"--single-file",
action="store_true",
- help=("Combine all chapters into a single output file. By default books with "
- "chapters (e.g. EPUB) are converted to one file per chapter.")
+ help=("Combine all chapters into a single mp3. By default books with "
+ "chapters (e.g. EPUB) are converted to one mp3 per chapter. "
+ "Ignored for m4b, which is always a single file.")
)
args = parser.parse_args()
diff --git a/converter/converter.py b/converter/converter.py
index ce2c351..f63a412 100644
--- a/converter/converter.py
+++ b/converter/converter.py
@@ -99,10 +99,14 @@ class AudiobookConverter:
return False
stem = output_name or file_path.stem
- embed_chapters = (self.output_format == "m4b" and self.single_file
- and len(sections) > 1)
- if embed_chapters:
- return self._convert_single_m4b_with_chapters(sections, stem, start_time)
+
+ # 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)
+ output_path = config.AUDIOBOOKS_FOLDER / f"{stem}.{self.output_format}"
+ return self._convert_text(sections[0].text, output_path, start_time)
if self.single_file or len(sections) == 1:
text = "\n\n".join(section.text for section in sections)
@@ -124,7 +128,7 @@ class AudiobookConverter:
# Always cleanup, even on failure or interrupt
audio.cleanup_chunks()
- def _convert_single_m4b_with_chapters(self, sections, stem: str, start_time: float) -> bool:
+ def _convert_m4b_with_chapters(self, sections, stem: str, start_time: float) -> bool:
"""Convert each chapter to audio, then assemble a single m4b with
embedded chapter markers."""
chapter_files = []
@@ -250,7 +254,7 @@ class AudiobookConverter:
print(f"Reference audio: {Path(self.voice_clone_ref_audio).name}")
print(f"Language: {config.VOICE_CLONE_LANGUAGE}")
print(f"Output format: {self.output_format}")
- if self.single_file:
+ if self.single_file and self.output_format == "mp3":
print("Chapter mode: single file (--single-file)")
if abs(self.speed - 1.0) >= 1e-6:
print(f"Playback speed: {self.speed:g}x")