战

实战中心

EOU 话轮判停

← 返回实战列表
✂️ EOU 话轮判停

smart-turn v3.2:本地 ONNX 判停真实推理评测

2026-07-30 · 10 样本 × 3 轮真实推理 · 工具目录 smart-turn/ · 全程真实计算,无 mock

机型 Apple Silicon macOS Python 3.9.6(venv) 模型 smart-turn-v3.2-cpu.onnx 推理 onnxruntime CPU 单线程 窗口 末尾 8 秒
本页导航: 背景速览 环境准备 数据集自建 跑通步骤 验证结果 核心发现 单元测试 踩坑记录

一、背景速览:EOU 到底在解决什么

纯 VAD(静音检测)判停的问题是:用户说到"我想问一下我的套餐是……"停顿思考 0.6 秒,VAD 认为说完了, AI 直接抢话。反过来把静音阈值调到 1.5 秒,用户说完"好的。"要等 1.5 秒才有回应,对话变得迟钝。

EOU(End-of-Utterance)模型的作用是听语义和韵律,判断"这句话在语法/语义上说完了没有", 从而把静音等待从固定阈值变成动态决策。

配套调研结论见: EOU 选型调研:东南亚 + 南美语种话轮检测模型选型 ↗、 EOU 模型专题 · 话轮结束检测 ↗。 调研阶段已定选型为 pipecat-ai/smart-turn,本次实战要回答的是:它在本机真跑起来是什么水平。

本次实战全程真实推理:真实 ONNX 模型 + 真实音频 + 真实 Whisper 特征提取,没有任何 mock。 模型是纯 CPU 版本,不需要 GPU、不需要联网、不需要任何 API Key。

二、环境准备:装什么、模型从哪来

2.1 依赖

依赖用途说明
onnxruntime推理引擎CPU 版即可,不用装 GPU 版
transformersWhisperFeatureExtractor 做梅尔特征只用特征提取器,不需要装 PyTorch
librosa / soundfile音频加载与重采样模型只吃 16kHz 单声道 float32
numpy数组处理—
pytest跑单元测试可选
环境准备
$ cd smart-turn
$ python3 -m venv venv
# 只做推理用 requirements_inference.txt,比全量依赖小很多
$ ./venv/bin/pip install -r requirements_inference.txt

# 确认模型文件在位(约 30MB,CPU 量化版)
$ ls -la smart-turn-v3.2-cpu.onnx
版本坑先说在前面:仓库代码里默认写的是 v3.1 的文件名,但权重仓库现在发的是 v3.2。 inference.py 已改成支持环境变量覆盖:SMART_TURN_ONNX_PATH, 而且这个变量必须在 import inference 之前设置——因为模块在导入时就构建了 ONNX session。

三、数据集自建:为什么不用现成样本

目录里原本有一批 *_zh.wav 样本,但它们的朗读文本没有出处记录——不知道当初念的到底是哪句话, 也就无法确定"期望判定"该标 0 还是 1。用来源不明的样本算准确率,结论是不可信的。

所以写了 batch_eval.py,自带 --regen:用 macOS say + afconvert 现场合成整套数据集,朗读文本与人工标注在代码里一一对应,任何人重跑都能得到同一份数据。

10 条样本设计(正 5 负 5)

样本朗读文本期望为什么要这条
p1_statement帮我查询一下我的话费余额。说完最标准的陈述句,基线
p2_question请问人工客服的工作时间是几点到几点?说完疑问句语调上扬,容易被误判成未完
p3_long我的宽带今天上午突然断网了,你们能不能派个师傅上门来修一下。说完长句,接近 8 秒窗口边界
p4_short好的。说完极短音频(0.41s),考察补零后是否仍能判对
p5_number我的订单号是八八一二三。说完数字结尾,韵律与文字句不同
n1_cutoff我想问一下我的套餐是未完动词后缺宾语,句子被硬截断
n2_filler我想要查询一下那个未完以指示词结尾
n3_hesitate嗯,我这边是想说未完填充词犹豫,最容易被 VAD 误切
n4_conj我上个月已经交过费了,但是未完以连词结尾,后面还有转折
n5_prep这个费用能不能从未完以介词结尾,语法上不可能结束

