#!/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()