Fix Connect button scope error and add custom STT cards to conversation dropdown
cardType() was defined inside initLlmsSection() IIFE but called from renderLocalContainers() which is outside that scope, causing a silent ReferenceError that reset every Connect click to failure. Moved cardType to module scope. Custom STT cards (e.g. whisperx-gpu) are now included in /api/stt-backends and appear in the Conversation STT dropdown. Added _normalize_service_url() so 0.0.0.0 URLs in stored cards are rewritten to host.docker.internal for server-side health checks. _transcribe_audio() tries /transcribe as fallback for custom backends that don't expose /v1/audio/transcriptions. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
7bced9f126
commit
d2fe6790ee
78
server.py
78
server.py
@ -186,8 +186,13 @@ def _safe_child_path(root: Path, candidate: Path) -> Path:
|
|||||||
return candidate_resolved
|
return candidate_resolved
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_service_url(url: str) -> str:
|
||||||
|
"""Replace 0.0.0.0 with host.docker.internal so server-side probes reach the host."""
|
||||||
|
return re.sub(r"(https?://)0\.0\.0\.0([\/:$])", r"\1host.docker.internal\2", str(url or ""))
|
||||||
|
|
||||||
|
|
||||||
def _validate_http_url(raw: str, *, allow_private: bool = True) -> str:
|
def _validate_http_url(raw: str, *, allow_private: bool = True) -> str:
|
||||||
raw = str(raw or "").strip()
|
raw = _normalize_service_url(str(raw or "")).strip()
|
||||||
if not raw:
|
if not raw:
|
||||||
raise HTTPException(400, "URL is required")
|
raise HTTPException(400, "URL is required")
|
||||||
parts = urlsplit(raw)
|
parts = urlsplit(raw)
|
||||||
@ -1838,13 +1843,22 @@ _STT_BACKEND_METRICS: dict[str, dict] = {
|
|||||||
_STT_VALID_BACKENDS = {"configured", "nvidia_parakeet", "nvidia_router", "faster_whisper", "whisper_cpp", "groq_whisper"}
|
_STT_VALID_BACKENDS = {"configured", "nvidia_parakeet", "nvidia_router", "faster_whisper", "whisper_cpp", "groq_whisper"}
|
||||||
|
|
||||||
def _clean_stt_backend(value: str) -> str:
|
def _clean_stt_backend(value: str) -> str:
|
||||||
key = re.sub(r"[^a-z0-9]+", "_", str(value or "configured").lower()).strip("_")
|
original = str(value or "").strip()
|
||||||
|
if original.startswith("custom:"):
|
||||||
|
return original # pass through custom card IDs unchanged
|
||||||
|
key = re.sub(r"[^a-z0-9]+", "_", original.lower()).strip("_")
|
||||||
key = _STT_BACKEND_ALIASES.get(key, key)
|
key = _STT_BACKEND_ALIASES.get(key, key)
|
||||||
return key if key in _STT_VALID_BACKENDS else "configured"
|
return key if key in _STT_VALID_BACKENDS else "configured"
|
||||||
|
|
||||||
|
|
||||||
def _stt_backend_url(settings: dict, backend: str) -> str:
|
def _stt_backend_url(settings: dict, backend: str) -> str:
|
||||||
backend = _clean_stt_backend(backend)
|
backend = _clean_stt_backend(backend)
|
||||||
|
if backend.startswith("custom:"):
|
||||||
|
custom_id = backend[len("custom:"):]
|
||||||
|
for card in settings.get("custom_engine_cards", []):
|
||||||
|
if str(card.get("id", card.get("name", ""))) == custom_id:
|
||||||
|
return card.get("url", "")
|
||||||
|
return ""
|
||||||
if backend == "nvidia_parakeet":
|
if backend == "nvidia_parakeet":
|
||||||
return settings.get("nvidia_asr_url") or _NVIDIA_ASR_DEFAULT
|
return settings.get("nvidia_asr_url") or _NVIDIA_ASR_DEFAULT
|
||||||
if backend == "nvidia_router":
|
if backend == "nvidia_router":
|
||||||
@ -1943,6 +1957,33 @@ async def stt_backends():
|
|||||||
"models": models,
|
"models": models,
|
||||||
"metrics": _STT_BACKEND_METRICS.get(backend, {}),
|
"metrics": _STT_BACKEND_METRICS.get(backend, {}),
|
||||||
})
|
})
|
||||||
|
# Custom STT cards from settings
|
||||||
|
for card in settings.get("custom_engine_cards", []):
|
||||||
|
if card.get("role") not in ("stt", "stt+tts"):
|
||||||
|
continue
|
||||||
|
raw_url = card.get("url", "").strip()
|
||||||
|
url = _validate_http_url(raw_url, allow_private=True).rstrip("/")
|
||||||
|
if not url:
|
||||||
|
continue
|
||||||
|
card_id = "custom:" + str(card.get("id", card.get("name", "")))
|
||||||
|
if (card_id, url) in seen_urls:
|
||||||
|
continue
|
||||||
|
seen_urls.add((card_id, url))
|
||||||
|
ok, models = _stt_backend_health(url)
|
||||||
|
port = _backend_port_label(url)
|
||||||
|
label = card.get("label") or card.get("name") or "Custom STT"
|
||||||
|
if port:
|
||||||
|
label = f"{port} {label}"
|
||||||
|
items.append({
|
||||||
|
"id": card_id,
|
||||||
|
"label": label,
|
||||||
|
"url": url,
|
||||||
|
"port": port,
|
||||||
|
"available": ok,
|
||||||
|
"model": "whisper-1",
|
||||||
|
"models": models,
|
||||||
|
"metrics": {},
|
||||||
|
})
|
||||||
return {"backends": items}
|
return {"backends": items}
|
||||||
|
|
||||||
|
|
||||||
@ -1966,18 +2007,26 @@ def _transcribe_audio(src: Path, settings: dict, backend: str = "configured") ->
|
|||||||
stt_key = _stt_backend_api_key(settings, backend)
|
stt_key = _stt_backend_api_key(settings, backend)
|
||||||
hdrs = {"Authorization": f"Bearer {stt_key}"} if stt_key else {}
|
hdrs = {"Authorization": f"Bearer {stt_key}"} if stt_key else {}
|
||||||
model = _stt_backend_model(backend)
|
model = _stt_backend_model(backend)
|
||||||
with src.open("rb") as f:
|
# Ordered list of transcription paths to try; custom backends may use /transcribe
|
||||||
resp = requests.post(
|
paths = ["/v1/audio/transcriptions", "/transcribe"] if backend.startswith("custom:") else ["/v1/audio/transcriptions"]
|
||||||
f"{stt_url}/v1/audio/transcriptions",
|
resp = None
|
||||||
files={"file": ("audio.wav", f, "audio/wav")},
|
for path in paths:
|
||||||
data={"model": model, "response_format": "text"},
|
|
||||||
headers=hdrs,
|
|
||||||
timeout=_STT_REQUEST_TIMEOUT,
|
|
||||||
)
|
|
||||||
if resp.status_code in {400, 404, 422, 500} and model != "whisper-1":
|
|
||||||
with src.open("rb") as f:
|
with src.open("rb") as f:
|
||||||
resp = requests.post(
|
resp = requests.post(
|
||||||
f"{stt_url}/v1/audio/transcriptions",
|
f"{stt_url}{path}",
|
||||||
|
files={"file": ("audio.wav", f, "audio/wav")},
|
||||||
|
data={"model": model, "response_format": "text"},
|
||||||
|
headers=hdrs,
|
||||||
|
timeout=_STT_REQUEST_TIMEOUT,
|
||||||
|
)
|
||||||
|
if resp.status_code == 404 and len(paths) > 1:
|
||||||
|
continue # try next path
|
||||||
|
break
|
||||||
|
if resp.status_code in {400, 404, 422, 500} and model != "whisper-1":
|
||||||
|
path = paths[-1] # retry on whichever path last responded
|
||||||
|
with src.open("rb") as f:
|
||||||
|
resp = requests.post(
|
||||||
|
f"{stt_url}{path}",
|
||||||
files={"file": ("audio.wav", f, "audio/wav")},
|
files={"file": ("audio.wav", f, "audio/wav")},
|
||||||
data={"model": "whisper-1", "response_format": "text"},
|
data={"model": "whisper-1", "response_format": "text"},
|
||||||
headers=hdrs,
|
headers=hdrs,
|
||||||
@ -3501,6 +3550,11 @@ async def restart_local_container(name: str):
|
|||||||
async def probe_url(url: str, type: str = ""):
|
async def probe_url(url: str, type: str = ""):
|
||||||
"""Server-side API probe — checks service-specific endpoints and validates JSON responses."""
|
"""Server-side API probe — checks service-specific endpoints and validates JSON responses."""
|
||||||
base = url.rstrip("/")
|
base = url.rstrip("/")
|
||||||
|
# Strip common API prefixes so callers can paste full base URLs (e.g. .../v1)
|
||||||
|
for _suffix in ("/v1", "/api/v1"):
|
||||||
|
if base.endswith(_suffix):
|
||||||
|
base = base[: -len(_suffix)]
|
||||||
|
break
|
||||||
hdrs = {"User-Agent": "TTS-Voice-Creator/probe"}
|
hdrs = {"User-Agent": "TTS-Voice-Creator/probe"}
|
||||||
|
|
||||||
# Ordered list of (path, json_key_that_must_exist_or_None)
|
# Ordered list of (path, json_key_that_must_exist_or_None)
|
||||||
|
|||||||
@ -6974,6 +6974,11 @@ function editCustomEngineCard(card) {
|
|||||||
|
|
||||||
// ── Local Docker container management ─────────────────────────────────────
|
// ── Local Docker container management ─────────────────────────────────────
|
||||||
|
|
||||||
|
function cardType(card) {
|
||||||
|
const page = card?.closest('[data-page]');
|
||||||
|
return page ? page.dataset.page : '';
|
||||||
|
}
|
||||||
|
|
||||||
async function loadLocalContainers() {
|
async function loadLocalContainers() {
|
||||||
const gridTts = $('dc-grid-tts');
|
const gridTts = $('dc-grid-tts');
|
||||||
const gridStt = $('dc-grid-stt');
|
const gridStt = $('dc-grid-stt');
|
||||||
@ -7321,12 +7326,6 @@ document.querySelectorAll('.dc-refresh-btn').forEach(b => b.addEventListener('cl
|
|||||||
return r.json();
|
return r.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
function cardType(card) {
|
|
||||||
const page = card.closest('[data-page]');
|
|
||||||
if (page) return page.dataset.page; // 'llm', 'stt', 'tts'
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
function applyCardState(card, key, connected, failed) {
|
function applyCardState(card, key, connected, failed) {
|
||||||
card.classList.toggle('llm-local-card-online', connected);
|
card.classList.toggle('llm-local-card-online', connected);
|
||||||
card.classList.toggle('llm-local-card-offline', !connected && !!failed);
|
card.classList.toggle('llm-local-card-offline', !connected && !!failed);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user