用 Web Speech API 给 AI 回答加"朗读"功能,边读边高亮 🔊

需求很简单:AI 回答出来后,点个喇叭能朗读,而且读到哪句高亮哪句。Web Speech API 的 speechSynthesis 自带,不用接任何 TTS 服务,纯前端就能干。但"边读边高亮"这块有点门道,我做下来记几条。

基础朗读三行起步

ini 复制代码
const u = new SpeechSynthesisUtterance(text)
u.lang = 'zh-CN'
speechSynthesis.speak(u)

就能出声了。但直接把一大段 AI 回答整个 speak,问题来了:它是一口气读完的黑盒,我没法知道读到第几个字,也就没法高亮。

关键:boundary 事件

SpeechSynthesisUtterance 有个 onboundary 事件,每读到一个词/句的边界会触发,带 charIndex------当前读到原文的第几个字符。这就是边读边高亮的钥匙:

scss 复制代码
u.onboundary = (e) => {
  highlightAt(e.charIndex)   // 根据字符位置定位到对应的句子,加高亮 class
}

我把回答按句号、问号切成句子数组,每句记录它的起止 charIndex。boundary 来了就二分找当前 charIndex 落在哪句,给那句加 .reading 高亮,移除上一句的。

javascript 复制代码
function findSentence(index) {
  return sentences.findIndex(s => index >= s.start && index < s.end)
}

几个真实的坑

1. 中文的 boundary 触发不稳。 这是最坑的。Chrome 上中文 onboundary 经常只在句子级触发,甚至有些版本干脆不触发 word 边界。所以我没法做到字级高亮,退而求其次做句级------反正句级高亮体验也够了。英文倒是 word 级很准。

2. voices 异步加载。 speechSynthesis.getVoices() 第一次调常常返回空数组,得等 voiceschanged 事件:

ini 复制代码
speechSynthesis.onvoiceschanged = () => {
  const v = speechSynthesis.getVoices().find(x => x.lang === 'zh-CN')
  if (v) u.voice = v
}

不处理这个,首次点朗读经常用的是默认英文音色读中文,出来一股机翻味。

3. 长文本会被浏览器掐断。 Chrome 对单条 utterance 长度有隐性限制,大概两百多字符后会莫名停。解法是把文本切成小段,排队 speak,前一段 onend 了再 speak 下一段。我用个简单队列搞定。

4. 暂停/恢复在某些系统上是坏的。 pause()/resume() 在部分 Linux/旧 Chrome 上行为诡异,有时 resume 不回来。我干脆把暂停做成"取消 + 记住进度,重新从那句 speak",绕过去了。

没做完的

切到后台标签页时朗读会被自动暂停又不一定恢复,这个跨浏览器行为太杂,我还没统一处理好。

模型这端

朗读是锦上添花,内容本身还得靠模型。我这套对话的回答是走讯飞 MaaS(模型即服务)接口出的,模型和算力它管,我前端拿到文本再交给浏览器念,分工清爽。


中文字级高亮你们有辙吗?onboundary 不给力的情况下有没有人用 TTS 服务返回的时间戳对齐的?评论区指条路,我这句级高亮还想再精一点。

相关推荐
ALianBlank1 小时前
一个 Unity 框架能做多少事?86 个模块 + 21 个小游戏平台
前端·后端·游戏开发
m0_547722921 小时前
从零搭建乒乓球比赛管理系统——Spring Boot + 原生 HTML 实战
spring boot·后端·html
用户637328456111 小时前
MyBatis与MyBatis-Plus区别
后端
爆浇牛肉面1 小时前
手写消息队列(一):从零搭建Spring Boot + MyBatis + SQLite
后端
Oo_行者_oO1 小时前
Spring Schedule + ShedLock + RabbitMQ 生产级落地方案 - 云楼(中国)
java·后端
Hical611 小时前
百万 TCP 长连接内存实测:50 万点回归,R²=1.0000,每连接 7.58 KB
后端·github
Mahir081 小时前
HashMap 底层原理深度解密:从数据结构到 JDK1.7/1.8 演进全解
java·后端·面试·hashmap
uhakadotcom1 小时前
get_event_loop(),和 get_running_loop() + ThreadPoolExecutor 有啥区别
后端·面试·github
小马爱打代码2 小时前
Spring Boot 自动装配流程
java·spring boot·后端