1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
|
#!/usr/bin/env python3
"""
Qwen-Based Audiobook Converter
Converts TXT, PDF and EPUB files into audiobooks using a local Qwen3-TTS server.
Edit converter/config.py to change voice and processing settings.
License: MIT
"""
import argparse
import sys
import traceback
# Fix Windows console encoding for unicode output
if sys.platform == "win32":
try:
sys.stdout.reconfigure(encoding="utf-8")
sys.stderr.reconfigure(encoding="utf-8")
except AttributeError:
pass
from converter import config
from converter.converter import AudiobookConverter, setup_directories, setup_logging
from converter.tts import normalize_language
def main() -> None:
"""Entry point with argparse."""
parser = argparse.ArgumentParser(
description="Convert books to audiobooks using the Qwen3-TTS voice model",
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog="""
Examples:
# Use custom voice (default - Vivian speaker)
python audiobook.py
# Use voice cloning with reference audio
python audiobook.py --clone path/to/reference.wav
# Use the faster-qwen3-tts server (voice cloning, configured server-side)
python audiobook.py --faster
"""
)
parser.add_argument(
"--clone",
type=str,
metavar="PATH",
help=("Path to reference audio file for voice cloning (WAV format). "
"Passing this flag switches the converter to voice clone mode.")
)
parser.add_argument(
"--transcription",
type=str,
default=None,
help=("Transcript of the reference audio for in-context cloning (recommended for "
"highest quality). If omitted, a local Whisper backend is used if installed; "
"otherwise the converter falls back to x-vector-only mode.")
)
parser.add_argument(
"--no-transcription",
action="store_true",
help=("Skip automatic transcription of the reference audio (use x-vector-only "
"cloning). Ignored when --transcription is provided.")
)
parser.add_argument(
"--language",
type=str,
default=None,
metavar="LANG",
help=("Output language for the synthesized speech, e.g. English, Japanese, "
"or Auto (language names and short codes like en/ja are accepted). "
"Defaults to the mode's setting in converter/config.py (English).")
)
parser.add_argument(
"--speed",
type=float,
default=1.0,
help="Playback speed factor for the final audiobook (1.0 = normal). Pitch-preserving."
)
parser.add_argument(
"--format",
choices=list(config.AUDIO_FORMATS),
default=config.AUDIO_FORMAT,
help=f"Output container format (default: {config.AUDIO_FORMAT}). m4b uses AAC audio."
)
parser.add_argument(
"--single-file",
action="store_true",
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.")
)
parser.add_argument(
"--faster",
action="store_true",
help=("Use a faster-qwen3-tts OpenAI-compatible server instead of the Qwen "
"Gradio demos (5-10x faster inference via CUDA graphs). Always voice "
"cloning: the reference audio and transcript are configured on the "
"server itself (see the 'Faster backend' section of the README).")
)
parser.add_argument(
"--faster-voice",
type=str,
default=None,
metavar="NAME",
help=("Voice entry to request from the faster server's voice config "
"(default: the FASTER_TTS_VOICE setting in converter/config.py, "
"typically 'default'). Must match a key in the server's voices.json, "
"or 'default' when the server was started with --ref-audio.")
)
args = parser.parse_args()
if args.speed <= 0:
parser.error(f"--speed must be a positive number (got {args.speed:g})")
if args.faster:
if args.clone:
print("[WARNING] --clone is ignored with --faster: the faster backend "
"always uses voice cloning, and the reference voice is configured "
"on the faster server (see README)")
args.clone = None
if args.transcription or args.no_transcription:
print("[WARNING] --transcription/--no-transcription are ignored with "
"--faster: the reference transcript is configured on the faster "
"server (--ref-text or voices.json, see README)")
args.transcription = None
args.no_transcription = False
if args.language is not None:
print("[WARNING] --language is ignored with --faster: language is "
"configured on the faster server (see README)")
args.language = None
else:
if args.language is not None:
try:
args.language = normalize_language(args.language)
except ValueError as exc:
parser.error(str(exc))
if not args.clone and (args.transcription or args.no_transcription):
print("[WARNING] --transcription/--no-transcription "
"are ignored without --clone")
setup_logging()
setup_directories()
try:
converter = AudiobookConverter(
voice_mode=config.VOICE_MODE_CLONE if (args.clone or args.faster)
else config.VOICE_MODE_CUSTOM,
voice_clone_ref_audio=args.clone,
voice_clone_ref_text=args.transcription,
skip_transcription=args.no_transcription,
speed=args.speed,
single_file=args.single_file,
output_format=args.format,
language=args.language,
faster=args.faster,
faster_voice=args.faster_voice,
)
ok = converter.run()
except KeyboardInterrupt:
print("\n[WARNING] Shutdown requested by user")
sys.exit(130)
except Exception as exc:
print(f"[FATAL] Fatal error: {exc}")
traceback.print_exc()
sys.exit(1)
sys.exit(0 if ok else 1)
if __name__ == "__main__":
main()
|