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
+ +