一、实验目的
在 v2.2 研发复盘中,语音链路被列为 P0 缺口:花盆只能通过手机端发起对话,无法像真正的生物一样被「叫醒」,交互的发起权始终在用户手里。本次实验的目标是:在 ESP32-S3 上落地一个常驻的本地关键词唤醒(KWS)模型,做到 离线可唤醒、唤醒后接云端 ASR + LLM 的完整链路,并在真实家庭噪声环境下把误唤醒压到可接受水平。
衡量标准有三条:待机电流降到 20mA 以下(可长期插电不烫手)、安静环境唤醒率 ≥ 95%、日均误唤醒 ≤ 1 次/小时(不会让用户想把它关掉)。
二、实验硬件与环境
| 项目 | 型号 / 参数 |
|---|---|
| 主控 | ESP32-S3-WROOM-1-N16R8(双核 240MHz、16MB Flash、8MB PSRAM) |
| 麦克风 | INMP441(I2S、24bit、底部收音孔) |
| 功放 | MAX98357A(I2S、3W) |
| 开发框架 | ESP-IDF v5.1 / Arduino-ESP32 2.0.14 |
| 训练平台 | Edge Impulse Studio(免费开发者版) |
| 推理运行时 | TensorFlow Lite Micro + ESP-NN 加速内核 |
三、唤醒链路设计(本地唤醒 + 云端理解)
整体采用两段式架构:端侧只做一件事——判断「有没有人在叫我」;一旦唤醒,才把后续音频交给云端做理解与生成。这样待机阶段既不联网也不上传音频,隐私承诺可以真正兑现,流量与 token 成本也随之下降。
常驻循环(端侧,18mA)
├─ I2S 采集 16kHz/16bit → 30ms 滑动窗口
├─ MFCC 特征 → TFLM 模型推理(int8)
└─ 置信度 > 0.82 且连续 2 帧命中 → 触发唤醒
唤醒后(联网)
├─ 播放「在呢」提示音 + LED 光语
├─ VAD 端点检测 → 云端 ASR
└─ LLM(注入 LMPI 心境 + 记忆)→ TTS 流式 → I2S 播放阈值与「连续 2 帧」是抑制误唤醒的关键:单帧命中不触发,瞬时噪声(锅铲声、门铃)很难连续两帧都超过阈值。
四、语音数据采集与增强
唤醒词定为四字叠词「花丫花丫」:叠词在声学上重复性强、易与日常对话区分,且与花盆的品牌人格一致。数据集全部自采,未使用公开语料。
| 类别 | 条数 | 说明 |
|---|---|---|
| 正样本(1m 内) | 620 | 26 位志愿者(14 男 / 12 女) |
| 正样本(1-3m) | 480 | 含普通话与 6 种口音 |
| 正样本(3-5m 带混响) | 380 | 客厅 / 厨房远场场景 |
| 负样本-日常语料 | 1650 | 电视剧、播客、家庭对话 |
| 负样本-近音易混词 | 720 | 「花呀」「发芽」「好呀」「丫头」等 |
| 环境噪声 | 350 | 油烟机、水龙头、电视、扫地机器人 |
| 合计 | 4200 | 约 3.6 小时 |
数据增强策略:音量扰动 ±6dB、加性噪声(SNR 5~20dB)、时移 ±100ms、房间混响模拟(RT60 取 0.3 / 0.6 / 0.9s 三档)。增强后训练集约 1.2 万条。
五、模型训练与量化
特征提取用 MFCC:帧长 25ms、帧移 10ms、40 个 Mel 滤波器、取前 13 阶系数并拼接一阶与二阶差分,输入窗口 1000ms。网络采用 4 层深度可分离卷积 + 全局平均池化 + 2 类输出,这是端侧 KWS 的经典结构,参数量与乘加运算都压得很低。
| 指标 | float32 | int8 量化后 |
|---|---|---|
| 模型体积 | 78 KB | 21 KB |
| 峰值内存 | 46 KB | 13 KB |
| 单帧推理耗时 | 31 ms | 9 ms |
| 验证集准确率 | 97.1% | 96.4% |
| CPU 占用(240MHz 双核) | 约 11% | 约 3% |
量化用 Edge Impulse 自带的 int8 全整型量化(代表性数据集校准),准确率仅掉 0.7 个百分点,但推理耗时降到原来的 1/3,这是能把它塞进常驻循环的前提。
六、端侧部署要点
- 任务绑核:I2S 采集放 Core0 高优先级任务,推理放 Core1,中间用 4 帧环形缓冲衔接,避免采样被推理阻塞;
- PSRAM 开启:设置
CONFIG_SPIRAM_USE=y,模型与音频缓冲放 PSRAM,内部 RAM 全部留给 DMA 与协议栈; - 模型编入固件:以
EMBED_FILES方式编译进 app 分区(预留 4MB),不做文件系统读取,避免读写抖动与掉电损坏; - 双帧确认:置信度阈值 0.82 + 连续 2 帧命中才触发,单帧命中只更新内部状态;
- 自唤醒抑制:扬声器播放开始后的 200ms 内关闭 KWS 判定窗口,避免花盆自己说话把自己叫醒。
七、实测数据与误唤醒压测
| 场景 | 距离 | 唤醒率 | 误唤醒(次/小时) |
|---|---|---|---|
| 安静书房 | 1-3m | 98.2% | 0.1 |
| 电视开着(约 60dB) | 2m | 95.6% | 0.6 |
| 厨房(油烟机 + 水流) | 3m | 91.3% | 1.2 |
| 夜间(空调约 45dB) | 3m | 96.4% | 0.3 |
| 加权日均 | - | 96.4% | 0.8 |
另做 3 天 24 小时连续挂机:累计误唤醒 58 次,其中 41 次来自含「呀 / 花」的电视台词。优化方向已明确——补充近音负样本,并把阈值从 0.82 提到 0.85。
八、踩坑复盘
- I2S 采样率错配(最坑):固件配置 16kHz,训练数据却按 44.1kHz 采集后未重采样,唤醒率从 96% 直接跌到 41%。统一重采样到 16kHz 后恢复;
- DMA 缓冲溢出爆音:默认 512 字节缓冲在 WiFi 发送时被打断,改为 1024 字节双缓冲并把 WiFi 任务绑核后消失;
- 麦克风增益过高削波:近距离样本出现削顶失真,模型学到的是失真特征,补录近距离数据时把增益从 0.7 降到 0.45;
- 供电噪声:与功放共用 USB 供电时底噪抬升约 12dB,改用独立 LDO 给麦克风供电;
- 开机自检误触发:上电瞬间 I2S 有直流偏置脉冲,改为开机 300ms 内不进入 KWS 判定;
- 模型版本混乱:三次迭代的模型都叫 model.tflite,改为把 git commit 短 hash 写入固件版本字符串,串口日志可直接读到。
九、实验总结与下一步
本次实验把唤醒能力下沉到端侧,花盆第一次具备了「被叫醒」的能力,待机电流 18mA、模型 21KB、安静环境唤醒率 98.2%,三条衡量标准全部达成。更重要的是验证了端侧 TinyML + 云端大模型的两段式链路在 ESP32-S3 上跑得通,为后续声纹、情绪识别的下沉铺好了路。
下一步:① 引入双阶段唤醒(KWS → 声纹二次确认),解决多人家庭的误唤醒;② 唤醒提示音与 LMPI 心境联动,花盆高兴时和蔫了时的应答音色不同;③ 误唤醒样本自动回传(仅回传脱敏特征,不回传音频)用于增量训练。


💬 过往技术交流