aboutsummaryrefslogtreecommitdiff
path: root/audiobook.py
diff options
context:
space:
mode:
authorhistoria <historiavg@proton.me>2026-09-02 01:26:09 -0400
committerhistoria <historiavg@proton.me>2026-09-02 01:26:09 -0400
commit8579517a35ef1865fc9b428899d73d52dcb27a14 (patch)
treedba52f8d99cfe4014e0b787367de99f238e5a0db /audiobook.py
parent391f50da7a085bec75155c0eb9b47910266058cc (diff)
downloadtts-audiobook-generator-8579517a35ef1865fc9b428899d73d52dcb27a14.tar.gz
feat: sglang backend support
Diffstat (limited to 'audiobook.py')
-rwxr-xr-xaudiobook.py135
1 files changed, 112 insertions, 23 deletions
diff --git a/audiobook.py b/audiobook.py
index b881b6e..88e8dca 100755
--- a/audiobook.py
+++ b/audiobook.py
@@ -18,6 +18,7 @@ externally-run server and never touches server state.
"""
import argparse
+import logging
import sys
import traceback
from pathlib import Path
@@ -53,6 +54,7 @@ from converter.clients import (
BACKEND_AUDIOCPP,
BACKEND_FASTER,
BACKEND_QWEN,
+ BACKEND_SGLOMNI,
VOICE_MODE_CLONE,
VOICE_MODE_CUSTOM,
VOICE_MODE_DESIGN,
@@ -363,6 +365,23 @@ def convert(backend: str, voice: str = None, clone: str = None,
voice_mode = VOICE_MODE_CLONE
elif backend == BACKEND_AUDIOCPP:
voice_mode = VOICE_MODE_CLONE if voice else VOICE_MODE_CUSTOM
+ elif backend == BACKEND_SGLOMNI:
+ # Resolve the model first (the capability decides the voice mode):
+ # managed runs need the weights on disk, remote runs accept any
+ # catalog key. The same resolution runs again in the converter.
+ from backends.sglomni import models as sg_models
+ from backends.sglomni.catalog import entry_by_key
+ if api_url is None:
+ sg_entry = sg_models.resolve_model(model_id)
+ else:
+ sg_entry = entry_by_key((model_id or "").strip())
+ if sg_entry is None:
+ raise RuntimeError(
+ f"Unknown SGLang-Omni model {model_id!r} — pick a "
+ "catalog key for --model (see the backend docs).")
+ model_id = sg_entry.key
+ voice_mode = voice_mode_for(backend, voice, clone, instructions,
+ model=model_id)
else:
# qwen: instructions design the voice (VoiceDesign model), a
# reference .wav clones one (Base), otherwise a built-in speaker.
@@ -396,8 +415,17 @@ def convert(backend: str, voice: str = None, clone: str = None,
try:
if manage_server and api_url is None:
from backends import managed
- server = managed.ensure_running(backend, voice_mode)
+ server = managed.ensure_running(backend, voice_mode,
+ model=model_id)
if server is not None and not server.ok:
+ # The boot failed (crashed, refused, or timed out): the
+ # detail went to the server's own log and the console, so
+ # record it here too — the dated run log the failure
+ # pointers name must not stay empty.
+ from backends import servers as _servers
+ logging.error("the %s server failed to start; its output "
+ "is in %s", server.spec.name,
+ _servers.server_log_path(server.spec.name))
return 1
converter = AudiobookConverter(
voice_mode=voice_mode, voice_clone_ref_audio=clone,
@@ -501,6 +529,18 @@ Examples:
python audiobook.py --backend qwen \\
--instructions "A warm adult female narrator with a British accent"
+ # Use the SGLang-Omni server with a catalog model (one model per server
+ # process; the CLI starts/stops the managed instance around the run)
+ python audiobook.py --backend sglomni --model higgs_audio_v3_tts
+
+ # SGLang-Omni voice cloning from a reference clip
+ python audiobook.py --backend sglomni --model moss_tts_local \\
+ --clone path/to/reference.wav
+
+ # SGLang-Omni voice design (Qwen3-TTS VoiceDesign model)
+ python audiobook.py --backend sglomni --model qwen3_tts_1_7b_voicedesign \\
+ --instructions "A warm adult female narrator with a British accent"
+
# Use the faster-qwen3-tts server (voice cloning, configured server-side)
python audiobook.py --backend faster [--voice NAME]
@@ -582,13 +622,16 @@ Examples:
"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,
+ BACKEND_SGLOMNI],
required=True,
help=("TTS server to talk to: the qwen-tts demo server (qwen), the "
- "faster-qwen3-tts OpenAI-compatible server (faster), or an "
+ "faster-qwen3-tts OpenAI-compatible server (faster), an "
"audio.cpp audiocpp_server (audiocpp) hosting any of its TTS "
"model families — Qwen3-TTS, Higgs Audio, VoxCPM2, IndexTTS2, "
- "and more.")
+ "and more — or an SGLang-Omni server (sglomni) hosting one "
+ "of its catalog models (Qwen3-TTS, Higgs Audio v3, MOSS-TTS, "
+ "Voxtral TTS, Fish Speech, and more).")
)
parser.add_argument(
"--voice", type=str, default=None, metavar="NAME",
@@ -599,7 +642,10 @@ Examples:
"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).")
+ "--instructions for the other voice modes). sglomni: a "
+ "preset voice name on speaker-capable models (Qwen3-TTS "
+ "CustomVoice, Voxtral); other models use their default "
+ "voice, or --clone / --instructions.")
)
parser.add_argument(
"--debug", action="store_true",
@@ -610,22 +656,25 @@ 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). 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 auto-select when the server hosts exactly one "
- "entry.")
+ help=("audio.cpp server model entry id (--backend audiocpp) or "
+ "SGLang-Omni catalog model key (--backend sglomni, e.g. "
+ "higgs_audio_v3_tts) to use for this run. audiocpp: "
+ "required when the server hosts several lazily-loaded "
+ "models. sglomni: required when several models are "
+ "installed (one model per server process); auto-selected "
+ "when exactly one is installed.")
)
parser.add_argument(
"--instructions", type=str, default=None, metavar="TEXT",
help=("Voice design or style instruction sent with every request "
- "(--backend audiocpp or qwen). With audiocpp it is required "
- "for voice design models (server entries with task 'vdes', "
- "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'.")
+ "(--backend audiocpp, qwen or sglomni). With audiocpp it is "
+ "required for voice design models (server entries with task "
+ "'vdes', 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; with sglomni it is required for the VoiceDesign "
+ "model, e.g. 'A warm adult female narrator with a British "
+ "accent'.")
)
parser.add_argument(
"--option", action="append", type=str, default=None, metavar="KEY=VALUE",
@@ -753,6 +802,43 @@ Examples:
args.language = normalize_language(args.language)
except ValueError as exc:
parser.error(str(exc))
+ elif args.backend == BACKEND_SGLOMNI:
+ # sglomni: --clone hands a reference clip to the server per
+ # request (transcribed locally when no --transcription is given);
+ # --instructions design the voice on the VoiceDesign model;
+ # --voice names a preset on speaker-capable models. Language is
+ # sent through (the server accepts a display-name hint).
+ 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")
+ try:
+ from backends.sglomni import models as sg_models
+ installed = sg_models.installed_keys()
+ except Exception:
+ installed = None
+ if installed is not None:
+ if not installed:
+ parser.error("--backend sglomni: no models are downloaded "
+ "— install one via the TUI's Configure "
+ "Backends → SGLang-Omni first")
+ if args.model is None:
+ if len(installed) > 1:
+ parser.error(
+ "--backend sglomni requires --model when several "
+ "models are installed (installed: "
+ f"{', '.join(installed)})")
+ args.model = installed[0]
+ elif sg_models.entry_by_key(args.model) is None:
+ parser.error(f"--model {args.model!r} is not an "
+ "SGLang-Omni catalog key (installed: "
+ f"{', '.join(installed)})")
+ if args.clone and not Path(args.clone).is_file():
+ parser.error(f"--clone: no such reference audio file: {args.clone}")
else:
# qwen: --voice names a built-in CustomVoice speaker (the only
# voice mode that needs one; --clone and --instructions pick the
@@ -779,14 +865,17 @@ Examples:
"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 "
- "audio.cpp server model entry id")
+ if args.model is not None and args.backend not in \
+ (BACKEND_AUDIOCPP, BACKEND_SGLOMNI):
+ parser.error("--model requires --backend audiocpp or sglomni; it "
+ "selects an audio.cpp server model entry id or an "
+ "SGLang-Omni catalog model key")
if args.instructions is not None and args.backend not in \
- (BACKEND_AUDIOCPP, BACKEND_QWEN):
- parser.error("--instructions requires --backend audiocpp or qwen; "
- "it is sent as the request's voice design/style field")
+ (BACKEND_AUDIOCPP, BACKEND_QWEN, BACKEND_SGLOMNI):
+ parser.error("--instructions requires --backend audiocpp, qwen or "
+ "sglomni; it is sent as the request's voice "
+ "design/style field")
request_options = {}
if args.option: