easytranscriber-demo / index.html
Lauler's picture
Update index.html
3ef04ae verified
<!doctype html>
<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>