AI增强实时字幕与降噪
面向已有 WebRTC 通信链路的工程师,讨论如何在端侧或云端引入 AI 模型,做实时降噪与实时字幕(ASR),并把字幕通过 DataChannel 或 WebVTT 广播到接收端。覆盖 RNNoise / DeepFilterNet WASM 集成、AudioWorklet 与 SharedArrayBuffer 的数据流、Whisper Streaming 的端侧与云端取舍、VAD 触发与 partial transcript 修正、字幕渲染与多语言翻译流水线、性能预算与失败模式。
1. 端到端架构总览
1.1 数据流
1.2 关键约束
- WebRTC 采集默认 48 kHz、单声道、Float32;Whisper 系列固定 16 kHz、Mono、Float32,需要重采样与去直流。
- 降噪要在发送侧的音频图里完成,让对端听到的是已经降噪过的流,而不是只在本地"看到"降噪后的频谱。
- 字幕是元数据,不是音频,必须走独立通道(DataChannel 或 SFU 元数据扩展),不要尝试把字幕塞进 Opus 注释。
- AudioWorklet 是硬实时上下文,单次
process()调用预算约 2.7 ms(128 帧 @48 kHz),超时会引发 glitch;任何阻塞、GC、console.log都要谨慎。
1.3 何时不适用
- 通话端是低端 Android(4×A53、2 GB RAM):端侧 Whisper-tiny 也可能超出预算,应该退化为云端 ASR。
- 服务端是纯 SFU 不解码:云端字幕方案需要再额外接入 SFU 旁路转码,整体复杂度高于直接端侧出字幕。
- 端到端加密(Insertable Streams / E2EE)开启时:服务器拿不到 PCM,云端方案必须改为发送端就近上传到 ASR 服务,而不是 SFU 旁路。
2. AudioWorklet 与 WASM 集成
2.1 为什么必须用 AudioWorklet
ScriptProcessorNode跑在主线程,会被 React 渲染、postMessage队列、GC 阻塞,已经被 Web Audio API spec 标记为废弃。- AudioWorklet 跑在独立的渲染线程,与主线程通过
MessagePort+SharedArrayBuffer通信,可以做到稳定的 2.7 ms 周期。 - WASM 模块如果在主线程加载、在主线程执行,等于把降噪 / ASR 抢回到主线程,所有 AudioWorklet 的实时性优势就废了。
2.2 SharedArrayBuffer 启用前置条件
部署侧响应头必须满足 cross-origin isolation,否则 SharedArrayBuffer 在浏览器里直接是 undefined:
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
CDN、第三方脚本、内嵌的 iframe 都需要相应配合,常见反模式是只在主文档加了头但 SFU 的信令域名没加,导致 worker 加载就失败。
2.3 Lock-free RingBuffer
降噪 WASM(RNNoise 480 samples、DFN 480 或 960 samples)和 AudioWorklet(128 samples)的 frame 大小不同,必须用 ring buffer 桥接。SAB 上做单写单读 lock-free ring buffer,借鉴 LMAX Disruptor 的内存序:
// shared-ringbuffer.ts
// 单生产者单消费者 ring buffer:AudioWorklet 写、WASM worker 读,或反过来。
export class SharedRingBuffer {
static readonly HEADER_LEN = 2; // [writeIndex, readIndex]
private readonly header: Int32Array;
private readonly data: Float32Array;
private readonly capacity: number;
constructor(sab: SharedArrayBuffer, capacityFrames: number) {
this.header = new Int32Array(sab, 0, SharedRingBuffer.HEADER_LEN);
this.data = new Float32Array(sab, SharedRingBuffer.HEADER_LEN * 4, capacityFrames);
this.capacity = capacityFrames;
}
static create(capacityFrames: number): SharedArrayBuffer {
const bytes = SharedRingBuffer.HEADER_LEN * 4 + capacityFrames * 4;
return new SharedArrayBuffer(bytes);
}
/** 生产者写入;返回实际写入帧数(环满则丢弃尾部)。 */
push(chunk: Float32Array): number {
const w = Atomics.load(this.header, 0);
const r = Atomics.load(this.header, 1);
const free = this.capacity - ((w - r + this.capacity) % this.capacity) - 1;
const n = Math.min(chunk.length, free);
for (let i = 0; i < n; i++) {
this.data[(w + i) % this.capacity] = chunk[i];
}
Atomics.store(this.header, 0, (w + n) % this.capacity);
Atomics.notify(this.header, 0);
return n;
}
/** 消费者读取固定大小的 frame;不足则返回 0。 */
pull(out: Float32Array): number {
const w = Atomics.load(this.header, 0);
const r = Atomics.load(this.header, 1);
const avail = (w - r + this.capacity) % this.capacity;
if (avail < out.length) return 0;
for (let i = 0; i < out.length; i++) {
out[i] = this.data[(r + i) % this.capacity];
}
Atomics.store(this.header, 1, (r + out.length) % this.capacity);
return out.length;
}
}
为什么 capacity - 1:经典 ring buffer 留一个空位区分"空"和"满",否则 w == r 同时表示两种状态,读取端会读到旧数据。
2.4 AudioWorkletProcessor 与回写
// denoise-processor.ts,需要通过 audioWorklet.addModule 注册
declare const sampleRate: number;
declare const currentTime: number;
class DenoiseProcessor extends AudioWorkletProcessor {
private inRb!: SharedRingBuffer;
private outRb!: SharedRingBuffer;
private readonly tmp = new Float32Array(128);
constructor(options: AudioWorkletNodeOptions) {
super();
const { inSab, outSab, capacity } = options.processorOptions as {
inSab: SharedArrayBuffer;
outSab: SharedArrayBuffer;
capacity: number;
};
this.inRb = new SharedRingBuffer(inSab, capacity);
this.outRb = new SharedRingBuffer(outSab, capacity);
}
process(inputs: Float32Array[][], outputs: Float32Array[][]): boolean {
const input = inputs[0]?.[0];
const output = outputs[0]?.[0];
if (!input || !output) return true;
// 1. 写入麦克风采样(48kHz)
this.inRb.push(input);
// 2. 拉出 WASM worker 已经处理好的降噪后采样
const got = this.outRb.pull(this.tmp);
if (got === this.tmp.length) {
output.set(this.tmp);
} else {
output.fill(0); // underflow:宁可静音也别复读旧帧
}
return true;
}
}
registerProcessor('denoise-processor', DenoiseProcessor);
降噪算法本体在 worker 里跑:worker 不断 pull 48 kHz 输入、累积到 480 帧(RNNoise)后调用 WASM、把降噪后的 480 帧 push 回 outRb。
2.5 主线程装配
async function setupDenoisePipeline(stream: MediaStream): Promise<MediaStream> {
const ctx = new AudioContext({ sampleRate: 48000, latencyHint: 'interactive' });
await ctx.audioWorklet.addModule('/denoise-processor.js');
const capacity = 48000; // 1s 缓冲
const inSab = SharedRingBuffer.create(capacity);
const outSab = SharedRingBuffer.create(capacity);
const src = ctx.createMediaStreamSource(stream);
const node = new AudioWorkletNode(ctx, 'denoise-processor', {
processorOptions: { inSab, outSab, capacity },
channelCount: 1,
channelCountMode: 'explicit',
});
const dst = ctx.createMediaStreamDestination();
src.connect(node).connect(dst);
// worker 拿到同样的 SAB
const worker = new Worker('/denoise-worker.js', { type: 'module' });
worker.postMessage({ type: 'init', inSab, outSab, capacity });
return dst.stream; // 这条流挂到 RTCPeerConnection.addTrack
}
反模式:把 MediaStreamSource 直接连到 MediaStreamDestination,让 worker 只做"旁听"。这样对端听到的还是原始未降噪的音频。
3. RNNoise 与 DeepFilterNet 取舍
3.1 一句话定位
- RNNoise:2018 年 Xiph 出品,10 万参数级别的小 GRU,48 kHz 输入下专攻稳态噪声、键盘声、风扇。
- DeepFilterNet(DFN2/DFN3):2022—2023 年提出的 two-stage 深度滤波网络,48 kHz,结构复杂得多,覆盖非稳态噪声、回响、低频噪声。
3.2 对比表
| 维度 | RNNoise | DeepFilterNet 2/3 |
|---|---|---|
| 模型大小 | 约 85 KB(WASM 总体约 200 KB) | DFN2 约 2.3 MB,DFN3 约 1.4 MB |
| 采样率 | 48 kHz 原生 | 48 kHz 原生 |
| 帧长 | 10 ms(480 samples) | 20 ms(960 samples),算法时延更大 |
| 单核 CPU 占用(桌面 i5) | 0.5%—1% | DFN2 约 4%—6%,DFN3 约 2%—3% |
| 移动端(Snapdragon 7 Gen 1) | 1%—2% | DFN3 约 6%—10% |
| 降稳态噪声 | 优秀 | 优秀 |
| 降非稳态(敲键盘、关门、婴儿哭) | 一般 | 明显更好 |
| 抑制回响 | 不处理 | DFN3 内置 dereverb 模型 |
| 语音损伤(PESQ/STOI) | 中等 | 更低 |
| 许可证 | BSD-3-Clause | DFN 自身 Apache-2.0,预训练模型 Apache-2.0 |
3.3 选型建议
- 桌面端会议、对延迟和 CPU 敏感:RNNoise 起步,足够把空调、风扇、键盘声压下去。
- 移动端户外、嘈杂咖啡馆、多人后台噪声:DeepFilterNet 3 性价比更高,但要给中低端机做降级(关掉或回退到 RNNoise)。
- 已经接了云端会议平台(Daily、Agora)自带降噪:不要叠加两层,第二层往往把语音也磨掉。
3.4 失败时的样子
- RNNoise 在母语外的语种(强爆破音、强咝音)上偶尔会把辅音判为噪声,听起来"吞字"。
- DeepFilterNet 在极高 SNR 场景(基本无噪声)下会过度抑制低频,男声变薄;可以把 attenuation lim 调到 30 dB 以下缓解。
- WASM 模型未做 SIMD 编译时性能塌方:必须用
-msimd128编译,并在 worker 里 feature detect。
4. Whisper Streaming:端侧 vs 云端
4.1 关键事实
Whisper 本体不是流式模型,它的设计输入是 30 秒窗口;所谓"streaming"是工程层面用 VAD 分段 + 滑动窗口 + 增量解码模拟出来的。任何方案都绕不开这一点。
4.2 端侧:whisper.cpp WASM / WebGPU
- 模型:tiny(39M)、base(74M)、small(244M)。tiny 多语种约 75 MB,量化到 q5_1 约 40 MB;small 量化后约 250 MB。
- 推理后端:纯 WASM SIMD 在桌面 i5 上 tiny 大约 1.5—2× realtime;WebGPU(实验中)能拉到 4× 以上。
- 优势:零网络往返、隐私可控、断网可用、不产生云端账单。
- 劣势:首次下载模型体量大;中低端机只能跑 tiny,识别率明显劣于云端 large-v3。
4.3 云端:Faster-Whisper / WhisperX
- Faster-Whisper:CTranslate2 推理 large-v3,A10/A100 上对单路 1× realtime 占用很低,单卡可并发数十路。
- WhisperX:在 Faster-Whisper 之上叠加 wav2vec2 强制对齐,得到词级时间戳,适合做卡拉 OK 式字幕、口型同步。
- 优势:识别率高、支持 large-v3、词级对齐、便于做翻译串联。
- 劣势:每秒钟一次网络往返,p95 延迟受地理位置和网络抖动影响;账单与并发线性相关。
4.4 量化对比(实测中位数,仅作量级参考)
| 指标 | 端侧 whisper.cpp tiny WASM | 端侧 base WebGPU | 云端 Faster-Whisper large-v3 |
|---|---|---|---|
| 首字延迟 | 0.8—1.2 s | 0.6—1.0 s | 0.4—0.8 s(含网络 RTT) |
| 稳态 partial 延迟 | 1.5—2.5 s | 1.0—1.8 s | 0.6—1.2 s |
| WER(英文 LibriSpeech clean) | 约 7%—9% | 约 5%—6% | 约 2%—3% |
| WER(中文普通话) | 约 15%—20% | 约 10%—14% | 约 4%—7% |
| 单路成本 | 用户 CPU 时间 | 用户 CPU/GPU 时间 | 0.005—0.02 USD / 分钟(自建 GPU 摊销) |
| 离线可用 | 是 | 是 | 否 |
4.5 何时端侧,何时云端
- 隐私敏感(医疗、法务、企业内部)、客户端硬件可控(专属设备)→ 端侧。
- 多语种、专业术语、用户自带词表、需要词级对齐 → 云端。
- 大量并发但客户端配置参差不齐 → 云端,统一在 GPU 上摊薄成本。
- 弱网或跨境(中国 ↔ 海外)→ 端侧或者就近部署 ASR 边缘节点,不要硬扛网络。
4.6 失败时的样子
- 端侧 tiny 模型遇到强口音或低 SNR:输出大段无意义的常用词("the the the"),需要靠 VAD 能量阈值过滤掉。
- 云端方案断网:必须有 partial 缓冲队列,重连后丢弃过期片段,不要回放过时字幕。
- WebGPU 在 Safari 上覆盖率不足:必须做能力检测并回退到 WASM。
5. VAD、滑动窗口与 partial 修正
5.1 为什么必须 VAD
- 静音段如果也送给 Whisper,模型会"幻觉"出"thank you for watching"之类的训练集尾部噪声。
- 全程推理浪费算力:通话里实际有效语音通常占 30%—50%。
- VAD 给出的语音段边界还能直接喂给前端 UI 做"对方正在说话"的指示。
5.2 选型
- WebRTC 内置 VAD(libwebrtc 的
Vad):算力极低,但准确率一般,对低 SNR 场景容易漏检。 - Silero VAD(ONNX,ort-web):约 1.8 MB,准确率显著好于 WebRTC VAD,可在 worker 里跑,单帧 1—2 ms。
- 推荐组合:Silero VAD + 能量门限双重确认,避免单一模型在极端场景误判。
5.3 分段策略
interface VadSegmenterOptions {
minSpeechMs: number; // 低于这个时长的语音段直接丢弃,默认 250
maxSegmentMs: number; // 强制切段,避免一段超过 Whisper 30s 窗口,默认 15000
endpointSilenceMs: number; // 末尾静音达到这个值视为一段结束,默认 600
}
class VadSegmenter {
private buf: Float32Array[] = [];
private bufMs = 0;
private silenceMs = 0;
private speaking = false;
constructor(
private readonly opts: VadSegmenterOptions,
private readonly onSegment: (pcm16k: Float32Array) => void,
) {}
feed(frame: Float32Array, isSpeech: boolean, frameMs: number) {
if (isSpeech) {
this.speaking = true;
this.silenceMs = 0;
this.buf.push(frame);
this.bufMs += frameMs;
if (this.bufMs >= this.opts.maxSegmentMs) this.flush();
return;
}
if (this.speaking) {
this.buf.push(frame);
this.bufMs += frameMs;
this.silenceMs += frameMs;
if (this.silenceMs >= this.opts.endpointSilenceMs) this.flush();
}
}
private flush() {
if (this.bufMs >= this.opts.minSpeechMs) {
const total = this.buf.reduce((a, b) => a + b.length, 0);
const out = new Float32Array(total);
let off = 0;
for (const f of this.buf) { out.set(f, off); off += f.length; }
this.onSegment(out);
}
this.buf = [];
this.bufMs = 0;
this.silenceMs = 0;
this.speaking = false;
}
}
5.4 滑动窗口与 partial 锁定
直接每段 flush 后才解码,p95 延迟会到 1.5 秒以上,体感差。生产方案:
- 维护一个 6—10 秒的滑动窗口缓冲,每 300—500 ms 触发一次"中途解码",输出 partial。
- 把窗口左边沿前 N 秒(通常 3—4 秒)的文本作为"已锁定",不会再变;窗口右边沿的文本是"流动 partial",允许后续覆盖。
- 锁定边界要落在 VAD 给出的语音段边界或词边界(WhisperX 才有词边界),不能落在中文字符的半个字 token 上。
- 渲染层只有跨过锁定线的文本才会进入"最终字幕",未锁定的 partial 用浅色或不同样式区分。
5.5 失败模式
- VAD 把咳嗽、笑声判为语音 → 模型输出乱码 → 用置信度(avg logprob)+ no_speech_prob 过滤。
- 双人同时说话 → Whisper 只会识别能量更大的那一路,弱者的话被吞掉。需要做 source separation(如 Sepformer)或在 SFU 侧拆轨分别识别。
- partial 抖动:同一句话连续 5 次输出,每次结尾都不一样。解决方法是只在两次连续 partial 文本前缀稳定时才向 UI 推送,避免闪烁。
6. 字幕渲染与多语言翻译
6.1 传输通道选型
| 通道 | 适用场景 | 不适用场景 |
|---|---|---|
RTCDataChannel(ordered=true, maxRetransmits=null) | 一对一/小房间 P2P,字幕作为元数据广播 | 大房间,扇出复杂 |
SFU 旁路(自定义元数据 / mid 携带) | 大房间统一分发,需要 SFU 支持 | 纯 mesh,无 SFU |
| WebVTT 内嵌(HLS/DASH 直播) | WHIP→ 转 HLS 的低延迟直播 | 互动通话,WebVTT 段切片粒度太大 |
| 业务侧 WebSocket | 字幕需要落库、回放、检索 | 强 P2P 私密通话 |
6.2 字幕消息结构
interface SubtitleMessage {
/** 单调递增的字幕段 id,便于覆盖与去重 */
segId: number;
/** 该段是否已锁定,不再变化 */
final: boolean;
/** 说话人 id,对应 SFU 的 mid 或业务 userId */
speakerId: string;
/** UTC 毫秒,发送侧 ASR 完成时刻 */
ts: number;
/** 主语言文本 */
text: string;
/** 译文,按 BCP-47 语言标签 */
translations?: Record<string, string>;
/** 词级时间戳(可选,WhisperX) */
words?: Array<{ w: string; s: number; e: number }>;
}
6.3 翻译流水线
- 串联:ASR → 文本队列 → 翻译模型(NLLB、M2M-100、商用 API)→ DataChannel。
- 翻译只应该跑在 final 字幕上,不要翻 partial:partial 经常半句不通顺,翻译延迟也叠加上去。
- 中转语言要明确:中文 → 英文 → 西班牙语 中转会大量语义损耗,宁可直接训练或选择支持目标语对的模型。
- 关键术语词表(如医学/法律名词)通过 prompt 注入或自定义词典覆盖,模型本体很难"专项强化"。
6.4 渲染层
- 字幕容器用 fixed positioning,不要塞进音视频元素本身,避免和 picture-in-picture 冲突。
- 每段字幕单独一个节点,长度受限(约 42 字符/行 × 2 行,CEA-608 字幕惯例);超过则做滚动或截断。
- partial 用 CSS
opacity: 0.6或不同颜色,final 完成后做一次 cross-fade,不要直接替换文本——会触发屏幕阅读器重复朗读。 - 渲染节奏:
requestAnimationFrame节流到 30 Hz 足够,partial 推送频率高于此时合并最后一次。
6.5 失败时的样子
- DataChannel 在 SFU 转发路径上 packet loss → 字幕跳段。需要在消息里带
segId,接收端检测到不连续就请求重传或直接丢弃。 - 翻译服务限速:本端字幕正常,远端 UI 缺译文,需要降级为只显示原文 + "翻译中…"。
7. 性能预算与硬件画像
7.1 4 核桌面(Intel i5-1135G7 量级)
| 模块 | CPU 占用 | 内存 | 备注 |
|---|---|---|---|
| WebRTC 音视频本身 | 8%—12% | 80—120 MB | 含 Opus、AEC、AGC |
| RNNoise WASM SIMD | 1%—2% | 5 MB | 单 worker |
| Silero VAD WASM | 1% | 8 MB | 32 ms 跳步 |
| whisper.cpp tiny q5_1 WASM | 18%—25% | 90 MB | 实时 partial 周期 400 ms |
| 主线程渲染 + UI | 5%—10% | — | React 中等复杂度 |
| 合计 | 33%—50% | 约 200 MB | 留出 50% 余量给浏览器其他工作 |
- 首字延迟:约 0.9 秒(含 VAD 200 ms + 模型推理 600 ms + 渲染 100 ms)。
- 稳态 partial 延迟:约 1.6 秒。
7.2 中端 Android(Snapdragon 7 Gen 1)
- RNNoise + WebRTC 本身合计 12%—15% CPU,可以接受。
- DeepFilterNet 3 单独 6%—10%,再叠加端侧 whisper-tiny WASM 会把整机推到 60%+,发热明显,建议改为云端 ASR。
- WebGPU 支持参差不齐,不要把推理后端硬绑定在 WebGPU 上。
7.3 何时主动降级
navigator.hardwareConcurrency < 4或deviceMemory < 4:禁用 DFN,降到 RNNoise;禁用端侧 Whisper。getStats()报audio outboundRtp的nackCount持续上升、jitterBufferDelay> 300 ms:暂停字幕推理,把 CPU 让给音视频本身。- 笔记本电池模式(
navigator.getBattery())+ 低电量:把 partial 周期从 400 ms 拉到 1 s。
8. 反模式
下面这些组合在 PR 评审里直接打回。
- 在主线程加载并执行 WASM 模型。
WebAssembly.instantiateStreaming必须在 worker 里调用,否则 React 渲染、动画、视频解码会被周期性卡顿撞上。 - 不做 VAD,把麦克风原始流 24×7 灌进 Whisper。账单和电池都会爆,并且模型会反复产生幻觉文本。
- 在
ScriptProcessorNode里做降噪。该节点已废弃,无法保证实时性,移动端必丢字。 - 字幕直接拼接到
textContent或用innerHTML替换。partial 频繁变化时屏幕阅读器会重复读、移动端会触发回流抖动。应该用受控的 React/Vue 组件 + key 跟随segId。 - 不做 partial 锁定。用户看到字幕"长出来又改回去",体感像 GPT 流式但是更短更怪,专业场景(庭审、医疗)完全不可用。
- 把 RNNoise 和系统自带 AEC/NS 同时打开,且都开到最强。语音会被磨得发飘,对端听感差于不开降噪。
- 翻译走 partial。partial 半句不通,翻译输出常常完全错位;只翻 final。
- DataChannel
ordered=true, maxRetransmits=null(默认)下塞高频 partial 文本。一旦丢包重传,字幕会阻塞数秒。partial 应该走ordered=false, maxRetransmits=0的"不可靠快递",final 才走可靠通道。 - 把模型文件直接打进主 bundle。tiny 都有几十 MB,应该走独立 CDN 路径、按需下载、加
Cache-Control: immutable。 - 把云端 ASR API key 直接塞前端代码。必须走签名换 token,token 短期有效,并按用户限流。
9. 工程落地清单
- HTTP 响应头开启 COOP/COEP,验证
crossOriginIsolated === true。 - AudioContext 采样率显式 48 kHz,并做 16 kHz 重采样后再喂 Whisper。
- RNNoise/DFN WASM 用
-msimd128 -O3编译,验证WebAssembly.validate检测 SIMD。 - Worker 加载使用
type: 'module',便于 Tree shaking。 - Silero VAD 与 Whisper 解码放同一 worker 减少 SAB 跨线程调度。
- partial 推送频率 ≤ 5 Hz;final 推送一次性提交。
- 字幕消息走专用 DataChannel(label
subtitle/v1);partial 与 final 分两条 channel:partial unreliable、final reliable。 - 接收端按
segId单调递增,乱序丢弃,缺失打洞 < 3 s 则忽略。 - 客户端能力检测:CPU 核数、设备内存、
navigator.connection.effectiveType、getBattery(),给出降级矩阵。 - 监控指标:partial 平均延迟、final 平均延迟、WER 抽样(用对照样本)、降噪开启率、降级触发率。
10. 权威资料
- Valin, J.-M. A Hybrid DSP/Deep Learning Approach to Real-Time Full-Band Speech Enhancement. MMSP 2018. RNNoise 原始论文:arXiv:1709.08243。
- Schröter, H. et al. DeepFilterNet: A Low Complexity Speech Enhancement Framework for Full-Band Audio based on Deep Filtering. ICASSP 2022。
- Schröter, H. et al. DeepFilterNet2: Towards Real-Time Speech Enhancement on Embedded Devices for Full-Band Audio. IWAENC 2022。
- Schröter, H. et al. DeepFilterNet3. Interspeech 2023。
- Radford, A. et al. Robust Speech Recognition via Large-Scale Weak Supervision. OpenAI Whisper 论文,2022:arXiv:2212.04356。
- Bain, M. et al. WhisperX: Time-Accurate Speech Transcription of Long-Form Audio. Interspeech 2023。
- Silero Team. Silero VAD. GitHub: snakers4/silero-vad。
- W3C. Web Audio API. Recommendation:https://www.w3.org/TR/webaudio/。
- W3C. AudioWorklet. https://www.w3.org/TR/webaudio/#AudioWorklet。
- WHATWG. SharedArrayBuffer and cross-origin isolation. MDN 与 HTML spec 中的相关章节。
- W3C. WebRTC 1.0: Real-time Communication Between Browsers. Recommendation。
- W3C. WebCodecs. Working Draft(与 AudioWorklet 配合时的参考)。
- ggerganov / whisper.cpp 与 SYSTRAN / faster-whisper 项目主页。
核对日期:2026-06-22。