四、跑通步骤

  1. 单条试跑,确认模型能加载
    $ ./venv/bin/python predict.py samples/eou_p1_statement.wav
    Loading audio file: samples/eou_p1_statement.wav
    Loaded audio with sample rate: 16000 Hz, duration: 2.62 seconds
    Running endpoint prediction...
    
    Results:
    Prediction: Complete
    Probability of complete: 0.6345
  2. 合成数据集 + 批量评测(每条重复 3 次取中位数)

    重复 3 次是为了排除单次抖动;模型本身是确定性的(无 dropout/采样),概率完全可复现,重复只影响耗时统计。

    批量评测
    $ ./venv/bin/python batch_eval.py --regen --repeats 3 --json /tmp/eou_result.json
    
    [合成] eou_p1_statement.wav  <- 「帮我查询一下我的话费余额。」
    [合成] eou_p2_question.wav   <- 「请问人工客服的工作时间是几点到几点?」
    ... 共 10 条
  3. 读结果表
    ===== smart-turn v3.2 本机批量判停结果 =====
    
    样本                    时长s  预期  实际   完成概率   耗时ms  结果
    eou_p1_statement.wav   2.62  说完  说完   0.6345    34.2  PASS
    eou_p2_question.wav    3.85  说完  说完   0.9836    34.0  PASS
    eou_p3_long.wav        6.31  说完  未完   0.0597    34.9  FAIL
    eou_p4_short.wav       0.41  说完  说完   0.9425    34.6  PASS
    eou_p5_number.wav      2.35  说完  未完   0.3564    33.6  FAIL
    eou_n1_cutoff.wav      2.28  未完  未完   0.1142    33.7  PASS
    eou_n2_filler.wav      2.04  未完  未完   0.0164    33.5  PASS
    eou_n3_hesitate.wav    2.00  未完  未完   0.0151    34.1  PASS
    eou_n4_conj.wav        2.98  未完  未完   0.0057    33.9  PASS
    eou_n5_prep.wav        1.68  未完  未完   0.4902    44.9  PASS
    
    准确率 8/10 = 80.0%
    单次推理耗时 P50=34.1ms  P95=34.9ms  最小=33.5ms  最大=44.9ms
    正类均值=0.5953  负类均值=0.1283  区分度=0.4670

五、本机验证结果

80%
总准确率 8/10
34.1ms
单次推理 P50
5/5
负类(未说完)全对
3/5
正类(说完)有漏判

10 条样本的完成概率分布(红线 = 0.5 判定阈值)

绿色 = 期望"说完",橙色 = 期望"未说完"。理想情况绿色全在 0.5 线上、橙色全在线下。实测两根绿柱掉到线下(p3_long 0.0597、p5_number 0.3564),一根橙柱贴着线(n5_prep 0.4902)。

样本时长期望实际完成概率耗时判定
p1_statement2.62s说完说完0.634534.2msPASS
p2_question3.85s说完说完0.983634.0msPASS
p3_long6.31s说完未完0.059734.9msFAIL
p4_short0.41s说完说完0.942534.6msPASS
p5_number2.35s说完未完0.356433.6msFAIL
n1_cutoff2.28s未完未完0.114233.7msPASS
n2_filler2.04s未完未完0.016433.5msPASS
n3_hesitate2.00s未完未完0.015134.1msPASS
n4_conj2.98s未完未完0.005733.9msPASS
n5_prep1.68s未完未完0.490244.9msPASS

六、核心发现

6.1 错误是单向的:只漏判"说完",不误判"未说完"

负类 5/5 全对,且概率极低(0.0057 ~ 0.1142,四条都在 0.12 以下)——说明模型对 「以连词/介词/指示词/填充词结尾」这类语法未完信号抓得非常稳。

而正类只有 3/5,两个错误都是把"说完了"判成"没说完"。这两种错误在真实通话里的代价完全不同:

错误类型本次实测用户感受严重度
假阴性:说完了判成没说完2 例AI 傻等,对话出现尴尬空白,最后靠静音超时兜底中(延迟增加,不致命)
假阳性:没说完判成说完0 例AI 抢话打断用户,体验断崖式下降高(用户直接挂机)
这个错误方向是可接受的。假阴性只让 AI 多等一会(可以用静音超时兜底),假阳性会抢话直接毁掉体验。 模型偏保守 = 工程上更安全。也就是说 80% 这个数字比它看起来更可用。

6.2 两个漏判的共同点:长句与数字结尾

p3_long(6.31 秒长句)概率只有 0.0597,漏得非常彻底。文本"……你们能不能派个师傅上门来修一下。" 在语义上明确说完了,但音频接近 8 秒窗口上限,且合成语音在长句末尾的降调不明显。 推论:模型对韵律的依赖比对语义的依赖更重——句末降调不够清晰,语义完整也救不回来。
p5_number(数字结尾)概率 0.3564,虽然错但没有错得太远。 「八八一二三」这种数字串朗读的节奏是平的,缺少句末的语调收束,模型因此犹豫。 工程影响:订单号、金额、验证码复述这类高频客服场景,恰好都是数字结尾, 这是外呼链路必须单独处理的一类。

