aboutsummaryrefslogtreecommitdiff
path: root/audiobook.py
diff options
context:
space:
mode:
authorhistoria <historiavg@proton.me>2026-08-28 14:57:48 -0400
committerhistoria <historiavg@proton.me>2026-08-28 14:57:48 -0400
commitdb38085d07ce75f8961eecdc1919e98748254c53 (patch)
treebeaea2ba84de05bfc2fa90e9b92a24568d3b4cb9 /audiobook.py
parentafb2c2d5b297c5aa28bcced0e3f90e207d799c2a (diff)
downloadtts-audiobook-generator-db38085d07ce75f8961eecdc1919e98748254c53.tar.gz
refactor: overhaul config.py, remove cli default options
Diffstat (limited to 'audiobook.py')
-rwxr-xr-xaudiobook.py93
1 files changed, 55 insertions, 38 deletions
diff --git a/audiobook.py b/audiobook.py
index 811ffdf..5da78ae 100755
--- a/audiobook.py
+++ b/audiobook.py
@@ -7,7 +7,8 @@ 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.
--input-file/--output-file convert one individual book file instead of a
directory (the two flag pairs are mutually exclusive). Edit
-app/converter/config.py to change voice and processing settings.
+app/converter/config.py for server URLs and processing settings; the TTS
+backend, model and voice are chosen per run with CLI flags.
"""
import argparse
@@ -48,6 +49,7 @@ from converter.clients import (
VOICE_MODE_CLONE,
VOICE_MODE_CUSTOM,
VOICE_MODE_DESIGN,
+ is_builtin_speaker,
normalize_language,
)
from converter.converter import (
@@ -72,7 +74,7 @@ def resolve_book_path(path: Path) -> Path:
return resolved if resolved.is_absolute() else _BASE_DIR / resolved
-def convert(backend: str = None, voice: str = None, clone: str = None,
+def convert(backend: str, voice: str = None, clone: str = None,
transcription: str = None, no_transcription: bool = False,
language: str = None, speed: float = None, single_file: bool = False,
output_format: str = None, debug: bool = None,
@@ -85,14 +87,14 @@ def convert(backend: str = None, voice: str = None, clone: str = None,
"""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.
- INPUT_DIR/OUTPUT_DIR override the configured INPUT_DIR/OUTPUT_DIR
- folders when given (relative paths resolve against the project
- root); SPEED/DEBUG default to the config.SPEED/config.DEBUG
- settings. API_URL, when given, overrides the configured server URL
- for the selected backend (used by the hub's "[remote]" entries and
- --api-url).
+ Ctrl-C). BACKEND is required (the CLI flag or the hub's Generate form
+ supplies it); OUTPUT_FORMAT defaults to config.AUDIO_FORMAT. LANGUAGE
+ is already-normalized where required. INPUT_DIR/OUTPUT_DIR override the
+ configured INPUT_DIR/OUTPUT_DIR folders when given (relative paths
+ resolve against the project root); SPEED/DEBUG default to the
+ config.SPEED/config.DEBUG settings. API_URL, when given, overrides the
+ configured server URL for the selected backend (used by the hub's
+ "[remote]" entries and --api-url).
INPUT_FILE converts a single book file instead of scanning the
input folder (the CLI validates it and resolves relative paths
@@ -109,7 +111,8 @@ def convert(backend: str = None, voice: str = None, clone: str = None,
the overwrite prompts are not asked again) wire the conversion into
the TUI run view; without them everything behaves like the CLI.
"""
- backend = backend or config.BACKEND
+ if backend is None:
+ raise ValueError("backend is required (pass --backend)")
output_format = output_format or config.AUDIO_FORMAT
speed = config.SPEED if speed is None else speed
debug = config.DEBUG if debug is None else debug
@@ -221,8 +224,13 @@ def main() -> None:
if interactive:
from ui import hub
sys.exit(hub.run())
- # Non-interactive with no args: a default conversion run (cron/etc).
- sys.exit(convert())
+ # Non-interactive with no args: there is no default backend/model/
+ # voice anymore, so a scripted run must say what it wants.
+ print("[ERROR] No --backend given. Scripted (non-interactive) runs "
+ "must pass --backend plus the backend's voice/model flags "
+ "(see --help); run with no arguments in a terminal for the "
+ "TUI.")
+ sys.exit(2)
parser = argparse.ArgumentParser(
description="Convert books to audiobooks using a local TTS server",
@@ -242,8 +250,8 @@ Examples:
python audiobook.py --backend audiocpp --model Qwen3-TTS-12Hz-1.7B-VoiceDesign-GGUF \\
--instructions "A warm adult female narrator with a British accent"
- # Use the qwen-tts demo server with a custom voice
- python audiobook.py --backend qwen
+ # Use the qwen-tts demo server with a built-in speaker
+ python audiobook.py --backend qwen --voice Vivian
# Use the qwen-tts demo server with voice cloning from reference audio
python audiobook.py --backend qwen --clone path/to/reference.wav
@@ -334,26 +342,23 @@ Examples:
)
parser.add_argument(
"--backend", choices=[BACKEND_AUDIOCPP, BACKEND_QWEN, BACKEND_FASTER],
- default=config.BACKEND,
+ required=True,
help=("TTS server to talk to: the qwen-tts demo server (qwen), the "
"faster-qwen3-tts OpenAI-compatible server (faster), or an "
"audio.cpp audiocpp_server (audiocpp) hosting any of its TTS "
"model families — Qwen3-TTS, Higgs Audio, VoxCPM2, IndexTTS2, "
- "and more. Defaults to the BACKEND setting in "
- "app/converter/config.py (audiocpp).")
+ "and more.")
)
parser.add_argument(
"--voice", type=str, default=None, metavar="NAME",
- help=("Voice to request. faster: a key in the server's voices.json "
- "('default' when it was started with --ref-audio). audiocpp: "
- "the name of the voice for the selected model entry — for the "
- "Qwen3-TTS CustomVoice entry a built-in speaker (e.g. Vivian, "
- "Ryan, Uncle Fu), for every other family a voice_preset or "
- "voice_dir entry (cloning). When AUDIOCPP_CLONE_MODEL_ID "
- "names a second server entry (typically the Qwen Base model), "
- "a non-speaker --voice reroutes to it. Not used by the qwen "
- "backend (use app/converter/config.py SPEAKER or --clone "
- "there).")
+ help=("Voice to request. faster: (required) a key in the server's "
+ "voices.json. audiocpp: the name of the voice for the "
+ "selected model entry — for the Qwen3-TTS CustomVoice entry "
+ "a built-in speaker (e.g. Vivian, Ryan, Uncle Fu; required "
+ "there), for every other family a voice_preset or voice_dir "
+ "entry (cloning). qwen: a built-in CustomVoice speaker name "
+ "(required for built-in-speaker runs; use --clone or "
+ "--instructions for the other voice modes).")
)
parser.add_argument(
"--debug", action="store_true",
@@ -365,12 +370,11 @@ Examples:
parser.add_argument(
"--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 "
- "app/converter/config.py, which is useful for a server hosting "
+ "(--backend audiocpp only). Required when the server hosts "
"several lazily-loaded models: generate one server.json with "
"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.")
+ "Leave unset to auto-select when the server hosts exactly one "
+ "entry.")
)
parser.add_argument(
"--instructions", type=str, default=None, metavar="TEXT",
@@ -380,8 +384,7 @@ Examples:
"e.g. Qwen3-TTS VoiceDesign) and optional style/delivery "
"control elsewhere; with qwen it selects the VoiceDesign "
"model and describes the voice to synthesize with, e.g. "
- "'A warm adult female narrator with a British accent' "
- "(defaulting to INSTRUCT in app/converter/config.py).")
+ "'A warm adult female narrator with a British accent'.")
)
parser.add_argument(
"--option", action="append", type=str, default=None, metavar="KEY=VALUE",
@@ -485,6 +488,9 @@ Examples:
print("[WARNING] --language is ignored with --backend faster: language "
"is configured on the server (see README)")
args.language = None
+ if not args.voice:
+ parser.error("--backend faster requires --voice: a key in the "
+ "server's voices.json (see README)")
elif args.backend == BACKEND_AUDIOCPP:
if args.clone:
print("[WARNING] --clone is ignored with --backend audiocpp: cloning "
@@ -503,10 +509,15 @@ Examples:
except ValueError as exc:
parser.error(str(exc))
else:
- if args.voice is not None:
- parser.error("--voice requires --backend faster or audiocpp; the "
- "qwen backend uses built-in speakers "
- "(app/converter/config.py SPEAKER) or --clone")
+ # qwen: --voice names a built-in CustomVoice speaker (the only
+ # voice mode that needs one; --clone and --instructions pick the
+ # Base/VoiceDesign models instead).
+ if args.voice is not None and not is_builtin_speaker(args.voice):
+ parser.error(f"--backend qwen: --voice {args.voice!r} is not a "
+ "built-in speaker (want one of: Vivian, Serena, "
+ "Uncle_Fu, Dylan, Eric, Ryan, Aiden, Ono_Anna, "
+ "Sohee); use --clone or --instructions for the "
+ "other voice modes")
if args.language is not None:
try:
args.language = normalize_language(args.language)
@@ -516,6 +527,12 @@ Examples:
if not args.clone and (args.transcription or args.no_transcription):
print("[WARNING] --transcription/--no-transcription "
"are ignored without --clone")
+ if not args.clone and not (args.instructions or "").strip() \
+ and not args.voice:
+ parser.error("--backend qwen needs a voice: pass --voice SPEAKER "
+ "for a built-in speaker, --clone PATH to clone a "
+ "reference .wav, or --instructions \"...\" to "
+ "design a voice")
if args.model is not None and args.backend != BACKEND_AUDIOCPP:
parser.error("--model requires --backend audiocpp; it selects an "