返回探索
指南VibeFix 编辑部更新于 2026年10月18日

vibe 项目的语音交互实战:ASR/TTS 选型、延迟预算与降级

Web Speech API vs 云 ASR、speechSynthesis vs 云 TTS、VAD→ASR→LLM→TTS 四段延迟预算、打断处理、成本账与降级链——附可运行的浏览器语音输入框 demo。

题图:对着录音室麦克风说话的人(Unsplash 摄影师 Tim Mossholder 作品)

先划两条界。第一,站内今年有三篇语音相关的资讯:Sesame 的语音 agent、微软的 MAI 语音 agent 技术栈、Simon Willison 用语音给博客做了个功能——那三篇是新闻事件,本文讲工程落地:资讯告诉你"发生了什么",这篇告诉你"自己做要踩哪些坑、代码怎么写"。第二,站内《流式输出 UX 指南》(ai-streaming-output-ux-guide)讲的是"文本流式输出"的界面体验,本文讲的是"语音通道"本身:麦克风进来、声音出去,中间那条管线怎么搭。

语音交互是 vibe 项目里"demo 惊艳、上线翻车"最典型的功能。浏览器里十行代码就能让网页"听懂"你说话,于是很多人以为语音功能做完了。真正上线后才发现:Safari 上根本没声音、Firefox 上按钮是灰的、用户在地铁里说话识别出来全是错的、云账单第一个月就爆了。本文按真实工程顺序讲:先判断你的产品需不需要语音,再选 ASR/TTS 方案,然后搭实时对话管线、算成本账、做降级,最后给一个浏览器端可运行的语音输入框 demo。

先想清楚:你的产品真的需要语音吗

动工之前先过三问,任一答案为"否"都建议先不做语音:

  • 场景是"手腾不出来"还是"图新鲜"?开车、做饭、走路、戴手套作业——手被占用时语音是刚需;坐在电脑前给 vibe 项目加个语音按钮,大概率是图新鲜。新鲜感的留存曲线很残酷:第一周 30% 的用户试一次,第二周剩 3%。
  • 输入是"说不清"还是"打字更快"?语音适合开放式、模糊的表达("帮我找个周末能去的、有猫的咖啡馆");结构化输入(日期、金额、账号)打字更快更准,还不用担心识别错一位数。
  • 环境允许说话吗?办公室、通勤、地铁——一半以上的真实使用场景不适合出声。语音只能是"可选通道",永远不能是唯一通道。

三问都通过,再往下看。语音在 vibe 项目里最稳的三个落点:AI 助手/陪伴类产品的语音对话、工具类的语音输入(搜索框、备注)、内容类的 TTS 播报(文章朗读、导航播报)。

ASR 语音输入:Web Speech API vs 云 ASR

两条路线,选型看四个维度:

路线一:Web Speech API(浏览器原生)

webkitSpeechRecognition / SpeechRecognition 是浏览器自带的语音识别,十行代码就能跑。它的真实面貌:

  • 成本:免费。音频发到浏览器厂商的服务器识别,不走你的账单。
  • 延迟:中等。走公网到厂商服务器再回来,首字结果通常 1 秒左右。
  • 准确率:日常用语够用,专有名词、人名、地名、口音和噪音环境下明显拉胯。Chrome 和 Safari 背后是两套不同的引擎,准确率表现不一样,必须分开测。
  • 离线:不支持。Chrome 的实现把音频发到 Google 服务器,断网即不可用。
  • 兼容性(2026 年现状,MDN 口径):Chrome/Edge(webkit 前缀)可用;Safari 14.1+(macOS)/14.5+(iOS)可用(同样 webkit 前缀);Firefox 默认不支持(藏在 flag 后面);应用内浏览器(如微信 webview)经常直接禁用麦克风。Electron 更坑:API 可能在,但识别服务是浏览器厂商的私有服务,不随 Chromium 开源部分一起发布——有接口、无结果。

结论:Web Speech API 是"最快做出 demo"的路线,适合 MVP 验证、低频场景、预算为零的个人项目。不适合:对准确率有要求的生产功能、Firefox 用户占比高的产品、需要离线的场景。

路线二:云 ASR(自己采音频、调接口)

浏览器只负责采音频(MediaRecorder 或 AudioWorklet),识别走云厂商的流式 ASR 接口。优势是一致性:准确率不随浏览器变、支持的语种和领域模型可选、可控性高(自定义热词、标点、说话人分离)。代价是:要自己搭音频采集和重采样(各浏览器采样率不一致,AudioWorklet 里统一转成 16kHz/16bit PCM 是标准做法)、要管 WebSocket 长连接、按量付费。

