我最早对离线语音助手的执念,源于一次在无网环境下的尴尬——对着智能音箱喊了半天,它只回我一句“网络连接失败”。那一刻我意识到,如果语音助手的核心能力全部依赖云端,它在断网场景下就是个摆设。于是有了这篇文章的主角:基于ESP32-S3从零打造的离线语音助手,支持自定义唤醒词,并且把大模型推理拉到本地局域网内完成,全程不依赖公网。
这篇实战指南不是教科书,是我自己从选型、训练、部署到调优一路踩过来的记录。我会把microwakeword训练自定义唤醒词的完整流程、ESP32-S3上的TFLite Micro部署细节、以及本地大模型推理的两条路线(MCU直推和网关推理)都拆开讲清楚。适合三类人看:一是想用ESP32-S3做语音交互产品的嵌入式开发者,二是想把大模型私有化、但又不想总开着电脑跑服务的AI应用爱好者,三是对数据隐私敏感、追求断网可用的折腾派玩家。
1. 为什么要自己动手做离线语音助手:ESP32-S3的能力边界与选型逻辑
1.1 离线语音助手的真实需求
做离线语音助手这件事,需求其实很具体。你自己想想,语音助手最常见的三个痛点是什么:隐私、延迟、断网不可用。云端的语音助手,麦克风录到的声音要上传到服务器,识别结果再返回。就算厂家说“数据加密”,数据经过第三方服务器始终是个心理疙瘩。延迟方面,一次云端识别走一圈,快则几百毫秒,慢则一两秒,对话节奏很受影响。最尴尬的是断网——家里宽带一断,智能音箱直接变砖。
离线语音助手把唤醒、识别、理解这几段全部搬到本地设备,好处是显而易见的:数据不出局域网,响应速度取决于本地算力而非网络状况,断网也能用。而ESP32-S3就是目前做这件事性价比极高的一块芯片。
1.2 ESP32-S3的硬件参数与语音场景适配
我一开始也想过用树莓派或者旧手机当语音助手的主控,但对比一圈下来,ESP32-S3的优势在于成本和功耗的平衡点选得非常好。
ES32-S3的关键参数我列一下:
| 项目 | 参数 | 对语音助手的意义 |
|---|---|---|
| 核心 | 双核Xtensa LX7,240MHz | 可双核并行,一个核跑音频采集,一个核跑推理 |
| SRAM | 512KB | 唤醒词级别的模型完全够放 |
| PSRAM | 最高16MB(Octal) | 跑更大一点的命令识别模型,暂存音频缓冲 |
| 向量指令 | 支持PIE扩展指令 | 对矩阵乘法和卷积有硬件加速,TFLite Micro推理有加成 |
| 音频接口 | I2S ×2 | 可直接接MEMS麦克风,INMP441这类芯片即插即用 |
| 无线 | WiFi + BLE 5 | 唤醒后通过WiFi把请求发给本地网关做LLM推理 |
| 功耗 | 平均80~200mA(射频/推理峰值更高) | 比树莓派动辄0.5A强一个量级,可电池供电 |
关键在于,ESP32-S3不是靠堆算力取胜,而是靠“够用就好”的精准定位。它跑不了大语言模型,但跑一个几十KB到几百KB的神经网络唤醒词检测器绰绰有余;它放不下Llama,但它能把音频处理好,把唤醒做利索,然后把理解任务交付给局域网里性能更强的设备。
1.3 为什么选择“MCU唤醒+网关推理”而不是单设备全包
如果追求极致的“本地”,为什么不直接用一台带GPU的电脑做全部事情?
因为成本、功耗、形态完全不同。一台PC全年开机做语音助手,电费和噪音先不说,它需要固定放置。而ESP32-S3做成一个小板子,塞在客厅角落、床头柜,几乎无感。整套系统拆成两半:
- 前端:ESP32-S3负责拾音、唤醒词检测、命令词理解,做到毫瓦级待机功率。
- 后端:局域网内的PC/树莓派/NAS负责真正的大模型推理,通过Ollama这类工具提供HTTP API。
这两半通过WiFi通信,数据全程不走公网。用户直观体验就是喊一嗓子设备就响应,和云方案几乎一样快,但隐私和可用性都稳了。这套架构是经过实际验证比较顺的组合。
1.4 整体方案的链路设计
我最终跑通的完整链路是:
麦克风采集(INMP441/ESP32-S3 I2S) → VAD检测到人声 → 唤醒词检测(microwakeword训练的自定义词) → 本地意图识别(可选,跑在ESP32-S3上的超轻量分类模型) → 通过WiFi调用局域网内Ollama API → 大模型返回文本结果 → 用预置语音片段或TTS合成结果播放整个过程没有任何环节依赖外网。下面每个环节的细节我逐一展开,先从最核心的自定义唤醒词说起。
2. 自定义唤醒词从0到1:microwakeword数据集制作与训练全流程
2.1 为什么选microwakeword而不是直接用ESP-SR自带唤醒词
乐鑫官方有ESP-SR框架,内置了唤醒词和语音识别方案。但这里有个现实问题:ESP-SR自带的唤醒词是固定的,比如“Hi 乐鑫”、“小智小智”这类,中文自定义唤醒词需要向乐鑫申请模型训练服务,流程走下来挺麻烦。关键是你想让设备响应“你好小助手”“嘿管家”这种自己定义的词,用现成方案基本没戏。
microwakeword这个开源项目的思路就不一样。它本质上是一个面向MCU的唤醒词训练工具链,你只需要准备目标唤醒词的音频样本,就能在本地训练出自己的模型,然后导出成TFLite格式部署到ESP32-S3上。模型结构以DNN/CNN为主,经过int8量化后体积通常在几十到两百KB之间,非常适合ESP32-S3的资源。
我最初也担心自定义唤醒词的效果是不是不如人家工厂级训练的好。实际验证下来,只要数据集做得合格,自定义唤醒词在安静环境下唤醒率能做到95%以上,误唤醒率也能控制在可接受水平。对于个人项目和中小产品来说,完全够用。
2.2 搭建训练环境的完整命令
microwakeword依赖的是Python + TensorFlow,官方支持Linux环境。我用的是Ubuntu 22.04,Python 3.10,具体安装步骤:
# 克隆项目 git clone https://github.com/josesamuel/microwakeword.git cd microwakeword # 建议创建虚拟环境,避免污染系统Python python3 -m venv venv source venv/bin/activate # 安装依赖 pip install numpy soundfile python_speech_features tensorflow # 注意:训练用CPU版本即可,这个项目模型不大,GPU并非必须项目里的核心目录结构大概是这样的:train.py负责训练主流程,utils.py里有特征提取和数据加载的工具,models/里放着模型定义。我用的分支是v0.2及以上版本,对ESP32-S3的适配相对成熟。
2.3 数据集制作:唤醒词唤醒率的命门
做唤醒词训练,数据集质量决定最终效果。这一步不能偷懒,我一开始只用自己录的100条样本训练,结果在嘈杂环境中误唤醒率惨不忍睹,后来重新采集扩充数据,效果才上来。
数据集怎么准备:
正面样本(目标唤醒词)需要覆盖以下维度:
- 发音人群:至少3~5人,男女都要有,尽量有不同口音。我找了5个人,每人录2~3遍,共约500条。
- 距离变化:距离麦克风0.3米、1米、2米各录一部分。远场样本特别重要,否则实际使用时设备听得不清醒。
- 环境差异:安静房间、开着电视的房间、有空调底噪的房间各录一部分。
- 音量变化:正常音量、轻声说、大声喊都要覆盖。
负面样本(容易触发误报的音频)同样关键:
- 日常对话片段、电视人声、音乐片段。
- 与唤醒词发音相近的词,比如我的唤醒词是“你好小机”,那“你好小鸡”“你好小技”这些发音相似的全要收录。
- 咳嗽声、关门声、键盘敲击声等环境音。
单条样本长度控制在1秒左右。microwakeword默认特征提取是按16kHz采样率做的,所以录制时最好统一规格。我用Audacity录制的,直接设成16kHz、16bit、单声道WAV格式。
数据增强是低成本提效果的手段。我的数据集里清洗干净后大概1300条,又做了三倍的扩充:
# 在microwakeword的utils.py里有augment辅助函数(部分版本有) # 常见操作:加噪声、变调变速变音量如果没有内置增强,也可以自己写简单脚本:用减速5%~15%、加速5%、音量随机增益0.8~1.2倍、混入低音量背景噪声。这些增强方式对唤醒词识别鲁棒性的提升非常明显。
2.4 训练参数与模型导出
microwakeword的训练入口是train.py,关键参数如下:
python train.py \ --wakeword "你好小机" \ --positive_dir ./data/positive \ --negative_dir ./data/negative \ --output ./models/my_wakeword \ --epochs 40 \ --batch_size 32 \ --learning_rate 0.001模型结构默认是几层DNN加一个分类头,输出两类:目标唤醒词和非唤醒词。训练过程中重点观察两个指标:
- 训练集和验证集准确率接近,说明没有过拟合。
- 如果验证集准确率在95%以下,优先回头补数据而不是调模型。
训练完成后导出TFLite模型,这一步要做int8量化,否则模型体积和推理速度都不理想:
# 量化脚本思路(根据项目内export脚本封装) converter = tf.lite.TFLiteConverter.from_keras_model(model) converter.optimizations = [tf.lite.Optimize.DEFAULT] converter.representative_dataset = generate_representative_dataset() converter.target_spec.supported_ops = [tf.lite.OpsSet.TFLITE_BUILTINS_INT8] tflite_model = converter.convert()量化后模型落在大约60~150KB,具体看网络结构。我最终导出的是90KB的int8模型,在ESP32-S3上单次推理约80ms,符合实时检测需求。
2.5 测试集上的真实表现
我在训练完成后单独留了200条样本做测试,最终结果:
| 指标 | 实测值 | 说明 |
|---|---|---|
| 唤醒率(安静) | 97% | 0.5米距离,正常音量 |
| 唤醒率(噪声环境) | 88% | 电视人声背景下,1米距离 |
| 误唤醒率 | 2次/小时 | 播放新闻联播/播客时的表现 |
| 单次推理延迟 | 80ms | int8量化模型,ESP32-S3实测 |
误唤醒率还差强人意,但如果你的场景对误唤醒特别敏感(比如办公室),建议把负面样本里相近发音的词多加一些,同时在部署时把触发阈值调高一点。这部分后文会专门说。
3. 把唤醒词模型跑进ESP32-S3:TFLite Micro部署与音频链路打通
3.1 创建ESP-IDF工程与基础配置
部署阶段我用的是ESP-IDF v5.x。麦克风方案我选了INMP441,这颗I2S MEMS麦克风很成熟,淘宝几块钱一片,焊到面包板上就能跑,数据是24bit,内部用I2S协议和主控通信。
创建工程:
idf.py create-project offline_voice_assistant cd offline_voice_assistant idf.py menuconfig需要在menuconfig里开的配置项:
Component config → ESP32S3-Specific → Support for external, SPI-connected RAM:打开,因为后面模型TFLite Micro的Tensor Arena要放在PSRAM。Component config → Partition Table:选Single factory app (large)或者自定义一个factory分区。因为模型和代码会占不少空间。Component config → ESP System Settings → Memory protection:保持默认即可,如果遇到PSRAM的cache问题在调。
3.2 I2S麦克风驱动与音频采集
INMP441接线很简单:VDD接3.3V,GND接地,SCK接ESP32-S3的I2S_SCK引脚,SD接I2S_SD引脚,WS接I2S_WS引脚,L/R接地表示使用左声道。
I2S初始化关键代码:
#include "driver/i2s_std.h" #define I2S_NUM I2S_NUM_0 #define I2S_SCK GPIO_NUM_4 #define I2S_WS GPIO_NUM_5 #define I2S_SD GPIO_NUM_6 #define SAMPLE_RATE 16000 #define SAMPLE_BITS 16 #define DMA_BUF_LEN 512 #define DMA_BUF_COUNT 4 i2s_std_config_t i2s_config = { .clk_cfg = { .sample_rate_hz = SAMPLE_RATE, .clk_src = I2S_CLK_SRC_DEFAULT, }, .slot_cfg = { .data_bit_width = I2S_DATA_BIT_WIDTH_16BIT, .slot_bit_width = I2S_SLOT_BIT_WIDTH_16BIT, .slot_mode = I2S_SLOT_MODE_MONO, .slot_mask = I2S_STD_SLOT_LEFT, }, .gpio_cfg = { .mclk = I2S_GPIO_UNUSED, .bclk = I2S_SCK, .ws = I2S_WS, .dout = I2S_SD, .din = I2S_GPIO_UNUSED, }, }; i2s_new_std_rx(&i2s_config, &rx_handle); i2s_channel_enable(rx_handle);注意INMP441输出的是24bit数据,但采样率16kHz时用16bit截断足够唤醒词特征使用,还能省一半DMA带宽。我这里是直接配置成16bit接收,实测没问题。
3.3 音频滑动窗口与VAD策略
唤醒词检测不能等整句话说完才开始,我采用的办法是滑动窗口:
- 窗口长度1秒,每次前进160ms,重叠率84%。
- 每帧音频经过预处理后送入TFLite Micro模型推理。
- 连续2帧检测到唤醒词才确认触发,过滤偶发误判。
VAD(语音活动检测)也很关键,否则设备在静音时会不断跑推理,白白耗电。我直接在ESP32-S3上做了一个简单的能量检测:如果滑动窗口内的RMS能量低于阈值(比如300/32768),直接跳过推理。这个策略很朴实,但在室内场景足够有效,静音时几乎零推理开销。
3.4 TFLite Micro集成与推理核心
TFLite Micro的集成方式我选择了直接使用tflite-micro的ESP-IDF组件版本。在工程根目录:
idf.py add-dependency "espressif/esp-tflite-micro"模型文件放在工程main/目录下,通过EMBED_FILES引入构建系统,运行时直接从flash读取,不占SRAM:
# main/CMakeLists.txt idf_component_register( SRCS "main.c" "audio.c" "wakeword.c" INCLUDE_DIRS "." EMBED_FILES "models/my_wakeword.tflite" )推理代码核心流程:
#include "tensorflow/lite/micro/micro_interpreter.h" #include "tensorflow/lite/micro/micro_mutable_op_resolver.h" // 模型二进制 extern const unsigned char my_wakeword_tflite[] asm("_binary_my_wakeword_tflite_start"); extern const unsigned char my_wakeword_tflite_end[] asm("_binary_my_wakeword_tflite_end"); static tflite::MicroInterpreter* interpreter = nullptr; static uint8_t* input_buffer = nullptr; static uint8_t* output_buffer = nullptr; // Tensor Arena放在PSRAM,实测128KB足够这个模型 static uint8_t tensor_arena[128 * 1024] __attribute__((section(".ext_ram.bss"))); void wakeword_init(void) { const tflite::Model* model = tflite::GetModel(my_wakeword_tflite); static tflite::MicroMutableOpResolver<10> resolver; resolver.AddFullyConnected(); resolver.AddSoftmax(); resolver.AddReshape(); resolver.AddQuantize(); resolver.AddDequantize(); static tflite::MicroInterpreter static_interpreter(model, resolver, tensor_arena, sizeof(tensor_arena)); interpreter = &static_interpreter; interpreter->AllocateTensors(); input_buffer = interpreter->input(0)->data.uint8; output_buffer = interpreter->output(0)->data.uint8; }推理前的特征提取(MFCC)步骤,我是按照microwakeword训练时用的参数在C代码里复刻的,默认帧长25ms,帧移10ms,MFCC维度取13阶(部分做了一阶差分)。ESP32-S3上计算约耗时10~20ms,在可接受范围内。
3.5 唤醒触发后的响应逻辑
唤醒成功后,ESP32-S3进入命令收听状态。命令识别在这里有两种做法:
把“开灯”“关灯”“查天气”等固定命令做成另一个分类模型,在ESP32-S3本地跑,适合简单控制场景。
直接把1~2秒音频传给局域网网关,在网关上跑Whisper或更轻量的STT模型做识别,再交给LLM理解。
我做的版本是两者结合:本地命令分类模型覆盖高频固定指令,低频自由对话则走网关。这样唤醒词和常用指令零延迟响应,只有复杂查询才需要大模型介入,体验和云方案已经很接近。
4. 本地大模型该跑在哪:ESP32-S3直推与局域网网关推理两条路线
4.1 ESP32-S3上能直接跑多大的“模型”
很多新手会天真地问“ESP32-S3能不能跑DeepSeek”。答案是物理上不可能:大语言模型参数动辄数亿,即使极致量化也需要至少几百MB内存,且推理速度无法接受。ESP32-S3的512KB SRAM连一个完整的1B模型参数都放不下,16MB PSARM虽然容量够了,但带宽和算力撑不住逐Token生成。
那“本地大模型推理”在ESP32-S3上到底能做到什么程度?可以跑一到几十MB的超轻量神经网络模型,比如:
- 命令词分类模型(几KB到几百KB):开灯/关灯/播放音乐/暂停。
- 意图识别+槽位提取模型(几百KB):识别“把客厅灯调到最亮”的目标和动作。
- 小型语音分类模型(几十KB):识别音乐声、婴儿哭声等特殊音频事件。
这些本质上都是“小而专”的模型,不是通用对话模型。我不建议硬塞通用LLM到ESP32-S3上,工程风险大且体验糟糕。
4.2 更务实的路线:局域网网关+Ollama
把大模型放到局域网内的PC、树莓派或NAS上,ESP32-S3通过WiFi调用,这是目前体验和隐私兼顾的最优解。
网关侧我用的是Ollama,因为它部署简单,API接口干净,模型管理方便。步骤:
# 在网关设备上安装Ollama(Linux示例) curl -fsSL https://ollama.com/install.sh | sh # 拉取适合CPU/轻量GPU推理的小模型 ollama pull qwen2.5:1.5b # 或者更轻量的 ollama pull tinyllama:1.1b # 如果想要思维链能力 ollama pull deepseek-r1:1.5bOllama默认监听网络的方式是绑定127.0.0.1,要让局域网内的ESP32-S3访问,需要配置环境变量再重启服务:
# Systemd服务的话,编辑 /etc/systemd/system/ollama.service # 或者在启动时设置 OLLAMA_HOST=0.0.0.0:11434 ollama serve注意局域网里做推理服务,最好在路由器里把这个IP锁定,或者设置防火墙只允许特定设备访问。我个人建议不要直接暴露到公网,哪怕你觉得自己家网安全。局域网内加密需求不强,但访问控制还是要的。
4.3 模型选型与量化策略
网关跑大模型,不是模型越大越好。我在PC上做了对比:
| 模型 | 量化等级 | 模型大小 | CPU推理速度(8线程) | 体验评价 |
|---|---|---|---|---|
| tinyllama:1.1b | Q4_K_M | 约0.8GB | 约35 token/s | 快但逻辑有限 |
| qwen2.5:1.5b | Q4_K_M | 约1.1GB | 约22 token/s | 中文好,逻辑尚可 |
| deepseek-r1:1.5b | Q4_K_M | 约1.1GB | 约18 token/s | 有推理链,但输出慢 |
| qwen2.5:7b | Q4_K_M | 约4.7GB | 约6 token/s | 要占用大量内存,CPU慢 |
对语音助手场景,我实测下来qwen2.5:1.5b是甜点选项:中文能力强、响应速度合话、对CPU要求不高。7b模型虽然理解力更强,但响应延迟拉长到3~5秒,语音交互会显得“迟钝”。
如果网关是树莓派5这类ARM设备,建议选Qwen2.5-0.5B或TinyLlama,量化用Q4_K_M即可。树莓派跑1.5b会偏慢,实测约8~10 token/s,勉强可用。
4.4 ESP32-S3侧调用Ollama API的实现
ESP32-S3侧通过HTTP请求调用Ollama的生成接口:
// 使用esp_http_client esp_http_client_config_t config = { .host = "192.168.1.100", // 网关局域网IP .port = 11434, .path = "/api/generate", .method = HTTP_METHOD_POST, }; esp_http_client_handle_t client = esp_http_client_init(&config); char post_data[512]; snprintf(post_data, sizeof(post_data), "{\"model\":\"qwen2.5:1.5b\",\"prompt\":\"%s\",\"stream\":false}", user_text); esp_http_client_set_post_field(client, post_data, strlen(post_data)); esp_http_client_perform(client);注意几个工程细节:
- 超时控制:大模型生成可能要几秒,HTTP客户端要设置足够的读取超时(我设的是10秒),否则读一半就EOF。
- 内存:响应JSON可能比较大,不能全量缓存在ESP32-S3里。我用的是数据回调函数逐块解析,只要
response字段里的文本,边收边拷到外部PSRAM的缓冲区。 - 断线降级:如果网关没在线,HTTP请求会失败。这时候ESP32-S3应该播放一段“网络未连接”的本地提示音,而不是傻等。
这个方案跑通之后,体验和云语音助手已经非常接近了,更重要的是所有请求都停留在家庭局域网内,隐私边界清晰可控。
5. 全链路调通的实测数据与四大典型坑
5.1 实测性能数据
我最终跑通的版本,核心性能数据如下(ESP32-S3 @ 240MHz,INMP441麦克风,网关为八线程PC跑qwen2.5:1.5b):
| 指标 | 实测值 |
|---|---|
| 静音待机功耗 | 约65mA |
| 唤醒词检测占用RAM | 约170KB(含Tensor Arena 128KB) |
| 唤醒词检测单次推理耗时 | 80ms |
| 语音活动检测到唤醒确认耗时 | 约450ms |
| 本地命令分类响应延迟 | 约350ms |
| 局域网Ollama端到端响应 | 2.5~4.5秒(取决于问题长度) |
| 断网/网关离线降级提示 | 约200ms触发本地提示音 |
对离线语音助手来说,这个表现已经算舒服。唤醒后350ms内能响应本地命令,复杂问题等几秒也符合语音交互的心里预期。
5.2 坑一:PSRAM带宽导致推理变慢
第一版我把Tensor Arena放进外部PSRAM,结果唤醒词推理一次要跑到350ms,比SRAM慢了四倍多。原因不难猜到:PSRAM带宽远低于片内SRAM,且PSRAM是挂在cache上的,如果访问不连续,性能损耗更明显。
解决思路是分层放置内存:
- 模型参数(权重)放在Flash只读段,推理时通过cache读取,不需要占用SRAM。
- Tensor Arena优先放内部SRAM,我的模型配128KB Arena可以在内部SRAM放下。
- 只有音频环形缓冲这种大块但低实时要求的数据放PSRAM。
调整后推理时间从350ms降回80ms。如果模型确实大到放不进SRAM,也要尽量让热点数据(输入输出张量)留在SRAM,把非热点放PSRAM。
5.3 坑二:TFLite Micro算子不兼容
我第一次训练唤醒词模型时用了带LSTM的RNN结构,结果在TFLite Micro上跑了半天跑不通。MicroMutableOpResolver支持的算子有限,LSTM虽然在TFLite里有算子支持,但在Micro版本的算子集里往往缺失或不稳定。
排查方法很简单:
- 把模型结构限制在全连接层(FullyConnected)、卷积(Conv2D)、池化(MaxPool2D)、Softmax这些基础算子上。
- 如果一定要用序列建模,用简单的1D卷积叠加感受野,或者用两帧拼接代替时序建模。
我把模型结构换成两层卷积+两层全连接后,完美兼容,效果也没有明显下降。结论是:MCU上跑模型,结构越朴素越踏实,不要追求花哨。
5.4 坑三:麦克风底噪与供电干扰导致误唤醒率飙升
有一段时间怎么调阈值都压不住误唤醒,后来发现是电源问题。ESP32-S3在WiFi发射时射频频段会有瞬间大电流,如果和模拟麦克风共用一条LDO线路,电源噪声会直接串进I2S采样数据里。
排查和修复过程:
- 供电路径:USB 5V → 3.3V LDO(AMS1117)→ ESP32-S3开发板与INMP441。
- 问题表现:WiFi发包瞬间,I2S采样值出现周期性尖峰,唤醒模型把这些尖峰当成语音片段。
- 修复方案:给麦克风独立加一颗低噪声LDO(RT9013),并在INMP441的VDD引脚就近加一个10uF+0.1uF退耦电容。改动后误唤醒率从每小时十几次降到每小时两次以内。
这个坑提醒我:在音频项目里,模拟链路的供电干净程度往往比算法本身更影响最终体验。
5.5 坑四:阈值不是越高越安全
唤醒词检测模型的输出是一个概率值,microwakeword会有一个默认触发阈值,通常设在0.5。部署时如果觉得误唤醒多,直觉是往高了调。我试过调到0.9,误唤醒确实少了,但唤醒率直线掉到70%,屋里喊破嗓子都叫不醒。
这里的关键在于:概率输出是连续值,阈值影响的是查准率和查全率的平衡点,不存在一个万能最优值。我最后用的是“双阈值+时间过滤”策略:
- 触发阈值0.6,低于这个值直接忽略。
- 连续2个滑动窗口都超过阈值才触发,规避单帧偶发高分。
- 如果超过0.8,则单帧即可触发(应对用户喊得很急的场景)。
效果是在不牺牲唤醒率的前提下,把误唤醒降到了可以接受的水平。你也可以根据自己的使用环境调整这两个值,不必照搬。
最后分享一个小技巧:自定义唤醒词的数据集不用侥一次就定型。跑一段时间后,把你实际使用中误唤醒的音频片段收集起来(ESP32-S3可以回传最近10秒音频到网关做记录),定期补充进负面样本重新训练。我第二轮训练加完这些“真实场景毒样本”后,误唤醒率又降了一半,这个习惯建议保留。