aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md22
-rwxr-xr-xaudiobook_converter.py4
-rw-r--r--converter/audio.py4
-rw-r--r--converter/config.py4
-rw-r--r--converter/converter.py2
-rw-r--r--tests/test_audio.py6
-rw-r--r--tests/test_converter.py2
7 files changed, 27 insertions, 17 deletions
diff --git a/README.md b/README.md
index 20ee20a..f3ce5d9 100644
--- a/README.md
+++ b/README.md
@@ -1,13 +1,13 @@
# Qwen3 Audiobook Converter
-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.
+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 more output files, transcription/speed options, better text cleanup, and clearer instructions. It also expects the qwen-tts server to be on different ports per model, so two server processes can run at once.
## Overview
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.
- Input: `.txt`, `.pdf`, or `.epub`
-- Output: `.mp3` or `.m4b`
+- Output: `.mp3`, `.m4b`, `.ogg`, or `.flac`
- Output a single mp3 or one per chapter
- Two voice modes:
- Custom voice: pre-built speakers
@@ -64,7 +64,7 @@ qwen-tts-demo Qwen/Qwen3-TTS-12Hz-1.7B-Base --ip 127.0.0.1 --port 7861
## Converting books
-Put your book files (epub, txt, etc.) in the `input/` folder. Then run the script. The output goes to `output/`.
+Put your book files (epub, etc.) in the `input/` folder. Then run the script. The output goes to `output/`.
### Custom voice
@@ -86,19 +86,19 @@ 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 (3 second minimum, 60 second maximum; ~15 seconds is ideal).
+The reference `.wav` should be ~10-15 seconds (3 second minimum, 60 second maximum; ~15 seconds is ideal). Longer is **not** better.
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`.
### Options
-| 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 has built-in chapters. |
-| `--single-file` | mp3 only: Merge all chapters into a single mp3 (default: one mp3 per chapter). |
-| `--voice-sample-text "..."` | Override whisper auto-transcription with your own manual reference audio transcript. Not required. |
-| `--no-transcription` | Skip auto-transcription of the reference audio. Usually worse, but can give a different voice affect. |
+| Flag | Description |
+| ----------------------------- | ----------------------------------------------------------------------------------------------------- |
+| `--speed <n>` | Playback speed, pitch-preserving (`1.0` = normal). A normal-speed copy is also output. |
+| `--format {mp3,m4b,ogg,flac}` | Output format (default `mp3`). `m4b` uses AAC audio and has built-in chapters. |
+| `--single-file` | Not m4b: Merge all chapters into a single file (default: one file per chapter). |
+| `--voice-sample-text "..."` | Override whisper auto-transcription with your own manual reference audio transcript. Not required. |
+| `--no-transcription` | Skip auto-transcription of the reference audio. Usually worse, but can give a different voice affect. |
## Running tests
diff --git a/audiobook_converter.py b/audiobook_converter.py
index 82eae9a..eb9beba 100755
--- a/audiobook_converter.py
+++ b/audiobook_converter.py
@@ -84,8 +84,8 @@ Examples:
parser.add_argument(
"--single-file",
action="store_true",
- help=("Combine all chapters into a single mp3. By default books with "
- "chapters (e.g. EPUB) are converted to one mp3 per chapter. "
+ help=("Combine all chapters into a single audio file. By default books with "
+ "chapters (e.g. EPUB) are converted to one file per chapter. "
"Ignored for m4b, which is always a single file.")
)
diff --git a/converter/audio.py b/converter/audio.py
index 5a7ec35..98864f6 100644
--- a/converter/audio.py
+++ b/converter/audio.py
@@ -52,6 +52,10 @@ def _encode_args(output_format: str) -> List[str]:
"""Return ffmpeg output codec/bitrate args for the requested container."""
if output_format == "m4b":
return ["-c:a", "aac", "-b:a", config.AUDIO_BITRATE]
+ if output_format == "ogg":
+ return ["-c:a", "libvorbis", "-b:a", config.AUDIO_BITRATE]
+ if output_format == "flac":
+ return ["-c:a", "flac"]
if output_format == "wav":
# Lossless intermediate for per-chapter scratch audio; avoids
# generational loss when the final m4b re-encodes to AAC.
diff --git a/converter/config.py b/converter/config.py
index b4d7f22..ec84986 100644
--- a/converter/config.py
+++ b/converter/config.py
@@ -82,8 +82,8 @@ HEARTBEAT_INTERVAL_SECONDS = 30 # Print "still working" this often during a chu
# AUDIO OUTPUT SETTINGS
# =============================================================================
-AUDIO_FORMAT = "mp3" # Default output container ("mp3" or "m4b")
-AUDIO_FORMATS = ("mp3", "m4b")
+AUDIO_FORMAT = "mp3" # Default output container
+AUDIO_FORMATS = ("mp3", "m4b", "ogg", "flac")
AUDIO_BITRATE = "128k"
SUPPORTED_FORMATS = [".txt", ".pdf", ".epub"]
diff --git a/converter/converter.py b/converter/converter.py
index bc64f38..c0e97e4 100644
--- a/converter/converter.py
+++ b/converter/converter.py
@@ -304,7 +304,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 and self.output_format == "mp3":
+ if self.single_file and self.output_format != "m4b":
print("Chapter mode: single file (--single-file)")
if abs(self.speed - 1.0) >= 1e-6:
print(f"Playback speed: {self.speed:g}x")
diff --git a/tests/test_audio.py b/tests/test_audio.py
index 7e782cc..7116e2b 100644
--- a/tests/test_audio.py
+++ b/tests/test_audio.py
@@ -90,6 +90,12 @@ class EncodeArgsTests(unittest.TestCase):
def test_wav_is_lossless_pcm(self):
self.assertEqual(_encode_args("wav"), ["-c:a", "pcm_s16le"])
+ def test_ogg_uses_libvorbis(self):
+ self.assertEqual(_encode_args("ogg"), ["-c:a", "libvorbis", "-b:a", config.AUDIO_BITRATE])
+
+ def test_flac_is_lossless(self):
+ self.assertEqual(_encode_args("flac"), ["-c:a", "flac"])
+
class M4bContainerArgsTests(unittest.TestCase):
def setUp(self):
diff --git a/tests/test_converter.py b/tests/test_converter.py
index 9c03e30..fce0439 100644
--- a/tests/test_converter.py
+++ b/tests/test_converter.py
@@ -28,7 +28,7 @@ class ConfigurationValidationTests(unittest.TestCase):
def test_unknown_format_rejected(self):
with self.assertRaises(ValueError):
- AudiobookConverter(output_format="ogg")
+ AudiobookConverter(output_format="wma")
if __name__ == "__main__":