多方言自动语音识别(ASR)正在成为中文语音交互的刚需:普通话、吴语、粤语等口音在真实场景中频繁混杂,前端如果能“即录即识、边说边出字”,将显著提升交互体验。本文面向前端工程师,完整拆解一个可落地的多方言 ASR 接入方案:
前端多方言 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。
伪代码要点:
// 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
// 进入预处理→编码→上传
};
在嘈杂/外放场景,做一点“不过度”的前端增强很有价值:
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;
}
注意:这只是“轻处理”,不要期待替代专业降噪。低端设备可关闭或降低强度以省电。
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);
}
大多数流式 ASR 服务要求:
status=0
起始帧:一次;可不携带音频status=1
中间帧:多次;携带连续的 Base64 PCM16status=2
结束帧:一次;标识会话结束seq
:自增序列,严禁断档或回退构造上行消息示例:
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 || ''
}
}
};
}
发送主循环:
let ws, seq = 0, streaming = false;
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();
}
工程建议:
多数 ASR 云服务使用 HMAC-SHA256 + Base64 的鉴权签名拼接到 WebSocket URL。核心点:
date
使用 new Date().toUTCString()
request-line
与服务端接口路径必须一致wsUrl
示意实现(前端侧,仅供测试):
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}`;
}
流式识别往往返回两类片段:
典型合并策略:
1) 维护 stableText
和 unstableText
两段
2) 收到新片段时,若标记为稳定则合并入 stableText
并清空 unstableText
3) 若为不稳定则覆盖 unstableText
4) 展示时渲染 stableText + unstableText
这样就能实现“边说边出字,逐步稳定”。
识别与播报同时进行容易造成回灌(扬声器声音被麦克风拾入),建议在开始识别时自动暂停 TTS,或强制静音;播放结束/用户停止识别后再恢复。浏览器 speechSynthesis
足够应付基础播报需求:
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
request-line
路径、客户端时钟同步。多方言 ASR 的关键并不在“是否能用”,而在于“能否稳定可用、体验平滑”。从录音、预处理、编码、传输到 UI 呈现,每一环都要做工程化约束:严格的帧协议、容错的增量合并、与 TTS 的互斥、以及签名与时间同步。按本文清单实施,即可快速构建“边说边出字”的中文多方言识别体验,并具备良好的可维护性与扩展性。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。