diff options
| author | historia <historiavg@proton.me> | 2026-09-02 01:26:09 -0400 |
|---|---|---|
| committer | historia <historiavg@proton.me> | 2026-09-02 01:26:09 -0400 |
| commit | 8579517a35ef1865fc9b428899d73d52dcb27a14 (patch) | |
| tree | dba52f8d99cfe4014e0b787367de99f238e5a0db /app/converter/converter.py | |
| parent | 391f50da7a085bec75155c0eb9b47910266058cc (diff) | |
| download | tts-audiobook-generator-8579517a35ef1865fc9b428899d73d52dcb27a14.tar.gz | |
feat: sglang backend support
Diffstat (limited to 'app/converter/converter.py')
| -rw-r--r-- | app/converter/converter.py | 92 |
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) |
