From 8579517a35ef1865fc9b428899d73d52dcb27a14 Mon Sep 17 00:00:00 2001 From: historia Date: Wed, 2 Sep 2026 01:26:09 -0400 Subject: feat: sglang backend support --- audiobook.py | 135 +++++++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 112 insertions(+), 23 deletions(-) (limited to 'audiobook.py') 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: -- cgit v1.2.3