You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在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);

关键说明

  1. 段落预处理:prepareParagraph函数将每个单词转为独立<span>,通过data-*属性存储该单词在原文本中的字符起始/结束位置,这是实现精准匹配的核心。
  2. 顺序朗读:改用顺序朗读而非批量调用speechSynthesis.speak,避免多个语音同时播放导致onboundary事件混乱。
  3. 语音加载处理:通过Promise处理speechSynthesis.getVoices()的异步加载问题,确保获取到可用语音。
  4. onboundary事件:每次触发时根据当前字符位置找到对应的单词<span>,切换高亮状态,朗读结束后重置所有样式。

内容的提问来源于stack exchange,提问作者ThevinuDan

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.02 03:33:40