Files
audiobook-studio-pro-v3/static/js/interactive-reader.js
Ashim Kumar 11d715eb85 first commit
2026-01-09 21:06:30 +06:00

271 lines
7.8 KiB
JavaScript

/**
* Interactive Reader Module
* Handles text display, word highlighting, and audio sync
*/
// ==========================================
// READER STATE VARIABLES
// ==========================================
let allWordSpans = [];
let wordMap = [];
let sentenceData = [];
let lastHighlightedWordSpan = null;
let lastHighlightedSentenceSpans = [];
// ==========================================
// READER INITIALIZATION
// ==========================================
/**
* Initialize reader with markdown text
* @param {string} markdownText - Text content in markdown format
*/
function initReader(markdownText) {
const container = document.getElementById('readerContent');
container.innerHTML = '';
allWordSpans = [];
wordMap = [];
const html = marked.parse(markdownText || '', { breaks: true });
const tempDiv = document.createElement('div');
tempDiv.innerHTML = html;
// Process nodes to wrap words in spans
processNode(tempDiv);
while (tempDiv.firstChild) {
container.appendChild(tempDiv.firstChild);
}
runSmartSync();
}
/**
* Process DOM node to wrap words in clickable spans
* @param {Node} node - DOM node to process
*/
function processNode(node) {
if (node.nodeType === Node.TEXT_NODE) {
const words = node.textContent.split(/(\s+|[^\w'])/g);
const fragment = document.createDocumentFragment();
words.forEach(part => {
if (part.trim().length > 0) {
const span = document.createElement('span');
span.className = 'word';
span.textContent = part;
span.onclick = handleWordClick;
allWordSpans.push(span);
fragment.appendChild(span);
} else {
fragment.appendChild(document.createTextNode(part));
}
});
node.parentNode.replaceChild(fragment, node);
} else if (node.nodeType === Node.ELEMENT_NODE) {
Array.from(node.childNodes).forEach(processNode);
}
}
/**
* Handle click on a word span
* @param {MouseEvent} e - Click event
*/
function handleWordClick(e) {
e.stopPropagation();
const spanIndex = allWordSpans.indexOf(e.target);
const aiIdx = wordMap[spanIndex];
if (aiIdx !== undefined && transcriptionData[aiIdx] && wavesurfer) {
wavesurfer.setTime(transcriptionData[aiIdx].start);
wavesurfer.play();
}
}
// ==========================================
// TEXT-AUDIO SYNC FUNCTIONS
// ==========================================
/**
* Run smart sync between text words and transcription data
*/
function runSmartSync() {
wordMap = new Array(allWordSpans.length).fill(undefined);
let aiIdx = 0;
let matchCount = 0;
allWordSpans.forEach((span, i) => {
const clean = span.textContent.toLowerCase().replace(/[^\w]/g, '');
if (clean.length === 0) {
span.classList.add('unmatched');
return;
}
// Look ahead up to 5 positions for a match
for (let off = 0; off < 5; off++) {
if (aiIdx + off >= transcriptionData.length) break;
const transcriptWord = transcriptionData[aiIdx + off].word.toLowerCase().replace(/[^\w]/g, '');
if (transcriptWord === clean) {
wordMap[i] = aiIdx + off;
aiIdx += off + 1;
span.classList.remove('unmatched');
matchCount++;
return;
}
}
span.classList.add('unmatched');
});
mapSentences();
updateSyncStatus(matchCount);
}
/**
* Update sync status badge
* @param {number} matchCount - Number of matched words
*/
function updateSyncStatus(matchCount) {
const badge = document.getElementById('syncStatus');
badge.textContent = `Synced (${matchCount}/${allWordSpans.length})`;
badge.className = matchCount > allWordSpans.length * 0.8
? 'badge bg-success'
: 'badge bg-warning text-dark';
}
/**
* Map sentences for sentence-level highlighting
*/
function mapSentences() {
sentenceData = [];
let buffer = [];
let startIdx = 0;
allWordSpans.forEach((span, i) => {
buffer.push(span);
// Check for sentence-ending punctuation
if (/[.!?]["']?$/.test(span.textContent.trim())) {
let startT = 0;
let endT = 0;
// Find start time
for (let k = startIdx; k <= i; k++) {
if (wordMap[k] !== undefined) {
startT = transcriptionData[wordMap[k]].start;
break;
}
}
// Find end time
for (let k = i; k >= startIdx; k--) {
if (wordMap[k] !== undefined) {
endT = transcriptionData[wordMap[k]].end;
break;
}
}
if (endT > startT) {
sentenceData.push({
spans: [...buffer],
start: startT,
end: endT
});
}
buffer = [];
startIdx = i + 1;
}
});
}
/**
* Sync reader highlighting with audio playback time
* @param {number} t - Current playback time in seconds
*/
function syncReader(t) {
// Highlight current word
highlightCurrentWord(t);
// Highlight current sentence
highlightCurrentSentence(t);
}
/**
* Highlight the current word based on playback time
* @param {number} t - Current playback time
*/
function highlightCurrentWord(t) {
const aiIdx = transcriptionData.findIndex(d => t >= d.start && t < d.end);
if (aiIdx !== -1) {
const txtIdx = wordMap.findIndex(i => i === aiIdx);
if (txtIdx !== -1 && allWordSpans[txtIdx] !== lastHighlightedWordSpan) {
// Remove previous highlight
if (lastHighlightedWordSpan) {
lastHighlightedWordSpan.classList.remove('current-word');
}
// Add new highlight
lastHighlightedWordSpan = allWordSpans[txtIdx];
lastHighlightedWordSpan.classList.add('current-word');
// Scroll into view if needed
scrollWordIntoView(lastHighlightedWordSpan);
}
}
}
/**
* Highlight the current sentence based on playback time
* @param {number} t - Current playback time
*/
function highlightCurrentSentence(t) {
const sent = sentenceData.find(s => t >= s.start && t <= s.end);
if (sent && sent.spans !== lastHighlightedSentenceSpans) {
// Remove previous highlight
if (lastHighlightedSentenceSpans) {
lastHighlightedSentenceSpans.forEach(s => s.classList.remove('current-sentence-bg'));
}
// Add new highlight
lastHighlightedSentenceSpans = sent.spans;
lastHighlightedSentenceSpans.forEach(s => s.classList.add('current-sentence-bg'));
}
}
/**
* Scroll word into view if outside visible area
* @param {Element} wordSpan - Word span element
*/
function scrollWordIntoView(wordSpan) {
const r = wordSpan.getBoundingClientRect();
const c = document.querySelector('.reader-section').getBoundingClientRect();
if (r.top < c.top || r.bottom > c.bottom) {
wordSpan.scrollIntoView({
behavior: 'smooth',
block: 'center'
});
}
}
// ==========================================
// MISMATCH TOGGLE
// ==========================================
/**
* Toggle display of mismatched words
*/
function toggleMismatches() {
const toggle = document.getElementById('mismatchToggle');
document.getElementById('readerContent').classList.toggle('show-mismatches', toggle.checked);
}