上一篇拆全双工语音对话综述时,有个场景反复出现:你说「我想要……那个……」,停顿一秒半,助手立刻抢答——声学上的静默被当成了「说完了」。综述把这类失败归到 L0 模块级的一格:缺一个语义级的轮次结束检测器(EoT,End-of-Turn)。

这一篇拆一个正好补这格的开源项目:Smart Turn。它来自 Pipecat 生态(Daily 公司主导的开源语音 agent 框架),自我定位很清楚——一个「你说完了吗」的二分类器:听一段音频,输出一个概率,说完还是没说完。模型只有 8 MB,CPU 上十几毫秒出结果,权重、训练数据、训练代码全部开源(BSD 2-clause)。

麻雀虽小,五脏俱全:它把「借大模型的编码器、砍到只剩需要的部分、量化到 CPU 白菜价」这条小模型路线走了个完整来回,数据管线里全是值得抄的工程细节。下面按「为什么难 → 模型长什么样 → 三代怎么演进 → 数据怎么造 → 怎么装进 agent → 边界在哪」的顺序拆。

一、判停为什么难:VAD 的两难

语音 agent 的回合切换,业内叫 turn detection(轮次检测):决定「此刻该不该接话」。传统做法是 VAD(Voice Activity Detection,语音活动检测):把音频分成「有人声/没人声」,静音持续超过阈值就判定用户说完。

问题在于阈值没有好的取值:

阈值短(0.5 秒):抢话用户系统我想要,嗯……静音 0.5 s为您推荐以下商品…✕ 你还没想完,它已开讲……退掉那张机票阈值长(2 秒):迟钝用户系统帮我查明天去上海的航班干等 2 s(每一轮都要等)好的,为您查询…✕ 像卫星通话语义判停(Smart Turn):同样的停顿,不同的裁决我想要,嗯……没说完 p = 0.1✓ 语法悬置 + 语调上扬 → 安静等待帮我查明天的航班说完了 p = 0.9✓ 句子完整 + 语调收尾 → 立即接话
VAD 判停的两难:阈值短则抢话,阈值长则迟钝;Smart Turn 直接听内容——同样的停顿,「um…」后面是等待,说完整句才是接话
  • 阈值设短(比如 500 毫秒):用户想事、喘气、说「嗯……」都会触发抢答。上一篇的说法:固定阈值 VAD 在犹豫场景必然抢跑。
  • 阈值设长(比如 2 秒):不抢话了,但每一次正常交接都要干等 2 秒,对话像隔着卫星链路。

根源是 VAD 只测能量,不懂内容。人类判断对方说完没有,靠的是语法完整性、语调走向、语速节奏——这些信息全在音频里,VAD 全扔了。Smart Turn 项目 README 里给了一组最小对比:

  • “I can’t seem to, um …” —— 没说完,等着;
  • “I can’t seem to, um, find the return label.” —— 说完了,接话。

静音长度可以完全一样,答案却相反。这就是「语义判停」(项目自称 semantic VAD)要解决的事。

值得强调的一个设计决定:Smart Turn 直接吃 PCM 音频,不走「先转写成文字再判断」的路。官方博客给的理由有两条:其一,语调和语速本身就是关键线索——「说完了」的降调和「没说完」的悬置感,转写后荡然无存;其二,ASR 模型往往会把 “um”「嗯」这类填充词直接吞掉,而这恰恰是判停最需要的词。

二、模型长什么样:借 Whisper 的耳朵,装自己的头

Smart Turn v3 的架构可以一句话说完:Whisper Tiny 的编码器 + 注意力池化 + 一个小分类头。总共约 800 万参数,int8 量化后 8 MB。

最后 8 秒 PCM(16 kHz)补零音频永远贴右端(截头留尾)特征提取log-mel 频谱 80 × 80080 个 mel 频带 × 每 10 ms 一帧Whisper Tiny 编码器4 层 Transformer · 384 维上下文 30 s → 改 8 s解码器整个扔掉:39M → 8M隐状态序列 400 × 384注意力池化每帧打分 → softmax 加权求和句尾的帧权重自己学出来单个向量384 维分类头(三层 MLP)384 → 256(LayerNorm·GELU·Dropout)→ 64(GELU)→ 1 → sigmoidp(说完了) = 0.87> 0.5 → complete,否则 incomplete总参数 ≈ 800 万 · int8 量化后 8 MB · CPU 推理十几毫秒(预处理约 3 ms 另计)
Smart Turn v3 前向流程:最后 8 秒音频转成 80×800 的 log-mel 特征,过 Whisper Tiny 编码器得到 400 帧向量,注意力池化压成一个向量,三层 MLP 输出「说完了」的概率

