Files
lucky-cat/docs/local-speech-endpoint.zh-CN.md
T

6.3 KiB
Raw Blame History

本地 VAD 语音端点检测

更新:2026-10-15。适用于 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 检测帧向上取整。

900 ms 指 VAD 输出连续静音,不等于从物理声音结束到 StopSpeech 的精确时长; 库内判定延迟、20 ms 分帧、现有 80 ms 控制轮询、调度及尾包发送都会影响时间。 句中停顿超过阈值可能被截断;慢语速、轻声、儿童、远场及噪声需实机调参。

所有权、并发与收尾

  • platform 提供固定两个 PCM 观察者槽位,同一份 AEC 后 16 kHz 单声道 PCM 分别交给已有本地命令观察者与领域端点检测器。无第二条采集链或新音频队列。
  • domain_speech_endpoint.c 使用 WebRTC vad_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:本轮回退原输入窗口。
  • 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 时序。

实机至少检查:

  1. 按键和唤醒各测试短句、长句、未说话、轻声、噪声和不同长度句中停顿。
  2. 连续多轮、快速取消/重新触发、服务端提前结束、TTS 播放及唤醒提示音门禁。
  3. 拾音期间断网、OTA/待机切换、发送拥塞、内存不足与重复初始化。
  4. 记录最后人声 → 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 动态堆和栈峰值数据。