6.3 阈值不是安全的:有样本贴着 0.5

n5_prep("这个费用能不能从")概率 0.4902,判对了,但只比阈值低 0.0098。 任何把阈值从 0.5 调到 0.49 的操作,都会让这一条翻错。

阈值调优的正确姿势:既然错误是单向的(只漏判正类),可以把阈值下调(如 0.35)来提高正类召回。 按本次数据推算,阈值调到 0.35 会让 p5_number(0.3564)翻对,同时 n5_prep(0.4902)会翻错—— 净收益为零。这说明单纯调阈值解决不了问题,正确做法是分场景处理:数字结尾的句子交给 ASR 文本层做语义补判,或直接缩短静音超时兜底。

6.4 延迟完全不是瓶颈

单次推理 P50 34.1ms、P95 34.9ms,CPU 单线程。放在语音链路的延迟预算里几乎可以忽略 (相比 ASR 首包 200ms+、LLM 首字 300ms+)。唯一的异常是 n5_prep 的 44.9ms—— 比其它样本高 10ms,是系统调度抖动而非模型问题(同一模型对固定输入是确定性的)。

工程含义:EOU 可以放在每次 VAD 静音事件后同步调用,不需要异步化、不需要 GPU、不需要批处理。 按 34ms 算,单核可支撑约 29 次判定/秒,一台机器跑几百路并发外呼的 EOU 完全够用。

七、单元测试:21 项全过

除了批量评测,仓库里还有三层单元测试,这次一并跑通:

层次测什么典型断言
契约测试输出结构、取值范围、阈值一致性、确定性概率必须在 [0,1];prediction == (probability > 0.5);同输入两次结果必须一致
健壮性测试纯静音、15 秒超长、100ms 极短、float64 输入都不能崩;超长要内部截尾、极短要补零
语义测试真实中文语音的方向性完整句概率必须比 filler 结尾高出 0.2 以上
单元测试
$ ./venv/bin/python -m pytest tests/ -q
.....................                                     [100%]
21 passed, 1 warning in 1.59s
值得抄的测试写法:语义测试的断言故意写得宽松——只断言"完整句概率 > filler 概率 + 0.2"这个方向, 不断言绝对数值。因为 TTS 韵律比人声扁平,绝对值会随合成音色漂移,锁死数值会让测试变成脆弱的假警报。

八、踩坑记录

坑 1:环境变量必须在 import 之前设置。 inference.py 在模块级就执行了 session = build_session(ONNX_MODEL_PATH), 所以 os.environ["SMART_TURN_ONNX_PATH"] = ... 一旦写在 import inference 之后就完全无效, 而且不会报错,只会静默加载错的模型文件(或者报"文件不存在"让人一头雾水)。
坑 2:用来源不明的样本算准确率,等于自欺欺人。 第一次评测用的是目录里现成的 *_zh.wav,我按文件名猜了朗读文本去标注,跑出 4/6 = 66.7%。 但那个数字毫无意义——标注可能本身就是错的。改成脚本现场合成、文本与标注在代码里绑定后, 结果变成 8/10 = 80%,这次的数字才是可信、可复现的。 评测的第一条纪律是:先保证标注可信,再谈模型准确率。
坑 3:合成语音的韵律比人声扁平,会系统性压低正类召回。 macOS say 的句末降调远不如真人明显,而模型很依赖韵律信号。 所以本次的 80% 是下界不是真实水平——真人录音上正类召回大概率更高。 反过来说:不能用合成语音的评测结果去否定一个 EOU 模型,正式选型必须用真实通话录音。
提醒 4:只装 transformers 不装 PyTorch 会打印一行警告,不影响使用。 "None of PyTorch, TensorFlow >= 2.0, or Flax have been found" —— 这是正常的, 我们只用 WhisperFeatureExtractor 做梅尔特征,不需要任何深度学习框架,推理走 onnxruntime。 装 PyTorch 只会让镜像大好几个 G。
提醒 5:模型只吃末尾 8 秒,超长音频内部截尾。 truncate_audio_to_last_n_seconds(audio, n_seconds=8) 保留的是结尾而非开头—— 符合判停逻辑(只关心最近说的话)。但这意味着超过 8 秒的连续说话,前面的语义上下文全丢, 长句场景要靠 ASR 文本层补判。
← 返回实战列表 相关调研:EOU 多语种选型 ↗ EOU 模型专题 ↗