From 77a54b848f5805e9eb44e4b339fd12bbfea1c6ef Mon Sep 17 00:00:00 2001 From: mARTin-B78 Date: Thu, 28 May 2026 09:27:59 +0200 Subject: [PATCH] Add inline STT test panel and fix whisperx pyannote auth error message - STT section now has a Quick test panel: select backend, hit mic button, see transcript. Records via MediaRecorder, posts to /api/transcribe-bytes. - _transcribe_audio detects the whisperx-gpu 'NoneType/to' error (caused by pyannote/speaker-diarization-3.1 requiring a HuggingFace token) and replaces it with an actionable message explaining how to fix it. - _to_wav_16k added for STT audio conversion (Whisper/wav2vec2 expect 16kHz). Co-Authored-By: Claude Sonnet 4.6 --- server.py | 6 +++ static/app.js | 82 +++++++++++++++++++++++++++++++++++++ static/sections/s-llms.html | 13 ++++++ static/style.css | 67 ++++++++++++++++++++++++++++++ 4 files changed, 168 insertions(+) diff --git a/server.py b/server.py index 0347998..856bd74 100644 --- a/server.py +++ b/server.py @@ -2104,6 +2104,12 @@ def _transcribe_audio(src: Path, settings: dict, backend: str = "configured") -> detail = resp.text[:300].strip() if not detail or detail.lower() in {"internal server error", "unknown error"}: detail = f"HTTP {resp.status_code} — backend may be misconfigured or missing CUDA support" + # Detect pyannote speaker-diarization gated-model auth error + if "'NoneType'" in detail and "'to'" in detail: + detail = ("Speaker diarization failed — pyannote/speaker-diarization-3.1 requires " + "a HuggingFace token. Get one at hf.co/settings/tokens and accept the " + "model license at hf.co/pyannote/speaker-diarization-3.1, then add the " + "token to the whisperx-gpu container env as HF_TOKEN.") raise RuntimeError(f"STT ({stt_url}): {detail}") return _transcription_text_from_response(resp), backend diff --git a/static/app.js b/static/app.js index a82badc..f4d97e1 100644 --- a/static/app.js +++ b/static/app.js @@ -7455,6 +7455,88 @@ document.querySelectorAll('.dc-refresh-btn').forEach(b => b.addEventListener('cl }); }); + // ── Inline STT quick-test panel ───────────────────────────────────────── + (async function initSttTestPanel() { + const panel = $('stt-test-panel'); + const sel = $('stt-test-backend'); + const micBtn = $('stt-test-mic-btn'); + const statusEl = $('stt-test-status'); + const resultEl = $('stt-test-result'); + if (!panel || !micBtn) return; + + // Populate backend dropdown + async function refreshSttTestBackends() { + try { + const d = await fetch('/api/stt-backends').then(r => r.json()); + const prev = sel.value; + sel.innerHTML = (d.backends || []).map(b => + `` + ).join(''); + if (prev && sel.querySelector(`option[value="${CSS.escape(prev)}"]`)) sel.value = prev; + } catch(_) {} + } + refreshSttTestBackends(); + + if (!navigator.mediaDevices?.getUserMedia) { + micBtn.disabled = true; + micBtn.title = 'Microphone unavailable (requires HTTPS or localhost)'; + return; + } + + let mediaRecorder = null; + let chunks = []; + + micBtn.addEventListener('click', async () => { + if (mediaRecorder && mediaRecorder.state === 'recording') { + mediaRecorder.stop(); + return; + } + if (micBtn.classList.contains('busy')) return; + chunks = []; + try { + const stream = await navigator.mediaDevices.getUserMedia({ audio: true }); + mediaRecorder = new MediaRecorder(stream); + mediaRecorder.ondataavailable = e => { if (e.data.size) chunks.push(e.data); }; + mediaRecorder.onstop = async () => { + stream.getTracks().forEach(t => t.stop()); + micBtn.className = 'stt-test-mic busy'; + micBtn.innerHTML = ''; + statusEl.textContent = 'Transcribing…'; + resultEl.style.display = 'none'; + const blob = new Blob(chunks, { type: 'audio/webm' }); + const form = new FormData(); + form.append('file', blob, 'audio.webm'); + form.append('backend', sel.value || 'configured'); + try { + const r = await fetch('/api/transcribe-bytes', { method: 'POST', body: form }); + const d = await r.json(); + if (d.text !== undefined) { + resultEl.textContent = d.text || '(no speech detected)'; + statusEl.textContent = 'Done'; + } else { + resultEl.textContent = '⚠ ' + (d.detail || JSON.stringify(d)); + statusEl.textContent = 'Error'; + } + } catch(e) { + resultEl.textContent = '⚠ ' + e.message; + statusEl.textContent = 'Error'; + } + resultEl.style.display = ''; + micBtn.className = 'stt-test-mic'; + micBtn.innerHTML = ''; + }; + mediaRecorder.start(); + micBtn.className = 'stt-test-mic recording'; + micBtn.innerHTML = ''; + statusEl.textContent = 'Recording…'; + resultEl.style.display = 'none'; + } catch(e) { + statusEl.textContent = 'Mic error'; + toast('Microphone error: ' + e.message, 'error'); + } + }); + })(); + })(); // ── LLM snippet collapse ─────────────────────────────────────────────────── diff --git a/static/sections/s-llms.html b/static/sections/s-llms.html index a09799f..262b012 100644 --- a/static/sections/s-llms.html +++ b/static/sections/s-llms.html @@ -292,6 +292,19 @@ Check "Enable CORS" for browser access
+ +
+
+ Quick test + + + Ready +
+ +
+

