Skip to content

指南

基于 MDN Web Docs 与 W3C WebAudio Community Group 规范草案(2026 年)编写

速查

  • 选型第一问:只要朗读(TTS)还是要听写(STT)?TTS 跨浏览器稳,STT 仅 Chromium/Safari
  • 跨浏览器 STT:检测 SpeechRecognition 不存在时降级到 Whisper API / AssemblyAI
  • 离线/隐私要求高:不用 Chrome 原生 STT(传云),改用 whisper.cpp WASM 或本地模型
  • 长文本 TTS:Chrome 有约 15 秒自动停止 Bug,需分句 + onboundary 或定时 resume
  • 选语言:lang 用 BCP-47(zh-CN / en-US / ja-JP),选错识别率骤降
  • 连续听写:continuous=true + onend 自动重启(Chrome 会因网络自行 end)
  • 指令识别:别用已废弃的 Grammar,自己在 onresult 里做关键词匹配
  • 权限 UX:start() 必须用户手势触发,提前提示「即将使用麦克风」
  • 中文 TTS 选声:getVoices()lang.startsWith('zh'),优先 localService=true
  • 实时字幕:interimResults=true 显示 partial,isFinal 时入库

选型决策树

需求?

├─ 只需文字转语音(朗读)?
│     └ ✅ window.speechSynthesis(Baseline 全平台支持)
│        音质要求高 → Azure TTS / ElevenLabs

├─ 只需语音转文字(听写/命令)?
│     ├ Chrome/Edge/Safari 受众 → ✅ 原生 SpeechRecognition(免费、低延迟)
│     ├ 必须跨浏览器/含 Firefox → Whisper API / AssemblyAI(WebSocket)
│     ├ 必须离线/隐私 → whisper.cpp WASM / Vosk
│     └ 高精度/多语种/说话人分离 → Whisper large / 商业 STT

├─ 语音助手(双向对话)?
│     └ STT + LLM + TTS 组合,注意回声消除(用 headset 或 AEC)

└─ 无障碍(accessibility)?
      └ ✅ TTS 朗读页面内容,WCAG 友好

跨浏览器降级方案

能力检测

javascript
function getSTTEngine() {
  const NativeSR = window.SpeechRecognition || window.webkitSpeechRecognition;
  if (NativeSR) return { type: "native", Ctor: NativeSR };
  return { type: "whisper", Ctor: null }; // 降级
}

降级到 Whisper API

javascript
async function whisperTranscribe(audioBlob) {
  const form = new FormData();
  form.append("file", audioBlob, "record.webm");
  form.append("model", "whisper-1");
  form.append("language", "zh");

  const res = await fetch("https://api.openai.com/v1/audio/transcriptions", {
    method: "POST",
    headers: { Authorization: `Bearer ${API_KEY}` },
    body: form,
  });
  const data = await res.json();
  return data.text; // 识别文字
}

原生 STT(实时、免费)与 Whisper API(跨浏览器、高精度、按量付费)的取舍:

维度原生 SpeechRecognitionWhisper API
浏览器Chromium/Safari全部(能录音即可)
实时性实时 interim 结果录完再传,秒级延迟
离线不可(Chrome 云端)不可
成本免费按音频分钟计费
精度中(短句好)高(长文/多语种/标点)
标点/分段自动加标点
说话人分离不支持部分(diarization)

录音用 MediaRecorder

javascript
async function startRecording() {
  const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
  const recorder = new MediaRecorder(stream);
  const chunks = [];
  recorder.ondataavailable = (e) => chunks.push(e.data);
  recorder.onstop = async () => {
    const blob = new Blob(chunks, { type: "audio/webm" });
    const text = await whisperTranscribe(blob);
    console.log(text);
  };
  recorder.start();
  // 停止:recorder.stop();
}

连续听写实战

完整听写组件骨架

javascript
class Dictation {
  constructor() {
    const SR = window.SpeechRecognition || window.webkitSpeechRecognition;
    this.recognition = new SR();
    this.recognition.lang = "zh-CN";
    this.recognition.continuous = true;
    this.recognition.interimResults = true;
    this.listening = false;
    this.finalText = "";

    this.recognition.onresult = (e) => {
      let interim = "";
      for (let i = e.resultIndex; i < e.results.length; i++) {
        const t = e.results[i][0].transcript;
        if (e.results[i].isFinal) this.finalText += t;
        else interim += t;
      }
      this.render(this.finalText, interim);
    };

    this.recognition.onerror = (e) => {
      if (e.error === "no-speech") return;       // 静默,忽略
      if (e.error === "not-allowed") {           // 权限被拒
        this.listening = false;
        alert("请授予麦克风权限");
      }
    };

    // Chrome 连续模式会在静默/网络中断时自动 end,需要重启
    this.recognition.onend = () => {
      if (this.listening) {
        try { this.recognition.start(); }
        catch (_) { /* 已在 start 中 */ }
      }
    };
  }

  start() {
    this.listening = true;
    this.recognition.start();
  }

  stop() {
    this.listening = false;
    this.recognition.stop();
  }

  render(final, interim) { /* 更新 DOM */ }
}

防止「重启抛错」

start() 在已运行时调用会抛 InvalidStateError。在 onend 重启时需 try/catch,或用标志位:

javascript
recognition.onend = () => {
  if (shouldListen && !recognitionRunning) {
    recognitionRunning = true;
    recognition.start();
  }
};
recognition.onstart = () => { recognitionRunning = true; };

Chrome 长文本 TTS Bug 规避

