如何在Web项目中同步SpeechSynthesis与文本颜色变化
实现SpeechSynthesis朗读单词与文本颜色实时同步
核心问题在于直接操作整个段落样式,未将文本拆分为可单独控制的单词单元。以下是可行方案,通过拆分单词为独立<span>并结合onboundary事件实现精准高亮:
实现步骤
- 将段落文本拆分为单个单词,每个单词用
<span>包裹,记录每个单词对应的字符位置范围 - 利用
SpeechSynthesisUtterance的onboundary事件跟踪当前朗读的字符位置,匹配对应单词<span>并高亮 - 朗读结束后重置所有单词的颜色
修改后的完整代码
// 预处理段落,将每个单词包裹成span,并记录字符位置 function prepareParagraph(paragraph) { const text = paragraph.innerText; const words = text.split(/\s+/); // 按空格拆分单词 let charIndex = 0; const wordSpans = []; // 清空原段落内容,重新构建带span的结构 paragraph.innerHTML = ''; words.forEach(word => { const span = document.createElement('span'); span.textContent = word + ' '; // 保留空格 span.dataset.start = charIndex; span.dataset.end = charIndex + word.length; wordSpans.push(span); paragraph.appendChild(span); // 更新字符位置(包含空格) charIndex += word.length + 1; }); return wordSpans; } // 顺序朗读段落(避免多个utterance同时播放导致高亮混乱) function speakParagraphsSequentially(page) { const paragraphs = Array.from(document.getElementById(`page${page}`).getElementsByTagName('p')); let currentIndex = 0; // 处理语音加载异步问题 function getVoices() { return new Promise(resolve => { let voices = speechSynthesis.getVoices(); if (voices.length > 0) { resolve(voices); return; } speechSynthesis.onvoiceschanged = () => { voices = speechSynthesis.getVoices(); resolve(voices); }; }); } async function speakNextParagraph() { if (currentIndex >= paragraphs.length) return; const paragraph = paragraphs[currentIndex]; const wordSpans = prepareParagraph(paragraph); const text = paragraph.innerText.trim(); // 创建utterance const utterance = new SpeechSynthesisUtterance(text); // 获取语音 const voices = await getVoices(); const targetVoice = voices.find(voice => voice.name === "Microsoft Emily Online (Natural) - English (Ireland)" ); utterance.voice = targetVoice || voices[0]; // 高亮当前朗读的单词 let lastHighlightedSpan = null; utterance.onboundary = (event) => { const currentCharIndex = event.charIndex; // 找到当前字符位置对应的单词span const currentSpan = wordSpans.find(span => { const start = parseInt(span.dataset.start); const end = parseInt(span.dataset.end); return currentCharIndex >= start && currentCharIndex <= end; }); if (currentSpan && currentSpan !== lastHighlightedSpan) { // 重置上一个高亮的单词 if (lastHighlightedSpan) { lastHighlightedSpan.style.color = ''; } // 高亮当前单词 currentSpan.style.color = 'red'; lastHighlightedSpan = currentSpan; } }; // 朗读结束后重置所有单词颜色 utterance.onend = () => { wordSpans.forEach(span => span.style.color = ''); currentIndex++; speakNextParagraph(); // 继续朗读下一段 }; // 开始朗读 speechSynthesis.speak(utterance); } // 启动顺序朗读 speakNextParagraph(); } // 调用示例:朗读page1的段落 // speakParagraphsSequentially(1);
关键说明
- 段落预处理:
prepareParagraph函数将每个单词转为独立<span>,通过data-*属性存储该单词在原文本中的字符起始/结束位置,这是实现精准匹配的核心。 - 顺序朗读:改用顺序朗读而非批量调用
speechSynthesis.speak,避免多个语音同时播放导致onboundary事件混乱。 - 语音加载处理:通过Promise处理
speechSynthesis.getVoices()的异步加载问题,确保获取到可用语音。 - onboundary事件:每次触发时根据当前字符位置找到对应的单词
<span>,切换高亮状态,朗读结束后重置所有样式。
内容的提问来源于stack exchange,提问作者ThevinuDan
相关产品推荐
相关产品推荐

