apibuildconnection / demo.html
kanha077's picture
Add nlp-asr-kit API
37558a0
Raw
History Blame Contribute Delete
14.8 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>nlp-asr-kit demo</title>
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0 }
:root {
--bg: #0f0f0f; --bg2: #1a1a1a; --bg3: #242424;
--border: #2e2e2e; --text: #e4e4e4; --muted: #777;
--accent: #7c6af7; --green: #5dd6a4; --red: #f97066; --amber: #f9a820;
}
body { background:var(--bg); color:var(--text); font-family:'Segoe UI',system-ui,sans-serif;
font-size:15px; line-height:1.6; min-height:100dvh; display:flex; flex-direction:column }
header { padding:1rem 1.5rem; border-bottom:1px solid var(--border);
display:flex; align-items:center; gap:.75rem }
header h1 { font-size:1rem; font-weight:600 }
.tag { font-size:11px; background:#7c6af722; color:var(--accent);
border:1px solid #7c6af744; border-radius:4px; padding:2px 8px }
main { flex:1; display:grid; grid-template-columns:260px 1fr; gap:1.5rem;
max-width:1080px; margin:0 auto; width:100%; padding:1.5rem }
.card { background:var(--bg2); border:1px solid var(--border); border-radius:10px; padding:1rem }
.card h2 { font-size:11px; text-transform:uppercase; letter-spacing:.07em;
color:var(--muted); margin-bottom:.75rem }
.left { display:flex; flex-direction:column; gap:1rem }
.right { display:flex; flex-direction:column; gap:1rem }
/* Status */
.status-bar { font-size:12px; color:var(--muted); display:flex; align-items:center; gap:.5rem }
.sdot { width:6px; height:6px; border-radius:50%; background:var(--muted); flex-shrink:0 }
.sdot.on { background:var(--green); animation:blink .8s ease infinite }
@keyframes blink { 0%,100%{opacity:1} 50%{opacity:.3} }
/* Model pills */
.model-row { display:flex; justify-content:space-between; align-items:center; font-size:13px; margin-bottom:.4rem }
.pill { font-size:11px; padding:2px 8px; border-radius:20px; font-weight:500 }
.pill.ok { background:#5dd6a422; color:var(--green) }
.pill.loading { background:#f9a82022; color:var(--amber) }
.pill.err { background:#f9706622; color:var(--red) }
.pill.idle { background:#ffffff11; color:var(--muted) }
/* Tasks */
.task-list { display:flex; flex-direction:column; gap:3px }
.task-btn { background:transparent; border:1px solid var(--border); border-radius:7px;
padding:.4rem .7rem; color:var(--text); text-align:left; cursor:pointer;
font-size:12px; transition:all .15s }
.task-btn:hover { border-color:var(--accent) }
.task-btn.active { background:#7c6af722; border-color:var(--accent); color:var(--accent) }
/* Buttons */
.btn { width:100%; padding:.65rem; background:var(--accent); color:#fff;
border:none; border-radius:8px; font-size:13px; cursor:pointer;
font-weight:600; transition:opacity .15s; margin-top:.5rem }
.btn:disabled { opacity:.4; cursor:not-allowed }
/* Record */
.record-wrap { display:flex; flex-direction:column; align-items:center; gap:.65rem }
#record-btn { width:64px; height:64px; border-radius:50%; border:2px solid var(--accent);
background:transparent; cursor:pointer; position:relative; transition:all .2s }
#record-btn:hover:not(:disabled) { background:#7c6af711 }
#record-btn:disabled { opacity:.35; cursor:not-allowed }
#record-btn.rec { border-color:var(--red); animation:pulse 1.2s ease infinite }
.dot { width:22px; height:22px; border-radius:50%; background:var(--accent);
position:absolute; top:50%; left:50%; transform:translate(-50%,-50%); transition:all .2s }
#record-btn.rec .dot { background:var(--red); border-radius:3px; width:18px; height:18px }
@keyframes pulse { 0%,100%{box-shadow:0 0 0 0 #f9706640} 50%{box-shadow:0 0 0 10px transparent} }
.rec-label { font-size:12px; color:var(--muted); text-align:center }
.vol-bar { height:3px; background:var(--bg3); border-radius:2px; width:100%; overflow:hidden }
.vol-fill { height:100%; background:var(--green); border-radius:2px; width:0; transition:width .05s }
/* Output */
.out-box { background:var(--bg2); border:1px solid var(--border); border-radius:10px; padding:1rem }
.out-box h2 { font-size:11px; text-transform:uppercase; letter-spacing:.07em;
color:var(--muted); margin-bottom:.6rem }
.out-text { font-size:14px; white-space:pre-wrap; line-height:1.75; min-height:52px }
.out-text.ph { color:var(--muted); font-style:italic }
.json-out { font-family:monospace; font-size:12px; background:var(--bg3);
border-radius:7px; padding:.75rem; color:#a8d8a8; overflow-x:auto }
.err-box { background:#f9706611; border:1px solid #f9706633; border-radius:8px;
padding:.75rem 1rem; font-size:13px; color:var(--red);
font-family:monospace; margin-top:.5rem; display:none }
.err-box.show { display:block }
textarea { width:100%; background:var(--bg3); border:1px solid var(--border);
border-radius:7px; padding:.5rem .65rem; color:var(--text);
font-size:12px; font-family:inherit; resize:vertical; min-height:52px;
outline:none; transition:border-color .15s }
textarea:focus { border-color:var(--accent) }
/* API config */
.api-input { width:100%; background:var(--bg3); border:1px solid var(--border);
border-radius:7px; padding:.4rem .65rem; color:var(--text);
font-size:12px; outline:none; transition:border-color .15s }
.api-input:focus { border-color:var(--accent) }
@media (max-width:640px) { main { grid-template-columns:1fr; padding:1rem } }
</style>
</head>
<body>
<header>
<h1>nlp-asr-kit</h1>
<span class="tag">Python API Β· Whisper + SmolLM2</span>
</header>
<main>
<div class="left">
<!-- API Config -->
<div class="card">
<h2>API endpoint</h2>
<input class="api-input" id="api-url" value="http://localhost:8000" placeholder="http://your-server:8000">
<button class="btn" id="check-btn" style="margin-top:.5rem;font-size:12px">Check connection</button>
<div style="margin-top:.5rem">
<div class="model-row"><span>Whisper ASR</span><span class="pill idle" id="asr-pill">β€”</span></div>
<div class="model-row"><span>LLM</span><span class="pill idle" id="llm-pill">β€”</span></div>
</div>
</div>
<!-- Task -->
<div class="card">
<h2>Task</h2>
<div class="task-list" id="task-list"></div>
</div>
<!-- Custom instruction -->
<div class="card">
<h2>Custom instruction</h2>
<textarea id="custom-inst" placeholder="Optional: e.g. reply in Hindi, extract only names..."></textarea>
</div>
<!-- Record -->
<div class="card">
<h2>Record</h2>
<div class="record-wrap">
<button id="record-btn" disabled><div class="dot"></div></button>
<span class="rec-label" id="rec-label">Check connection first</span>
<div class="vol-bar"><div class="vol-fill" id="vol-fill"></div></div>
</div>
</div>
</div>
<div class="right">
<div class="status-bar">
<div class="sdot" id="sdot"></div>
<span id="stext">Ready</span>
</div>
<div class="out-box">
<h2>Transcript</h2>
<div class="out-text ph" id="transcript-out">Your spoken words will appear here...</div>
</div>
<div class="out-box" style="flex:2">
<h2>LLM output Β· <span id="task-label" style="color:var(--accent)">β€”</span></h2>
<div id="result-out"><div class="out-text ph">Result will appear here after recording...</div></div>
<div id="run-err" class="err-box"></div>
</div>
<div class="card" style="font-size:12px;color:var(--muted);line-height:1.8">
<h2>Setup</h2>
<b style="color:var(--text)">1. Install & run:</b><br>
<code style="background:var(--bg3);padding:2px 6px;border-radius:4px;color:var(--green)">pip install -r requirements.txt</code><br>
<code style="background:var(--bg3);padding:2px 6px;border-radius:4px;color:var(--green)">uvicorn app.main:app --host 0.0.0.0 --port 8000</code><br><br>
<b style="color:var(--text)">2. Open this file in browser, set API URL, click Check connection.</b>
</div>
</div>
</main>
<script>
const API_URL = () => document.getElementById('api-url').value.replace(/\/$/, '')
const $ = id => document.getElementById(id)
let recording = false
let mediaRecorder = null
let audioChunks = []
let activeTask = 'summarize'
// ── Tasks ──────────────────────────────────────────────────────────────────
const TASKS = [
{ key: 'transcribe', label: 'Transcribe' },
{ key: 'summarize', label: 'Summarize' },
{ key: 'action_items', label: 'Extract action items' },
{ key: 'intent', label: 'Command intent' },
{ key: 'qa', label: 'Answer question' },
{ key: 'correct', label: 'Grammar correction' },
{ key: 'translate', label: 'Translate to English' },
{ key: 'sentiment', label: 'Sentiment' },
{ key: 'title', label: 'Generate title' },
]
const taskList = $('task-list')
TASKS.forEach(t => {
const btn = document.createElement('button')
btn.className = 'task-btn' + (t.key === activeTask ? ' active' : '')
btn.textContent = t.label
btn.onclick = () => {
activeTask = t.key
taskList.querySelectorAll('.task-btn').forEach(b => b.classList.remove('active'))
btn.classList.add('active')
$('task-label').textContent = t.label
}
taskList.appendChild(btn)
})
$('task-label').textContent = 'Summarize'
// ── Check connection ───────────────────────────────────────────────────────
$('check-btn').onclick = async () => {
setStatus('Checking connection...', true)
try {
const res = await fetch(`${API_URL()}/health`)
const data = await res.json()
if (data.models_loaded) {
setPill('asr', 'ok', 'Ready βœ“')
setPill('llm', 'ok', 'Ready βœ“')
$('record-btn').disabled = false
$('rec-label').textContent = 'Click to record'
setStatus('Connected β€” models ready')
} else {
setPill('asr', 'loading', 'Loading...')
setPill('llm', 'loading', 'Loading...')
setStatus('Server found but models still loading...')
}
} catch (e) {
setPill('asr', 'err', 'Error')
setPill('llm', 'err', 'Error')
setStatus('Cannot reach API β€” is the server running?')
}
}
// ── Record ─────────────────────────────────────────────────────────────────
$('record-btn').onclick = async () => {
if (recording) {
mediaRecorder?.stop()
return
}
try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true })
// Volume meter
const ctx = new AudioContext()
const source = ctx.createMediaStreamSource(stream)
const analyser = ctx.createAnalyser()
analyser.fftSize = 512
source.connect(analyser)
const buf = new Uint8Array(analyser.frequencyBinCount)
const volTick = () => {
if (!recording) return
analyser.getByteTimeDomainData(buf)
const vol = Math.max(...Array.from(buf).map(v => Math.abs(v - 128))) / 128
$('vol-fill').style.width = (vol * 100) + '%'
requestAnimationFrame(volTick)
}
audioChunks = []
mediaRecorder = new MediaRecorder(stream)
mediaRecorder.ondataavailable = e => audioChunks.push(e.data)
mediaRecorder.onstop = async () => {
recording = false
$('record-btn').classList.remove('rec')
$('rec-label').textContent = 'Processing...'
$('vol-fill').style.width = '0'
stream.getTracks().forEach(t => t.stop())
ctx.close()
await sendAudio()
}
mediaRecorder.start()
recording = true
$('record-btn').classList.add('rec')
$('rec-label').textContent = 'Recording... tap to stop'
$('transcript-out').textContent = ''
$('transcript-out').classList.add('ph')
$('result-out').innerHTML = '<div class="out-text ph">Listening...</div>'
$('run-err').classList.remove('show')
setStatus('Recording...', true)
volTick()
// Auto-stop at 30s
setTimeout(() => { if (recording) mediaRecorder?.stop() }, 30000)
} catch (e) {
setStatus('Microphone error: ' + e.message)
}
}
async function sendAudio() {
const blob = new Blob(audioChunks, { type: 'audio/webm' })
const form = new FormData()
form.append('audio', blob, 'recording.webm')
form.append('task', activeTask)
form.append('custom_instruction', $('custom-inst').value.trim())
setStatus('Transcribing...', true)
try {
const res = await fetch(`${API_URL()}/api/transcribe`, { method: 'POST', body: form })
if (!res.ok) throw new Error(`Server error: ${res.status}`)
const data = await res.json()
$('transcript-out').textContent = data.transcript
$('transcript-out').classList.remove('ph')
renderResult(data)
setStatus('Done βœ“')
} catch (e) {
$('run-err').textContent = e.message
$('run-err').classList.add('show')
setStatus('Error β€” see details below')
} finally {
$('rec-label').textContent = 'Click to record again'
}
}
// ── Render ─────────────────────────────────────────────────────────────────
function renderResult(data) {
const parsed = data.parsed
if (parsed?.type === 'json') {
$('result-out').innerHTML = `<pre class="json-out">${esc(JSON.stringify(parsed.value, null, 2))}</pre>`
} else if (parsed?.type === 'list') {
const items = (parsed.value || []).map(i => `<li style="margin-bottom:.3rem">${esc(i)}</li>`).join('')
$('result-out').innerHTML = `<ol style="padding-left:1.4rem;font-size:14px;line-height:1.8">${items}</ol>`
} else {
$('result-out').innerHTML = `<div class="out-text">${esc(data.raw || '')}</div>`
}
}
// ── Helpers ────────────────────────────────────────────────────────────────
function setStatus(msg, active = false) {
$('stext').textContent = msg
$('sdot').classList.toggle('on', active)
}
function setPill(id, state, label) {
const el = $(id + '-pill')
el.className = 'pill ' + state
el.textContent = label
}
function esc(s) {
return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')
}
</script>
</body>
</html>