按训练代码 train.py 逐段过一遍:

输入端:永远只看最后 8 秒。 16 kHz 单声道 PCM,超过 8 秒截头留尾;不足 8 秒在前面补零,让真实音频始终贴着窗口右端——「最近的瞬间」永远在固定位置,模型不用学「说话声可能出现在任何地方」。8 秒音频经 Whisper 特征提取器变成 80 个 mel 频带 × 800 帧的 log-mel 频谱图。

编码器:Whisper Tiny,只要一半。 Whisper 是 OpenAI 的开源语音识别模型家族,Tiny 是其中最小的一档(3900 万参数)。语音识别是编码器-解码器结构:编码器把音频压成向量序列,解码器把向量翻译成文字。判停不需要知道每个字是什么,只需要「这段话听起来完没完」——所以 Smart Turn 把解码器整个扔掉,只留编码器,并把上下文长度从 Whisper 默认的 30 秒改成 8 秒(max_source_positions = 400)。3900 万参数就这样砍到约 800 万。

池化:让模型自己决定听哪里。 编码器输出 400 个时间步、每步 384 维。判停线索分布不均匀——句尾的语调、最后一个词是不是连接词,比 5 秒前的内容重要得多。所以这里不用平均池化,而是一个小注意力:每个时间步过两层线性网络打一个分,softmax 归一化成权重,按权重加权求和成单个 384 维向量。哪些帧重要,模型训练中自己学。

分类头:三层 MLP。 384 → 256(LayerNorm + GELU + Dropout)→ 64(GELU)→ 1,sigmoid 输出「说完了」的概率,阈值 0.5。

训练配方同样朴素:BCE 损失(按 batch 内正负比例动态加权)、学习率 5e-5、4 个 epoch、batch 384、cosine 调度。没有花活——这个任务的难点根本不在优化器上,在数据上(第四节)。

三、三代演进:一部「越小越准」的反直觉史

Smart Turn 的版本史有个漂亮的反直觉弧线:模型从 2.3 GB 一路缩到 8 MB,缩小近 300 倍,准确率反而一路上涨。

模型体积(柱高为对数刻度):缩小近 300 倍,准确率反升v1wav2vec2-BERT2.3 GB仅英语押注 450 万小时预训练底子v2(2025.7)wav2vec2 + 线性头360 MB14 种语言 · L40S 12.5 ms大底座过拟合,小的反而准v3(2025.9)Whisper Tiny 编码器 + int88 MB23 种语言 · CPU ≈12 msc8g.medium:6272 → 60 ms(100×)v3.1(2025.12):真人数据补课三家数据公司的真人录音(英/西)新测试集上英语 88.3% → 94.7%v3.2(2026.1):实战补丁短句("yes"「好的」)误判 −40%,修 padding bug训练/测试集混入咖啡馆、办公室噪声
三代 Smart Turn:v1 用 wav2vec2-BERT(2.3 GB,仅英语),v2 换更小的 wav2vec2(360 MB,14 种语言),v3 换 Whisper Tiny 编码器(8 MB,23 种语言)——体积缩小近 300 倍,准确率反升

v1:大力没出奇迹。 第一版选了 wav2vec2-BERT 做底座——它在 450 万小时多语言数据上预训练过,团队希望这份底子能让判停自动泛化到多语言。整个模型 2.3 GB,只支持英语。

v2(2025 年 7 月):小的反而准。 训练 v2 时团队发现,wav2vec2-BERT 在未见数据上的表现反而不如更小的普通 wav2vec2,怀疑是大模型过拟合。他们还试了 LSTM、在 wav2vec2 后面加 Transformer 分类层等方案,最后胜出的是最朴素的「wav2vec2 + 线性分类头」:360 MB(缩小 6 倍),L40S 上推理 12.5 毫秒(快 3 倍),语言从 1 种扩到 14 种。在人工录制并清洗过的英语测试集上约 99% 准确率。

