diff options
Diffstat (limited to 'audiobook.py')
| -rwxr-xr-x | audiobook.py | 141 |
1 files changed, 141 insertions, 0 deletions
diff --git a/audiobook.py b/audiobook.py new file mode 100755 index 0000000..34806f2 --- /dev/null +++ b/audiobook.py @@ -0,0 +1,141 @@ +#!/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 + """ + ) + + 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.") + ) + + args = parser.parse_args() + + if args.speed <= 0: + parser.error(f"--speed must be a positive number (got {args.speed:g})") + + 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 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, + ) + 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() |
