流式听写
Parakeet-EOU-120M 是一个小型 RNN-T 流式 ASR 模型,带显式的句末(EOU)头,专为 Apple Silicon Neural Engine 上的实时听写而构建。本指南同时介绍 DictateDemo——macOS 菜单栏参考应用,它把流式模型与 Silero VAD 串起来,实现免手、可粘贴到任意应用的听写体验。
它是什么
- 实时部分结果 — 说话时文本就在更新,每个 chunk 后约 340 ms
- 显式 EOU — 由模型决定一段话何时结束,无需手动按键
- VAD 驱动的强制提交 — 当 EOU 在背景噪声中卡住时,Silero 兜底强制提交
- 120 MB INT8 CoreML — 在 Neural Engine 上运行,不占用 GPU
- 25 种欧洲语言 — 与上游 NeMo Parakeet TDT 使用同一词表
架构
每个音频 chunk 依次通过 3 个 CoreML 模型:
| 组件 | 说明 |
|---|---|
| Encoder | Cache-aware Conformer。接收一个 64 帧的 mel chunk(640 ms)加上 6 个状态张量——注意力 KV cache、depthwise conv cache,以及一个 pre_cache mel 回环,用来在 chunk 边界前面接上最近的历史音频,让 FFT 看到连续信号。 |
| Decoder | 单步 LSTM 预测网络。消费前一个非 blank token,输出一个 embedding 和更新后的 (h, c) 状态。 |
| Joint + EOU 头 | 将编码器和解码器输出融合为 vocab + blank + EOU 上的 logits。EOU 类别是模型给出的一段话已结束的硬信号。 |
为什么需要单独的 EOU token
普通 RNNT 在静音时会发出 blank,解码器会愉快地吸收它,却并不发出"一段话已结束"的信号。独立的 EOU 头让模型可以做出硬切,把 partial 提交为 final,重置标点/大小写状态,并触发诸如粘贴到应用等下游动作。
键盘敲击、鼠标移动,以及"安静"停顿中的房间底噪,都可能让 joint 偶尔发出一个非 blank token,从而重置 EOU 去抖计时器,卡住提交。生产流水线会把 joint 的 EOU 与外部 VAD 驱动的 forceEndOfUtterance() 兜底配对——见下方的 DictateDemo。
模型
| 模型 | 大小 | HuggingFace |
|---|---|---|
| Parakeet-EOU-120M (CoreML INT8) | ~120 MB | aufklarer/Parakeet-EOU-120M-CoreML-INT8 |
性能
| 指标 | 值 |
|---|---|
| 权重内存 | ~120 MB (INT8) |
| 推理峰值内存 | ~200 MB |
| Chunk 延迟(M 系列) | ~30 ms 计算 / 640 ms 音频(RTF ~0.056) |
| 端到端 partial 延迟 | ~340 ms(一个 chunk) |
| 提交延迟(VAD 路径) | 停止说话后 ~1 s |
| 计算目标 | Neural Engine (CoreML) |
快速开始 — 批量转写
流式模型同样符合 SpeechRecognitionModel,因此可以直接替换任何需要通用 STT 模型的代码:
import ParakeetStreamingASR
let model = try await ParakeetStreamingASRModel.fromPretrained()
let text = try model.transcribeAudio(audioSamples, sampleRate: 16000)
快速开始 — 异步流式
for await partial in model.transcribeStream(audio: samples, sampleRate: 16000) {
if partial.isFinal { print("FINAL: \(partial.text)") }
else { print("... \(partial.text)") }
}
每个 PartialTranscript 携带 text、isFinal、confidence、eouDetected(joint 触发 vs. 强制提交),以及单调递增的 segmentIndex。
长生命周期会话 API(麦克风输入)
在实时听写中,创建一次 session,然后在麦克风音频到来时逐块推给它。session 会在内部缓冲,并在累计到足够的样本后运行编码器,因此你可以推送任意大小的 chunk:
let session = try model.createSession()
// 每个麦克风 chunk:
let partials = try session.pushAudio(float32Chunk16kHz)
for p in partials {
if p.isFinal { commit(p.text) }
else { showPartial(p.text) }
}
// 流结束时:
let trailing = try session.finalize()
VAD 强制提交模式
如果你的流水线中已经在运行 Silero VAD,就用它驱动一个兜底提交,这样背景噪声就无法卡住 EOU 去抖计时器:
if hasPendingUtterance && !vadSpeechActive && vadSilentChunks >= 30 {
// 按 Silero 计算,大约 960 ms 持续静音
if let forced = session.forceEndOfUtterance() {
commit(forced.text)
}
hasPendingUtterance = false
}
// 保护措施:joint 已经触发 EOU 时不要重复提交
if partials.contains(where: { $0.isFinal }) {
hasPendingUtterance = false
}
DictateDemo — macOS 菜单栏参考应用
DictateDemo 是一个完整的 macOS 菜单栏 agent,基于流式 session 构建。它作为后台 app 运行,实时从麦克风转写并显示部分结果,在 EOU 或 VAD 静音时自动提交,并将结果粘贴到最前面的应用。
- 菜单栏 app,带全局快捷键
Cmd+Shift+D - 实时部分结果,带浮动 HUD 和音量指示
- VAD 保护的强制提交(上面给出的生产模式)
- 通过
Cmd+Shift+V粘贴到最前面的应用 - 模型在首次启动时自动下载(~120 MB)
cd Examples/DictateDemo
swift build
.build/debug/DictateDemo
完整实现位于 Examples/DictateDemo/DictateDemo/DictateViewModel.swift:带锁保护缓冲区的非主线程音频 sink、300 ms 定时器的抽取逻辑、带残余样本携带的 Silero VAD,以及带保护的强制提交。匹配的回归测试见 Examples/DictateDemo/Tests/DictateDemoTests.swift,覆盖多段话、EOU 卡住和嘈杂静音等场景。
流式 Parakeet vs 批量 Parakeet
| Parakeet-EOU-120M(流式) | Parakeet TDT 0.6B(批量) | |
|---|---|---|
| 用途 | 实时听写、实时字幕 | 文件转写、离线任务 |
| 解码器 | RNN-T + EOU 头 | Token-and-Duration Transducer |
| Chunk 大小 | 640 ms 流式 | 整个文件批处理 |
| 权重内存 | ~120 MB | 500 MB |
| 吞吐量 | ~18 倍实时 | ~32 倍实时 |
| 延迟 | ~340 ms partial | 仅在文件结束时 |
当你需要在用户说完之前就得到部分结果时,选流式模型。对于音频文件的批量转写,更大的 Parakeet TDT 0.6B 在端到端上更快,也更准确。两个模型共享同一个 SentencePiece 词表,因此可以无缝切换而不改变分词。