v3(2025 年 9 月):换底座,上 CPU。 再换 Whisper Tiny 编码器,配上 v2 的分类头,8 MB int8 量化——比 v2 小近 50 倍,语言扩到 23 种,测试集准确率还高于 v2。这一版的真正意义是把推理搬上了 CPU:现代 CPU 上约 12 毫秒,廉价 AWS 实例上约 60 毫秒(c8g.medium 上从 v2 的 6272 毫秒到 59.8 毫秒,快了 100 倍)——判停从「要配 GPU 的组件」变成了「跑在 agent 同一台机器上白送的组件」。官方给的 CPU 推理调优也实在:ONNX 单线程顺序执行,另设 OMP_NUM_THREADS=1,避免线程争抢反而更快更稳。

之后是两次小步快跑(都是换数据不换架构,新权重直接替换旧文件):

  • v3.1(2025 年 12 月):引入三家数据公司(Liva AI、Midcentury、MundoAI)贡献的真人录音(英语 + 西班牙语)。在新测试集上,英语准确率从 88.3% 提到 94.7%——一次数据更新 +6.4 个百分点,比任何架构改动都值钱(下一节展开)。同时开始提供两个变体:8 MB int8 版给 CPU,32 MB fp32 版给 GPU(更快,准确率再高约 1 个百分点;注意在 CPU 上反而是 int8 版快得多)。
  • v3.2(2026 年 1 月):专治两个实战痛点。短句(“yes”「好的」这类单词回答)误判率降 40%——靠新增短句数据集,外加修掉一个社区报告的训练时 padding bug;再往训练和测试集里混入真实的咖啡馆/办公室背景噪声,提升嘈杂环境的稳健性。

量化方案也值得记一笔:静态 int8 量化,用 1024 条训练样本做 Entropy 校准,只量化 Conv/MatMul/Gemm 三类算子,权重 int8、激活 uint8。8 MB 的文件里,这套组合拳保住了绝大部分精度。

四、数据的故事:合成数据的天花板,真人数据的补课

判停模型的数据有个先天难题:哪里去找几十万条「话说一半」的录音?正常人不会对着麦克风专门录「我想要,嗯……」。Smart Turn 的解法是一条全合成流水线,v2 博客把配方完整公开了:

① 开源多语言句子库原料很脏:语法错、像文章标题不像口语的比比皆是② LLM 逐句清洗Gemini 按七类打标筛除:按语言扔掉 50–80%③ 逐语言填充词表英语 um · 日语 えーと、あの外加 and / but 等连接词④ 截断成半句LLM 找自然断点 + 省略号「帮我查一下,嗯……」(无空格的中文也能切)⑤ TTS 朗读(Chirp3)填充词发音自然是选它的原因句尾逗号 = 「没说完」的语调一个标点控制关键声学特征成对训练样本完整句 → 标签 1(说完了)截断半句 → 标签 0(没说完)反例教学:填充词在句中的完整句「我,嗯,想退货」→ 标签 1 —— 防止模型学会「听到嗯就判没完」的捷径合成数据的天花板:TTS 的「犹豫」是演出来的v3.1 掺入真人录音(英/西)→ 英语 88.3% → 94.7%;v3.2 再补短句数据集与咖啡馆/办公室背景噪声
合成数据流水线:多语言句子库经 LLM 清洗,按语言注入填充词并在句尾截断,再由 TTS 读出「没说完」的语调;句中填充词样本专门教模型别把「嗯」当结束
  1. 句子从哪来:从开源多语言句子库出发。原始数据很脏——不合语法的、像文章标题不像口语的比比皆是,先用 Gemini 2.5 Flash 逐句分类清洗(不合语法/标点异常/语义不完整/不像口语/涉敏/语言不符),按语言不同扔掉 50–80%。
  2. 填充词哪里来:每种语言的「嗯」不一样——英语 “um”,日语「えーと」「あの」。用 Claude 和 GPT-o3 给 每种语言建填充词表,再加上 “and” “but” 这类几乎不可能出现在句尾的连接词。
  3. 怎么截成半句:早期英语数据直接在空格处随机截断,但中文日文没有空格,于是升级成让 LLM 找自然的截断点:把句子在接近结尾处剪断、接上填充词和省略号。
  4. 怎么读出「没说完」:这是最巧的一步。多数 TTS 读不好半句话,团队测下来 Google 的 Chirp3 例外——填充词发音自然,而且句尾放个逗号,它就会用「话没说完」的悬置语调收尾。一个标点符号,控制了整条数据管线最关键的声学特征。
  5. 反例教学:专门造一批填充词在句中的完整句——「我,嗯,想退货」是说完了的。不然模型会学到「有嗯就没完」的捷径。

