From 2547619998a0844d3a3d6d08343b96a71ae5be21 Mon Sep 17 00:00:00 2001 From: mARTin-B78 Date: Fri, 5 Jun 2026 14:44:30 +0200 Subject: [PATCH] Refine benchmark tabs and model pickers --- routes/docker.py | 49 +++++++++++++++++ static/js/benchmark.js | 117 ++++++++++++++++++++++++++++++++++++++++- static/style.css | 30 +++++++++-- 3 files changed, 191 insertions(+), 5 deletions(-) diff --git a/routes/docker.py b/routes/docker.py index d25e6ff..d089568 100644 --- a/routes/docker.py +++ b/routes/docker.py @@ -154,3 +154,52 @@ async def probe_url(url: str, type: str = ""): except Exception as e: last_err = str(e) return {"ok": False, "error": last_err} + +@router.get("/api/engine-models") +async def engine_models(url: str, type: str = ""): + """Return model IDs from an OpenAI-style engine URL without exposing host-only URLs to the browser.""" + base = _normalize_service_url(url).rstrip("/") + if not base: + raise HTTPException(400, "URL is required") + candidates: list[str] + if base.endswith("/v1"): + candidates = [base + "/models", base[:-3].rstrip("/") + "/v1/models"] + else: + candidates = [base.rstrip("/") + "/v1/models", base.rstrip("/") + "/models"] + if type == "ollama": + candidates.append(base.rstrip("/") + "/api/tags") + last_err = "No response" + seen: set[str] = set() + models: list[str] = [] + for endpoint in candidates: + try: + r = requests.get(endpoint, timeout=6, headers={"Authorization": "Bearer no-key", "User-Agent": "TTS-Voice-Creator/models"}) + if r.status_code >= 400: + last_err = f"HTTP {r.status_code} from {endpoint}" + continue + payload = r.json() + data = payload.get("data") if isinstance(payload, dict) else None + if data is None and isinstance(payload, dict): + data = payload.get("models") + if data is None and isinstance(payload, list): + data = payload + if not isinstance(data, list): + last_err = f"Unexpected model response from {endpoint}" + continue + for item in data: + mid = "" + if isinstance(item, str): + mid = item + elif isinstance(item, dict): + mid = str(item.get("id") or item.get("name") or item.get("model") or "") + mid = mid.strip() + if mid and mid not in seen: + seen.add(mid) + models.append(mid) + if models: + return {"ok": True, "url": base, "endpoint": endpoint, "models": models} + except requests.exceptions.Timeout: + last_err = "Timeout fetching models" + except Exception as e: + last_err = str(e) + return {"ok": False, "url": base, "models": models, "error": last_err} diff --git a/static/js/benchmark.js b/static/js/benchmark.js index b27f25e..08e6938 100644 --- a/static/js/benchmark.js +++ b/static/js/benchmark.js @@ -257,6 +257,105 @@ return out; } + + function benchmarkModelList(engine) { + const values = []; + const add = (v) => { + const s = String(v || '').trim(); + if (s && !values.includes(s)) values.push(s); + }; + add(engine.model); + (engine.models || []).forEach(add); + if (!values.length) add('whisper-1'); + return values; + } + + async function fetchBenchmarkModels(engine) { + if (!engine?.url) return []; + try { + const d = await fetchJson('/api/engine-models?type=stt&url=' + encodeURIComponent(engine.url)); + return Array.isArray(d.models) ? d.models.filter(Boolean) : []; + } catch (e) { + console.warn('[benchmark models]', engine.label || engine.id, e); + return []; + } + } + + async function hydrateBenchmarkModels(engines) { + await Promise.all(engines.map(async (engine) => { + const found = await fetchBenchmarkModels(engine); + engine.models = benchmarkModelList({ ...engine, models: found.length ? found : engine.models }); + if (!engine.models.includes(engine.model)) engine.model = engine.models[0] || engine.model || 'whisper-1'; + })); + } + + function renderBenchmarkModelOptions(index, filter = '') { + const engine = sttEngines[index]; + const picker = document.querySelector(`.bench-model-picker[data-index="${index}"]`); + const list = picker?.querySelector('.bench-model-options'); + if (!engine || !list) return; + const qv = String(filter || '').trim().toLowerCase(); + const models = benchmarkModelList(engine).filter(m => !qv || m.toLowerCase().includes(qv)); + list.innerHTML = models.length ? models.map(m => ` + `).join('') : '
No matching models
'; + } + + function setBenchmarkModel(index, model) { + const engine = sttEngines[index]; + if (!engine) return; + engine.model = String(model || '').trim() || engine.model || 'whisper-1'; + const picker = document.querySelector(`.bench-model-picker[data-index="${index}"]`); + const hidden = picker?.querySelector('.bench-engine-model'); + const label = picker?.querySelector('.bench-model-label'); + if (hidden) hidden.value = engine.model; + if (label) label.textContent = engine.model; + picker?.querySelector('.bench-model-menu')?.setAttribute('hidden', ''); + picker?.querySelector('.bench-model-trigger')?.classList.remove('open'); + } + + function closeBenchmarkModelPickers(except = null) { + document.querySelectorAll('.bench-model-picker').forEach(picker => { + if (except && picker === except) return; + picker.querySelector('.bench-model-menu')?.setAttribute('hidden', ''); + picker.querySelector('.bench-model-trigger')?.classList.remove('open'); + }); + } + + function bindBenchmarkModelPickers() { + document.querySelectorAll('.bench-model-picker').forEach(picker => { + picker.addEventListener('click', (ev) => ev.stopPropagation()); + const index = Number(picker.dataset.index); + const trigger = picker.querySelector('.bench-model-trigger'); + const menu = picker.querySelector('.bench-model-menu'); + const search = picker.querySelector('.bench-model-search'); + renderBenchmarkModelOptions(index); + trigger?.addEventListener('click', (ev) => { + ev.preventDefault(); + ev.stopPropagation(); + const opening = menu?.hasAttribute('hidden'); + closeBenchmarkModelPickers(picker); + if (opening) { + menu?.removeAttribute('hidden'); + trigger.classList.add('open'); + search.value = ''; + renderBenchmarkModelOptions(index); + setTimeout(() => search?.focus(), 0); + } else { + menu?.setAttribute('hidden', ''); + trigger.classList.remove('open'); + } + }); + search?.addEventListener('input', () => renderBenchmarkModelOptions(index, search.value)); + picker.querySelector('.bench-model-options')?.addEventListener('click', (ev) => { + const opt = ev.target.closest('.bench-model-option'); + if (!opt) return; + setBenchmarkModel(index, opt.dataset.model || ''); + }); + }); + } + async function loadBenchmarkSttEngines() { const list = q('bench-stt-engines'); const count = q('bench-stt-engine-count'); @@ -274,6 +373,7 @@ label: b.label || b.id, url: b.url, model: (b.models && b.models[0]) || b.model || 'whisper-1', + models: b.models || [], available: !!b.available, source: 'configured', })); @@ -291,6 +391,7 @@ label: c.label || c.name, url, model, + models: c.models || [], available: c.running === true || c.status === 'running', source: c.stack || 'docker', }); @@ -300,6 +401,8 @@ } sttEngines = dedupeEngines(items); + if (count) count.textContent = sttEngines.length ? `Loading models for ${sttEngines.length} engine${sttEngines.length === 1 ? '' : 's'}...` : 'No engines found.'; + await hydrateBenchmarkModels(sttEngines); if (count) count.textContent = sttEngines.length ? `${sttEngines.length} engine${sttEngines.length === 1 ? '' : 's'} loaded.` : 'No engines found.'; if (!sttEngines.length) { list.innerHTML = '
No STT engines found. Check Engines -> Speech to Text.
'; @@ -313,8 +416,19 @@ ${esc(e.url || '')} ${e.available ? 'Running' : 'Stopped'} - +
+ + + +
`).join(''); + bindBenchmarkModelPickers(); } function selectedSttEngines() { @@ -617,6 +731,7 @@ if (initialized || !q('bench-stt-run')) return; initialized = true; document.querySelectorAll('.bench-tab').forEach(btn => btn.addEventListener('click', () => setBenchTab(btn.dataset.benchTab))); + document.addEventListener('click', () => closeBenchmarkModelPickers()); q('bench-stt-refresh')?.addEventListener('click', loadBenchmarkSttEngines); q('bench-stt-run')?.addEventListener('click', runSttBenchmark); q('bench-tts-run')?.addEventListener('click', runTtsBenchmark); diff --git a/static/style.css b/static/style.css index 69d9576..a7a2405 100644 --- a/static/style.css +++ b/static/style.css @@ -2363,7 +2363,7 @@ code { background: var(--panel); border-radius: 4px; padding: 1px 5px; font-fami .bench-tabs { display: flex; gap: 4px; flex-wrap: wrap; align-items: flex-end; - margin: 0 0 14px; padding: 0; border-bottom: 1px solid var(--border); + margin: 0; padding: 0 0 0 0; border-bottom: none; } .bench-tab { display: inline-flex; align-items: center; justify-content: center; gap: 7px; @@ -2378,7 +2378,8 @@ code { background: var(--panel); border-radius: 4px; padding: 1px 5px; font-fami .bench-tab .mdi { font-size: 18px; } .bench-tab span:last-child { font-size: 11px; font-weight: 600; color: var(--subtext); } .bench-pane { display: none; } -.bench-pane.active { display: flex; flex-direction: column; gap: 14px; } +.bench-pane.active { display: flex; flex-direction: column; gap: 14px; margin-top: 0; } +.bench-pane.active > .card:first-child { border-top-left-radius: 0; } .bench-card { gap: 14px; } .bench-card-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; flex-wrap: wrap; } .bench-card-head h3 { margin: 0; font-size: 15px; color: var(--text); } @@ -2387,7 +2388,7 @@ code { background: var(--panel); border-radius: 4px; padding: 1px 5px; font-fami .bench-engine-panel { border: 1px solid var(--border); border-radius: var(--radius); background: var(--panel); overflow: hidden; } .bench-engine-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 10px 12px; border-bottom: 1px solid var(--border); font-size: 13px; } .bench-engine-head span { color: var(--subtext); } -.bench-engine-list { max-height: 320px; overflow: auto; display: flex; flex-direction: column; } +.bench-engine-list { max-height: 420px; min-height: 180px; overflow: auto; display: flex; flex-direction: column; resize: vertical; } .bench-engine-empty, .bench-empty { color: var(--subtext); font-size: 13px; padding: 16px; text-align: left; } .bench-engine-item { display: grid; grid-template-columns: auto minmax(180px, 1fr) auto minmax(160px, .45fr); @@ -2402,7 +2403,28 @@ code { background: var(--panel); border-radius: 4px; padding: 1px 5px; font-fami .bench-engine-state { font-size: 11px; font-weight: 700; padding: 2px 7px; border-radius: 999px; background: var(--panel); color: var(--subtext); border: 1px solid var(--border); } .bench-engine-item.is-online .bench-engine-state { color: var(--green); background: rgba(22,163,74,.09); border-color: rgba(22,163,74,.28); } .bench-engine-item.is-offline .bench-engine-state { color: var(--red); background: rgba(220,38,38,.07); border-color: rgba(220,38,38,.24); } -.bench-engine-model { min-width: 0; background: var(--panel); color: var(--text); border: 1px solid var(--border); border-radius: 5px; padding: 6px 8px; font-family: monospace; font-size: 12px; } +.bench-engine-model { min-width: 0; } +.bench-model-picker { position: relative; min-width: 0; } +.bench-model-trigger { + width: 100%; min-height: 32px; display: flex; align-items: center; justify-content: space-between; gap: 8px; + background: var(--panel); color: var(--text); border: 1px solid var(--border); border-radius: 5px; + padding: 6px 8px; font-family: monospace; font-size: 12px; cursor: pointer; text-align: left; +} +.bench-model-trigger.open, .bench-model-trigger:hover { border-color: var(--accent); filter: none; } +.bench-model-label { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.bench-model-menu { + position: absolute; z-index: 240; top: calc(100% + 4px); left: 0; right: 0; + background: var(--surface); border: 1px solid var(--border); border-radius: 7px; box-shadow: var(--shadow); + overflow: hidden; min-width: min(520px, 90vw); +} +.bench-model-menu[hidden] { display: none !important; } +.bench-model-search-row { display: flex; align-items: center; gap: 8px; padding: 8px 10px; border-bottom: 1px solid var(--border); color: var(--subtext); } +.bench-model-search { flex: 1; min-width: 0; border: none; outline: none; background: transparent; color: var(--text); font-size: 13px; } +.bench-model-options { max-height: 260px; overflow: auto; padding: 4px; } +.bench-model-option { width: 100%; display: flex; align-items: center; justify-content: space-between; gap: 10px; padding: 8px 9px; border: 0; border-radius: 5px; background: transparent; color: var(--text); font-family: monospace; font-size: 12px; cursor: pointer; text-align: left; } +.bench-model-option:hover, .bench-model-option.selected { background: var(--hover); filter: none; } +.bench-model-option.selected { color: var(--accent); font-weight: 700; } +.bench-model-empty { color: var(--subtext); font-size: 12px; padding: 12px; } .bench-status { font-size: 13px; color: var(--subtext); } .bench-summary { display: flex; gap: 10px 18px; flex-wrap: wrap; min-height: 20px; } .bench-table td strong { display: block; }