Spaces:
Running
Running
| <html> | |
| <head> | |
| <meta charset="utf-8" /> | |
| <meta name="viewport" content="width=device-width" /> | |
| <title>My static Space</title> | |
| <link rel="stylesheet" href="style.css" /> | |
| </head> | |
| <body> | |
| <div class="card"> | |
| <h1>easytranscriber — interactive transcript demo</h1> | |
| <p> | |
| This demo shows synchronized audio playback with word-level transcript highlighting, | |
| powered by <a href="https://kb-labb.github.io/easytranscriber/" target="_blank">easytranscriber</a>'s | |
| transcription <a href="https://huggingface.co/spaces/KBLab/easytranscriber-demo/blob/main/taleoftwocities_01_dickens_64kb_trimmed.json">output</a>. Press play to see each word highlighted as it is spoken. | |
| Click any sentence to jump to that point in the audio. | |
| </p> | |
| <p> | |
| Want to browse your own transcribed files the same way? Install | |
| <a href="https://github.com/kb-labb/easytranscriber" target="_blank">easytranscriber</a> | |
| with search support and point it at your alignment output: | |
| </p> | |
| <pre><code>pip install easytranscriber[search] | |
| easysearch --alignments-dir output/alignments --audio-dir data/audio</code></pre> | |
| <p> | |
| This starts a local search interface at <code>http://127.0.0.1:8642</code> with full-text search | |
| across all your transcriptions and the same synchronized playback shown here. | |
| </p> | |
| </div> | |
| <div id="audio-player" class="audio-card"> | |
| <div class="audio-card-label">Sample audio</div> | |
| <div class="audio-card-title"><em>A Tale of Two Cities</em> — Chapter 1 (LibriVox)</div> | |
| <audio controls> | |
| <source src="https://huggingface.co/datasets/Lauler/easytranscriber_tutorials/resolve/main/tale-of-two-cities_short-en/taleoftwocities_01_dickens_64kb_trimmed.mp3" | |
| type="audio/mpeg"> | |
| </audio> | |
| </div> | |
| <div id="transcript-container" class="transcript-container transcript-card"></div> | |
| <script> | |
| const audioPlayer = document.querySelector("#audio-player audio"); | |
| const container = document.getElementById("transcript-container"); | |
| const wordMap = []; | |
| const alignmentMap = []; | |
| let prevWord = null; | |
| let prevAlignment = null; | |
| fetch("taleoftwocities_01_dickens_64kb_trimmed.json") | |
| .then((r) => r.json()) | |
| .then((data) => { | |
| data.speeches.forEach((speech) => { | |
| let para = document.createElement("p"); | |
| para.className = "chunk"; | |
| speech.alignments.forEach((alignment) => { | |
| const sentenceSpan = document.createElement("span"); | |
| sentenceSpan.className = "alignment"; | |
| sentenceSpan.addEventListener("click", () => { | |
| audioPlayer.currentTime = alignment.start; | |
| audioPlayer.play(); | |
| }); | |
| alignment.words.forEach((word) => { | |
| const wordSpan = document.createElement("span"); | |
| wordSpan.className = "word"; | |
| wordSpan.textContent = word.text; | |
| wordSpan.dataset.start = word.start; | |
| wordSpan.dataset.end = word.end; | |
| sentenceSpan.appendChild(wordSpan); | |
| wordMap.push({ el: wordSpan, start: word.start, end: word.end }); | |
| }); | |
| para.appendChild(sentenceSpan); | |
| alignmentMap.push({ | |
| el: sentenceSpan, | |
| start: alignment.start, | |
| end: alignment.end, | |
| }); | |
| if (!alignment.text.endsWith(" ")) { | |
| container.appendChild(para); | |
| para = document.createElement("p"); | |
| para.className = "chunk"; | |
| } | |
| }); | |
| if (para.childElementCount > 0) { | |
| container.appendChild(para); | |
| } | |
| }); | |
| }); | |
| function updateHighlight() { | |
| const t = audioPlayer.currentTime; | |
| const curWord = wordMap.find((w) => t >= w.start && t < w.end); | |
| if (curWord && curWord.el !== prevWord) { | |
| if (prevWord) prevWord.classList.remove("highlight-word"); | |
| curWord.el.classList.add("highlight-word"); | |
| prevWord = curWord.el; | |
| } | |
| const curAlignment = alignmentMap.find((a) => t >= a.start && t < a.end); | |
| if (curAlignment && curAlignment.el !== prevAlignment) { | |
| if (prevAlignment) prevAlignment.classList.remove("highlight-sentence"); | |
| curAlignment.el.classList.add("highlight-sentence"); | |
| prevAlignment = curAlignment.el; | |
| // Auto-scroll to keep active sentence visible | |
| curAlignment.el.scrollIntoView({ behavior: "smooth", block: "nearest" }); | |
| } | |
| } | |
| audioPlayer.addEventListener("seeked", updateHighlight); | |
| function tick() { | |
| if (!audioPlayer.paused) { | |
| updateHighlight(); | |
| } | |
| requestAnimationFrame(tick); | |
| } | |
| requestAnimationFrame(tick); | |
| </script> | |
| </body> | |
| </html> | |