10 月 2 日,Cactus Compute 放出开源语音识别模型 Whistle,单文件只有 16.9 兆。
它跑在 CPU 上,无依赖,不碰显卡,也不碰任何云端接口。
官方给的首 token 延迟是 11 毫秒,七种语言转写,词级时间戳与语音嵌入一起给。
最特别的是它和自家 Needle 模型共用同一个引擎,一个二进制同时吃语音和文本。
你给它一段「关掉厨房灯」的录音,它吐回的不只是文字,而是一段能直接执行的函数调用。
语音到工具调用中间没有文字层,这一条就是本文想算清的账。
16.9 兆到底装下了什么
Whistle 面向的不是数据中心,而是手机、手表、机器人、智能家居、车载和单片机。
官方定位很直白:一个 16.9 MB 的文件,在设备本地完成全部语音理解。
它同时承担三份工作,转写、词时间戳和语音嵌入。
转写吃 16kHz 单声道,单次最多 30 秒,七种语言自动检测。
这七种语言是英语、德语、法语、西班牙语、意大利语、荷兰语和波兰语。
语言检测在本地完成,你也可以显式点名一种语言跳过检测。
词级时间戳给每个词标出开始时间、结束时间和置信度,全部来自解码器注意力对齐。
语音嵌入直接输出编码器结果,每 80 毫秒一行,不需要先解码成文字。
音频全程不离开设备,第一下点击才下载那 16.9 兆模型。
这三份能力叠加在一个文件里,是它能塞进嵌入式设备的前提。
官方把它定位成口袋里的语音入口,而不是又一个云端转写接口。
前端:3000 帧怎么压成 375 帧
音频进来先过前端,16kHz 单声道按 25 毫秒窗口、10 毫秒步长切帧。
每帧算 80 个 log-mel 特征,频带限制在 250 到 3500 赫兹。
这个频带的裁剪本身就是一重降噪,超低音和高频噪声直接被扔掉。
每个通道独立归一化,30 秒音频总共得到 3000 帧。
接着是一个 128 通道、卷积核 9 的卷积主干,把帧数连续减半三次。
3000 帧先变 1500,再变 750,最后落在 375 帧,正好每 80 毫秒一帧。
从这里往后,每一层都按 375 帧这个节奏工作。
语音嵌入返回的就是这一层节奏下的结果,一行对应一帧。
也就是说,30 秒的音频在进入注意力之前,已经被压掉了八分之七。
卷积主干对语音这种连续信号特别合适,相邻帧高度相关,下采样损失很小。
前端这一笔先把计算量砍到八分之一,后面所有层都跟着受益。
编码器:八个非因果注意力块
编码器由八个 Simple Attention 块组成,和 Needle 用的是同一套块。
每个块包含四条 mHC 残差通道,前馈部分换成 Monarch Hadamard MLP。
mHC 是多头跨通道残差的缩写,让信息在通道间流动得更直接。
Monarch Hadamard MLP 用层级结构近似全连接,计算量远小于标准前馈。
这里的注意力不做因果掩码,任意帧都能看到整段音频的任何位置。
官方举例说,第 3 秒的帧可以 attend 到第 12 秒的帧。
对语音识别来说这是合理的,因为词义经常依赖后面的上下文。
一个词的发音要等到它后面的词出现后才能稳定解释。
后端解码时才需要因果性,那是另一套账。
编码器全部八个块在任何深度配置下都会完整运行,从不切片。
这是 Ladder 训练体系里唯一不许动的部分。
解码器:注意力账怎么省
解码端是八个 Laddered Simple Attention 块,宽度 512。
8 个查询头压缩成 2 个 KV 头,查询和键各 48 维,值 64 维。
KV 头压缩是 token 生成阶段缓存友好的关键设计。
Q、K、V 各接一个 3 抽头因果卷积,第 3 层和第 7 层挂 engram 查找。
engram 槽位一共 18432 个,这是 Needle 块清单换了个层数而已。
语音特有部分只有一处,每层一个带门的交叉注意力。
公式是 x 加上门控后的 softmax 注意力结果,门由每层自己学习。
K 和 V 直接取自音频片段,这段投影只在片段到来时计算一次。
375 帧乘 8 层,算完就一直持有到整个解码结束。
所以五个 beam 只花五份短转写缓存,而不是五遍重放音频。
这个设计把解码成本从音频长度转移到了文本长度。
音频再长,重听音频的开销也只在投影那一次。
这是它敢在 CPU 上实时转写的关键一笔省账。
解码:五束搜索与关键词偏置
解码用五个 beam,按长度归一化的对数概率打分。
长度归一化防止短句因为概率天然偏低而长期吃亏。
关键词偏置走一个 Aho-Corasick 自动机,沿着短语匹配推进。
自动机匹配到关键词时,对应 beam 的对数概率被抬升。
这样像 Siobhan 或 Krzysztof 这种罕见的专名也能被纠正出来。
转写文本封顶 320 个 token,防止失控输出。
词表有 8192 个文本片段,外加七个语言 token,每种语言一个。
检测到哪种语言,就输出对应的语言 token,而不是在带外单独返回。
如果你点名语言,默认自动检测也可以被覆盖。
静音处理也内置在引擎里,解码启动前先量响度范围。
响度低于阈值就直接返回空转写和空语言,根本不进 beam 搜索。
这一条对嵌入式常开的拾音场景很实用,省电也省时间。
静音返回空结果意味着调用方不用误解码错误,逻辑可以写得很干净。
Ladder:每层深度都是独立模型
解码器从 2 层起,每一档深度都单独训练成一个模型。
加载时用 --audio-depth 选深度,编码器永远全 8 块跑完。
官方解释,编码器从不切片,任何深度都完整过一遍。
这样同一个文件可以按设备算力裁解码器,速度与质量按需取舍。
手表用浅档,机器人用深档,我们只用一份权重。
Ladder 训练的成本是每档深度都要训一遍,但推理侧完全免费。
它把部署时的量化选择变成了加载参数,连重新导出模型都省了。
成绩单:三个模型的对比
官方用 Whisper normalizers 统一打分,Whistle 测了 86174 条话语。
Whisper base 和 Moonshine tiny v2 的数字取自作者公布的成绩。
Whistle 用的是多语 checkpoint,不是只练英文的版本。
官方还声明,所有报告测试集的音频都不在训练和验证数据里。
这个声明靠音频校验和与说话人 ID 逐条比对验证过。
避免测试集污染的声明在开源模型里很少见,值得专门点出来。
| 指标 | Whistle | Whisper base | Moonshine tiny v2 |
|---|---|---|---|
| 模型大小 | 16.9 MB | 145.3 MB | 41.9 MB |
| 首 token(10 秒音频) | 11.1 ms | 73.2 ms | 22.8 ms |
| 解码速度 | 1319 tok/s | 266 tok/s | 262 tok/s |
| LibriSpeech test-clean | 胜 | 负 | 未公布* |
| LibriSpeech test-other | 胜 | 负 | 未公布* |
| SPGISpeech | 胜 | 负 | 未公布* |
| Earnings-22 | 胜 | 负 | 未公布* |
| FLEURS 平均 | 胜 | 负 | 未公布* |
| TED-LIUM | 负 | 胜 | 未公布* |
| AMI(口径不同) | 负 | 胜 | 未公布* |
| MLS 平均 | 负 | 胜 | 未公布* |
表格里带星号的地方是模型作者从未公布过对应基准。
Moonshine 只做英文,Whisper 没报 SPGISpeech、Earnings-22 和 AMI cleaned。
Whisper 的 AMI 是 AMI-IHM,和其他两个模型报的 AMI 不是同一子集。
十秒音频全部跑在 Apple M4 Pro 的 CPU 上,数字可复现。
Whistle 在八个基准里赢下五个,LibriSpeech 的 clean 和 other 都拿下了。
输掉的三个对手都大它一个量级,这个落差更说明问题。
首 token 延迟还会随音频长度变化,5 秒 5.9 毫秒,30 秒 36.3 毫秒。
Whisper 每次输入都补齐到 30 秒,所以它的首 token 延迟是平的。
Whistle 的首 token 跟着片段走,这是流式体感的关键。
解码速度上差异更悬殊,Whistle 每秒 1319 token,对手都只有二百多。
尺寸、延迟、吞吐三项全胜,这是小模型难得的完胜项。
Needle 生态:一个二进制吃语音和文本
Whistle 不是孤立模型,它被装进 Needle 的 C++ 引擎里。
needle_load 只看 .cact 文件里是什么,所以同一个二进制什么都能干。
官方给了三条命令,第一行纯转写,第二行纯工具调用。
needle --model whistle.cact --audio clip.wav needle --model needle3.cact --tools tools.json --prompt "turn off the kitchen lights" needle --model needle3.cact --model whistle.cact --tools tools.json --audio clip.wav第三行最值得看,needle_complete 直接把音频当输入吃下去。
引擎先转写,再用转写结果对着你的工具列表作答,最后吐一个 JSON。
JSON 里带 function_calls 和语音字段,语音字段统一用 audio_ 前缀。
整条链路里调用方见不到文字转写,语音直接变成函数调用。
音频、文本、工具三种能力在一个进程里,省掉了跨服务调用的全部开销。
对智能家居和机器人这种延迟敏感场景,本地闭环意义很大。
Python 与 C 的接入方式
pip 安装 cactus-needle 之后,Python 侧一行就能转写。
import needle print(needle.transcribe("clip.wav")["text"]) # turn off the kitchen lights16kHz 的 WAV 或裸采样什么都不用多装,基础安装就够。
其他采样率和麦克风采集需要 [mic] 附加组件,会带上 soxr 和 sounddevice。
每次调用都会返回文本、语言、首 token 毫秒数和解码速度。
word_timestamps=True 追加每个词的时间和概率。
keywords 参数可以抬升指定短语的对数概率,例如人名 Siobhan 和 Krzysztof。
language 参数直接锁定语种,跳过自动检测。
needle.Whistle() 把模型包成对象,用于 embed 或持有微调过的 .cact。
终端里还有个 playground 子命令,从麦克风实时转写。
compare 子命令把同一段音频喂给 Whistle、Whisper 和 Moonshine 做并排对比。
C 侧同样简洁,整块语音 API 只有三个函数。
needle_load、needle_transcribe、needle_embed 就是全部了。
引擎不读任何环境变量,行为要么是编译默认,要么是显式 flag。
官方说这个设计是为了让每个平台的二进制行为完全一致。
嵌入式工程最怕隐式配置,三函数加零环境变量非常难得。
十七个目标平台
引擎为 17 个目标预构建,从 macOS、Linux 一路到 ARM 上的 Windows。
Android、iOS、watchOS 都在清单里,RISC-V 和 MIPS 也在。
浏览器和 WASI 组件同样有份,网页里那个沙箱就是这么跑的。
每个目录都放一个 needle 二进制、libneedle.a 和 needle.h。
任何 .cact 文件都能被这些二进制加载,跨平台不需要重编译。
模型权重挂在 Hugging Face 的 Cactus-Compute/whistle 下。
引擎和平台目录在 Cactus-Compute/needle3,源码在 GitHub 的 cactus-compute/needle。
一个 16.9 兆的模型配上十七套预编译二进制,端侧落地的门槛被压得很低。
社区的实测反馈
Hacker News 上这条发布收获 312 分和 31 条评论,热度不低。
不少人说英语识别准得离谱,试着打断它也能理解清楚。
一位测试者表示几句带复杂时态和用词的句子都被完整转写。
西班牙语是公认短板,会写出不存在的词或者满篇错别字。
带西班牙口音的英语倒没问题,测试者特意强调了这一点。
印度口音同样被识别良好,社区里有实例佐证。
一个反复出现的限制是单次 30 秒上限,超出直接报运行时错误。
不少人问能不能上 Android,能不能加更多语言。
一条高赞评论说,这个尺寸的成绩单让人惊讶,做得漂亮。
还有人在评论区安利自家基于 Streaming Zipformer 的 10 语言 CLI。
那条安利反而印证了社区对子 20MB CPU 模型的集体兴趣。
对于量产端侧硬件的人来说,这个趋势比单个模型更重要。
评论里的短板集中在语种覆盖和长音频,架构本身几乎没有被质疑。
账本合起来怎么读
Whistle 的 16.9 兆不是靠压缩硬挤出来的,是架构设计省出来的。
非因果编码器、门控交叉注意力投影只算一次、Ladder 深度裁剪,各省一笔。
和 Needle 共用引擎又省掉一套推理栈和一份二进制体积。
它用 11 毫秒首 token 换了传统语音管线的整套部署负担。
传统方案要先转写、再调云端、再等返回,Whistle 把这条链压进一个函数。
和很多模型拆解一样,数字会随硬件与基准口径浮动。
但「语音直接进工具调用」这条路径是全新的。
它把麦克风变成了一个函数入口,这可能是端侧小模型最被低估的一步。