多方言自动语音识别(ASR)正在成为中文语音交互的刚需:普通话、吴语、粤语等口音在真实场景中频繁混杂,前端如果能“即录即识、边说边出字”,将显著提升交互体验。本文面向前端工程师,完整拆解一个可落地的多方言 ASR 接入方案:
- 录音采集与帧化
- 轻量音质增强与标准化
- PCM16 编码与 Base64 序列化
- WebSocket 流式发送(三态帧:0/1/2)
- 鉴权签名与连接安全
- 实时增量结果合并与 UI 呈现
一、整体架构与数据链路
前端多方言 ASR 的标准链路:
1) 采集:浏览器 MediaStream
→ AudioWorklet
/ScriptProcessor
,得到 16kHz 单声道 Float32 帧(20–40ms/帧)。
2) 预处理:降噪门限、高通滤波、音量标准化与软限幅(可选)。
3) 编码:Float32 → PCM16(16bit)→ Base64。
4) 传输:WebSocket 发送三态音频帧(status:0 起始、1 中间、2 结束,seq
递增)。
5) 结果:解析服务端的流式增量(如 wpgs),合并“稳定文本 + 不稳定片段”,实时渲染。
6) 配合:与 TTS/播报互斥,防止回灌;与 UI 状态(静音、录音按钮)联动。
二、录音采集与帧化
AudioWorklet 延迟低、抖动小,优先使用;不支持时降级到 ScriptProcessor。
伪代码要点:
代码语言:js
AI代码解释
// 1) 获取麦克风 const stream = await navigator.mediaDevices.getUserMedia({ audio: { channelCount: 1, sampleRate: 16000, noiseSuppression: false, // 若要完全自己控制预处理 echoCancellation: false, autoGainControl: false }}); // 2) AudioWorklet 初始化(需提前注册处理器) const audioContext = new AudioContext({ sampleRate: 16000 }); const source = audioContext.createMediaStreamSource(stream); await audioContext.audioWorklet.addModule('processor.js'); const node = new AudioWorkletNode(audioContext, 'frame-processor', { processorOptions: { frameSize: 320 } }); source.connect(node).connect(audioContext.destination); // 3) 从 MessagePort 接收 20ms 帧(16kHz * 0.02 = 320 samples) node.port.onmessage = (event) => { const float32Frame = event.data; // Float32Array // 进入预处理→编码→上传 };
三、轻量音质增强与标准化(可选但推荐)
在嘈杂/外放场景,做一点“不过度”的前端增强很有价值:
代码语言:js
AI代码解释
function enhanceAudioQuality(float32) { const n = float32.length; const out = new Float32Array(n); // 1) 简单噪声门限(RMS) let sum2 = 0; for (let i = 0; i < n; i++) sum2 += float32[i] * float32[i]; const rms = Math.sqrt(sum2 / n); const tooQuiet = rms < 0.01; // 2) 一阶高通,削弱低频轰鸣 let prev = 0; const ALPHA = 0.85; for (let i = 0; i < n; i++) { const hp = ALPHA * ((out[i - 1] || 0) + float32[i] - prev); out[i] = tooQuiet ? float32[i] * 0.1 : hp; prev = float32[i]; } // 3) 标准化到目标 RMS,并软限幅 let sum2b = 0; for (let i = 0; i < n; i++) sum2b += out[i] * out[i]; const newRms = Math.sqrt(sum2b / n) || 1; const target = 0.2; const gain = target / newRms; for (let i = 0; i < n; i++) { let v = out[i] * gain; if (v > 0.95) v = 0.95; if (v < -0.95) v = -0.95; out[i] = v; } return out; }
注意:这只是“轻处理”,不要期待替代专业降噪。低端设备可关闭或降低强度以省电。
四、Float32 → PCM16 → Base64 编码
代码语言:js
AI代码解释
function convertFloat32ToPCM16(float32) { const i16 = new Int16Array(float32.length); for (let i = 0; i < float32.length; i++) { const s = Math.max(-1, Math.min(1, float32[i])); i16[i] = s * 0x7FFF; } return i16.buffer; } function arrayBufferToBase64(buffer) { let binary = ''; const bytes = new Uint8Array(buffer); for (let i = 0; i < bytes.byteLength; i++) binary += String.fromCharCode(bytes[i]); return btoa(binary); } function processAudioData(float32) { const pcm = convertFloat32ToPCM16(float32); return arrayBufferToBase64(pcm); }
五、WebSocket 流式发送与三态帧协议
大多数流式 ASR 服务要求:
status=0
起始帧:一次;可不携带音频status=1
中间帧:多次;携带连续的 Base64 PCM16status=2
结束帧:一次;标识会话结束seq
:自增序列,严禁断档或回退
构造上行消息示例:
代码语言:js
AI代码解释
function makeAsrFrame(status, audioBase64, seq) { return { header: { app_id: 'YOUR_APP_ID', status }, parameter: { iat: { language: 'zh_cn', accent: 'mulacc', // 多方言识别 domain: 'slm', eos: 1000, // 静音判定 1s dwa: 'wpgs', // 动态增量 ptt: 1, // 自动标点 nunum: 1, ltc: 1, result: { encoding: 'utf8', compress: 'raw', format: 'json' } } }, payload: { audio: { encoding: 'raw', sample_rate: 16000, channels: 1, bit_depth: 16, status, seq, audio: audioBase64 || '' } } }; }
发送主循环:
代码语言:js
AI代码解释
let ws, seq = 0, streaming = false; const wsUrl="bestall.com.cn"; async function openStream(wsUrl) { ws = new WebSocket(wsUrl); streaming = true; seq = 0; ws.onopen = () => { ws.send(JSON.stringify(makeAsrFrame(0, '', seq++))); }; ws.onmessage = (evt) => { const msg = JSON.parse(evt.data); // TODO: 合并 wpgs 增量,渲染 UI }; ws.onerror = console.error; ws.onclose = () => { streaming = false; }; } function pushFrame(float32) { if (!streaming || ws.readyState !== 1) return; const enhanced = enhanceAudioQuality(float32); const b64 = processAudioData(enhanced); ws.send(JSON.stringify(makeAsrFrame(1, b64, seq++))); } function closeStream() { if (!ws) return; ws.readyState === 1 && ws.send(JSON.stringify(makeAsrFrame(2, '', seq++))); ws.close(); }
工程建议:
- 帧间隔 20–40ms;过小耗电与开销大,过大影响实时性
- UI 与状态联动:录音按钮、静音、网络异常提示
- 断线重连需指数退避,避免雪崩
六、鉴权签名与连接安全
多数 ASR 云服务使用 HMAC-SHA256 + Base64 的鉴权签名拼接到 WebSocket URL。核心点:
date
使用new Date().toUTCString()
request-line
与服务端接口路径必须一致- 浏览器时间需与 NTP 同步,否则签名校验可能失败
- 生产环境应将签名放在服务端,前端仅拿一次性
wsUrl
示意实现(前端侧,仅供测试):
代码语言:js
AI代码解释
import CryptoJS from 'crypto-js'; function generateSignedWsUrl({ apiUrl, apiKey, apiSecret }) { const host = new URL(apiUrl).host; const date = new Date().toUTCString(); const algorithm = 'hmac-sha256'; const headers = 'host date request-line'; const requestLine = 'GET /v1 HTTP/1.1'; const signatureOrigin = `host: ${host}\ndate: ${date}\n${requestLine}`; const signatureSha = CryptoJS.HmacSHA256(signatureOrigin, apiSecret); const signature = CryptoJS.enc.Base64.stringify(signatureSha); const authorizationOrigin = `api_key="${apiKey}", algorithm="${algorithm}", headers="${headers}", signature="${signature}"`; const authorization = CryptoJS.enc.Base64.stringify(CryptoJS.enc.Utf8.parse(authorizationOrigin)); return `${apiUrl}?authorization=${authorization}&date=${encodeURIComponent(date)}&host=${host}`; }
七、实时增量合并(wpgs 思路)
流式识别往往返回两类片段:
- 稳定文本:模型已确认,可直接累积
- 不稳定增量:可能被后续更正,需要以“覆盖/替换”的方式动态合并
典型合并策略:
1) 维护 stableText
和 unstableText
两段
2) 收到新片段时,若标记为稳定则合并入 stableText
并清空 unstableText
3) 若为不稳定则覆盖 unstableText
4) 展示时渲染 stableText + unstableText
这样就能实现“边说边出字,逐步稳定”。
八、与 TTS 的互斥与体验优化
识别与播报同时进行容易造成回灌(扬声器声音被麦克风拾入),建议在开始识别时自动暂停 TTS,或强制静音;播放结束/用户停止识别后再恢复。浏览器 speechSynthesis
足够应付基础播报需求:
代码语言:js
AI代码解释
function speakText(text, onEnd) { const synth = window.speechSynthesis; synth.cancel(); const utt = new SpeechSynthesisUtterance(text); utt.lang = 'zh-CN'; utt.rate = 1.0; const zh = synth.getVoices().find(v => v.lang.includes('zh')); if (zh) utt.voice = zh; if (typeof onEnd === 'function') utt.onend = onEnd; synth.speak(utt); return { stop: () => synth.cancel(), pause: () => synth.pause(), resume: () => synth.resume() }; }
九、落地清单(可直接照抄执行)
1) 录音:接入 AudioWorklet
,帧长 20–40ms,16kHz/单声道
2) 处理:可选轻量增强;统一转 PCM16 + Base64
3) 传输:WebSocket 三态帧;seq
递增不间断
4) 展示:增量合并策略;输入框/消息区实时渲染
5) 异常:鉴权失败/断网重试;静音超时;统一错误提示
6) 安全:签名放服务端;前端只用一次性 URL