给AI对话加语音输入和播报,我踩的那些坑

2 阅读3分钟

上个月给一个内部问答页加语音功能,老板一句话:"能不能像跟人聊天一样,我说话它就懂,回答还能念出来?"听着简单,真做下去全是坑。这篇把我用 Web Speech API 做"边说边转写 + 回答自动朗读"的过程和翻车记录摊开讲讲,前端的同行少走点弯路。

先说结论:两个 API,别搞混

浏览器里这俩是分开的:

  • 语音转文字(识别)SpeechRecognition(Chrome 里要写 webkitSpeechRecognition)。
  • 文字转语音(播报)speechSynthesis + SpeechSynthesisUtterance

一个负责"听",一个负责"读",互不相干。我一开始还以为是一套东西,文档翻了半天才反应过来。

语音输入:边说边转写

核心就这几行:

const SR = window.SpeechRecognition || window.webkitSpeechRecognition
const rec = new SR()
rec.lang = 'zh-CN'
rec.interimResults = true   // 关键:要实时草稿就得开这个
rec.continuous = true       // 想一直听着别自动停就开

rec.onresult = (e) => {
  let finalText = '', interim = ''
  for (let i = e.resultIndex; i < e.results.length; i++) {
    const t = e.results[i][0].transcript
    if (e.results[i].isFinal) finalText += t
    else interim += t
  }
  // interim 是灰色草稿,finalText 才是定稿,分开渲染
  setDraft(interim)
  if (finalText) appendToInput(finalText)
}
rec.start()

interimResults 开了之后,能拿到一段还没定稿的"草稿文字",体验上就是你说一个字屏幕跳一个字,那种"边说边转"的感觉全靠它。我没分清 isFinal,结果把草稿和定稿揉在一起,输入框里同一句话疯狂闪烁重复,丑得不行。

坑 1: continuous = true 在手机端基本不顶用。 安卓 Chrome 上说一句话停顿一下,它自己就 onend 了,还得在 onend 里判断是不是用户主动停的、不是就重新 start()。我写了个标志位 manualStop 来区分,不然会陷入"自己停自己又起"的死循环。

坑 2:必须 HTTPS。 localhost 还行,一上测试环境用 IP 访问直接 not-allowed,因为麦克风权限要安全上下文。我在这卡了快一小时,以为代码错了,最后发现是协议问题。

语音播报:把回答念出来

播报这头简单点:

function speak(text) {
  const u = new SpeechSynthesisUtterance(text)
  u.lang = 'zh-CN'
  u.rate = 1.05   // 默认语速偏慢,调到 1.05 顺耳点
  speechSynthesis.cancel()  // 先取消上一段,不然会排队叠着念
  speechSynthesis.speak(u)
}

那句 cancel() 是血泪换来的——流式回答时我每来一个 chunk 就 speak 一次,结果十几段语音排队,AI 都答完五秒了它还在念第三句,错位得离谱。后来改成:等流式输出停了、整段定下来再念一次,世界清静了。

坑 3:中文发音人得自己挑。 speechSynthesis.getVoices() 第一次调经常是空数组,要监听 voicechanged 事件再读。而且不同系统自带的中文音色差很多,Windows 上那个有点机械,没法强求统一。

一个意外的省事点

本来我以为还得自己接个语音识别的服务端、配模型,结果纯浏览器 API 就把"听"和"读"都搞定了,后端只管对话那段。对话那段我用的是一个零代码就能搭智能体的平台,前端只发文本、收文本,语音这层完全在浏览器本地处理,链路一下清爽了。说点不好的:Web Speech 的识别准确率在嘈杂环境里会拉胯,专业术语、人名经常听岔,真要高准确率还是得上专门的识别服务。

模型那块我没自己折腾,回答是走的讯飞 MaaS 现成调出来的,省了部署算力的事,前端这边就专心抠交互。

你们做语音交互踩过啥奇葩坑?尤其手机端那个自动断的问题有没有更优雅的解法,评论区教教我 🙏