8.6 KiB
本地 VAD 语音端点检测
更新:2026-10-23。适用于 lucky-cat;实现所有权见 ADR 0006。
当前行为
音频仍然边采集边上传。服务端保持 push2talk,设备用本地 VAD 更早决定何时
发送 StopSpeech,不切换云端识别模式或修改语音回复语义。
| 配置 | 默认值 | 含义 |
|---|---|---|
CONFIG_TQ_SPEECH_ENDPOINT_ENABLE |
y | 仅单击对话模式自动判停,可关闭回退 |
CONFIG_TQ_SPEECH_ENDPOINT_MIN_SPEECH_MS |
200 | 开始判停前需要连续有效人声 |
CONFIG_TQ_SPEECH_ENDPOINT_SILENCE_MS |
900 | 确认人声后,连续 VAD 静音达到该值结束输入 |
CONFIG_TQ_PTT_CLICK_CAPTURE_TIMEOUT_MS |
8000 | 保留原最长窗口兜底 |
本地唤醒仍先完成联网和本地回应提示音,再开始网络拾音和 VAD。按键入口复用 原窗口起点,可能包含连接等待;本次不改变这一点。持续静音或未达到人声阈值的 短促声音不触发提前结束,继续走原超时。阈值以 20 ms 检测帧向上取整。
等待连接、等待提示音和拾音期间,重复按键/唤醒请求沿用当前轮次,不递增
generation、不重置输入窗口,也不重新启用 VAD。收尾期间保留原有下一轮排队机制。
当前单击模式关闭长按识别,长按松开也可能产生 SHORT_PRESS;该事件同样受上述
幂等保护,不能让仍在录音的控制层换代并遗留旧代次 VAD。
900 ms 指 VAD 输出连续静音,不等于从物理声音结束到 StopSpeech 的精确时长; 库内判定延迟、20 ms 分帧、现有 80 ms 控制轮询、调度及尾包发送都会影响时间。 句中停顿超过阈值可能被截断;慢语速、轻声、儿童、远场及噪声需实机调参。
所有权、并发与收尾
platform提供固定两个 PCM 观察者槽位,同一份 AEC 后 16 kHz 单声道 PCM 分别交给已有本地命令观察者与领域端点检测器。无第二条采集链或新音频队列。domain_speech_endpoint.c使用 WebRTCvad_create(VAD_MODE_1)和vad_process(), 将输入重分帧为 320 样本。PCM 回调零等待取锁,最多处理 512 样本,不执行 协议、等待或资源销毁;状态查询和生命周期使用有界互斥锁。- 丢失回调、非法输入或超过 100 ms 的输入间断重置连续人声/静音计时,保持保守 判停。只保留“曾确认有人声”,不能把缺失数据当静音补算。
- 控制层只接受活动
STREAMING、提示音门禁关闭且 generation 匹配的结束快照。 旧事件不会结束新一轮。初始化失败记录日志,保留原超时。 - 每轮仅尝试启动一次检测。收尾/取消/正常停机注销订阅并释放 VAD;注销超时 禁用订阅、保留实例和在途计数以便重试,不后台释放。
- 平台停止先暂停并等待在途采集,再由原采集任务补零编码尾帧。编码留在原
32 KB 采集任务栈,控制线程只等待结果。发送队列与在途包全部排空后才正常
发送
StopSpeech。停止失败走传输废弃路径,不提交正常结束、不无限重试。 - stop 超时后仍可能完成之前请求的尾帧和发送;资源保留,禁止启动新一轮,必须 重试收尾或走 deinit。deinit 才拥有常驻 worker 的退出/join/强制删除和资源销毁。
日志与资源
VAD armed:代次、阈值及启用后的内部 RAM/PSRAM 空闲快照。VAD endpoint:代次、处理音频时长、连续静音、最大回调处理墙钟时间与间断数。StopSpeech sent after uplink drain:正常输入结束指令已发出。VAD unavailable; retain capture timeout:本轮回退原输入窗口。PTT activation ignored: turn already active:重复触发已忽略,原代次与超时窗口保留。VAD capture timeout:兜底结束前的一次领域快照,包含 PTT/端点代次、活动状态、 是否已确认人声、是否结束、处理音频时长、当前连续静音、最大回调墙钟时间和间断数。speech=0表示尚未确认连续人声;audio_ms很小或gaps较多可提示输入问题。 这是瞬时状态,不能据此还原整段声学分类;查询失败只记录错误,继续原超时收尾。Abort voice turn after incomplete drain:失败废弃,不能记作成功回复。
固定 PCM 重分帧数组为 640 B,另有领域状态/互斥锁和平台第二个观察者槽、
采集与生命周期锁。不新增 worker、任务栈、DMA 或模型分区资源。
VAD 库内部动态分配、原采集栈高水位和联网后的最小内部 RAM 必须实机测量;
internal_free 是并发系统的空闲快照,不能直接当成 VAD 的精确分配量。
max_process_us 为墙钟时间,含抢占,不是纯 CPU 时间。PSRAM 不增加固定像素/
音频队列缓冲,但库分配采用的实际堆区域仍以运行日志为准。
验证与验收
python3 tools/test_speech_endpoint.py 编译并执行生产 C,使用可控 VAD/RTOS 桩:
覆盖分帧、纯静音、短噪声、句中停顿、代次、输入间断、初始化失败、注销超时、
订阅并发替换、队列出队与 busy 竞态、原采集上下文尾帧补零、排空与失败重试;
控制层直接执行生产激活函数,覆盖各等待阶段/拾音阶段重复触发、原端点继续生效、
超时不续期、收尾排队、前置检查期间并发触发和快照查询失败后的兜底。
这些测试验证控制逻辑,不验证真实声学识别率或 ESP32 时序。
实机至少检查:
- 按键和唤醒各测试短句、长句、未说话、轻声、噪声和不同长度句中停顿。
- 连续多轮、拾音中重复单击/长按松开、快速取消/重新触发、服务端提前结束、 TTS 播放及唤醒提示音门禁。重复触发应保持代次和原窗口,收尾后新一轮仍可正常启动。
- 拾音期间断网、OTA/待机切换、发送拥塞、内存不足与重复初始化。
- 记录最后人声 → endpoint → StopSpeech → 首个下行音频 → 实际播放的分段延迟; 同时统计误截断、未判停、队列丢包、回调耗时、栈高水位和内部 RAM 最低余量。
本次构建/主机验证结果见下方记录;没有实机日志前,不承诺提速秒数或声学阈值合适。
2026-10-15 本次验证记录
- 主机:
test_speech_endpoint.py全部通过,启用/关闭分支均编译并执行; 五个现有test_lcd_{status_hud,emote_transport,emote_timeline,listening_gfx,fortune_gfx}.py回归脚本通过(脚本自带的 profiling 开/关路径也通过)。 - 构建:ESP-IDF 5.5.4,VAD 开启和关闭两个独立 sdkconfig 均构建成功; 项目默认配置保持开启。关闭配置与构建在临时目录,未覆盖项目 sdkconfig。
- 固件:开启 3,167,888 B,关闭 3,157,248 B,开关差异 +10,640 B; 同源 MAP 的内部静态 data +16 B、BSS +832 B,合计 +848 B,IRAM 代码无增量。 这是同一份修改中 VAD 开关的对比,双方均包含共享 PCM/尾包收尾修正, 不能当成整个变更相对旧提交的净成本。模型分区未变,没有新增固定 PSRAM/DMA 缓冲。
- 架构:原检查脚本在已有
control_plane_battery_service.c的portMAX_DELAY处失败,已对照 HEAD 确认不是本次引入。仅排除该既有检查后,其余架构检查通过; 本次新增路径无无限等待或跨层内部头依赖。git diff --check通过。 - 设备:未烧录,尚无实机误截断率、延迟收益、VAD 动态堆和栈峰值数据。
2026-10-23 重复激活修复验证
- 修复:控制层在前置检查前及提交意图的临界区内检查活动轮次,忽略重复激活; 保持领域端点的代次隔离,不替换正在使用的 VAD。新增超时诊断只在兜底时记录。
- 主机:重新运行
test_speech_endpoint.py全部通过,包含 VAD 开启/关闭的领域和 控制层 C 分支;上述五个显示回归脚本也全部通过。 - 构建:激活 ESP-IDF 5.5.4 后执行
idf.py build成功,使用当前 VAD 开启配置; 本次未执行关闭配置的固件构建。应用镜像 3,168,368 B,单个 OTA 槽剩余 1,550,224 B。 本次不新增任务、队列、堆分配或固定音频/DMA 缓冲。 - 架构:原守卫仍停在电量模块既有的
portMAX_DELAY,已对照 HEAD 确认;仅排除 该既有检查后,其余守卫通过。git diff --check通过。 - 设备:未烧录。首次录音未提前断句的声学原因仍需新日志和实机复测;本次主机测试 与构建不能证明真实噪声环境下的人声/静音识别效果。