Spaces:
Sleeping
Sleeping
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <title>Audio Recorder & Transcription UI</title> | |
| <style> | |
| body { | |
| font-family: 'Segoe UI', Arial, sans-serif; | |
| background: linear-gradient(120deg, #f5f6fa 60%, #dbeafe 100%); | |
| margin: 0; | |
| padding: 0; | |
| } | |
| .container { | |
| max-width: 750px; | |
| margin: 40px auto; | |
| background: #fff; | |
| border-radius: 14px; | |
| box-shadow: 0 4px 24px #0002; | |
| padding: 32px 32px 24px 32px; | |
| } | |
| h1 { | |
| margin-top: 0; | |
| font-size: 2.2em; | |
| letter-spacing: 1px; | |
| color: #2563eb; | |
| text-align: center; | |
| } | |
| label { | |
| display: block; | |
| margin-top: 18px; | |
| font-weight: 600; | |
| color: #334155; | |
| } | |
| select, | |
| input[type="number"] { | |
| margin-top: 6px; | |
| padding: 8px; | |
| font-size: 1em; | |
| border-radius: 6px; | |
| border: 1px solid #cbd5e1; | |
| background: #f1f5f9; | |
| width: 100%; | |
| box-sizing: border-box; | |
| } | |
| button { | |
| margin-top: 12px; | |
| margin-right: 10px; | |
| padding: 10px 22px; | |
| font-size: 1em; | |
| font-weight: 600; | |
| border: none; | |
| border-radius: 6px; | |
| background: #2563eb; | |
| color: #fff; | |
| cursor: pointer; | |
| transition: background 0.2s; | |
| } | |
| button:disabled { | |
| background: #94a3b8; | |
| cursor: not-allowed; | |
| } | |
| .stop-btn { | |
| background: #dc2626; | |
| } | |
| .status { | |
| margin-top: 18px; | |
| font-weight: bold; | |
| color: #0ea5e9; | |
| text-align: center; | |
| font-size: 1.1em; | |
| } | |
| .live { | |
| margin-top: 32px; | |
| background: #f1f5f9; | |
| border-radius: 8px; | |
| padding: 18px 18px 10px 18px; | |
| } | |
| .live h2 { | |
| margin-top: 0; | |
| color: #0ea5e9; | |
| font-size: 1.2em; | |
| } | |
| .chunk { | |
| background: #e0e7ef; | |
| margin-bottom: 8px; | |
| padding: 8px 12px; | |
| border-radius: 5px; | |
| font-size: 1em; | |
| color: #334155; | |
| box-shadow: 0 1px 2px #0001; | |
| } | |
| .files { | |
| margin-top: 32px; | |
| background: #f1f5f9; | |
| border-radius: 8px; | |
| padding: 18px 18px 10px 18px; | |
| } | |
| .files h2 { | |
| margin-top: 0; | |
| color: #2563eb; | |
| font-size: 1.2em; | |
| } | |
| .file { | |
| background: #e0e7ef; | |
| margin-bottom: 8px; | |
| padding: 8px 12px; | |
| border-radius: 5px; | |
| font-size: 1em; | |
| color: #334155; | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| box-shadow: 0 1px 2px #0001; | |
| } | |
| .file a { | |
| color: #2563eb; | |
| text-decoration: none; | |
| font-weight: 500; | |
| } | |
| .file a:hover { | |
| text-decoration: underline; | |
| } | |
| .footer { | |
| margin-top: 36px; | |
| text-align: center; | |
| color: #64748b; | |
| font-size: 0.95em; | |
| } | |
| @media (max-width: 600px) { | |
| .container { | |
| padding: 12px 4vw 12px 4vw; | |
| } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <h1>Audio Recorder & Transcription</h1> | |
| <div> | |
| <label for="mic">Microphone Device</label> | |
| <select id="mic" disabled> | |
| <option value="1" selected>Microphone Device (#1)</option> | |
| </select> | |
| <label for="sys">System/Loopback Device (optional)</label> | |
| <select id="sys" disabled> | |
| <option value="16" selected>System Loopback Device (#16)</option> | |
| </select> | |
| <label for="chunk_secs">Chunk Length (seconds)</label> | |
| <input type="number" id="chunk_secs" value="5" min="1" max="60" readonly> | |
| <label for="model">Transcription Model</label> | |
| <select id="model" disabled> | |
| <option value="small">small</option> | |
| <option value="medium" selected>medium</option> | |
| <option value="large">large</option> | |
| </select> | |
| <div style="margin-top:18px; text-align:center;"> | |
| <button id="startBtn">Start Recording</button> | |
| <button id="stopBtn" class="stop-btn" disabled>Stop Recording</button> | |
| </div> | |
| </div> | |
| <div class="status" id="status"></div> | |
| <div class="live"> | |
| <h2>Live Transcription</h2> | |
| <div id="live"></div> | |
| </div> | |
| <div class="files"> | |
| <h2>Final Files</h2> | |
| <div id="files"></div> | |
| </div> | |
| <div class="footer"> | |
| © 2025 Audio Multi-Transcript UI · Powered by Flask + PyAudio + Whisper | |
| </div> | |
| </div> | |
| <script> | |
| // --- Start/Stop Recording --- | |
| let polling = null; | |
| document.getElementById('startBtn').onclick = async function () { | |
| const mic = 1; // static value | |
| const sys = 16; // static value | |
| const chunk_secs = 5; // static value | |
| const model = "medium"; // static value | |
| const no_transcribe = false; | |
| document.getElementById('status').textContent = 'Starting...'; | |
| await fetch('/api/start-recording', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ mic, sys, chunk_secs, model, no_transcribe }) | |
| }); | |
| document.getElementById('startBtn').disabled = true; | |
| document.getElementById('stopBtn').disabled = false; | |
| pollStatus(); | |
| }; | |
| document.getElementById('stopBtn').onclick = async function () { | |
| await fetch('/api/stop-recording', { method: 'POST' }); | |
| document.getElementById('status').textContent = 'Stopping...'; | |
| document.getElementById('stopBtn').disabled = true; | |
| if (polling) clearInterval(polling); | |
| setTimeout(() => { loadFiles(); document.getElementById('startBtn').disabled = false; }, 2000); | |
| }; | |
| // --- Poll status --- | |
| function pollStatus() { | |
| polling = setInterval(async () => { | |
| const res = await fetch('/api/recording-status'); | |
| const data = await res.json(); | |
| document.getElementById('status').textContent = data.recording ? 'Recording...' : 'Idle'; | |
| // --- Show live transcription --- | |
| const liveDiv = document.getElementById('live'); | |
| liveDiv.innerHTML = ''; | |
| if (data.live_segments && data.live_segments.length) { | |
| data.live_segments.slice(-10).forEach(seg => { | |
| const div = document.createElement('div'); | |
| div.className = 'chunk'; | |
| div.innerHTML = `<b>${seg.speaker || 'Speaker'}:</b> [${formatTime(seg.start)} - ${formatTime(seg.end)}] ${seg.text}`; | |
| liveDiv.appendChild(div); | |
| }); | |
| } else { | |
| liveDiv.textContent = 'No transcription yet.'; | |
| } | |
| if (!data.recording) { | |
| clearInterval(polling); | |
| document.getElementById('startBtn').disabled = false; | |
| document.getElementById('stopBtn').disabled = true; | |
| loadFiles(); | |
| } | |
| }, 1000); | |
| } | |
| // Helper to format time | |
| function formatTime(s) { | |
| if (s == null) return "0:00"; | |
| const mm = Math.floor(s / 60); | |
| const ss = Math.floor(s % 60).toString().padStart(2, "0"); | |
| return `${mm}:${ss}`; | |
| } | |
| // --- Load final files --- | |
| async function loadFiles() { | |
| const filesDiv = document.getElementById('files'); | |
| filesDiv.innerHTML = ''; | |
| try { | |
| const res = await fetch('/api/final-files'); | |
| const data = await res.json(); | |
| if (!data.files.length) { | |
| filesDiv.textContent = 'No files yet.'; | |
| return; | |
| } | |
| data.files.forEach(f => { | |
| const div = document.createElement('div'); | |
| div.className = 'file'; | |
| div.innerHTML = `<span>${f.name}</span> <a href="${f.url || f.path}" target="_blank">Download</a>`; | |
| filesDiv.appendChild(div); | |
| }); | |
| } catch (e) { | |
| filesDiv.textContent = 'Error loading files.'; | |
| } | |
| } | |
| // --- On load --- | |
| loadFiles(); | |
| </script> | |
| </body> | |
| </html> |