Spaces:
Running
Running
| <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 & 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> |