选型建议:日活用户语音请求 < 1 万次先用 Web Speech API 把流程跑通,瓶颈真的出现在准确率或兼容性上再迁云 ASR。过早引入云 ASR,你会同时调试"音频采集"和"识别质量"两个变量,定位问题翻倍。

TTS 播报:speechSynthesis vs 云 TTS

TTS(文字转语音)的两条路线对比更微妙:

  • 浏览器 speechSynthesis:兼容性是 Web Speech API 里最好的——Chrome、Edge、Firefox、Safari 全支持(window.speechSynthesis)。零成本、离线可用(用系统自带语音引擎)。缺点:音色是"系统音",情感和自然度差;各平台声音列表完全不同,中文语音在某些 Android 机上干脆没有;Chrome 有个著名坑——getVoices() 首次调用经常返回空数组,声音列表是异步加载的,必须监听 voiceschanged 事件再取。
  • 云 TTS:自然度高一个时代,支持 SSML(语音合成标记语言:控制停顿、重音、语速、发音,比如把"123"读成"一百二十三"还是"一二三")、多音色可选、可克隆音色。代价是按字符计费,长文本播报的成本要仔细算(见后文成本账)。

实战建议:通知、提醒、短播报(<100 字)用 speechSynthesis 足够;内容朗读、AI 助手回播、品牌音色要求高的场景上云 TTS。一个常见组合:首版用 speechSynthesis 上线验证"用户真的会点播报吗",有数据了再迁云 TTS——别为还没验证的需求预付云账单。

SSML 值得单独说一句

云 TTS 的 SSML 不是"高级功能",是"把话说对"的基本功:<break time="500ms"/> 控制停顿(读地址、读验证码时必需)、<say-as interpret-as="telephone"> 让电话号码逐位读而不是读成一个大数字、<prosody rate="slow"> 给老年用户放慢语速。不写 SSML 的 TTS 播报,听起来像把标点全吃了的新闻联播。

实时语音对话管线:VAD → ASR → LLM → TTS

"语音助手"不是"ASR + TTS 拼起来",中间有一条四段管线。每段的延迟预算如下(经验目标值,实测以你的链路为准):

实时语音对话管线四段延迟预算堆叠条形图:VAD、流式 ASR、LLM 首 token、TTS 首音频,含 1200ms 优秀线与 2000ms 可用线

阶段延迟预算在做什么
VAD(语音活动检测)200–500ms判断"人开始说话了""人说完了"。说完判定(endpointing)是最难调的参数:太灵敏会打断用户思考,太迟钝对话像隔着太平洋。
ASR 流式识别300–800ms(首字)边说边出字。流式 ASR 的中间结果会改写("我想要个苹…苹果手机"),UI 上要处理这种"回马枪"。
LLM 推理500–1500ms(首 token)流式输出是刚需:等整句生成完再播报,对话延迟直接翻倍。LLM 输出流要边收边喂给 TTS。
TTS 首音频300–800ms第一句音频出来。云 TTS 支持流式合成时,LLM 的第一个分句一到就开始合成,不用等全文。

多少毫秒算可用?从"用户说完"到"助手开始出声":<1200ms 是优秀(接近真人对话节奏),1200–2000ms 可用(用户能感到停顿但可接受),>2000ms 用户会开始"喂?还在吗"。优化顺序:先压 LLM 首 token(流式+短 system prompt+小模型),再压 TTS 首音频(流式合成),最后抠 VAD 参数。绝大多数"语音助手好慢"的病根在"等 LLM 整句生成完才调 TTS"——把串行改成流式流水线,一句话的延迟能砍掉一半。

VAD 实现:浏览器端最简单的可用方案是能量检测(AudioWorklet 里算音量包络,超过阈值算有人声);要求高的用 Silero VAD 这类轻量模型(可跑在浏览器/端侧)。云方案里很多语音 agent API 自带 turn detection(轮次检测),调一个接口省掉自己调参。

可运行示例:Web Speech API 语音输入框 demo

一个生产可用的语音输入框要处理:特性检测、interim 中间结果、错误、自动停止。下面是完整可运行的单文件 demo(Chrome/Edge/Safari 直接打开可用,需 HTTPS 或 localhost):

<!doctype html><html><head><meta charset="utf-8"><title>语音输入框 demo</title></head>
<body style="font-family:sans-serif;max-width:560px;margin:40px auto">
<h2>语音输入框 demo</h2>
<textarea id="box" rows="4" style="width:100%" placeholder="点麦克风开始说话…"></textarea><br><br>
<button id="mic">🎤 开始说话</button> <span id="hint" style="color:#666"></span>
<script>
const SR = window.SpeechRecognition || window.webkitSpeechRecognition;
const mic = document.getElementById('mic'), box = document.getElementById('box'), hint = document.getElementById('hint');

