From d2fe6790eeecaf1632c23423d62ec58e2519c398 Mon Sep 17 00:00:00 2001 From: mARTin-B78 Date: Thu, 28 May 2026 02:35:38 +0200 Subject: [PATCH] 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 --- server.py | 78 +++++++++++++++++++++++++++++++++++++++++++-------- static/app.js | 11 ++++---- 2 files changed, 71 insertions(+), 18 deletions(-) diff --git a/server.py b/server.py index 344911c..0ca42d6 100644 --- a/server.py +++ b/server.py @@ -186,8 +186,13 @@ def _safe_child_path(root: Path, candidate: Path) -> Path: 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: - raw = str(raw or "").strip() + raw = _normalize_service_url(str(raw or "")).strip() if not raw: raise HTTPException(400, "URL is required") 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"} 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) return key if key in _STT_VALID_BACKENDS else "configured" def _stt_backend_url(settings: dict, backend: str) -> str: 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": return settings.get("nvidia_asr_url") or _NVIDIA_ASR_DEFAULT if backend == "nvidia_router": @@ -1943,6 +1957,33 @@ async def stt_backends(): "models": models, "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} @@ -1966,18 +2007,26 @@ def _transcribe_audio(src: Path, settings: dict, backend: str = "configured") -> stt_key = _stt_backend_api_key(settings, backend) hdrs = {"Authorization": f"Bearer {stt_key}"} if stt_key else {} model = _stt_backend_model(backend) - with src.open("rb") as f: - resp = requests.post( - f"{stt_url}/v1/audio/transcriptions", - files={"file": ("audio.wav", f, "audio/wav")}, - 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": + # Ordered list of transcription paths to try; custom backends may use /transcribe + paths = ["/v1/audio/transcriptions", "/transcribe"] if backend.startswith("custom:") else ["/v1/audio/transcriptions"] + resp = None + for path in paths: with src.open("rb") as f: 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")}, data={"model": "whisper-1", "response_format": "text"}, headers=hdrs, @@ -3501,6 +3550,11 @@ async def restart_local_container(name: str): async def probe_url(url: str, type: str = ""): """Server-side API probe — checks service-specific endpoints and validates JSON responses.""" 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"} # Ordered list of (path, json_key_that_must_exist_or_None) diff --git a/static/app.js b/static/app.js index 66c8a6e..ef8f680 100644 --- a/static/app.js +++ b/static/app.js @@ -6974,6 +6974,11 @@ function editCustomEngineCard(card) { // ── Local Docker container management ───────────────────────────────────── +function cardType(card) { + const page = card?.closest('[data-page]'); + return page ? page.dataset.page : ''; +} + async function loadLocalContainers() { const gridTts = $('dc-grid-tts'); const gridStt = $('dc-grid-stt'); @@ -7321,12 +7326,6 @@ document.querySelectorAll('.dc-refresh-btn').forEach(b => b.addEventListener('cl 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) { card.classList.toggle('llm-local-card-online', connected); card.classList.toggle('llm-local-card-offline', !connected && !!failed);