这条流水线撑起了 v2 和 v3 的多语言能力,但 v3.1 暴露了它的天花板:TTS 合成的「犹豫」终究是演的,缺少真人语音里的自然变化和微妙线索。v3.1 引入真人录音后,英语准确率从 88.3% 跳到 94.7%——注意这个 88.3% 和 v3 发布时报的 94.31% 不矛盾:前者是 v3.0 模型在掺入真人数据的新测试集上的成绩。换一个更接近真实的考卷,旧模型立刻掉了 6 个点,这本身就是「合成数据训练、合成数据评测」这个闭环的自我暴露(上一篇讲的评测口径问题,在一个 8 MB 的模型上原样复现)。

项目现在把「攒真人数据」做成了社区工程:一个数据标注站让志愿者逐条听样本剔除脏数据,一组轮次训练小游戏让你边玩边贡献录音。训练集和测试集全部公开在 HuggingFace 上(smart-turn-data-v3.2-train/-test),任何人可以复现训练和评测。

五、装进语音 agent:VAD 扣扳机,Smart Turn 做裁决

单看模型只是半个故事——它在真实 agent 里怎么用,藏在 Pipecat 的源码里,而且设计得相当漂亮:VAD 和 Smart Turn 不是二选一,是双保险。

用户音频Silero VADSmart TurnAgent 回复我想要……含前置 500 ms 缓冲静音 0.2 s停止说话事件推理 ①(≈12 ms)没说完 p = 0.12→ 继续听,不打扰……退掉明天的机票静音 0.2 s停止说话事件推理 ②(整轮重判)说完了 p = 0.93每次都取整轮音频的最后 8 秒,不是只看新片段好的,帮您退……兜底(stop_secs = 3 s):判「没说完」后若用户一直沉默,静音累计 3 秒即强制结束回合模型裁决 + 计时器兜底 = 双保险;VAD 起说/停说阈值 0.2 s、置信度 0.7 均为 Pipecat 默认值时间
Pipecat 中的双保险判停:Silero VAD 检测到 0.2 秒静音就触发一次 Smart Turn 推理,判「没说完」则继续等待并在下次停顿重新判断;静音累计到 3 秒由 stop_secs 兜底强制结束

按源码里的默认参数走一遍时序:

  1. Silero VAD 持续标注每一帧有没有人声(默认置信度 0.7,起说 0.2 秒、停说 0.2 秒)。Smart Turn 不需要一直跑——只在停顿出现时才需要裁决。
  2. 用户停顿 0.2 秒,VAD 报告「停止说话」,触发一次 Smart Turn 推理:取本轮开始前 500 毫秒(pre_speech_ms)至今的整段音频,截最后 8 秒,喂给模型。
  3. 模型说 complete:回合结束,下游立刻接话。配合流式 ASR,这就是 0.2 秒静音 + 十几毫秒推理的响应速度——上一篇「零间隙衔接」格子需要的语义预判,这里就是实现。
  4. 模型说 incomplete:什么都不做,继续听。用户若接着说,等下一次停顿再重新裁决——而且每次都拿整轮音频从头判一遍,不是只看新片段(模型需要完整上下文,README 明确不建议喂很短的音频段)。
  5. 兜底:用户若在「incomplete」裁决后一直沉默,不会再触发新的推理——静音累计到 stop_secs = 3 秒时,不管模型怎么说都强制结束回合。模型判断再准也可能遇到分布外的输入,一个纯计时器兜住最坏情况——这是把 ML 组件放进生产系统的标准姿势。