// 1) 特性检测:不支持就直接给键盘(降级,不是报错)
if (!SR) {
  mic.disabled = true;
  hint.textContent = '当前浏览器不支持语音识别,请用键盘输入(Chrome/Edge/Safari 可用)';
} else {
  const rec = new SR();
  rec.lang = 'zh-CN';          // 中文普通话;粤语用 zh-HK,英文用 en-US
  rec.interimResults = true;   // 要中间结果:边说边上屏,体验差一截
  rec.continuous = false;      // 单句模式:说完自动停,适合输入框
  let finalText = '';

  rec.onresult = (e) => {
    let interim = '';
    for (const r of e.results) {
      if (r.isFinal) finalText += r[0].transcript;
      else interim += r[0].transcript;
    }
    box.value = finalText + interim; // interim 会改写,这是正常的"回马枪"
  };
  rec.onerror = (e) => {
    // 常见错误如实处理:not-allowed(没给麦克风权限)、no-speech(没听到声音)、network(断网)
    hint.textContent = '识别出错:' + e.error + ',可直接用键盘输入';
    mic.textContent = '🎤 开始说话'; listening = false;
  };
  rec.onend = () => { mic.textContent = '🎤 开始说话'; listening = false; hint.textContent = ''; };

  let listening = false;
  mic.onclick = () => {
    if (listening) { rec.stop(); return; }
    finalText = box.value ? box.value + ' ' : ''; // 追加模式,不覆盖已有输入
    try { rec.start(); } catch (err) { return; }   // 重复 start 会抛异常,直接吞掉
    listening = true;
    mic.textContent = '⏹ 停止';
    hint.textContent = '正在聆听…说完会自动停止';
  };
}

// 2) TTS 播报:注意 voiceschanged 异步坑
function speak(text) {
  if (!window.speechSynthesis) return;
  const u = new SpeechSynthesisUtterance(text);
  u.lang = 'zh-CN'; u.rate = 1.0;
  const pickVoice = () => {
    const vs = speechSynthesis.getVoices().filter(v => v.lang.startsWith('zh'));
    if (vs.length) u.voice = vs[0];
    speechSynthesis.speak(u);
  };
  if (speechSynthesis.getVoices().length) pickVoice();
  else speechSynthesis.onvoiceschanged = pickVoice; // Chrome 首次经常返回空数组
}
</script></body></html>

demo 里藏了五个生产细节:interimResults=true 让文字边说边上屏(不加这个,用户会对着沉默的输入框怀疑人生);continuous=false 单句模式适合输入框,连续听写才开 true;错误码如实告诉用户(not-allowed/no-speech/network 三种最常见);rec.start() 重复调用会抛异常要包住;TTS 取声音必须走 voiceschanged。这五个每个都对应一个真实用户投诉。

移动端权限与 UX:麦克风权限申请时机 + 打断处理

权限申请时机

麦克风权限是"一次性"的心理门槛:用户点"不允许",很多浏览器里这个域名就永久静默拒绝了,再想申请只能去设置里翻。所以铁律:永远不要在页面加载时申请麦克风权限,只在用户明确点了麦克风按钮那一刻申请。点按钮→弹系统权限框→用户理解"为什么"(因为他刚点了麦克风),通过率最高。提前申请、或者在 onboarding 流程里顺手申请,通过率都显著更低。

iOS Safari 还有个额外坑:麦克风权限和"页面是否在前台"强相关,切后台再回来音频流可能断掉,要监听可见性变化重建。另外微信等应用内浏览器经常直接禁用麦克风——在这种环境里,语音按钮应该直接隐藏而不是显示一个点不开的按钮(feature-detect 麦克风可用性,而不只是 API 存在性)。

打断处理(barge-in)

真人对话里,打断是常态:"停停停,我不是这个意思"。语音助手没有打断处理,用户只能等它把一整段播完,体验极差。实现原理:TTS 播报期间,VAD 保持监听,一旦检测到用户声音超过阈值,立即 speechSynthesis.cancel()(或停掉云 TTS 音频流),把已播报截断点之前的文本丢弃,从用户的新输入重新走管线。

两个细节:打断阈值要比正常 VAD 高一档,否则播报声音被麦克风收进去会"自己打断自己"(回声消除没做好的设备上必现);被打断后 LLM 的上下文要标记"上一句没说完",否则它会接着没说完的话往下编。

成本账:按字符 / 按分钟,两种计费模型

