aboutsummaryrefslogtreecommitdiff
path: root/app/backends/probe.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/probe.py
parent391f50da7a085bec75155c0eb9b47910266058cc (diff)
downloadtts-audiobook-generator-8579517a35ef1865fc9b428899d73d52dcb27a14.tar.gz
feat: sglang backend support
Diffstat (limited to 'app/backends/probe.py')
-rw-r--r--app/backends/probe.py59
1 files changed, 57 insertions, 2 deletions
diff --git a/app/backends/probe.py b/app/backends/probe.py
index 818aadb..d54a9f3 100644
--- a/app/backends/probe.py
+++ b/app/backends/probe.py
@@ -26,7 +26,7 @@ it stays cheap to import alongside ``backends.common``.
import json
import urllib.parse
import urllib.request
-from typing import Optional
+from typing import List, Optional
from backends import common
@@ -35,6 +35,7 @@ IDENTITY_FASTER = "faster"
IDENTITY_QWEN_CUSTOM = "qwen-custom"
IDENTITY_QWEN_CLONE = "qwen-clone"
IDENTITY_QWEN_DESIGN = "qwen-design"
+IDENTITY_SGLOMNI = "sglomni"
# Endpoint names the converter resolves for each qwen demo server (see
# converter.clients QwenTTSClient). Mirror them here so identification matches
@@ -77,7 +78,7 @@ def _get_json(url: str, timeout: float) -> Optional[dict]:
def _identify_health(base: str, timeout: float) -> Optional[str]:
- """Identify audio.cpp / faster from their ``/health`` responses."""
+ """Identify audio.cpp / faster / sglang-omni from their /health responses."""
payload = _get_json(f"{base}/health", timeout)
if payload is None:
return None
@@ -93,6 +94,15 @@ def _identify_health(base: str, timeout: float) -> Optional[str]:
if isinstance(entries, list) and entries \
and any(isinstance(e, dict) and e.get("id") for e in entries):
return IDENTITY_AUDIOCPP
+ # sglang-omni's /health reports {"status": "healthy", "stages": [...]}
+ # (200 when serving, 503 with "unhealthy" while booting). A 503 body
+ # still parses as JSON here, so require the healthy word explicitly —
+ # an "unhealthy" sgl-omni must not count as usable. The pipeline
+ # "stages" list is confirmed as a secondary mark (present on every
+ # sgl-omni 0.1.x server) before trusting the generic-sounding status.
+ if payload.get("status") == "healthy" \
+ and isinstance(payload.get("stages"), list):
+ return IDENTITY_SGLOMNI
return None
@@ -184,3 +194,48 @@ def faster_model_loaded(url: str, timeout: float = DEFAULT_TIMEOUT) -> bool:
"""
payload = health_payload(url, timeout)
return bool(payload and payload.get("model_loaded"))
+
+
+def sglomni_served_model(url: str,
+ timeout: float = DEFAULT_TIMEOUT) -> Optional[str]:
+ """The HuggingFace repo id a sglang-omni server at URL hosts, or None.
+
+ ``GET /v1/models`` answers ``{"data": [{"id": <served repo>}, ...]}``
+ with exactly one entry (one model per server process) — the same
+ model-identity role qwen's probe plays for its three demos. Used to
+ name the running model in statuses and to decide when a managed
+ server must be restarted to host the model a run selected.
+ """
+ if not url:
+ return None
+ models = _get_json(f"{url.rstrip('/')}/v1/models", timeout)
+ if models is None:
+ return None
+ entries = models.get("data")
+ if isinstance(entries, list) and entries \
+ and isinstance(entries[0], dict):
+ model_id = entries[0].get("id")
+ if isinstance(model_id, str) and model_id:
+ return model_id
+ return None
+
+
+def sglomni_voice_names(url: str,
+ timeout: float = DEFAULT_TIMEOUT) -> Optional[List[str]]:
+ """The uploaded voice names registered on a sglang-omni server, or None.
+
+ ``GET /v1/audio/voices?names_only=true`` answers
+ ``{"uploaded_voice_names": [...]}`` — the server-side voices a remote
+ Convert form can offer in its voice picker (uploaded clips persist
+ across server restarts). None when the URL does not answer.
+ """
+ if not url:
+ return None
+ payload = _get_json(f"{url.rstrip('/')}/v1/audio/voices?names_only=true",
+ timeout)
+ if payload is None:
+ return None
+ names = payload.get("uploaded_voice_names")
+ if isinstance(names, list):
+ return [str(name) for name in names if isinstance(name, str) and name]
+ return None