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
|
||||
|
||||
|
||||
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)
|
||||
|
||||
@ -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);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user