Refine benchmark tabs and model pickers

This commit is contained in:
mARTin-B78 2026-06-05 14:44:30 +02:00
parent 10a11132c0
commit 2547619998
3 changed files with 191 additions and 5 deletions

View File

@ -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}

View File

@ -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 => `
<button type="button" class="bench-model-option${m === engine.model ? ' selected' : ''}" data-model="${esc(m)}">
<span>${esc(m)}</span>${m === engine.model ? '<span class="mdi mdi-check"></span>' : ''}
</button>`).join('') : '<div class="bench-model-empty">No matching models</div>';
}
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 = '<div class="bench-engine-empty">No STT engines found. Check Engines -> Speech to Text.</div>';
@ -313,8 +416,19 @@
<small>${esc(e.url || '')}</small>
</span>
<span class="bench-engine-state">${e.available ? 'Running' : 'Stopped'}</span>
<input class="bench-engine-model" data-index="${i}" type="text" value="${esc(e.model || '')}" placeholder="model">
<div class="bench-model-picker" data-index="${i}">
<button type="button" class="bench-model-trigger" title="Choose model">
<span class="bench-model-label">${esc(e.model || benchmarkModelList(e)[0] || 'whisper-1')}</span>
<span class="mdi mdi-chevron-down"></span>
</button>
<div class="bench-model-menu" hidden>
<div class="bench-model-search-row"><span class="mdi mdi-magnify"></span><input class="bench-model-search" type="search" placeholder="Search models..."></div>
<div class="bench-model-options"></div>
</div>
<input class="bench-engine-model" data-index="${i}" type="hidden" value="${esc(e.model || benchmarkModelList(e)[0] || 'whisper-1')}">
</div>
</label>`).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);

View File

@ -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; }