aboutsummaryrefslogtreecommitdiff
path: root/app/backends/sglomni/catalog.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 /app/backends/sglomni/catalog.py
parent391f50da7a085bec75155c0eb9b47910266058cc (diff)
downloadtts-audiobook-generator-8579517a35ef1865fc9b428899d73d52dcb27a14.tar.gz
feat: sglang backend support
Diffstat (limited to 'app/backends/sglomni/catalog.py')
-rw-r--r--app/backends/sglomni/catalog.py277
1 files changed, 277 insertions, 0 deletions
diff --git a/app/backends/sglomni/catalog.py b/app/backends/sglomni/catalog.py
new file mode 100644
index 0000000..c89e865
--- /dev/null
+++ b/app/backends/sglomni/catalog.py
@@ -0,0 +1,277 @@
+"""The sglang-omni model catalog: what can be installed, hosted, and how.
+
+One server process hosts one model (`sgl-omni serve --model-path <hf-repo>
+--config <yaml>`), so each entry is one launchable unit — unlike audio.cpp's
+multi-entry server.json. Every entry states how its voice is supplied
+(`capability`), whether a reference clip is mandatory for narration
+(`requires_reference`), and which model-companion packages (`extras`) must
+be pip-installed into the backend venv before its server will start.
+
+The catalog is static knowledge about sglang-omni's supported TTS models
+(v0.1.4-era), not a live query: the server's /v1/models answers only which
+model is currently hosted. Entries carry the HuggingFace repo id verbatim —
+it is both the `--model-path` value and the model name /v1/models reports,
+so a running server is matched back to its entry by that id.
+
+Voice capabilities (mirroring the audio.cpp client's vocabulary):
+ speaker the voice is a named preset shipped with the model (Qwen3-TTS
+ CustomVoice speaker table; Voxtral's preset voices)
+ clone the voice comes from a reference clip (per-request ref_audio +
+ ref_text, transcribed with Whisper when not provided)
+ design the voice is described by instructions (Qwen3-TTS VoiceDesign)
+"""
+
+from dataclasses import dataclass, field
+from typing import Dict, List, Optional, Tuple
+
+# Voice capabilities (same words the audio.cpp client uses).
+CAPABILITY_SPEAKER = "speaker"
+CAPABILITY_CLONE = "clone"
+CAPABILITY_DESIGN = "design"
+
+# A model-companion pip package: (requirement spec, no_deps). The no-deps
+# flag mirrors upstream install instructions that must not drag in their
+# own (conflicting) transformer pins — the Qwen3-TTS companion pins
+# Transformers 4 against sglang-omni's Transformers 5 stack.
+Extra = Tuple[str, bool]
+
+
+@dataclass(frozen=True)
+class ModelEntry:
+ """One installable/hostable sglang-omni TTS model."""
+ key: str # catalog id used by --model and the forms
+ label: str # human-readable name (menus, status lines)
+ repo: str # HuggingFace repo id == --model-path value
+ config: Optional[str] # vendored config yaml file name, None = none
+ capability: str # speaker / clone / design
+ requires_reference: bool # clone models: narration needs ref audio
+ extras: Tuple[Extra, ...] = field(default=())
+ system_dep: Optional[str] = None # system binary the extras need
+ system_hint: Optional[str] = None # remediation when the binary is absent
+ speakers: Optional[Tuple[str, ...]] = None # preset voices (speaker)
+ supports_seed: bool = False # request-scoped seed accepted (Qwen3-TTS Base)
+ notes: str = "" # one-line description (documentation)
+ # The model's DEFAULT pipeline dynamically quantizes its MoE experts to
+ # FP8 at load time (sglang-omni's zonos2 config hardcodes it) — a Triton
+ # fp8e4nv kernel that only compiles on compute capability 8.9+ (RTX
+ # 4090/5090, Hopper). On older GPUs the server dies mid-boot; BF16_CONFIG
+ # is the vendored config that turns FP8 off so the model runs in bf16
+ # (~2x the MoE VRAM) there instead.
+ fp8_moe: bool = False
+ fp8_min_compute_capability: Optional[Tuple[int, int]] = None
+ bf16_config: Optional[str] = None
+
+
+# The Qwen3-TTS CustomVoice speaker table — the same built-in speakers the
+# qwen-tts demo and audio.cpp's CustomVoice entry expose (kept as a local
+# copy so this catalog stays importable without the converter package; the
+# test suite asserts it matches converter.clients.speakers).
+QWEN_CUSTOMVOICE_SPEAKERS: Tuple[str, ...] = (
+ "Vivian", "Serena", "Uncle_Fu", "Dylan", "Eric",
+ "Ryan", "Aiden", "Ono_Anna", "Sohee")
+
+# The Qwen3-TTS companions follow the upstream TTS guide exactly: the
+# qwen-tts demo package installs WITHOUT dependencies (its Transformers 4
+# pin would replace sglang-omni's pinned 5.12 stack; sglang-omni shims the
+# API differences), as do sox/einops (a normal sox resolve pulls numpy past
+# the ceiling numba imposes). The sox *binary* is a system package the
+# wizard detects and guides (it is required at synthesis time).
+_QWEN_EXTRAS: Tuple[Extra, ...] = (
+ ("sox", True), ("einops", True), ("qwen-tts==0.1.1", True))
+_SOX_HINT = ("install the sox system package (e.g. sudo pacman -S sox, "
+ "sudo apt install sox, brew install sox)")
+# The Fish Audio and ZONOS2 pipelines use the Descript DAC codec, which
+# upstream installs WITH dependencies (nothing conflicts).
+_DAC_EXTRAS: Tuple[Extra, ...] = (
+ ("descript-audiotools==0.7.2", False),
+ ("descript-audio-codec==1.0.0", False))
+
+ENTRIES: Tuple[ModelEntry, ...] = (
+ ModelEntry(
+ key="qwen3_tts_0_6b_customvoice",
+ label="Qwen3-TTS 0.6B CustomVoice",
+ repo="Qwen/Qwen3-TTS-12Hz-0.6B-CustomVoice",
+ config="qwen3_tts_0_6b_customvoice.yaml",
+ capability=CAPABILITY_SPEAKER,
+ requires_reference=False,
+ extras=_QWEN_EXTRAS, system_dep="sox", system_hint=_SOX_HINT,
+ speakers=QWEN_CUSTOMVOICE_SPEAKERS,
+ notes="built-in speakers, lightest model",
+ ),
+ ModelEntry(
+ key="qwen3_tts_0_6b_base",
+ label="Qwen3-TTS 0.6B Base",
+ repo="Qwen/Qwen3-TTS-12Hz-0.6B-Base",
+ config="qwen3_tts_0_6b.yaml",
+ capability=CAPABILITY_CLONE,
+ requires_reference=True,
+ extras=_QWEN_EXTRAS, system_dep="sox", system_hint=_SOX_HINT,
+ supports_seed=True,
+ notes="voice cloning from a reference clip",
+ ),
+ ModelEntry(
+ key="qwen3_tts_1_7b_base",
+ label="Qwen3-TTS 1.7B Base",
+ repo="Qwen/Qwen3-TTS-12Hz-1.7B-Base",
+ config="qwen3_tts_1_7b.yaml",
+ capability=CAPABILITY_CLONE,
+ requires_reference=True,
+ extras=_QWEN_EXTRAS, system_dep="sox", system_hint=_SOX_HINT,
+ supports_seed=True,
+ notes="voice cloning, higher quality",
+ ),
+ ModelEntry(
+ key="qwen3_tts_1_7b_voicedesign",
+ label="Qwen3-TTS 1.7B VoiceDesign",
+ repo="Qwen/Qwen3-TTS-12Hz-1.7B-VoiceDesign",
+ config="qwen3_tts_1_7b_voicedesign.yaml",
+ capability=CAPABILITY_DESIGN,
+ requires_reference=False,
+ extras=_QWEN_EXTRAS, system_dep="sox", system_hint=_SOX_HINT,
+ notes="voice described by instructions",
+ ),
+ ModelEntry(
+ key="higgs_audio_v3_tts",
+ label="Higgs Audio v3 TTS",
+ repo="bosonai/higgs-audio-v3-tts-4b",
+ config=None,
+ capability=CAPABILITY_CLONE,
+ requires_reference=False,
+ notes="zero-shot narration, cloning from a reference clip",
+ ),
+ ModelEntry(
+ key="moss_tts",
+ label="MOSS-TTS v1.5",
+ repo="OpenMOSS-Team/MOSS-TTS-v1.5",
+ config="moss_tts.yaml",
+ capability=CAPABILITY_CLONE,
+ requires_reference=True,
+ notes="voice cloning from a reference clip",
+ ),
+ ModelEntry(
+ key="moss_tts_local",
+ label="MOSS-TTS Local v1.5",
+ repo="OpenMOSS-Team/MOSS-TTS-Local-Transformer-v1.5",
+ config="moss_tts_local.yaml",
+ capability=CAPABILITY_CLONE,
+ requires_reference=False,
+ notes="48 kHz, narration without a reference or cloning",
+ ),
+ ModelEntry(
+ key="voxtral_tts",
+ label="Voxtral TTS 4B",
+ repo="mistralai/Voxtral-4B-TTS-2603",
+ config="voxtral_tts.yaml",
+ capability=CAPABILITY_SPEAKER,
+ requires_reference=False,
+ notes="preset voices, text-only requests",
+ ),
+ ModelEntry(
+ key="dots_tts_mf",
+ label="dots.tts (MeanFlow)",
+ repo="dots-studio/dots.tts-mf",
+ config="dots_tts.yaml",
+ capability=CAPABILITY_CLONE,
+ requires_reference=True,
+ notes="voice cloning, requires reference audio + transcript",
+ ),
+ ModelEntry(
+ key="fish_s2_pro",
+ label="Fish Speech S2-Pro",
+ repo="fishaudio/s2-pro",
+ config="s2pro_tts.yaml",
+ capability=CAPABILITY_CLONE,
+ requires_reference=False,
+ extras=_DAC_EXTRAS,
+ notes="zero-shot narration or cloning from a reference clip",
+ ),
+ ModelEntry(
+ key="zonos2",
+ label="ZONOS2",
+ repo="Zyphra/zonos2",
+ config=None,
+ capability=CAPABILITY_CLONE,
+ requires_reference=True,
+ extras=_DAC_EXTRAS,
+ notes="voice cloning, 44.1 kHz DAC vocoder",
+ fp8_moe=True,
+ fp8_min_compute_capability=(8, 9),
+ bf16_config="zonos2_bf16.yaml",
+ ),
+)
+
+_BY_KEY: Dict[str, ModelEntry] = {entry.key: entry for entry in ENTRIES}
+_BY_REPO: Dict[str, ModelEntry] = {entry.repo: entry for entry in ENTRIES}
+
+
+def entry_by_key(key: str) -> Optional[ModelEntry]:
+ """The catalog entry for a catalog KEY, or None."""
+ return _BY_KEY.get(key)
+
+
+def entry_by_repo(repo: str) -> Optional[ModelEntry]:
+ """The catalog entry hosting REPO (a /v1/models id), or None."""
+ return _BY_REPO.get(repo)
+
+
+def entries_by_keys(keys) -> List[ModelEntry]:
+ """The ENTRIES for KEYS, in catalog order (unknown keys dropped)."""
+ wanted = set(keys)
+ return [entry for entry in ENTRIES if entry.key in wanted]
+
+
+def config_path(entry: ModelEntry):
+ """The vendored config yaml path for ENTRY, or None when it runs on
+ --model-path alone (Higgs, ZONOS2). A declared-but-missing file means
+ a broken install — callers treat that like a missing entry."""
+ return _config_file(entry.config)
+
+
+def fallback_config_path(entry: ModelEntry):
+ """The vendored bf16 config yaml for ENTRY (None when it has none).
+
+ The config a GPU below the entry's fp8_min_compute_capability launches
+ with instead of the model's default FP8-quantized pipeline."""
+ return _config_file(entry.bf16_config)
+
+
+def _config_file(name: Optional[str]):
+ """CONFIGS_DIR/NAME, or None when NAME is None (no config for ENTRY)."""
+ if name is None:
+ return None
+ from .constants import CONFIGS_DIR
+ return CONFIGS_DIR / name
+
+
+def install_tree_families(available: List[ModelEntry]) -> List[dict]:
+ """The checkbox-tree shape for the model picker, grouped by upstream.
+
+ One family per model origin with one option per model, so the picker
+ reads like the audio.cpp one (a collapsed tree of related packages).
+ No ``detail`` is set: the tree's status line under the buttons would
+ only repeat the catalog keys under the cursor. AVAILABLE filters what
+ is shown (already-installed models stay listed so they can be
+ re-checked or repaired).
+ """
+ groups: List[Tuple[str, List[ModelEntry]]] = [
+ ("Qwen3-TTS (Qwen)", [e for e in available if e.repo.startswith("Qwen/")]),
+ ("Boson AI", [e for e in available if e.repo.startswith("bosonai/")]),
+ ("OpenMOSS", [e for e in available if e.repo.startswith("OpenMOSS-Team/")]),
+ ("Mistral AI", [e for e in available if e.repo.startswith("mistralai/")]),
+ ("dots.studio", [e for e in available if e.repo.startswith("dots-studio/")]),
+ ("Fish Audio", [e for e in available if e.repo.startswith("fishaudio/")]),
+ ("Zyphra", [e for e in available if e.repo.startswith("Zyphra/")]),
+ ]
+ families = []
+ for label, entries in groups:
+ if not entries:
+ continue
+ families.append({
+ "label": label,
+ "options": [
+ {"key": entry.key, "label": entry.label,
+ "recommended": False}
+ for entry in entries
+ ],
+ })
+ return families