aboutsummaryrefslogtreecommitdiff
path: root/audiobook.py
blob: 34806f2b4f535b04d0c2894d5ef5238163af2886 (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
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
#!/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()