diff options
| author | historia <historiavg@proton.me> | 2026-08-17 19:39:31 -0400 |
|---|---|---|
| committer | historia <historiavg@proton.me> | 2026-08-17 19:39:31 -0400 |
| commit | 68ee76514a98169a5e7a075648b70ab40417fbdf (patch) | |
| tree | 906f9fb856eb1e66a26af0b57f5d65462b7977b1 | |
| parent | b80fa9db6bab6cdb2856874b606a93149cfc1af2 (diff) | |
| download | tts-audiobook-generator-68ee76514a98169a5e7a075648b70ab40417fbdf.tar.gz | |
fix(converter): make m4b a single file with embedded chapters
| -rw-r--r-- | README.md | 51 | ||||
| -rw-r--r-- | audiobook_converter.py | 5 | ||||
| -rw-r--r-- | converter/converter.py | 16 |
3 files changed, 26 insertions, 46 deletions
@@ -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") |
