摘要
Web 交互界面已经超越了传统的鼠标点击与键盘输入。由 W3C 制定的 Web Speech API 提供了两个独立的子系统:SpeechRecognition(语音转文本)用于捕获并转录用户的语音输入,而 SpeechSynthesis(文本转语音)则使用系统内置的语音库朗读文本。
本文详细介绍了这两个子系统的架构设计、代码实战以及跨浏览器兼容性解决方案(包括 Chrome 15 秒中断 Bug 和 iOS 限制)。
1. 两个独立的子系统
该 API 划分为两个完全解耦的模块:
| 子系统 | 交互方向 | 核心接口 | 兼容性现状 |
|---|---|---|---|
| SpeechRecognition | 用户 → 应用(麦克风输入) | SpeechRecognition / webkitSpeechRecognition | 受限(主要是 Chromium 核心) |
| SpeechSynthesis | 应用 → 用户(音频输出) | window.speechSynthesis + SpeechSynthesisUtterance | 全面支持 (Baseline) |
注意:SpeechSynthesis 在现代浏览器中已获得全面支持。然而 SpeechRecognition 仍处于实验阶段。在使用前请务必进行特性检测。
2. 语音识别 (Speech-to-Text)
2.1 特性检测与实例化
由于厂商前缀的存在,构造函数的获取应采取防御性写法:
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
if (!SpeechRecognition) {
console.error('当前浏览器不支持 SpeechRecognition。');
}
2.2 配置属性
实例化后,可通过以下四个核心属性对识别引擎进行配置:
| 属性 | 类型 | 描述 |
|---|---|---|
lang | string | BCP 47 语言标签(如 "zh-CN"、"en-US")。默认使用文档的 lang 属性。 |
continuous | boolean | 若为 true,将在返回结果后继续监听;默认值为 false(返回单次结果后自动停止)。 |
interimResults | boolean | 若为 true,将在识别过程中实时返回临时中间结果。 |
maxAlternatives | number | 每个结果片段返回的最大替代匹配数量。默认值为 1。 |
const recognition = new SpeechRecognition();
recognition.lang = 'zh-CN';
recognition.continuous = true;
recognition.interimResults = true;
recognition.maxAlternatives = 1;
2.3 识别结果的层级结构
返回的识别结果并非纯字符串,而是一个具有三层结构的嵌套对象:
SpeechRecognitionResultList— 可通过event.results访问的结果列表集合SpeechRecognitionResult— 包含isFinal状态标识及一个或多个替代匹配SpeechRecognitionAlternative— 包含转录文本transcript与置信度分值confidence(0 到 1)
recognition.onresult = (event) => {
for (let i = event.resultIndex; i < event.results.length; i++) {
const result = event.results[i];
const transcript = result[0].transcript;
const confidence = result[0].confidence;
if (result.isFinal) {
console.log(`最终结果: "${transcript}" (${(confidence * 100).toFixed(1)}%)`);
} else {
console.log(`临时结果: "${transcript}"`);
}
}
};
2.4 事件生命周期
识别引擎按照确定的顺序触发事件:
start → audiostart → soundstart → speechstart
↓
result(在说话过程中持续触发)
↓
speechend → soundend → audioend → end
需要重点处理的关键事件:
recognition.onspeechstart = () => {
console.log('检测到语音 — 正在实时转录...');
};
recognition.onerror = (event) => {
switch (event.error) {
case 'not-allowed':
console.error('麦克风权限被用户或安全策略拒绝。');
break;
case 'network':
console.error('无法连接到云端语音识别服务。');
break;
case 'no-speech':
console.warn('未检测到语音 — 仅检测到静音或背景噪音。');
break;
case 'aborted':
console.warn('语音识别已被中断。');
break;
}
};
recognition.onnomatch = () => {
console.warn('检测到声音,但无法识别为有效文字。');
};
recognition.onend = () => {
console.log('语音识别会话已结束。');
};
2.5 完整示例:实时语音听写
const SpeechRecognition = window.SpeechRecognition || window.webkitSpeechRecognition;
if (!SpeechRecognition) {
document.getElementById('status').textContent = '当前浏览器不支持语音识别。';
} else {
const recognition = new SpeechRecognition();
recognition.lang = 'zh-CN';
recognition.continuous = true;
recognition.interimResults = true;
const finalOutput = document.getElementById('final');
const interimOutput = document.getElementById('interim');
let finalTranscript = '';
recognition.onresult = (event) => {
let interim = '';
for (let i = event.resultIndex; i < event.results.length; i++) {
const transcript = event.results[i][0].transcript;
if (event.results[i].isFinal) {
finalTranscript += transcript;
} else {
interim += transcript;
}
}
finalOutput.textContent = finalTranscript;
interimOutput.textContent = interim;
};
recognition.onerror = (event) => {
document.getElementById('status').textContent = `错误: ${event.error}`;
};
document.getElementById('start-btn').addEventListener('click', () => {
finalTranscript = '';
recognition.start();
document.getElementById('status').textContent = '正在倾听...';
});
document.getElementById('stop-btn').addEventListener('click', () => {
recognition.stop();
document.getElementById('status').textContent = '已停止。';
});
}
3. 语音合成 (Text-to-Speech)
3.1 单例控制器
与 SpeechRecognition 不同,语音合成引擎通过一个全局单例进行访问:
const synth = window.speechSynthesis;
该控制器管理着一个 FIFO 朗读队列,并暴露了三个只读状态标识:
| 属性 | 描述 |
|---|---|
synth.pending | 若队列中有等待朗读的任务则返回 true |
synth.speaking | 若当前正在播放语音则返回 true |
synth.paused | 若播放已被暂停则返回 true |
控制方法:
synth.speak(utterance);
synth.pause();
synth.resume();
synth.cancel();
3.2 创建朗读任务 (Utterance)
SpeechSynthesisUtterance 对象是朗读的基本单元,包含待朗读的文本及其声学属性:
| 属性 | 取值范围 | 描述 |
|---|---|---|
text | String | 待朗读的文本内容 |
voice | SpeechSynthesisVoice | 指定使用的语音包 |
lang | String (BCP 47) | 覆盖默认语言设置 |
pitch | 0 – 2 | 音调(1 表示正常音调) |
rate | 0.1 – 10 | 语速(1 表示正常语速) |
volume | 0 – 1 | 音量(1 表示最大音量) |
const utterance = new SpeechSynthesisUtterance('你好,欢迎使用本应用。');
utterance.lang = 'zh-CN';
utterance.pitch = 1;
utterance.rate = 1;
utterance.volume = 0.8;
synth.speak(utterance);
3.3 异步加载语音包
浏览器加载语音库通常是异步的(特别是在 Chromium 中)。如果在页面刚加载时同步调用 getVoices(),往往会返回一个空数组。应该监听 voiceschanged 事件:
const synth = window.speechSynthesis;
let voices = [];
const loadVoices = () => {
voices = synth.getVoices();
console.log(`已成功加载 ${voices.length} 个语音包。`);
};
loadVoices();
if (synth.onvoiceschanged !== undefined) {
synth.onvoiceschanged = loadVoices;
}
每个 SpeechSynthesisVoice 对象包含:
voices.forEach((voice) => {
console.log(`${voice.name} (${voice.lang}) ${voice.default ? '— 默认' : ''}`);
});
3.4 完整示例:带声音选择的文本朗读器
const synth = window.speechSynthesis;
let voices = [];
const voiceSelect = document.getElementById('voice-select');
const textInput = document.getElementById('text-input');
const populateVoices = () => {
voices = synth.getVoices();
voiceSelect.innerHTML = '';
voices.forEach((voice, index) => {
const option = document.createElement('option');
option.value = index;
option.textContent = `${voice.name} (${voice.lang})`;
voiceSelect.appendChild(option);
});
};
populateVoices();
if (synth.onvoiceschanged !== undefined) {
synth.onvoiceschanged = populateVoices;
}
document.getElementById('speak-btn').addEventListener('click', () => {
synth.cancel();
const utterance = new SpeechSynthesisUtterance(textInput.value);
utterance.voice = voices[voiceSelect.value];
utterance.pitch = parseFloat(document.getElementById('pitch').value);
utterance.rate = parseFloat(document.getElementById('rate').value);
utterance.onstart = () => {
document.getElementById('status').textContent = '正在朗读...';
};
utterance.onend = () => {
document.getElementById('status').textContent = '朗读完毕。';
};
utterance.onerror = (event) => {
document.getElementById('status').textContent = `错误: ${event.error}`;
};
synth.speak(utterance);
});
document.getElementById('stop-btn').addEventListener('click', () => {
synth.cancel();
document.getElementById('status').textContent = '已取消。';
});
4. 云端处理 vs. 端侧离线推理
SpeechRecognition 可以通过两种截然不同的管道路由音频:
4.1 云端处理(默认模式)
默认情况下,Chrome 会将音频流序列化并发送至 Google 的云端服务器进行转录。这种方式准确率高(依托深度神经网络模型与大规模训练数据集),但也带来了以下权衡:
- 延迟:网络往返会降低实时交互体验
- 隐私:原始音频数据会离开设备
- 离线:在无网络连接时完全无法使用
4.2 端侧本地推理(实验性)
现代浏览器正在引入基于端侧模型的本地推理能力。这消除了网络延迟,支持离线使用,并确保音频数据保留在本地:
const recognition = new SpeechRecognition();
const availability = await SpeechRecognition.available();
if (availability === 'available') {
recognition.start();
} else if (availability === 'downloadable') {
await SpeechRecognition.install();
recognition.start();
} else {
console.warn('不支持端侧识别,降级使用云端服务。');
recognition.start();
}
注意:
SpeechRecognition.available()与SpeechRecognition.install()为实验性 API,可用于检查语言模型是否已缓存在本地并触发其下载。目前仅在较新版本的 Chromium 中受支持。
5. 安全与权限模型
为防止被动窃听,Web Speech API 实施了严格的安全限制:
- 必须由用户手势触发:
recognition.start()必须在用户主动触发的事件(点击、轻触)中调用。在没有用户手势的情况下以编程方式调用会抛出错误。 - 仅限 HTTPS:整个 API 仅在安全上下文 (Secure Context) 中可用,HTTP 页面无法访问麦克风。
- 视觉指示器:麦克风处于激活状态时,浏览器会显示持续且无法隐藏的录音指示(标签页图标、状态栏提示)。
- Permissions-Policy:服务器管理员可以通过 HTTP 响应头禁止跨源 iframe 访问麦克风:
Permissions-Policy: microphone=()
6. 浏览器兼容性 (2026)
| 浏览器 | SpeechSynthesis (TTS) | SpeechRecognition (STT) | 备注 |
|---|---|---|---|
| Chrome | ✅ 完全支持 (v33+) | ✅ 完全支持 (v33+) | 主要实现者,默认基于云端。 |
| Edge | ✅ 完全支持 (v14+) | ⚠️ 不稳定 | 暴露了接口,但相关事件可能永远不会触发。 |
| Firefox | ✅ 完全支持 (v49+) | ❌ 未实现 | 原则上反对将语音生物特征数据发送至第三方云服务。 |
| Safari | ✅ 完全支持 (v7+) | ⚠️ 部分支持 (v14.1+) | 需要 webkit 前缀,依赖于 Siri 是否启用。 |
| Opera | ✅ 完全支持 (v21+) | ❌ 未实现 | 与 Edge/Firefox 的限制一致。 |
| Brave | ⚠️ 视情况而定 | ❌ 已拦截 | 出于隐私考虑拦截云端 STT 连接。 |
6.1 防御性特性检测
在使用任一子系统之前,务必先验证浏览器支持情况:
const hasSpeechRecognition = 'SpeechRecognition' in window || 'webkitSpeechRecognition' in window;
const hasSpeechSynthesis = 'speechSynthesis' in window;
if (!hasSpeechRecognition) {
console.warn('SpeechRecognition 不可用。可考虑使用云端降级方案(Google Cloud STT、Azure、AWS Transcribe)。');
}
if (!hasSpeechSynthesis) {
console.warn('SpeechSynthesis 不可用。');
}
7. 已知问题与解决方案
7.1 Chrome 语音合成 15 秒中断问题
在 Chrome 中使用云端语音时,时长超过约 15 秒的朗读会被中止,且不会触发 end 或 error 事件,导致界面一直停留在“朗读中”状态。解决方法是将长文本切分为较短的朗读任务:
const speakLongText = (text) => {
const synth = window.speechSynthesis;
synth.cancel();
const sentences = text.match(/[^.!?。!?]+[.!?。!?]+/g) || [text];
sentences.forEach((sentence) => {
const utterance = new SpeechSynthesisUtterance(sentence.trim());
utterance.voice = voices[0];
synth.speak(utterance);
});
};
7.2 iOS Safari:语音合成需要“预热”手势
iOS 上的 Safari 会在用户触发手势之前阻止 TTS 音频播放。解决方法是在首次用户交互时静默地“预热”音频上下文:
document.addEventListener('click', () => {
const primer = new SpeechSynthesisUtterance('');
window.speechSynthesis.speak(primer);
}, { once: true });
完成预热后,后续对 synth.speak() 的调用无需额外的用户手势即可正常工作。
7.3 iOS PWA 中的 SpeechRecognition
当网页在 iOS 上被“添加到主屏幕”后,webkitSpeechRecognition 会被完全禁用。所有调用都会直接抛出 NotAllowed 错误,甚至不会显示权限请求提示。Web Speech API 内部没有任何规避方法,必须改用基于云端的 STT 服务。
8. 无障碍收益
Web Speech API 能够直接提升以下用户群体的可访问性:
- 行动障碍用户:语音指令可替代键盘与鼠标操作
- 视障用户:TTS 可朗读页面内容,作为屏幕阅读器的补充
- 读写能力有限的用户:音频反馈降低了对阅读能力的依赖
- 免手操作场景:烹饪、驾驶或医疗环境等不便于触控或键盘输入的场合
在无障碍界面中集成语音功能的最佳实践:
- 收起虚拟键盘:在识别开始时调用
element.blur(),以在移动设备上释放视口空间 - 显示临时结果:以可视方式展示临时结果,让用户在听写过程中获得实时反馈
- 使用 TTS 的
boundary事件:在朗读时高亮对应文本,提供视听同步的体验
9. 总结
Web Speech API 在语音与 Web 平台之间建立了有效的桥梁。SpeechSynthesis 已可在所有浏览器中用于生产环境,而 SpeechRecognition 由于支持程度参差不齐,需要谨慎的特性检测与降级策略。
建议从 TTS 入手改善可访问性,它在所有浏览器中均可使用。对于 STT,应以使用云端识别的 Chromium 浏览器为目标,实现完善的错误处理,并评估云端降级服务(Google Cloud Speech-to-Text、Azure Cognitive Services、AWS Transcribe)以覆盖更多浏览器。
参考来源:W3C Web Speech API 规范、MDN Web 文档、Chromium Issue Tracker、浏览器兼容性数据 (caniuse.com)。示例对应截至 2026 年 3 月的 API 状态。