aboutsummaryrefslogtreecommitdiff
path: root/audiobook_converter.py
blob: bb21e64ff1a130dde340ca114bbd1be79e75ae7f (plain)
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
#!/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:
        # Python < 3.7
        import codecs
        sys.stdout = codecs.getwriter("utf-8")(sys.stdout.buffer, "strict")
        sys.stderr = codecs.getwriter("utf-8")(sys.stderr.buffer, "strict")

from converter.converter import AudiobookConverter, setup_directories, setup_logging


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_converter.py

  # Use voice cloning with reference audio
  python audiobook_converter.py --voice-clone --voice-sample path/to/reference.wav
        """
    )

    parser.add_argument(
        "--voice-clone",
        action="store_true",
        help="Use voice cloning mode instead of custom voice (requires --voice-sample)"
    )

    parser.add_argument(
        "--voice-sample",
        type=str,
        help="Path to reference audio file for voice cloning (WAV format)."
    )

    parser.add_argument(
        "--voice-sample-text",
        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 --voice-sample-text is provided.")
    )

    parser.add_argument(
        "--speed",
        type=float,
        default=1.0,
        help="Playback speed factor for the final audiobook (1.0 = normal). Pitch-preserving."
    )

    args = parser.parse_args()

    if args.speed <= 0:
        parser.error(f"--speed must be a positive number (got {args.speed:g})")

    if args.voice_clone:
        if not args.voice_sample:
            print("[ERROR] --voice-clone requires --voice-sample")
            print('Usage: python audiobook_converter.py --voice-clone --voice-sample <path> [--voice-sample-text "..."]')
            sys.exit(1)
    elif args.voice_sample or args.voice_sample_text or args.no_transcription:
        print("[WARNING] --voice-sample/--voice-sample-text/--no-transcription "
              "are ignored without --voice-clone")

    setup_logging()
    setup_directories()

    try:
        converter = AudiobookConverter(
            voice_mode="voice_clone" if args.voice_clone else "custom_voice",
            voice_clone_ref_audio=args.voice_sample if args.voice_clone else None,
            voice_clone_ref_text=args.voice_sample_text if args.voice_clone else None,
            skip_transcription=args.no_transcription,
            speed=args.speed,
        )
        converter.run()
    except KeyboardInterrupt:
        print("\n[WARNING] Shutdown requested by user")
    except Exception as exc:
        print(f"[FATAL] Fatal error: {exc}")
        traceback.print_exc()
        sys.exit(1)


if __name__ == "__main__":
    main()