kasanoma_ASR / static /index.html
Kennethdotse
might take off this
033bc68
Raw
History Blame Contribute Delete
29 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Kasanoma ASR</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Instrument+Serif:ital@0;1&family=DM+Mono:wght@300;400&family=Geist:wght@300;400;500&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@latest/dist/tabler-icons.min.css" />
<style>
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #f4faf6;
--surface: #fbfdfc;
--card: #ffffff;
--border: rgba(42,92,69,0.10);
--border-md: rgba(42,92,69,0.18);
--text: #16261f;
--muted: #5f7a6c;
--hint: #9db3a6;
--accent: #2a5c45;
--accent-bg:#e8f5ee;
--red: #c0392b;
--red-bg: #fdf0ee;
--font-serif: 'Instrument Serif', Georgia, serif;
--font-sans: 'Geist', system-ui, sans-serif;
--font-mono: 'DM Mono', monospace;
--r-sm: 8px;
--r-md: 12px;
--r-lg: 18px;
}
html { scroll-behavior: smooth; }
body {
font-family: var(--font-sans);
background:
radial-gradient(ellipse 900px 600px at 12% 0%, rgba(154,211,181,0.35), transparent 60%),
radial-gradient(ellipse 900px 650px at 100% 15%, rgba(210,235,220,0.55), transparent 65%),
radial-gradient(ellipse 800px 700px at 50% 100%, rgba(180,220,197,0.30), transparent 60%),
linear-gradient(180deg, #ffffff 0%, #f2f9f5 45%, #eaf6ef 100%);
background-attachment: fixed;
color: var(--text);
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
padding: 48px 20px 80px;
}
.page { width: 100%; max-width: 600px; }
/* ── Topbar ── */
.topbar {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 52px;
}
.logo {
font-family: var(--font-serif);
font-size: 1.25rem;
color: var(--text);
letter-spacing: 0.01em;
}
.nav { display: flex; gap: 24px; list-style: none; }
.nav a {
font-size: 0.78rem;
font-weight: 400;
color: var(--muted);
text-decoration: none;
letter-spacing: 0.03em;
transition: color 0.2s;
}
.nav a:hover { color: var(--text); }
.samp{
display: flex;
align-items: center;
gap: 6px;
text-transform: none;
background: var(--accent-bg);
border-radius: 999px;
padding: 5px 13px;
margin-bottom: 18px;
}
.samp i{
color: var(--accent);
font-weight:500;
color: var(--accent);
font-style: normal;
letter-spacing: 0.05em;
}
/* ── Hero ── */
.hero { margin-bottom: 36px; }
.hero-logo {
height: 44px;
width: auto;
margin-bottom: 16px;
display: block;
}
.lang-tag {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 0.72rem;
font-weight: 500;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--accent);
background: var(--accent-bg);
border-radius: 999px;
padding: 5px 13px;
margin-bottom: 18px;
}
.lang-tag i { font-size: 13px; }
.hero h1 {
font-family: var(--font-serif);
font-size: clamp(2.2rem, 6vw, 3rem);
font-weight: 400;
font-style: italic;
color: var(--text);
line-height: 1.1;
margin-bottom: 12px;
letter-spacing: -0.01em;
}
.hero p {
font-size: 0.9rem;
font-weight: 300;
color: var(--muted);
line-height: 1.7;
max-width: 400px;
}
/* ── Card ── */
.card {
background: var(--card);
border: 1px solid var(--border);
border-radius: var(--r-lg);
overflow: hidden;
margin-bottom: 16px;
}
/* ── Section ── */
.sec { padding: 24px; }
.sec-label {
font-size: 0.65rem;
font-weight: 500;
letter-spacing: 0.16em;
text-transform: uppercase;
color: var(--hint);
margin-bottom: 14px;
}
/* ── Input rows ── */
.input-row {
display: flex;
align-items: center;
gap: 13px;
padding: 13px 14px;
border: 1px solid var(--border);
border-radius: var(--r-md);
cursor: pointer;
transition: border-color 0.18s, background 0.18s;
position: relative;
overflow: hidden;
background: var(--surface);
}
.input-row:hover { border-color: var(--border-md); background: #fff; }
.input-row + .input-row { margin-top: 8px; }
.row-icon {
width: 34px; height: 34px;
border-radius: 9px;
background: var(--bg);
border: 1px solid var(--border);
display: flex; align-items: center; justify-content: center;
flex-shrink: 0;
color: var(--muted);
font-size: 15px;
}
.row-body { flex: 1; }
.row-title { font-size: 0.85rem; font-weight: 500; color: var(--text); }
.row-sub { font-size: 0.72rem; color: var(--hint); margin-top: 1px; }
.row-arrow { color: var(--hint); font-size: 16px; }
input[type="file"] { position: absolute; inset: 0; opacity: 0; cursor: pointer; }
.input-row.drag { background: var(--accent-bg); border-color: var(--accent); }
/* ── Record state ── */
.rec-row.on { border-color: rgba(192,57,43,0.25); background: var(--red-bg); }
.rec-row.on .row-icon { background: var(--red-bg); border-color: rgba(192,57,43,0.2); color: var(--red); }
.rec-row.on .row-title { color: var(--red); }
.rec-row.on .row-arrow { display: none; }
.rec-dot { display: none; width: 7px; height: 7px; border-radius: 50%; background: var(--red); flex-shrink: 0; }
.rec-row.on .rec-dot { display: block; animation: blink 1s ease-in-out infinite; }
/* ── File chip ── */
.chip {
display: none;
align-items: center;
gap: 8px;
margin-top: 10px;
padding: 9px 13px;
border: 1px solid var(--border);
border-radius: var(--r-sm);
background: var(--surface);
font-family: var(--font-mono);
font-size: 0.75rem;
color: var(--muted);
}
.chip.on { display: flex; }
.chip-name { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.chip-x { background: none; border: none; cursor: pointer; color: var(--hint); font-size: 14px; padding: 0; line-height: 1; transition: color 0.15s; }
.chip-x:hover { color: var(--text); }
/* ── Playback ── */
.pb {
display: none;
align-items: center;
gap: 10px;
margin-top: 10px;
padding: 10px 13px;
border: 1px solid var(--border);
border-radius: var(--r-sm);
background: var(--surface);
}
.pb.on { display: flex; }
.pb-btn {
width: 30px; height: 30px;
border-radius: 50%;
border: 1px solid var(--border);
background: var(--card);
color: var(--text);
cursor: pointer;
display: flex; align-items: center; justify-content: center;
font-size: 11px;
transition: border-color 0.15s;
flex-shrink: 0;
}
.pb-btn:hover { border-color: var(--border-md); }
.pb-bars { flex: 1; display: flex; align-items: center; gap: 2px; height: 22px; cursor: pointer; }
.pb-bar { flex: 1; border-radius: 2px; background: var(--border); transition: background 0.08s; }
.pb-bar.p { background: var(--accent); opacity: 0.6; }
.pb-t { font-family: var(--font-mono); font-size: 0.68rem; color: var(--hint); white-space: nowrap; }
audio { display: none; }
/* ── Transcribe button ── */
.go-btn {
width: 100%;
margin-top: 14px;
padding: 13px;
border: 1px solid var(--border-md);
border-radius: var(--r-md);
background: var(--text);
color: #fff;
font-family: var(--font-sans);
font-size: 0.82rem;
font-weight: 500;
letter-spacing: 0.04em;
cursor: pointer;
display: flex; align-items: center; justify-content: center; gap: 8px;
transition: opacity 0.2s, transform 0.15s;
}
.go-btn:hover { opacity: 0.88; transform: translateY(-1px); }
.go-btn:active { transform: translateY(0); opacity: 1; }
.go-btn:disabled { opacity: 0.35; cursor: not-allowed; transform: none; }
.spin { display: none; width: 12px; height: 12px; border: 1.5px solid rgba(255,255,255,0.25); border-top-color: #fff; border-radius: 50%; flex-shrink: 0; }
.go-btn.loading .spin { display: block; animation: rot 0.65s linear infinite; }
/* ── Output box ── */
.out {
min-height: 76px;
padding: 14px 16px;
border: 1px solid var(--border);
border-radius: var(--r-md);
background: var(--surface);
font-family: var(--font-mono);
font-size: 0.83rem;
font-weight: 300;
line-height: 1.8;
color: var(--text);
position: relative;
}
.out-ph { color: var(--hint); font-family: var(--font-sans); font-size: 0.82rem; font-style: italic; font-weight: 300; }
.out-txt { display: none; }
.out.has .out-ph { display: none; }
.out.has .out-txt { display: block; }
.copy-btn {
position: absolute; top: 9px; right: 9px;
width: 26px; height: 26px;
border: 1px solid var(--border);
border-radius: var(--r-sm);
background: var(--card);
color: var(--hint);
font-size: 13px;
cursor: pointer;
display: flex; align-items: center; justify-content: center;
opacity: 0;
transition: opacity 0.15s, color 0.15s;
}
.out:hover .copy-btn { opacity: 1; }
.copy-btn:hover { color: var(--text); }
/* ── Textarea ── */
.edit-hint { font-size: 0.72rem; color: var(--hint); font-weight: 300; margin-bottom: 10px; line-height: 1.55; }
textarea {
width: 100%;
min-height: 84px;
padding: 13px 15px;
border: 1px solid var(--border);
border-radius: var(--r-md);
background: var(--surface);
font-family: var(--font-mono);
font-size: 0.83rem;
font-weight: 300;
line-height: 1.8;
color: var(--text);
resize: vertical;
outline: none;
transition: border-color 0.18s;
}
textarea:focus { border-color: var(--border-md); background: #fff; }
textarea::placeholder { color: var(--hint); font-style: italic; }
/* ── Action buttons ── */
.act { display: flex; gap: 8px; margin-top: 12px; }
.save-btn {
flex: 1;
padding: 12px;
border: 1px solid var(--accent);
border-radius: var(--r-md);
background: var(--accent-bg);
color: var(--accent);
font-family: var(--font-sans);
font-size: 0.82rem;
font-weight: 500;
cursor: pointer;
display: flex; align-items: center; justify-content: center; gap: 6px;
transition: background 0.18s, border-color 0.18s;
}
.save-btn:hover { background: #ddeee5; }
.save-btn:disabled { opacity: 0.45; cursor: not-allowed; }
.save-btn.saved { color: var(--accent); }
.disc-btn {
padding: 12px 16px;
border: 1px solid var(--border);
border-radius: var(--r-md);
background: var(--surface);
font-family: var(--font-sans);
font-size: 0.82rem;
font-weight: 400;
color: var(--muted);
cursor: pointer;
transition: background 0.18s, color 0.18s;
}
.disc-btn:hover { background: var(--red-bg); color: var(--red); border-color: rgba(192,57,43,0.2); }
/* ── Stats ── */
.stats {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 10px;
margin-bottom: 16px;
}
.stat {
background: var(--card);
border: 1px solid var(--border);
border-radius: var(--r-md);
padding: 18px 16px;
text-align: center;
}
.stat-v {
font-family: var(--font-serif);
font-style: italic;
font-size: 1.8rem;
color: var(--text);
line-height: 1;
}
.stat-l {
font-size: 0.65rem;
font-weight: 500;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--hint);
margin-top: 5px;
}
/* ── Footer ── */
.footer {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 8px;
padding-top: 8px;
}
.footer-content {
display: flex;
align-items: center;
gap: 24px;
}
.footer-copy { font-size: 0.72rem; color: var(--hint); }
.footer-links { display: flex; gap: 18px; }
.footer-links a { font-size: 0.72rem; color: var(--hint); text-decoration: none; transition: color 0.2s; }
.footer-links a:hover { color: var(--muted); }
.footer-logos {
display: flex;
gap: 12px;
align-items: center;
}
.footer-logo {
height: 72px;
width: 72px;
border-radius: 10%;
background: var(--surface);
transition: transform 0.2s ease, border-color 0.2s ease;
}
.footer-logo:hover {
transform: scale(1.08);
border-color: var(--accent);
}
/* ── Toast ── */
.toast {
position: fixed;
bottom: 24px;
left: 50%;
transform: translateX(-50%) translateY(10px);
padding: 9px 20px;
border-radius: 999px;
background: var(--text);
color: #fff;
font-size: 0.78rem;
font-weight: 400;
opacity: 0;
pointer-events: none;
transition: opacity 0.22s, transform 0.22s;
z-index: 999;
white-space: nowrap;
}
.toast.on { opacity: 1; transform: translateX(-50%) translateY(0); }
.toast.ok { background: var(--accent); }
.toast.err { background: var(--red); }
@keyframes blink { 0%,100%{opacity:1;transform:scale(1)}50%{opacity:.3;transform:scale(.55)} }
@keyframes rot { to { transform: rotate(360deg); } }
/* ── Header Logo Section ── */
.header-logo-section {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0px 2px 0px 2px;
margin-bottom: 28px;
animation: slideDown 0.5s ease;
border-radius: 10px;
border: 1px solid var(--border);
}
.header-logo-container {
display: flex;
align-items: center;
gap: 16px;
}
.header-logo-container img {
height: 64px;
width: 96px;
transition: transform 0.3s ease;
border-radius: 6px;
/* border: 1px solid var(--border); */
}
.header-logo-container img:hover {
transform: scale(1.06);
}
.header-title {
font-family: var(--font-serif);
font-size: 1.15rem;
font-weight: 600;
color: var(--text);
letter-spacing: -0.01em;
}
.header-nav {
display: flex;
gap: 16px;
align-items: center;
}
.header-nav-link {
display: inline-flex;
align-items: center;
gap: 5px;
padding: 7px 12px;
/* border-radius: 6px;
border: 1px solid var(--border); */
/* background: var(--accent-bg); */
color: var(--accent);
text-decoration: none;
font-size: 0.8rem;
font-weight: 500;
transition: all 0.2s ease;
font-family: var(--font-sans);
}
.header-nav-link:hover {
border-color: var(--accent);
background: rgba(42, 92, 69, 0.15);
color: var(--text);
}
@keyframes slideDown {
from { opacity: 0; transform: translateY(-12px); }
to { opacity: 1; transform: translateY(0); }
}
@media (max-width: 520px) {
body { padding: 28px 14px 60px; }
.stats { grid-template-columns: 1fr 1fr; }
.stats .stat:last-child { grid-column: span 2; }
.nav { display: none; }
.footer { flex-direction: column; gap: 10px; }
.header-nav { gap: 10px; }
.header-nav-link { font-size: 0.75rem; padding: 6px 10px; }
}
</style>
</head>
<body>
<div class="page">
<div class="header-logo-section">
<div class="header-logo-container">
<img src="/assets/logo.png" alt="Kasanoma logo" />
<div class="header-title">Kasanoma</div>
</div>
<div class="header-nav">
<a href="https://huggingface.co/datasets/Kennethdot/Ghana_English-Twi_Code-switching_ASR" class="header-nav-link">Docs</a>
<a href="https://github.com/Kennethdotse/project-kasa" class="header-nav-link">GitHub</a>
</div>
</div>
<div class="hero">
<div class="lang-tag"><i class="ti ti-language" aria-hidden="true"></i>English · Twi code-switching</div>
<h1>Record Speech<br>that mixes everyday English and Twi</h1>
<div class= "samp"><i>Eg: "Send-i file no mame seesei a, me boss ɛbisa me"</i></div>
<p>Record or upload audio, transcribe, then correct errors to help build the dataset.</p>
</div>
<div class="card">
<div class="sec">
<div class="sec-label">Audio input</div>
<label class="input-row" id="upload-row">
<div class="row-icon"><i class="ti ti-upload" aria-hidden="true"></i></div>
<div class="row-body">
<div class="row-title">Upload audio file</div>
<div class="row-sub">WAV · MP3 · M4A · OGG</div>
</div>
<i class="ti ti-chevron-right row-arrow" aria-hidden="true"></i>
<input type="file" id="audio-file-input" accept="audio/*" />
</label>
<div class="input-row rec-row" id="rec-btn">
<div class="row-icon"><i class="ti ti-microphone" aria-hidden="true"></i></div>
<div class="row-body">
<div class="row-title" id="rec-label">Record from microphone</div>
<div class="row-sub">Tap to start · tap again to stop</div>
</div>
<span class="rec-dot"></span>
<i class="ti ti-chevron-right row-arrow" aria-hidden="true"></i>
</div>
<div class="chip" id="chip">
<i class="ti ti-file-music" style="font-size:14px;flex-shrink:0" aria-hidden="true"></i>
<span class="chip-name" id="chip-name"></span>
<button class="chip-x" id="clear-file" aria-label="Remove file"></button>
</div>
<div class="pb" id="pb">
<button class="pb-btn" id="play-btn" aria-label="Play/pause">
<i class="ti ti-player-play" id="play-icon"></i>
</button>
<div class="pb-bars" id="pb-bars"></div>
<span class="pb-t" id="pb-t">0:00</span>
<audio id="audio-el"></audio>
</div>
<button class="go-btn" id="go-btn">
<span class="spin"></span>
<span class="go-text">Transcribe</span>
<i class="ti ti-arrow-right" aria-hidden="true"></i>
</button>
</div>
<div class="sec">
<div class="sec-label">Transcription</div>
<div class="out" id="out">
<span class="out-ph">Transcription will appear here…</span>
<span class="out-txt" id="out-txt"></span>
<button class="copy-btn" id="copy-btn" aria-label="Copy transcription">
<i class="ti ti-copy"></i>
</button>
</div>
</div>
<!-- COMMENTED OUT: Correct & save section
<div class="sec">
<div class="sec-label">Correct &amp; save</div>
<p class="edit-hint">Fix any errors — your correction helps improve the model.</p>
<textarea id="edit-ta" placeholder="Edit the transcription if needed…" rows="3"></textarea>
<div class="act">
<button class="save-btn" id="save-btn">
<i class="ti ti-check" aria-hidden="true"></i> Save to dataset
</button>
<button class="disc-btn" id="disc-btn">Discard</button>
</div>
</div>
-->
</div>
<!-- <div class="stats">
<div class="stat">
<div class="stat-v" id="stat-saved">0</div>
<div class="stat-l">Saved</div>
</div>
<div class="stat">
<div class="stat-v" id="stat-wer">—</div>
<div class="stat-l">WER</div>
</div>
<div class="stat">
<div class="stat-v" style="font-size:1.1rem;font-style:normal;font-family:var(--font-sans);font-weight:500;padding-top:4px;">Twi · EN</div>
<div class="stat-l">Language</div>
</div>
</div> -->
<footer class="footer">
<div class="footer-content">
<span class="footer-copy">Project Kasa · 2026</span>
<div class="footer-links">
<a href="#">Privacy</a>
<a href="#">Terms</a>
<a href="#">Contact</a>
</div>
</div>
<div class="footer-logos">
<img src="/assets/logo.png" alt="Kasanoma logo" class="footer-logo" />
<img src="/assets/gen.png" alt="Gen logo" class="footer-logo" />
</div>
</footer>
</div>
<div class="toast" id="toast"></div>
<script>
const $ = id => document.getElementById(id);
function toast(msg, type, ms = 2600) {
const t = $('toast');
t.textContent = msg;
t.className = 'toast on' + (type ? ' ' + type : '');
clearTimeout(t._t);
t._t = setTimeout(() => t.className = 'toast', ms);
}
const N = 42;
const pbBars = $('pb-bars');
for (let i = 0; i < N; i++) {
const b = document.createElement('div');
b.className = 'pb-bar';
b.style.height = Math.round(Math.random() * 68 + 20) + '%';
pbBars.appendChild(b);
}
let audioBlob = null, mediaRecorder = null, isRec = false, chunks = [], saved = 0;
const audioEl = $('audio-el');
// ── Pick the best MIME type the browser actually supports ──
function getBestMimeType() {
const types = [
'audio/webm;codecs=opus',
'audio/webm',
'audio/ogg;codecs=opus',
'audio/ogg',
'audio/mp4',
];
return types.find(t => MediaRecorder.isTypeSupported(t)) || '';
}
// ── Map MIME type → file extension ──
function mimeToExt(mime) {
if (mime.includes('ogg')) return '.ogg';
if (mime.includes('mp4')) return '.mp4';
return '.webm'; // default — ffmpeg on the backend can decode webm
}
function loadFile(file) {
audioBlob = file;
$('chip-name').textContent = file.name;
$('chip').classList.add('on');
$('pb').classList.add('on');
// Revoke any previous object URL to avoid memory leaks
if (audioEl._objUrl) URL.revokeObjectURL(audioEl._objUrl);
audioEl._objUrl = URL.createObjectURL(file);
audioEl.src = audioEl._objUrl;
audioEl.load(); // force the element to re-read the new source
}
$('audio-file-input').addEventListener('change', e => {
if (e.target.files[0]) loadFile(e.target.files[0]);
});
const uploadRow = $('upload-row');
uploadRow.addEventListener('dragover', e => { e.preventDefault(); uploadRow.classList.add('drag'); });
uploadRow.addEventListener('dragleave', () => uploadRow.classList.remove('drag'));
uploadRow.addEventListener('drop', e => {
e.preventDefault(); uploadRow.classList.remove('drag');
const f = e.dataTransfer.files[0];
if (f && f.type.startsWith('audio/')) loadFile(f);
else toast('Please drop an audio file.', 'err');
});
$('clear-file').addEventListener('click', e => {
e.preventDefault(); e.stopPropagation();
audioBlob = null;
$('audio-file-input').value = '';
$('chip').classList.remove('on');
$('pb').classList.remove('on');
if (audioEl._objUrl) { URL.revokeObjectURL(audioEl._objUrl); audioEl._objUrl = null; }
audioEl.src = '';
});
$('play-btn').addEventListener('click', () => {
if (audioEl.paused) { audioEl.play(); $('play-icon').className = 'ti ti-player-pause'; }
else { audioEl.pause(); $('play-icon').className = 'ti ti-player-play'; }
});
audioEl.addEventListener('ended', () => { $('play-icon').className = 'ti ti-player-play'; });
audioEl.addEventListener('timeupdate', () => {
const p = audioEl.duration ? audioEl.currentTime / audioEl.duration : 0;
const filled = Math.round(p * N);
[...pbBars.children].forEach((b, i) => b.classList.toggle('p', i < filled));
const m = Math.floor(audioEl.currentTime / 60);
const s = String(Math.floor(audioEl.currentTime % 60)).padStart(2, '0');
$('pb-t').textContent = `${m}:${s}`;
});
$('rec-btn').addEventListener('click', async () => {
if (!isRec) {
try {
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
const mimeType = getBestMimeType();
if (!mimeType) {
toast('Your browser doesn\'t support audio recording.', 'err');
stream.getTracks().forEach(t => t.stop());
return;
}
const options = mimeType ? { mimeType } : {};
mediaRecorder = new MediaRecorder(stream, options);
chunks = [];
mediaRecorder.ondataavailable = e => { if (e.data.size > 0) chunks.push(e.data); };
mediaRecorder.onerror = e => {
console.error('MediaRecorder error:', e);
toast('Recording error: ' + e.error, 'err');
stream.getTracks().forEach(t => t.stop());
};
mediaRecorder.onstop = () => {
// Use the actual MIME type the recorder used, not a forced 'audio/wav'
const actualMime = mediaRecorder.mimeType || mimeType || 'audio/webm';
const ext = mimeToExt(actualMime);
const blob = new Blob(chunks, { type: actualMime });
loadFile(new File([blob], `recording${ext}`, { type: actualMime }));
stream.getTracks().forEach(t => t.stop());
};
mediaRecorder.start();
isRec = true;
$('rec-btn').classList.add('on');
$('rec-label').textContent = 'Stop recording';
} catch (err) {
console.error('Microphone error:', err);
const msg = err.name === 'NotAllowedError'
? 'Microphone permission denied.'
: err.name === 'NotFoundError'
? 'No microphone found.'
: 'Microphone error: ' + err.message;
toast(msg, 'err');
}
} else {
mediaRecorder.stop();
isRec = false;
$('rec-btn').classList.remove('on');
$('rec-label').textContent = 'Record from microphone';
}
});
$('go-btn').addEventListener('click', async () => {
if (!audioBlob) { toast('Upload or record audio first.', 'err'); return; }
const btn = $('go-btn');
btn.classList.add('loading'); btn.disabled = true;
btn.querySelector('.go-text').textContent = 'Transcribing…';
try {
const fd = new FormData();
fd.append('audio', audioBlob, audioBlob.name || 'audio.webm');
const res = await fetch('/transcribe', { method: 'POST', body: fd });
if (!res.ok) {
const detail = await res.text();
throw new Error(`Server error ${res.status}: ${detail}`);
}
const { transcription = '' } = await res.json();
$('out').classList.add('has');
$('out-txt').textContent = transcription;
// $('edit-ta').value = transcription; // textarea is commented out
toast('Done', 'ok');
} catch (err) {
toast('Failed: ' + err.message, 'err');
} finally {
btn.classList.remove('loading'); btn.disabled = false;
btn.querySelector('.go-text').textContent = 'Transcribe';
}
});
$('copy-btn').addEventListener('click', () => {
const t = $('out-txt').textContent;
if (t) navigator.clipboard.writeText(t).then(() => toast('Copied', 'ok'));
});
// COMMENTED OUT: Save to dataset functionality
// $('save-btn').addEventListener('click', async () => {
// const text = $('edit-ta').value.trim();
// if (!text) { toast('Nothing to save.', 'err'); return; }
// if (!audioBlob) { toast('No audio attached.', 'err'); return; }
// const btn = $('save-btn'); btn.disabled = true;
// try {
// const fd = new FormData();
// fd.append('audio', audioBlob, audioBlob.name || 'audio.webm');
// fd.append('transcription', text);
// const res = await fetch('/save', { method: 'POST', body: fd });
// if (!res.ok) throw new Error(`Server error ${res.status}`);
// saved++; $('stat-saved').textContent = saved;
// btn.classList.add('saved');
// setTimeout(() => btn.classList.remove('saved'), 2000);
// toast('Saved to dataset', 'ok');
// } catch (err) {
// toast('Save failed: ' + err.message, 'err');
// } finally { btn.disabled = false; }
// });
// COMMENTED OUT: Discard transcription
// $('disc-btn').addEventListener('click', () => {
// $('edit-ta').value = '';
// $('out').classList.remove('has');
// $('out-txt').textContent = '';
// toast('Cleared');
// });
// COMMENTED OUT: Load real saved count from backend on page load
// fetch('/dataset/stats')
// .then(r => r.json())
// .then(d => { if (d.total) { saved = d.total; $('stat-saved').textContent = saved; } })
// .catch(() => {});
</script>
</body>
</html>