Spaces:
Paused
Paused
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <title>SABRe: Simple Audio Book Recorder</title> | |
| <link href="https://fonts.googleapis.com/css2?family=Mozilla+Text:wght@200..700&display=swap" rel="stylesheet"> | |
| <style> | |
| /* Mozilla Protocol Design System Variables */ | |
| :root { | |
| --moz-black: #000000; | |
| --moz-white: #ffffff; | |
| --moz-orange: #ff4f00; | |
| --moz-grey-light: #f9f9fa; | |
| --moz-grey-border: #cfcfd8; | |
| --moz-grey-dark: #4a4a4f; | |
| } | |
| body { | |
| font-family: "Inter", "Nunito Sans", Helvetica, Arial, sans-serif; | |
| margin: 0; | |
| padding: 0; | |
| background-color: var(--moz-white); | |
| color: var(--moz-black); | |
| -webkit-font-smoothing: antialiased; | |
| } | |
| h1 { | |
| font-family: "Zilla Slab", serif; | |
| font-weight: 900; /* Extra bold for that 'darker' look */ | |
| font-size: 3.5rem; /* Significantly larger */ | |
| text-transform: uppercase; | |
| letter-spacing: -0.02em; | |
| margin-bottom: 0.2em; | |
| color: var(--moz-black); | |
| display: inline-block; /* Wraps the underline to the text width */ | |
| border-bottom: 8px solid var(--moz-orange); /* Thick orange underline */ | |
| line-height: 1.1; | |
| padding-bottom: 5px; | |
| } | |
| h1 span { | |
| color: var(--moz-orange); | |
| } | |
| hr { | |
| border: 0; | |
| height: 1px; | |
| background: var(--moz-grey-border); | |
| margin: 20px auto; | |
| max-width: 900px; | |
| } | |
| /* Container for content */ | |
| .container { | |
| max-width: 900px; | |
| margin: 0 auto; | |
| padding: 0 20px; | |
| } | |
| /* Stats Box - White with grey outline, orange hover */ | |
| .stats-box { | |
| text-align: center; | |
| padding: 20px; | |
| background: var(--moz-white); | |
| color: var(--moz-grey-dark); | |
| margin: 20px auto; | |
| display: block; | |
| border: 1px solid var(--moz-grey-border); | |
| transition: all 0.2s ease-in-out; | |
| max-width: 400px; | |
| } | |
| .stats { | |
| font-size: 0.9rem; | |
| font-weight: 600; | |
| text-transform: uppercase; | |
| letter-spacing: 1px; | |
| } | |
| /* Reading Area - Minimalist */ | |
| #sentence { | |
| font-family: "Zilla Slab", serif; | |
| font-size: 1.5rem; | |
| margin: 40px auto; | |
| padding: 60px 40px; | |
| background-color: var(--moz-white); | |
| border: 1px solid var(--moz-grey-border); | |
| min-height: 150px; | |
| max-width: 800px; | |
| } | |
| /* Protocol Buttons */ | |
| button { | |
| background: var(--moz-black); | |
| color: var(--moz-white); | |
| border: 1px solid var(--moz-black); | |
| padding: 12px 24px; | |
| font-size: 1rem; | |
| font-weight: 600; | |
| cursor: pointer; | |
| transition: background 0.2s ease; | |
| margin: 5px; | |
| } | |
| button:hover:not(:disabled) { | |
| background: var(--moz-grey-dark); | |
| } | |
| /* The 'Record' button is often the primary action in your app */ | |
| #recordBtn { | |
| background-color: var(--moz-orange); | |
| border-color: var(--moz-orange); | |
| } | |
| #recordBtn:hover:not(:disabled) { | |
| background-color: #bf3b00; | |
| } | |
| button:disabled { | |
| background-color: var(--moz-grey-border); | |
| border-color: var(--moz-grey-border); | |
| color: #888; | |
| cursor: not-allowed; | |
| } | |
| /* Form styling - Clean Mozilla Look */ | |
| #uploadForm { | |
| padding: 40px; | |
| border: 1px solid var(--moz-grey-border); | |
| background: var(--moz-grey-light); | |
| max-width: 500px; | |
| margin: 20px auto; | |
| } | |
| #uploadForm button { | |
| background: var(--moz-black); | |
| margin-top: 20px; | |
| } | |
| /* Download/Delete section */ | |
| .secondary-actions { | |
| margin-top: 50px; | |
| padding: 30px; | |
| background: var(--moz-grey-light); | |
| border-top: 1px solid var(--moz-grey-border); | |
| } | |
| #downloadBtn { | |
| background: transparent; | |
| color: var(--moz-black); | |
| border: 1px solid var(--moz-black); | |
| } | |
| #downloadBtn:hover { | |
| background: var(--moz-black); | |
| color: var(--moz-white); | |
| } | |
| .delete-btn { | |
| background-color: transparent; | |
| color: #d70022; /* Mozilla Red */ | |
| border: 1px solid #d70022; | |
| } | |
| .delete-btn:hover { | |
| background-color: #d70022; | |
| color: var(--moz-white); | |
| } | |
| audio { | |
| margin-top: 20px; | |
| filter: brightness(0.95); | |
| } | |
| </style> | |
| </head> | |
| <body style="text-align: center; margin-left: 10em; margin-right: 10em"> | |
| <h1>SABRe: Simple Audio Book Recorder</h1> | |
| <div style="background: #e1b7e0"> | |
| <hr> | |
| <div class="stats-box"> | |
| <span class="stats" id="sentCntDisplay">Sentences recorded: 0 / 0</span><br> | |
| <span class="stats" id="durationDisplay">Total duration: 0 seconds</span> | |
| </div> | |
| <form id="uploadForm" enctype="multipart/form-data"> | |
| <p>Upload a .txt file (one sentence per line) to begin.</p> | |
| <input type="file" name="file" id="fileInput" accept=".txt" required> | |
| <button type="submit">Upload selected file</button> | |
| </form> | |
| <div id="recorder" style="display: none;"> | |
| <div id="sentence"></div> | |
| <div id="controls"> | |
| <button id="recordBtn">Record</button> | |
| <button id="stopBtn" disabled>Stop</button> | |
| <button id="nextBtn" disabled>Next Sentence</button> | |
| </div> | |
| <audio id="audioPlayback" controls style="display:none; margin-top:20px; margin-left: auto; margin-right: auto;"></audio> | |
| </div> | |
| <hr> | |
| </div> | |
| <div style="display: flex; justify-content: center; gap: 20px; margin-top: 30px;"> | |
| <button id="downloadBtn">Download Recordings (.zip)</button> | |
| <button id="deleteBtn" class="delete-btn">Delete All Data</button> | |
| </div> | |
| <script> | |
| let sentences = []; | |
| let recordedIndices = new Set(); | |
| let current = 0; | |
| let startTime, stopTime; | |
| let totalTime = 0; | |
| let mediaRecorder, audioChunks = []; | |
| async function loadSession() { | |
| const res = await fetch('/get-session'); | |
| const data = await res.json(); | |
| if (data.sentences && data.sentences.length > 0) { | |
| sentences = data.sentences; | |
| recordedIndices = new Set(data.recorded_indices.map(Number)); | |
| // Find first unrecorded | |
| let firstIncomplete = sentences.findIndex((_, i) => !recordedIndices.has(i)); | |
| current = firstIncomplete !== -1 ? firstIncomplete : sentences.length - 1; | |
| document.getElementById('recorder').style.display = 'block'; | |
| document.getElementById('uploadForm').style.display = 'none'; | |
| updateStats(); | |
| showSentence(); | |
| } | |
| } | |
| window.onload = loadSession; | |
| document.getElementById('uploadForm').onsubmit = async function(e) { | |
| e.preventDefault(); | |
| let form = new FormData(); | |
| form.append('file', document.getElementById('fileInput').files[0]); | |
| let res = await fetch('/upload-sentences', { method: 'POST', body: form }); | |
| sentences = await res.json(); | |
| current = 0; | |
| recordedIndices.clear(); | |
| updateStats(); | |
| showSentence(); | |
| document.getElementById('recorder').style.display = 'block'; | |
| document.getElementById('uploadForm').style.display = 'none'; | |
| }; | |
| document.getElementById('recordBtn').onclick = async function() { | |
| let stream = await navigator.mediaDevices.getUserMedia({ audio: true }); | |
| mediaRecorder = new MediaRecorder(stream); | |
| audioChunks = []; | |
| mediaRecorder.ondataavailable = e => audioChunks.push(e.data); | |
| mediaRecorder.onstop = async function() { | |
| let blob = new Blob(audioChunks, { type: 'audio/webm' }); | |
| let form = new FormData(); | |
| form.append('audio', blob, `sentence_${current}.webm`); | |
| form.append('sentence_idx', current); | |
| form.append("sentence_text", sentences[current]); | |
| const res = await fetch('/upload-audio', { method: 'POST', body: form }); | |
| if(res.ok) { | |
| recordedIndices.add(current); | |
| updateStats(); | |
| showSentence(); | |
| document.getElementById('audioPlayback').src = URL.createObjectURL(blob); | |
| document.getElementById('audioPlayback').style.display = 'block'; | |
| document.getElementById('nextBtn').disabled = false; | |
| } | |
| }; | |
| mediaRecorder.start(); | |
| startTime = performance.now(); | |
| document.getElementById('recordBtn').disabled = true; | |
| document.getElementById('stopBtn').disabled = false; | |
| }; | |
| document.getElementById('stopBtn').onclick = function() { | |
| mediaRecorder.stop(); | |
| stopTime = performance.now(); | |
| totalTime += (stopTime - startTime); | |
| document.getElementById('recordBtn').disabled = false; | |
| document.getElementById('stopBtn').disabled = true; | |
| }; | |
| document.getElementById('nextBtn').onclick = function() { | |
| if (current + 1 < sentences.length) { | |
| current++; | |
| showSentence(); | |
| } else { | |
| alert("All sentences recorded!"); | |
| } | |
| }; | |
| document.getElementById('deleteBtn').onclick = async function() { | |
| if (confirm("Delete all data? This cannot be undone.")) { | |
| await fetch('/delete-data', { method: 'POST' }); | |
| location.reload(); | |
| } | |
| }; | |
| document.getElementById('downloadBtn').onclick = () => window.location.href = '/download-recordings'; | |
| function showSentence() { | |
| const container = document.getElementById('sentence'); | |
| container.innerHTML = ''; | |
| const createP = (text, color, bold, size) => { | |
| const p = document.createElement("p"); | |
| p.style.color = color; | |
| if(bold) p.style.fontWeight = "bold"; | |
| if(size) p.style.fontSize = size; | |
| p.innerText = text; | |
| return p; | |
| }; | |
| container.appendChild(createP("previous: " + (current > 0 ? sentences[current-1] : "None"), "grey")); | |
| container.appendChild(createP((recordedIndices.has(current) ? "✅ " : "🎤 ") + sentences[current], "black", true, "1.4em")); | |
| container.appendChild(createP("next: " + (current < sentences.length - 1 ? sentences[current+1] : "End"), "grey")); | |
| document.getElementById('nextBtn').disabled = !recordedIndices.has(current); | |
| document.getElementById('audioPlayback').style.display = 'none'; | |
| } | |
| function updateStats() { | |
| document.getElementById("sentCntDisplay").textContent = `Sentences recorded: ${recordedIndices.size} / ${sentences.length}`; | |
| document.getElementById("durationDisplay").textContent = `Total duration: ${(totalTime/1000).toFixed(1)} seconds`; | |
| } | |
| </script> | |
| </body> | |
| </html> |