diff options
| author | historia <historiavg@proton.me> | 2026-08-23 23:48:25 -0400 |
|---|---|---|
| committer | historia <historiavg@proton.me> | 2026-08-23 23:48:25 -0400 |
| commit | 5bfbdcb5765fd4eb57d13c67169bb3c2706ead75 (patch) | |
| tree | a07a27976f56a449e8c33641161553aa0989f5c2 /audiobook.py | |
| parent | 07f7b351f2956b6c92761877c9a4314bcede3b6e (diff) | |
| download | tts-audiobook-generator-5bfbdcb5765fd4eb57d13c67169bb3c2706ead75.tar.gz | |
feat: audiobook.py tui: convert, modify, or install backends
Diffstat (limited to 'audiobook.py')
| -rwxr-xr-x | audiobook.py | 229 |
1 files changed, 106 insertions, 123 deletions
diff --git a/audiobook.py b/audiobook.py index 6ad056c..da67ac7 100755 --- a/audiobook.py +++ b/audiobook.py @@ -3,6 +3,8 @@ TTS Audiobook Generator Converts TXT, PDF and EPUB files into audiobooks using a local TTS server. +Run with no arguments in a terminal for the full TUI (set up backends, +process the input directory); pass flags to script a conversion directly. Edit converter/config.py to change voice and processing settings. """ @@ -35,17 +37,92 @@ from converter.tts import ( ) +def convert(backend: str = None, voice: str = None, clone: str = None, + transcription: str = None, no_transcription: bool = False, + language: str = None, speed: float = 1.0, single_file: bool = False, + output_format: str = None, debug: bool = False, chunk: bool = False, + model_id: str = None, instructions: str = None, + request_options: dict = None) -> int: + """Run one conversion pass with explicit options (used by the CLI and hub). + + Returns the process exit code (0 on success, 1 on failure, 130 on + Ctrl-C). BACKEND defaults to config.BACKEND, OUTPUT_FORMAT to + config.AUDIO_FORMAT. LANGUAGE is already-normalized where required. + """ + backend = backend or config.BACKEND + output_format = output_format or config.AUDIO_FORMAT + request_options = request_options or {} + setup_logging(debug=debug) + setup_directories() + + if backend == BACKEND_FASTER: + voice_mode = VOICE_MODE_CLONE + elif backend == BACKEND_AUDIOCPP: + voice_mode = VOICE_MODE_CLONE if voice else VOICE_MODE_CUSTOM + else: + voice_mode = VOICE_MODE_CLONE if clone else VOICE_MODE_CUSTOM + + book_files, planned = AudiobookConverter.preflight_overwrites( + backend=backend, voice=voice, voice_mode=voice_mode, + voice_clone_ref_audio=clone, output_format=output_format, + instructions=instructions, + ) + if not book_files: + print("[INFO] Nothing to convert. Add a .txt, .pdf, or .epub file " + "to the input folder and run again.") + return 0 + if not planned: + print("[INFO] Nothing to convert (all books skipped)") + return 0 + + try: + converter = AudiobookConverter( + voice_mode=voice_mode, voice_clone_ref_audio=clone, + voice_clone_ref_text=transcription, + skip_transcription=no_transcription, speed=speed, + single_file=single_file, output_format=output_format, + language=language, backend=backend, voice=voice, debug=debug, + chunk=chunk, model_id=model_id, instructions=instructions, + request_options=request_options, + ) + converter._book_files = book_files + converter._planned = planned + ok = converter.run() + except KeyboardInterrupt: + print("\n[WARNING] Shutdown requested by user") + return 130 + except Exception as exc: + print(f"[FATAL] Fatal error: {exc}") + traceback.print_exc() + return 1 + return 0 if ok else 1 + + def main() -> None: - """Entry point with argparse.""" + """Entry point: TUI hub with no args in a terminal, else argparse CLI.""" + # No arguments + interactive terminal -> the TUI hub (set up backends + # and process the input directory end-to-end). Anything else is the + # scriptable argparse CLI. + if not sys.argv[1:]: + try: + interactive = sys.stdin.isatty() and sys.stdout.isatty() + except (AttributeError, ValueError): + interactive = False + if interactive: + import hub + sys.exit(hub.run()) + # Non-interactive with no args: a default conversion run (cron/etc). + sys.exit(convert()) + parser = argparse.ArgumentParser( description="Convert books to audiobooks using a local TTS server", formatter_class=argparse.RawDescriptionHelpFormatter, epilog=""" Examples: - # Use the default audio.cpp audiocpp_server (speaker mode - Vivian speaker, or a server-side voice) + # No arguments, in a terminal: the full TUI (set up backends, convert). python audiobook.py - # Use the audio.cpp audiocpp_server with a server-side voice preset + # Use the audio.cpp audiocpp_server (speaker mode - Vivian speaker, or a server-side voice) python audiobook.py --backend audiocpp --voice narrator # Use the audio.cpp audiocpp_server with a voice design model (task 'vdes') @@ -64,34 +141,23 @@ Examples: ) parser.add_argument( - "--clone", - type=str, - metavar="PATH", + "--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, + "--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", + "--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", + "--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). " "With --backend audiocpp the language is adapted to the model " @@ -99,32 +165,22 @@ Examples: "omitted when the model detects the language itself. Defaults to " "the LANGUAGE setting in converter/config.py (English).") ) - parser.add_argument( - "--speed", - type=float, - default=1.0, + "--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(AUDIO_FORMATS), - default=config.AUDIO_FORMAT, + "--format", choices=list(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", + "--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( - "--backend", - choices=[BACKEND_AUDIOCPP, BACKEND_QWEN, BACKEND_FASTER], + "--backend", choices=[BACKEND_AUDIOCPP, BACKEND_QWEN, BACKEND_FASTER], default=config.BACKEND, help=("TTS server to talk to: the Qwen3-TTS demo server (qwen), the " "faster-qwen3-tts OpenAI-compatible server (faster), or an " @@ -133,12 +189,8 @@ Examples: "and more. Defaults to the BACKEND setting in " "converter/config.py (audiocpp).") ) - parser.add_argument( - "--voice", - type=str, - default=None, - metavar="NAME", + "--voice", type=str, default=None, metavar="NAME", help=("Voice to request from a server-side voice configuration. faster: " "a key in the server's voices.json ('default' when it was started " "with --ref-audio). audiocpp: a voice_preset or voice_dir entry " @@ -147,44 +199,32 @@ Examples: "the qwen backend (use converter/config.py SPEAKER or --clone " "there).") ) - parser.add_argument( - "--debug", - action="store_true", + "--debug", action="store_true", help=("Troubleshooting mode: dump each chunk's raw audio and the exact text " "sent for it under the debug/ folder (organized per book and chapter), " "and log every TTS request and response to the console and log file.") ) - parser.add_argument( - "--chunk", - action="store_true", + "--chunk", action="store_true", help=("Force client-side chunking into CHUNK_SIZE-word requests (see " "converter/config.py). Only matters for --backend audiocpp, which " "otherwise sends each chapter as one request and lets the server " "chunk long text itself; the qwen and faster backends always " "chunk.") ) - parser.add_argument( - "--model", - type=str, - default=None, - metavar="ID", + "--model", type=str, default=None, metavar="ID", help=("audio.cpp server model entry id to use for this run " "(--backend audiocpp only). Overrides AUDIOCPP_MODEL_ID in " "converter/config.py, which is useful for a server hosting " "several lazily-loaded models: generate one server.json with " - "tools/make_audiocpp_server_json.py, then pick the model per " - "run with --model. Leave unset to use the config id, or to " - "auto-select when the server hosts exactly one entry.") + "backends.audiocpp, then pick the model per run with --model. " + "Leave unset to use the config id, or to auto-select when the " + "server hosts exactly one entry.") ) - parser.add_argument( - "--instructions", - type=str, - default=None, - metavar="TEXT", + "--instructions", type=str, default=None, metavar="TEXT", help=("Voice design or style instruction sent with every request " "(--backend audiocpp only). Required for voice design models " "(server entries with task 'vdes', e.g. Qwen3-TTS " @@ -194,13 +234,8 @@ Examples: "the model supports one and is ignored otherwise. Defaults to " "AUDIOCPP_INSTRUCTIONS in converter/config.py (empty).") ) - parser.add_argument( - "--option", - action="append", - type=str, - default=None, - metavar="KEY=VALUE", + "--option", action="append", type=str, default=None, metavar="KEY=VALUE", help=("Request option passed through to the audio.cpp model " "(--backend audiocpp only); repeatable. Whatever the hosted " "family supports (emotion, voice_id, speed, speaking_rate, " @@ -290,68 +325,16 @@ Examples: parser.error(f"--option expects KEY=VALUE (got {item!r})") request_options[key.strip()] = value - setup_logging(debug=args.debug) - setup_directories() - - if args.backend == BACKEND_FASTER: - voice_mode = VOICE_MODE_CLONE - elif args.backend == BACKEND_AUDIOCPP: - voice_mode = VOICE_MODE_CLONE if args.voice else VOICE_MODE_CUSTOM - else: - voice_mode = VOICE_MODE_CLONE if args.clone else VOICE_MODE_CUSTOM - - # Ask every overwrite question up front, before spending time connecting - # to a TTS server: a user who declines (or has nothing to convert) never - # waits on a slow server handshake. Nothing in this step needs the server. - book_files, planned = AudiobookConverter.preflight_overwrites( - backend=args.backend, - voice=args.voice, - voice_mode=voice_mode, - voice_clone_ref_audio=args.clone, - output_format=args.format, - instructions=args.instructions, - ) - - if not book_files: - print("[INFO] Nothing to convert. Add a .txt, .pdf, or .epub file " - "to the input folder and run again.") - sys.exit(0) - - if not planned: - print("[INFO] Nothing to convert (all books skipped)") - sys.exit(0) - - try: - converter = AudiobookConverter( - voice_mode=voice_mode, - 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, - backend=args.backend, - voice=args.voice, - debug=args.debug, - chunk=args.chunk, - model_id=args.model, - instructions=args.instructions, - request_options=request_options, - ) - converter._book_files = book_files - converter._planned = planned - 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) + sys.exit(convert( + backend=args.backend, voice=args.voice, clone=args.clone, + transcription=args.transcription, no_transcription=args.no_transcription, + language=args.language, speed=args.speed, single_file=args.single_file, + output_format=args.format, debug=args.debug, chunk=args.chunk, + model_id=args.model, instructions=args.instructions, + request_options=request_options, + )) if __name__ == "__main__": main() + |