Other local STT

diff --git a/static/style.css b/static/style.css index b4a3a12..f5e1d71 100644 --- a/static/style.css +++ b/static/style.css @@ -1706,6 +1706,73 @@ code { background: var(--panel); border-radius: 4px; padding: 1px 5px; font-fami grid-column: 1 / -1; padding: 18px 0; text-align: center; font-size: 13px; color: var(--subtext); } +.stt-test-panel { + background: var(--surface2); + border: 1px solid var(--border); + border-radius: 10px; + padding: 10px 14px; + margin-bottom: 18px; + display: flex; + flex-direction: column; + gap: 8px; +} +.stt-test-row { + display: flex; + align-items: center; + gap: 10px; + flex-wrap: wrap; +} +.stt-test-label { + font-size: 12px; + font-weight: 600; + color: var(--subtext); + text-transform: uppercase; + letter-spacing: .04em; + white-space: nowrap; +} +.stt-test-select { + font-size: 13px; + padding: 4px 8px; + border-radius: 6px; + border: 1px solid var(--border); + background: var(--surface); + color: var(--text); + flex: 1; + min-width: 160px; + max-width: 320px; +} +.stt-test-mic { + width: 34px; height: 34px; + border-radius: 50%; + border: none; + background: var(--accent); + color: #fff; + font-size: 16px; + cursor: pointer; + display: flex; align-items: center; justify-content: center; + flex-shrink: 0; + transition: background .15s, transform .1s; +} +.stt-test-mic:hover { background: var(--accent-hover, #0060d0); } +.stt-test-mic.recording { background: #e53e3e; animation: stt-pulse 1s infinite; } +.stt-test-mic.busy { background: var(--subtext); cursor: not-allowed; } +@keyframes stt-pulse { 0%,100% { transform: scale(1); } 50% { transform: scale(1.1); } } +.stt-test-status { + font-size: 12px; + color: var(--subtext); + min-width: 60px; +} +.stt-test-result { + font-size: 13px; + color: var(--text); + background: var(--surface); + border: 1px solid var(--border); + border-radius: 6px; + padding: 8px 10px; + white-space: pre-wrap; + word-break: break-word; + min-height: 32px; +} .dc-load-err { color: var(--red) !important; } .dc-card { background: var(--surface); border: 1px solid var(--border);