aboutsummaryrefslogtreecommitdiff
path: root/app/converter/converter.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/converter/converter.py
parent391f50da7a085bec75155c0eb9b47910266058cc (diff)
downloadtts-audiobook-generator-8579517a35ef1865fc9b428899d73d52dcb27a14.tar.gz
feat: sglang backend support
Diffstat (limited to 'app/converter/converter.py')
-rw-r--r--app/converter/converter.py92
1 files changed, 87 insertions, 5 deletions
diff --git a/app/converter/converter.py b/app/converter/converter.py
index 32cd342..0769258 100644
--- a/app/converter/converter.py
+++ b/app/converter/converter.py
@@ -20,6 +20,7 @@ from .clients import (
BACKEND_AUDIOCPP,
BACKEND_FASTER,
BACKEND_QWEN,
+ BACKEND_SGLOMNI,
ConversionCancelled,
MODEL_SIZE,
VOICE_MODE_CLONE,
@@ -29,6 +30,7 @@ from .clients import (
AudioCppTTSClient,
FasterTTSClient,
QwenTTSClient,
+ SgOmniTTSClient,
normalize_language,
speaker_display_name_for,
)
@@ -126,20 +128,35 @@ def setup_directories() -> None:
def voice_mode_for(backend: str, voice: Optional[str] = None,
clone: Optional[str] = None,
- instructions: Optional[str] = None) -> str:
+ instructions: Optional[str] = None,
+ model: Optional[str] = None) -> str:
"""The voice mode a run with these options would use.
Mirrors the choice ``audiobook.convert`` makes from the same inputs
(faster always clones; audiocpp clones through a server-side voice;
- qwen designs with instructions, clones only with a reference .wav, and
- uses a built-in speaker otherwise), so the hub can run the pre-flight
- overwrite checks against exactly the output names the conversion will
- produce.
+ sglomni resolves from the selected model's capability — a design model
+ takes instructions, a clone-capable model clones when a reference .wav
+ is given and otherwise synthesizes its default voice, and a
+ speaker-capable model takes a preset name; qwen designs with
+ instructions, clones only with a reference .wav, and uses a built-in
+ speaker otherwise), so the hub can run the pre-flight overwrite checks
+ against exactly the output names the conversion will produce.
"""
if backend == BACKEND_FASTER:
return VOICE_MODE_CLONE
if backend == BACKEND_AUDIOCPP:
return VOICE_MODE_CLONE if voice else VOICE_MODE_CUSTOM
+ if backend == BACKEND_SGLOMNI:
+ from backends.sglomni.catalog import entry_by_key
+ entry = entry_by_key(model or "")
+ if entry is not None:
+ if entry.capability == "design":
+ return VOICE_MODE_DESIGN
+ if entry.capability == "clone":
+ return VOICE_MODE_CLONE if clone else VOICE_MODE_CUSTOM
+ return VOICE_MODE_CUSTOM
+ # Unresolved model (the caller resolves it later): the qwen-style
+ # heuristic is the closest pre-flight approximation.
if (instructions or "").strip():
return VOICE_MODE_DESIGN
return VOICE_MODE_CLONE if clone else VOICE_MODE_CUSTOM
@@ -243,6 +260,10 @@ class AudiobookConverter:
self.backend = backend
self.voice = voice
self.debug = bool(debug)
+ # The run's model selection (audio.cpp: a server entry id, sglomni:
+ # the resolved catalog key, None elsewhere) — the startup banner
+ # reports it.
+ self.model_id = model_id
# Output file names the book being converted will produce (filled in
# by convert_book; reported on the book_done/book_failed events).
self.current_outputs: List[str] = []
@@ -282,6 +303,34 @@ class AudiobookConverter:
api_url=api_url, quiet=quiet,
unload_models=unload_models,
cancel=cancel)
+ elif backend == BACKEND_SGLOMNI:
+ # SGLang-Omni hosts one model per server process; MODEL_ID
+ # names the catalog entry. Managed runs (no API_URL) require
+ # the model's weights on disk (resolved here, with the
+ # actionable message when they are not); remote runs accept
+ # any catalog key — the external server has its own weights.
+ # The client resolves the request shape from the entry's
+ # voice capability (preset speaker / per-request clone /
+ # described-voice design) at connect time.
+ from backends.sglomni import models as sg_models
+ if api_url is None:
+ entry = sg_models.resolve_model(model_id)
+ else:
+ from backends.sglomni.catalog import entry_by_key
+ entry = entry_by_key((model_id or "").strip())
+ if 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 = entry.key
+ self.model_id = model_id
+ self.tts = SgOmniTTSClient(
+ chunks_dir=CHUNKS_FOLDER, model=model_id, voice=voice,
+ ref_audio=voice_clone_ref_audio,
+ ref_text=voice_clone_ref_text,
+ skip_transcription=skip_transcription,
+ instructions=instructions, language=self.language,
+ api_url=api_url, quiet=quiet, cancel=cancel)
else:
# Qwen: the voice mode picks the request shape (built-in
# speaker, clone from a reference .wav, or a designed voice);
@@ -408,6 +457,15 @@ class AudiobookConverter:
# speaker-capable entry without --voice); keep a stable tag
# for the pre-flight of runs that will fail at connect time.
narrator = "narrator"
+ elif backend == BACKEND_SGLOMNI:
+ if voice_mode == VOICE_MODE_CLONE and voice_clone_ref_audio:
+ narrator = Path(voice_clone_ref_audio).stem
+ elif voice_mode == VOICE_MODE_DESIGN:
+ narrator = "designed"
+ else:
+ # A preset name on speaker-capable models, or the server's
+ # built-in default voice (clone models without a reference).
+ narrator = voice or "default"
elif voice_mode == VOICE_MODE_DESIGN:
# Qwen's VoiceDesign model: the voice is described by an
# instruction and has no speaker name.
@@ -738,6 +796,7 @@ class AudiobookConverter:
backend_labels = {
BACKEND_FASTER: "faster TTS API",
BACKEND_AUDIOCPP: "audio.cpp server",
+ BACKEND_SGLOMNI: "SGLang-Omni server",
}
backend = backend_labels.get(self.backend, "Qwen API")
self._say(f"[INFO] Processing {total_chunks} chunks via {backend}...")
@@ -810,6 +869,29 @@ class AudiobookConverter:
if self.request_options:
self._say(f"Request options: {self.request_options}")
self._say(f"Language: {self.language}")
+ elif self.backend == BACKEND_SGLOMNI:
+ entry = getattr(self.tts, "entry", None)
+ api_url = getattr(self.tts, "api_url", None) \
+ or config.SGLOMNI_API_URL
+ self._say(f"SGLang-Omni endpoint: {api_url}")
+ self._say(f"Model: {getattr(entry, 'label', self.model_id or '?')}"
+ f" ({getattr(entry, 'repo', '')})")
+ if getattr(entry, "capability", None) == "design":
+ self._say("Backend: SGLang-Omni (voice from --instructions "
+ "description)")
+ self._say(f"Instruction: {self.instructions}")
+ elif getattr(entry, "capability", None) == "clone":
+ if self.voice_clone_ref_audio:
+ self._say("Backend: SGLang-Omni (voice cloning from a "
+ "reference clip)")
+ self._say(f"Reference audio: "
+ f"{Path(self.voice_clone_ref_audio).name}")
+ else:
+ self._say("Backend: SGLang-Omni (model's default voice)")
+ else:
+ self._say("Backend: SGLang-Omni (built-in preset voice)")
+ self._say(f"Voice: {self.voice or 'default'}")
+ self._say(f"Language: {self.language}")
else:
tts_client = getattr(self, "tts", None)
api_url = (getattr(tts_client, "api_url", None)