云语音服务的计费就两种模型,算账时别搞混:

  • ASR 按音频分钟计费:你传了多少分钟的音频(通常按 15 秒向上取整),就收多少分钟的钱。注意"静音也算钱"——用户按住麦克风发呆的 30 秒,照单全收。所以 VAD 先行、只把有人声的片段发给 ASR,省的是真金白银。
  • TTS 按字符计费:按输入文本的字符数(含标点)计费。英文按字符、中文按字,SSML 标签通常不计费(各家口径有差异)。长文本播报前先估算:1 万字的文章朗读 ≈ 1 万字符,一天 1000 次播报就是 1000 万字符/天。

具体单价各家差异大且经常调价,以官网最新定价为准,本文不写具体数字(写了就是过期误导)。算账公式给你:月成本 ≈ 日活语音用户数 × 人均语音分钟数 × ASR 单价 + 日播报字符数 × TTS 单价。先按 10 倍于预期的量算一遍,如果这个数你付不起,说明架构要先做配额和降级(见下节),而不是先上线。

省钱的三板斧:VAD 裁掉静音(ASR 账单直接打折);TTS 结果缓存(同一句话第二次播报直接放录音,AI 助手的开场白、错误提示都是高频重复句);给免费用户设每日语音配额,超了回退到文字。

降级策略:ASR 失败永远回退到键盘

语音功能的降级不是"出错弹个 toast",而是一整条 fallback 链,每一级都是可用的产品:

  1. 浏览器不支持(Firefox、无麦克风权限、应用内 webview):语音按钮直接隐藏,键盘输入框就是全部。不要显示一个点不开的灰色按钮——那不叫降级,叫摆设。
  2. 识别失败(no-speech、network、识别置信度过低):保留用户已说出的部分文字(别清空!),在输入框里可编辑,把"重说"和"直接改字"两个选项都给用户。
  3. 云服务挂了:Web Speech API 和云 ASR 互为备份。主用云 ASR 的,异常时 30 秒内自动切浏览器原生识别,至少不断粮。
  4. 成本熔断:当日语音配额用完,自动降级为纯文字模式,并在 UI 上如实告诉用户"今日语音额度已用完",而不是悄悄变慢。

降级策略要在第一版就写好,而不是"等用户投诉再加"。语音是最不可靠的输入通道——网络、噪音、口音、权限,四个变量任何一个崩了,没有 fallback 就是一次完整的用户流失。

上线 checklist

  • 三问通过:手腾不出来、说比打快、环境允许说话。
  • ASR 选型:MVP 用 Web Speech API(记得 feature-detect + Firefox 隐藏按钮),量上来或准确率不够再迁云。
  • TTS 选型:短播报用 speechSynthesis(处理 voiceschanged),长内容/品牌音色上云 TTS + SSML。
  • 延迟:用户说完到出声 <2000ms 可用、<1200ms 优秀;先把"等 LLM 整句"改成流式流水线。
  • 移动端:权限只在点按钮时申请;打断处理(barge-in)做了回声防护;iOS 真机测过。
  • 成本:按 10 倍预期量算过账单;VAD 裁静音;TTS 高频句缓存;免费用户有配额。
  • 降级:四级 fallback 链写完;配额用完如实提示,不悄悄变慢。

语音交互的本质,是给产品加一条"最自然但最不可靠"的通道。工程上的全部工作,就是在"自然"和"可靠"之间搭脚手架:VAD 保证不瞎等,流式保证不瞎慢,降级保证不瞎崩。脚手架搭好了,语音才是加分项;否则 demo 里越惊艳,上线后摔得越惨。

阅读 0评论 0

评论 (0)

ME
0/1000
评论加载中...
浏览项目广场发布你的项目

相关文章

城市高空俯拍照片:暖色暮光下的密集街区与高楼,隐喻城市位置服务
指南
“附近的咖啡馆”怎么做:vibe 项目的位置与地图功能实战

从“附近的咖啡馆”到骑手实时位置:入库只存 WGS84、展示层再转 GCJ-02/BD-09,附近搜索走粗排精排两阶段,地图 SDK 先看合规再看价格,逆地理编码按 geohash 缓存,精确位置从采集侧就当敏感个人信息对待。

移动应用后端工程独立开发
视频剪辑时间线特写照片:显示器上可见剪辑轨道、调色轮与时间码
指南
用户传了个 4K 视频,服务器直接爆了:vibe 项目的视频处理管线实战

视频上传完只是开始:转码、多码率、HLS 切片、异步队列,缺一不可才能流畅播放。这篇给你完整管线架构图、FFmpeg 命令模板、HLS 参数甜点、BullMQ 状态机设计、缩略图/字幕/审核三个并行分支,以及上线前必须算的三笔成本账。

部署上线性能优化后端工程