Chrome 的 speechSynthesis 在朗读较长文本(约 15 秒/200+ 字)时会莫名停止。社区常用 workaround:

方案 1:分句朗读

javascript
function speakLong(text) {
  // 按标点切成短句
  const chunks = text.match(/[^。!?.!?]+[。!?.!?]?/g) || [text];
  let i = 0;
  const speakNext = () => {
    if (i >= chunks.length) return;
    const u = new SpeechSynthesisUtterance(chunks[i]);
    u.lang = "zh-CN";
    u.onend = () => { i++; speakNext(); };
    window.speechSynthesis.speak(u);
  };
  speakNext();
}

方案 2:定时 resume(hack)

javascript
function keepAlive() {
  if (window.speechSynthesis.speaking) {
    window.speechSynthesis.pause();
    window.speechSynthesis.resume();
  }
}
setInterval(keepAlive, 10000); // 每 10 秒

方案 3:boundary 切分

监听 onboundary 拿到当前 charIndex,超时则从该位置重新 speak

语音指令实现(无 Grammar)

废弃的 SpeechGrammarList 不能做有限词表,改在 onresult 中匹配:

javascript
const COMMANDS = {
  "打开首页": () => (location.href = "/"),
  "搜索": (q) => location.href = `/search?q=${encodeURIComponent(q)}`,
  "停止": () => recognition.stop(),
};

recognition.onresult = (e) => {
  const text = e.results[e.results.length - 1][0].transcript.trim();
  for (const [cmd, fn] of Object.entries(COMMANDS)) {
    if (text.includes(cmd)) {
      const arg = text.replace(cmd, "").trim();
      fn(arg);
      return;
    }
  }
  // 未命中指令 → 当作自由文本
};

更鲁棒的做法:把识别文本喂给 LLM 做意图分类 +槽位抽取。

TTS 选声与多语言

javascript
async function pickVoice(lang) {
  let voices = window.speechSynthesis.getVoices();
  if (!voices.length) {
    await new Promise((r) => {
      window.speechSynthesis.onvoiceschanged = r;
    });
    voices = window.speechSynthesis.getVoices();
  }
  // 优先本地服务、目标语言
  return (
    voices.find((v) => v.lang === lang && v.localService) ||
    voices.find((v) => v.lang.startsWith(lang.split("-")[0])) ||
    voices.find((v) => v.default)
  );
}

const voice = await pickVoice("zh-CN");
utterance.voice = voice;
平台典型中文语音
macOSTing-Ting(本地)
WindowsMicrosoft Huihui / Xiaoxiao
Chrome(Google 云)Google 普通话(中国大陆)(localService=false
Android系统TTS引擎语音

注意:localService=false 的语音(如 Google 云端)在某些网络环境下不可用。

错误处理

STT error 类型

event.error含义处理
no-speech没检测到语音忽略,继续
audio-capture麦克风硬件问题提示检查设备
not-allowed / service-not-allowed权限被拒引导授权
network网络故障(Chrome 云端)重试或降级
aborted主动 abort()正常
language-unsupportedlang 不支持换 BCP-47

TTS error

SpeechSynthesisErrorEvent.error 常见值:synthesis-failed / audio-busy / interrupted / canceled

性能与体验

  • 节流 start:用户频繁点击麦克风时 debounce,避免 InvalidStateError
  • 可视化反馈:识别中显示波形/指示灯,onaudiostart/onspeechstart 驱动
  • 回声消除:TTS 声音被 STT 重新拾取(自循环),用耳机或 WebRTC AEC
  • 移动端耗电continuous=true 极耗电,非前台时 stop()
  • iOS 解锁:iOS Safari 必须用户手势触发首次 speak(),否则静音

无障碍(A11y)应用

javascript
// 为视障用户朗读选中内容
document.addEventListener("mouseup", () => {
  const text = window.getSelection().toString();
  if (text) {
    window.speechSynthesis.cancel();
    const u = new SpeechSynthesisUtterance(text);
    u.lang = document.documentElement.lang || "zh-CN";
    window.speechSynthesis.speak(u);
  }
});
  • WCAG 2.1 SC 1.4 系列鼓励提供「朗读」控件
  • 配合 aria-live="polite" 让屏幕阅读器与 TTS 协同

常见陷阱

陷阱解决
getVoices() 返回空监听 voiceschanged 后重新取
Chrome STT 一会儿就停onend 中自动重启,注意防 InvalidStateError
Firefox 报 SpeechRecognition is not defined降级到 Whisper API,或引导用户开 about:config
长文本 TTS 中途停止分句朗读 或 定时 pause()/resume()
识别中文全是乱字lang 没设或设错(应为 zh-CN
iOS 不出声首次必须在用户点击回调里 speak()
grammars 无效已废弃,改 onresult 关键词匹配
麦克风权限被拒引导到浏览器地址栏权限设置

版本里程碑

时间主要变化
2012Web Speech API 草案发布;Chrome 率先实现 webkitSpeechRecognition
2014-2016SpeechSynthesis 在各主流浏览器陆续落地
2018.09SpeechSynthesis 进入 Baseline(Widely available)
2021Safari 14.1 支持 webkitSpeechRecognition;Grammar 接口标记 deprecated
2023SpeechRecognition(无前缀)规范推进;Grammar 从规范移除
2024-2025Firefox 仍默认禁用 STT;社区转向 Whisper/Whisper.cpp 等跨浏览器方案
2026SpeechSynthesis 稳定;SpeechRecognition 仍「Limited availability」,规范处于 WICG 草案阶段