工程细节也见功力:Pipecat 直接把 v3.2 的 CPU 权重打包进了 pip 包里,LocalSmartTurnAnalyzerV3 默认零配置可用;特征提取没有引入 torch/transformers 依赖,而是用 numpy 重写了 Whisper 的 log-mel 计算;管线采样率不是 16 kHz 时自动重采样;设一个环境变量(PIPECAT_SMART_TURN_LOG_DATA)就能把每次判定的音频落盘成 wav,按 complete/incomplete 分类——现成的坏例采集器,攒自己的微调数据正好用它。

想脱离 Pipecat 单独玩也很简单,仓库里有现成脚本:

git clone https://github.com/pipecat-ai/smart-turn
cd smart-turn
python3.12 -m venv venv && source venv/bin/activate
pip install -r requirements.txt
python record_and_predict.py

对着麦克风分别说「我好像没办法,嗯……」和「我好像没办法找到退货标签了」,看两种停顿得到的概率差。

六、横向对比、准确率与边界

v3 发布时官方给了一张与同类原生音频判停模型的对比(数字截至 2025 年 9 月):

Smart Turn v3KrispUltravox
体积8 MB65 MB1.37 GB
语言23 种仅英语训练/测试26 种
开放程度权重 + 数据 + 训练代码专有开放权重
架构取向单次推理的裁决时延多次推理堆置信度结合对话上下文

逐语言准确率(v3.0 发布时的官方测试集,节选,完整 23 种见官方博客):

语言准确率语言准确率
土耳其语97.10%俄语93.67%
韩语96.85%印地语93.44%
日语96.76%西班牙语91.97%
德语96.14%阿拉伯语88.60%
法语96.01%中文88.57%
英语94.31%越南语81.27%

两点提醒。其一,中文准确率在 23 种语言里排倒数第四(低于它的只有越南语、孟加拉语和马拉地语)——中文数据主要靠合成管线,还没吃到 v3.1 那波真人数据红利(那批只有英语和西班牙语);生产上用在中文场景,值得拿自己的音频先测一轮,或者干脆用开源的训练脚本微调。其二,这些数字来自各版本各自的测试集,纵向不可直接比——判停模型的「准确率」永远要连着「什么测试集」一起读。

最后是边界,用上一篇的六个试金石对号入座:Smart Turn 打的是「犹豫长停顿」这一格(外加给「零间隙衔接」提供语义预判能力)。它只在用户停顿后运行,回答「说完了没」这一个问题,所以:附和「嗯嗯」不打断(需要意图分类)、旁人说话不接茬(需要说话人过滤)、说话中被抢话(打断处理走 VAD 的另一条通路)——都不归它管。README 也列了下一步方向,最有意思的是文本条件化:让模型「知道」用户正在报信用卡号或电话号码,从而在数字串中间的停顿上更有耐心。这本质上是往模型里注入对话状态——一个 8 MB 组件正在朝上一篇 L0 层的天花板轻轻撞头。

七、结语

Smart Turn 值得写一篇的原因,不只是「有用的小工具」:

  • 它是小模型哲学的教科书案例。判停不需要世界知识,不需要生成能力,只需要听懂「没说完」的声音形状——所以 8 MB 够了,CPU 够了,十几毫秒够了。任务定义得足够窄,模型才能足够小;模型足够小,才能塞进每一个 agent 实例里白送。
  • 它把「开源」做全了。权重、数据、训练代码、评测脚本、数据清洗工具、社区标注站,一条链全开放。上一篇说全双工领域「公开的双流对话数据全球只有五千小时」,Smart Turn 的社区攒数据模式——标注站 + 小游戏 + 数据公司合作——是对这个瓶颈的一次具体回应,虽然它攒的只是判停这一格的数据。
  • 它的三代演进是一份免费的实验报告:更大的预训练底座会过拟合小任务;换底座比调超参收益大;真人数据比合成数据值钱;测试集换血时准确率数字会「跳水」——每一条都有公开数字背书。

回到开头那个抢答的助手:给它加一个 Smart Turn,代价是每次停顿多花十几毫秒 CPU 时间,换来的是「我想要……那个……」之后的一秒安静。对话体验里最贵的东西,往往就是这一秒的耐心。

参考