此博客为一篇针对初学者的详细教程,涵盖小智 AI 机器人的原理、硬件准备、软件环境搭建、代码实现、云端部署以及优化扩展。文章结合了现有的网络资源,取长补短,确保内容易于理解和操作。
简介: 本教程将指导初学者使用 ESP32 微控制器开发一个简单的语音对话机器人“小智”。我们将介绍所需的基础原理、硬件准备、软件环境搭建,以及如何编写代码实现语音唤醒和与云端大模型的对接。通过本教程,即使没有深厚的 AI 或嵌入式经验,也可以一步步制作出一个能听懂唤醒词并与人对话的简易 AI 机器人。本教程提供详细的操作步骤、代码示例和图示,帮助您轻松上手。
1. 基础原理
ESP32 架构及其在 AI 领域的应用: ESP32 是一款集成 Wi-Fi 和蓝牙的双核微控制器,具有较高的主频和丰富的外设接口,适合物联网和嵌入式 AI 应用。特别是新版的 ESP32-S3 芯片,不仅运行频率高达 240MHz,还内置了向量加速指令(有时称为“AI 指令”)并支持高速 PSRAM,从而可以在一定程度上加速神经网络推理 (以乐鑫语音开发框架为例,系统了解嵌入式设备的语音唤醒和语音识别-RoboticsCV)。在 AI 领域,ESP32 常用于边缘设备,执行一些轻量级的本地 AI 任务(如语音关键词检测、简单的图像识别等),或充当连接云端 AI 服务的桥梁。由于资源有限,ESP32 无法独立运行大型深度学习模型,但它可以负责前端的数据采集和初步处理(如音频处理),然后将数据传输给云端或本地服务器上的强大 AI 模型进行复杂计算,再将结果返回设备。这样的架构充分利用了 ESP32 的实时控制能力和云端大模型的强大推理能力。
语音唤醒模块(ESP-SR)的工作原理: 乐鑫官方提供了 ESP-SR (Speech Recognition)语音识别框架,包含唤醒词引擎(WakeNet)、命令词识别(MultiNet)等组件 (以乐鑫语音开发框架为例,系统了解嵌入式设备的语音唤醒和语音识别-RoboticsCV)。其中唤醒词功能用于在设备待机时持续监听音频流,当检测到特定的唤醒词时触发设备进入对话/识别状态。例如,我们可以将“小智小智”设定为唤醒词。当 ESP32 运行 WakeNet 模型时,它会不断从麦克风录制音频并计算梅尔频谱倒谱系数(MFCC)等特征,然后通过一个针对 ESP32-S3 优化的神经网络算法对特征进行分类 (esp-box/docs/technical_architecture_cn.md at master · espressif/esp-box · GitHub)。一旦检测到训练好的关键词序列,WakeNet 就输出唤醒信号,唤醒设备进入语音交互状态 (esp-box/docs/technical_architecture_cn.md at master · espressif/esp-box · GitHub)。这种本地唤醒机制即使在有环境噪声的情况下也能保持较高的准确率(官方数据显示在噪声环境下识别率不低于 80% (esp-box/docs/technical_architecture_cn.md at master · espressif/esp-box · GitHub))。ESP-SR 默认提供了一些开箱即用的唤醒词模型(如 “Hi, ESP” 或 “Hi, 乐鑫”),开发者也可以定制自己的唤醒词模型 (esp-box/docs/technical_architecture_cn.md at master · espressif/esp-box · GitHub)。工作流程是:麦克风采集到的模拟音频经前端处理(降噪、增益等),送入 WakeNet 模型进行关键词检测。如果未检测到唤醒词,设备保持低功耗待机;一旦检测到唤醒词,设备即进入后续的语音识别或对话流程。同时,为了节省运算资源,ESP32 在唤醒后通常会暂停 WakeNet,以便释放 CPU 处理后续音频;待对话完成后再重新启用 WakeNet 监听下一个唤醒词 (以乐鑫语音开发框架为例,系统了解嵌入式设备的语音唤醒和语音识别-RoboticsCV)。
流式对话的概念及 WebSocket/UDP 传输机制: 所谓流式对话,是指机器人在唤醒后能够实时地接收和发送数据,与用户进行连续的对话交流,而非一次性等待用户说完整句子再回复。要实现流式对话,ESP32 需要将用户的语音数据边录制边发送到云端的语音识别/大模型服务,并且及时接收对方返回的回复数据。这通常涉及到稳定高效的网络传输机制。常用的方式有两种:
-
WebSocket 通信: WebSocket 是基于 TCP 的全双工长连接协议,非常适合实时数据交换。一旦建立连接,服务器和客户端(ESP32)都可以随时发送数据而不需要重复握手。在本项目中,可在 ESP32 启动时就建立一个通向服务器的 WebSocket长连接 (Building a fully local LLM voice assistant to control my smart home | Hacker News)。ESP32 检测到唤醒词后,立即通过该 WebSocket 流式发送音频数据到服务器 (Building a fully local LLM voice assistant to control my smart home | Hacker News)。服务器一边接收音频流一边进行语音识别,并将部分结果或最终结果通过同一连接发回给 ESP32。得益于 WebSocket 保持的长连接,数据可以持续、快速地往返,实现即时的对话体验。例如,有开发者采用 ESP32 + Node.js 服务器架构,通过 WebSocket传输音频,实现实时的语音助手 (I created a Realtime Voice Assistant for my ESP-32, here is my journey - Part 2 : Node, OpenAI, Langchain - DEV Community)。
-
UDP 传输: UDP 是基于数据报的传输协议,开销小、延迟低,但不保证可靠送达。在一些对实时性要求极高且可以容忍少量数据丢失的场景,可以考虑使用 UDP 将音频帧连续地发送到服务器。不过 UDP 没有内建重发、排序机制,需要应用层自行处理丢包重传或顺序问题。因此,对于初学者项目来说,UDP 方案实现难度略高,而且在局域网环境下 WebSocket 已经可以提供足够低的延迟和可靠性。所以通常推荐使用 WebSocket 实现流式语音对话,在开发和调试阶段简便可靠 (PipeCat - 打造实时语音 AI 应用的开源架构方案 - 53AI-AI生产力的卓越领导者(大模型知识库|大模型训练|智能体开发))。待项目成熟后,再根据需要考虑是否切换更底层的传输方案(如 WebRTC 等高级方案 (PipeCat - 打造实时语音 AI 应用的开源架构方案 - 53AI-AI生产力的卓越领导者(大模型知识库|大模型训练|智能体开发)))。
总结来说,本项目将利用 ESP32-S3 的本地语音唤醒能力,让设备在检测到**“小智”唤醒词后,开始录音并流式**发送音频到云端,通过 WebSocket 与服务器上的大语言模型保持对话数据的实时交互。当云端返回文本应答后,ESP32 可将其显示在屏幕上(或语音播报),从而完成一次人机对话循环。
2. 硬件准备
要制作一个语音对话机器人,我们需要准备以下硬件组件,并确保它们兼容且正确连接:
-
ESP32-S3 开发板(带有 PSRAM) – 核心控制器,大脑所在。建议选择 ESP32-S3 系列开发板,因其具备AI加速指令集和高速PSRAM,可支持语音唤醒等 AI 功能 (以乐鑫语音开发框架为例,系统了解嵌入式设备的语音唤醒和语音识别-RoboticsCV)。常见选项包括官方的 ESP32-S3-DevKitC-1(WROVER模块带8MB PSRAM)、或多合一的语音开发板如 ESP32-S3-Korvo 系列和 ESP32-S3-BOX 等 (以乐鑫语音开发框架为例,系统了解嵌入式设备的语音唤醒和语音识别-RoboticsCV)。这些板子自带麦克风阵列、音频编解码等硬件,方便语音应用开发。
-
麦克风 – 用于采集用户语音。强烈推荐使用 I2S 接口的数字麦克风(MEMS麦克风),例如 INMP441 模块。数字麦克风可以直接将音频数据以数字信号传给 ESP32,抗干扰能力强,音质更好 (Building a fully local LLM voice assistant to control my smart home | Hacker News)。避免使用模拟麦克风加ADC的方法,因为ESP32自带的ADC精度有限且噪声较大,模拟方案音质往往不佳 (Building a fully local LLM voice assistant to control my smart home | Hacker News)。常用的 I2S 麦克风模块有 INMP441、ICS-43434 等,它们需要连接 ESP32 的 I2S接口引脚(WS, SCK, SD 等)以及电源。
-
扬声器和音频放大器(可选) – 用于语音播报机器人回复。如果希望机器人能“说话”,需要一个小型扬声器(如 4Ω 3W)的输出方案。ESP32 可以通过内置 DAC 输出模拟音频,但驱动扬声器需要功率放大器。常用方案是使用 I2S 数字功放模块(如 MAX98357A)将 ESP32 的I2S音频输出转换为扬声器驱动信号。该模块接收ESP32的I2S数据和时钟,输出功率音频信号,直接驱动小喇叭。如果暂时不需要语音输出,也可以先不接扬声器,后续通过串口日志或屏幕查看机器人回复的文本。
-
显示屏(OLED/LCD,可选) – 用于显示对话内容或机器人表情。一个小型显示屏可以显著增强人机交互体验。初学者可以选用I2C接口的单色 OLED(如0.96寸 128×64 OLED)或 SPI 接口的彩色 LCD(如1.3寸 240×240 TFT)。这些屏幕可以用来显示用户说的内容和机器人生成的回复文字,或显示一些图标、头像来提示当前状态(正在听/在想/在说等)。显示屏通过 I2C 或 SPI 接口连接到 ESP32 的对应引脚,并需要供电。后续可以使用图形库(如 LVGL)或驱动库来控制显示内容。
-
其他配件: 若使用独立的开发板和模块,还需要若干 面包板和连接线 方便搭建原型、电源线和 USB 数据线等。如果选用的是类似 ESP32-S3-BOX 这类集成度高的设备,许多组件(屏幕、麦克风、扬声器)已经内置,无需额外连接。
(以乐鑫语音开发框架为例,系统了解嵌入式设备的语音唤醒和语音识别-RoboticsCV) 图:ESP32-S3-Korvo-2 音频开发板(乐鑫官方语音开发套件)示例,板载双麦克风、音频编解码芯片、扬声器接口和LCD接口等,方便构建语音交互设备 (以乐鑫语音开发框架为例,系统了解嵌入式设备的语音唤醒和语音识别-RoboticsCV)。
(image) 图:使用 ESP32 开发板+麦克风+扬声器搭建语音助手原型的实际连接示例。ESP32 开发板(右下)通过面包板连接麦克风(中间小圆件)和扬声器(左上黑色喇叭),用于采集和播放语音。
选购建议: 如果希望尽量减少硬件连接的麻烦,可以选择类似 ESP32-S3-BOX 或 ESP32-S3-Korvo 这样的开发套件,它们集成了语音所需的大部分硬件,开箱即可使用唤醒词和语音功能 (esp-box/docs/technical_architecture_cn.md at master · espressif/esp-box · GitHub)。例如,ESP32-S3-BOX 自带触摸屏、双麦克风和扬声器,只需刷入程序即可成为一个简单语音助手。不过这类套件价格相对较高。对于学习和制作原型来说,使用普通的 ESP32-S3 开发板加上外接麦克风和屏幕也是常用方案,性价比更高。在购买麦克风模块时,要确认其引脚定义与ESP32兼容(一般接 3.3V、GND、WS、SCK、SD),并注意麦克风的指向性、灵敏度等参数,以满足您的应用(如近场对话一般使用全指向麦克风,远场则考虑阵列降噪技术)。扬声器方面,小功率扬声器即可满足语音提示功能;若追求更大音量,可考虑使用有源音箱或更高功率的放大器,但要注意供电能力。显示屏选型取决于需求和预算,初期可以使用廉价的0.96寸OLED显示文字,后期可升级为彩屏显示头像或图形界面。
总之,在硬件搭建阶段,请按照各模块的规格说明正确连接电路,并确保供电稳定(ESP32 开发板通常通过 USB 供电,确保电脑USB口或电源适配器有足够的电流供应)。完成硬件连接后,我们就可以着手软件环境的搭建与代码编写了。
3. 软件环境搭建
在开始编程之前,需要搭建好 ESP32 的开发环境,以及准备云端大模型所需的工具库。
安装 ESP-IDF 开发框架: ESP-IDF(Espressif IoT Development Framework)是乐鑫官方的底层开发框架。我们将使用 ESP-IDF 来编写 C/C++ 代码控制 ESP32。请根据您的操作系统安装相应版本的 ESP-IDF。建议使用最新版(如 v5.x),以便支持 ESP32-S3 和 ESP-SR 库。安装方法如下:
- Windows 用户: 可以使用乐鑫提供的安装包,其中集成了 IDF、编译工具链和 Python 环境等。一键安装后,运行 ESP-IDF 提供的命令行快捷方式进入开发环境。
- Linux/Mac 用户: 可通过 git 克隆 ESP-IDF 仓库,然后运行
install.sh
脚本安装所需工具链和Python包。安装完成后,运行export.sh
脚本配置环境变量。
详细步骤可参考乐鑫官方文档,但总的来说,安装完成后在命令行执行 idf.py --version
能看到版本信息即表示环境就绪。接下来创建一个 ESP-IDF 项目或者使用官方示例作为起点。
配置 VSCode/Cursor 开发环境: 为了提高开发效率,我们推荐使用 VSCode 作为主要的代码编辑器,并安装乐鑫官方的 ESP-IDF VSCode 插件。该插件可以方便地菜单配置项目信息、一键编译烧录、监视串口输出等。您可以在 VSCode 的扩展商店搜索“Espressif IDF”并安装,然后按照插件指引配置 ESP-IDF 安装路径和 Python 环境。除此之外,您也可以尝试使用最近流行的 Cursor 代码编辑器。Cursor 本质上是 VSCode 的一个改进版本,内置了 GPT-4、Claude 2 等大型语言模型作为编程助手 (用Cursor 写出第一个程序- 架构师汤师爷 - 博客园)。使用 Cursor,您可以在编写代码时得到 AI 对代码的提示、自动补全和错误检查等智能帮助 (用Cursor 写出第一个程序- 架构师汤师爷 - 博客园)。这对初学者调试语音项目这样的复杂工程非常有益。例如,当您不知道某个 ESP-IDF 函数如何使用时,可以直接在 Cursor 中询问 AI 获取帮助。当然,使用 Cursor 并非必需,如果您习惯纯 VSCode 或其他编辑器也完全可以。
安装所需库和工具: 本项目除了 ESP-IDF 自带的组件外,还可能需要用到一些专门的软件库,特别是在云端服务器部分,以实现语音识别和大语言模型对话功能。在开发电脑或服务器上,建议准备好以下工具:
-
SenseVoice 语音识别模型及 API: 这是阿里巴巴达摩院开源的一个多语言语音理解模型,支持语音转文本(ASR)、语言识别、情感识别等功能 (GitHub - HG-ha/SenseVoice-Api: 阿里SenseVoice的fastpi封装,采用onnx发布,体积更小,附带量化模型,支持GPU。支持从URL文件进行语音识别。)。相较于其他开源方案,SenseVoice 在中文等多语言识别上精度很高,据称训练了超40万小时语音数据,效果优于 OpenAI 的 Whisper 模型 (GitHub - HG-ha/SenseVoice-Api: 阿里SenseVoice的fastpi封装,采用onnx发布,体积更小,附带量化模型,支持GPU。支持从URL文件进行语音识别。)。我们可以使用开源社区提供的 SenseVoice 推理代码或封装好的 API 服务。例如,GitHub 上的项目 SenseVoice-API 提供了基于 ONNXRuntime 的轻量级封装和 FastAPI 接口,方便我们将 SenseVoice 部署成一个本地服务 (GitHub - HG-ha/SenseVoice-Api: 阿里SenseVoice的fastpi封装,采用onnx发布,体积更小,附带量化模型,支持GPU。支持从URL文件进行语音识别。)。请按照该项目说明安装所需依赖(如 Python 及 onnxruntime)并下载模型文件,然后启动服务用于将音频流转成文本。
-
DeepSeek / Qwen 大语言模型: 为了让机器人更智能地对答,我们需要接入一个强大的大语言模型(LLM)。DeepSeek 是国内开源的一个系列大模型,基于强化学习技术训练,性能媲美一些商业模型 (deepseek-ai/DeepSeek-R1-Distill-Qwen-7B · Hugging Face)。他们提供了多种版本,包括体积较小的蒸馏模型(如基于 Qwen-7B 的 DeepSeek-R1 Distill)以及更大的 Qwen-32B/72B 模型 (deepseek-ai/DeepSeek-R1-Distill-Qwen-7B · Hugging Face)。其中 Qwen (通义千问) 是阿里巴巴开源的大模型系列,7B 和 14B 版本已开源,而 72B 参数的模型则在阿里云上提供服务。您可以根据自身算力选择使用哪种模型:如果本地有高性能GPU,可以尝试部署7B左右参数的模型运行;如果没有,则可以考虑通过在线API调用这些模型的推理服务。阿里云提供了 DashScope API 接口来调用通义千问模型,只需注册获取 API Key 即可使用 (GitHub - zengjunc/VocaMirror: 语镜VocaMirror——基于sensevoice、cosyvoice和qwen模型实现与“自身声音”对话)。无论本地部署还是云端API,请先安装好相应的Python库:如 Hugging Face Transformers(用于加载DeepSeek/Qwen模型)或者官方提供的 SDK(如 DashScope SDK)。
-
其他工具: 音频处理库(如
pyaudio
或soundfile
)可能用于服务器录音或读取文件;用于 WebSocket 通信的库(如websockets
for Python 或 Node.js 的 socket 库)如果您计划实现实时流式传输;以及 HTTP 客户端工具(如requests
)用于调用RESTful API 等。如果打算使用 Node.js 作为服务器,也需要准备好 Node.js 环境以及相关依赖(如用于 WebSocket 的ws
库,用于调用AI服务的SDK等)。
配置好以上软件环境后,整个开发流程将会是:使用 VSCode/ESP-IDF 在本地编写并烧录 ESP32 端代码,同时使用 Python/Node 等在电脑上编写服务器端代码并运行 AI 模型服务。接下来,我们就进入代码实现部分,看看如何让各部分协同工作。
4. 代码实现
在这一部分,我们将编写 ESP32 端的主要代码,实现以下功能:唤醒词检测、音频采集并上传、接收并处理云端返回的文本。代码将基于 ESP-IDF 编写,语言为 C/C++。由于完整代码较长,这里拆分讲解关键步骤,并提供简化的代码示例。
4.1 初始化语音唤醒功能
首先,我们需要初始化 ESP-SR 提供的唤醒词引擎,使能麦克风采集和关键词检测。假设我们使用 ESP32-S3 和官方 ESP-SR 库,初始化主要包括:配置 AFE(声学前端)参数、启用 WakeNet 模型、设置唤醒回调等。伪代码示例如下:
#include "esp_sr_iface.h"
#include "esp_sr_models.h"
// 1. 配置声学前端 (AFE) 参数
afe_config_t afe_config = AFE_CONFIG_DEFAULT(); // 默认配置
afe_config.wakenet_init = true; // 启用唤醒词检测
afe_config.voice_communication_init = false; // 不启用通话模式,以免与唤醒冲突 ([以乐鑫语音开发框架为例,系统了解嵌入式设备的语音唤醒和语音识别-RoboticsCV](https://roboticscv.com/%E4%BB%A5%E4%B9%90%E9%91%AB%E8%AF%AD%E9%9F%B3%E5%BC%80%E5%8F%91%E6%A1%86%E6%9E%B6%E4%B8%BA%E4%BE%8B%EF%BC%8C%E7%B3%BB%E7%BB%9F%E4%BA%86%E8%A7%A3%E5%B5%8C%E5%85%A5%E5%BC%8F%E8%AE%BE%E5%A4%87%E7%9A%84/#:~:text=.wakenet_init%20%3D%20true%2C%20,%2F%2F%20%E9%85%8D%E7%BD%AE%E6%98%AF%E5%90%A6%E4%BD%BF%E8%83%BD%E8%AF%AD%E9%9F%B3%E9%80%9A%E8%AF%9D%E4%B8%AD%20AGC))
afe_config.wakenet_model_name = "wn9_xiaozhi"; // 指定唤醒词模型名称(假设我们有"小智"模型)
afe_config.wakenet_mode = DET_MODE_1CH; // 麦克风通道: 单通道模式
// 2. 初始化AFE,并加载唤醒词模型
sr_handle_t sr_handle = sr_handle_init(&afe_config);
if (!sr_handle) {
printf("ESP-SR 初始化失败\n");
return;
}
esp_err_t ret = sr_wakenet_init(sr_handle);
if (ret == ESP_OK) {
printf("唤醒词引擎加载成功\n");
}
// 3. 注册唤醒回调,当检测到唤醒词时执行
sr_set_wakenet_cb(sr_handle, on_wakeup_detected);
上面代码说明:
-
使用
AFE_CONFIG_DEFAULT()
宏获取默认的音频前端配置结构,然后根据需要调整参数。我们启用了wakenet_init
来打开唤醒词检测,而将voice_communication_init
置为 false,因为这两者不能同时为 true (以乐鑫语音开发框架为例,系统了解嵌入式设备的语音唤醒和语音识别-RoboticsCV)(通话模式用于双工通话情景,不在本项目范围)。wakenet_model_name
指定要加载的唤醒词模型的名字(例如这里假设我们有名为 “xiaozhi” 的模型,实际上需要先把模型文件烧录进 SPIFFS 或 Flash)。wakenet_mode
设为单通道检测,如果有双麦克风可以用相应的双通道模式以提升远场性能 (以乐鑫语音开发框架为例,系统了解嵌入式设备的语音唤醒和语音识别-RoboticsCV)。 -
使用
sr_handle_init
初始化语音识别句柄,然后调用sr_wakenet_init
加载唤醒词模型。如果返回 ESP_OK 则表示模型加载成功,可以开始检测。 -
通过
sr_set_wakenet_cb
注册一个回调函数on_wakeup_detected
,当 WakeNet 在音频流中检测到目标关键词时,这个回调就会被调用。在回调函数中我们可以进行接下来的处理(例如开始录音等)。
需要注意提前将所需的唤醒词模型文件添加到工程中(通常 ESP-SR 提供了一些预训练模型,可以在菜单配置中选中,如 “Hi 乐鑫” 等 (esp-box/docs/technical_architecture_cn.md at master · espressif/esp-box · GitHub);如果要自定义唤醒词,则需要使用乐鑫提供的工具训练模型,然后集成)。
4.2 处理语音输入并上传至大模型
当唤醒词被检测到时,ESP32 应该切换到对用户语音指令的录制和传输流程。典型做法是在回调中通知主程序开始录音。可以创建一个 音频采集任务,持续从 I2S 麦克风读取音频数据,直到检测到一段静音(表示用户说话结束)或达到最大录音时长,然后通过网络将音频发送出去。
音频采集: ESP32 IDF 提供 I2S 驱动来读取麦克风数据。麦克风模块通常以固定采样率输出,比如 16kHz 16位音频。我们可以设置 I2S DMA,每次读取一定帧数的数据到内存缓冲区。为了简单,我们假设每次读取 BUFFER_SIZE
大小的数据块,并将其暂存。如果需要判断何时用户说话结束,可以实现一个简单的静音检测(VAD):连续几百毫秒检测音量,如果音量低于阈值则认为用户讲话结束。
数据上传: 这里提供两种策略:
- 流式上传(推荐): 边录音边通过网络发送数据段。利用前文建立的 WebSocket 连接,将每个缓冲区的数据通过
esp_websocket_client_send_bin()
发给服务器。这样服务器可同步识别,不需等全部录完。这种方法实现对话响应更快 (I created a Realtime Voice Assistant for my ESP-32, here is my journey - Part 2 : Node, OpenAI, Langchain - DEV Community)。 - 后端上传(简单): 先本地录完整句话,存成PCM或WAV格式,然后通过一次 HTTP POST 请求将音频文件发送给服务器,等待服务器返回结果。在初期调试时此方法更直观,但交互上会有一点延迟。
我们以流式方式为例,展示录音与发送的伪代码:
// 假设已经建立好 WebSocket 连接,句柄为 ws_client
#define SAMPLE_RATE 16000
#define BYTES_PER_SAMPLE 2 // 16-bit audio
#define BUFFER_SIZE 512 // 每次读取512字节(256个采样)
bool recording = false;
void on_wakeup_detected()
{
printf("唤醒词检测到!开始录音...\n");
recording = true;
// 可选:暂停WakeNet以节省CPU ([以乐鑫语音开发框架为例,系统了解嵌入式设备的语音唤醒和语音识别-RoboticsCV](https://roboticscv.com/%E4%BB%A5%E4%B9%90%E9%91%AB%E8%AF%AD%E9%9F%B3%E5%BC%80%E5%8F%91%E6%A1%86%E6%9E%B6%E4%B8%BA%E4%BE%8B%EF%BC%8C%E7%B3%BB%E7%BB%9F%E4%BA%86%E8%A7%A3%E5%B5%8C%E5%85%A5%E5%BC%8F%E8%AE%BE%E5%A4%87%E7%9A%84/#:~:text=match%20at%20L314%20%E5%BD%93%E7%94%A8%E6%88%B7%E5%9C%A8%E5%94%A4%E9%86%92%E5%90%8E%E9%9C%80%E8%A6%81%E8%BF%9B%E8%A1%8C%E5%85%B6%E4%BB%96%E6%93%8D%E4%BD%9C%EF%BC%8C%E6%AF%94%E5%A6%82%E7%A6%BB%E7%BA%BF%E6%88%96%E5%9C%A8%E7%BA%BF%E8%AF%AD%E9%9F%B3%E8%AF%86%E5%88%AB%EF%BC%8C%E8%BF%99%E6%97%B6%E5%80%99%E5%8F%AF%E4%BB%A5%E6%9A%82%E5%81%9C%20WakeNet,%E5%88%87%E6%8D%A2%E5%88%B0%20%E2%80%9CHi%20Lexin%E2%80%9D%20%E5%94%A4%E9%86%92%E8%AF%8D%E3%80%82))
disable_wakenet(sr_handle);
}
void audio_record_task(void *arg)
{
char mic_buffer[BUFFER_SIZE];
size_t bytes_read = 0;
int silence_count = 0;
const int silence_threshold = 5; // 连续静音块计数阈值
// 连续循环读取麦克风数据
while (true) {
// 阻塞读取一块音频数据
i2s_read(I2S_NUM_0, mic_buffer, BUFFER_SIZE, &bytes_read, portMAX_DELAY);
if (!recording) {
continue; // 未触发录音就丢弃数据
}
// 将读取的数据通过 WebSocket 发出
if (bytes_read > 0) {
esp_websocket_client_send_bin(ws_client, mic_buffer, bytes_read, portMAX_DELAY);
}
// 简单静音检测:如果本次数据全是低幅值(接近静默)
bool is_silence = check_silence(mic_buffer, bytes_read);
if (is_silence) {
silence_count++;
} else {
silence_count = 0;
}
// 如果连续多帧静默,认为讲话结束
if (silence_count > silence_threshold) {
// 发送特殊标志表示音频结束
esp_websocket_client_send_text(ws_client, "<END>", 5, portMAX_DELAY);
recording = false;
enable_wakenet(sr_handle); // 重新启用唤醒监听 ([以乐鑫语音开发框架为例,系统了解嵌入式设备的语音唤醒和语音识别-RoboticsCV](https://roboticscv.com/%E4%BB%A5%E4%B9%90%E9%91%AB%E8%AF%AD%E9%9F%B3%E5%BC%80%E5%8F%91%E6%A1%86%E6%9E%B6%E4%B8%BA%E4%BE%8B%EF%BC%8C%E7%B3%BB%E7%BB%9F%E4%BA%86%E8%A7%A3%E5%B5%8C%E5%85%A5%E5%BC%8F%E8%AE%BE%E5%A4%87%E7%9A%84/#:~:text=match%20at%20L314%20%E5%BD%93%E7%94%A8%E6%88%B7%E5%9C%A8%E5%94%A4%E9%86%92%E5%90%8E%E9%9C%80%E8%A6%81%E8%BF%9B%E8%A1%8C%E5%85%B6%E4%BB%96%E6%93%8D%E4%BD%9C%EF%BC%8C%E6%AF%94%E5%A6%82%E7%A6%BB%E7%BA%BF%E6%88%96%E5%9C%A8%E7%BA%BF%E8%AF%AD%E9%9F%B3%E8%AF%86%E5%88%AB%EF%BC%8C%E8%BF%99%E6%97%B6%E5%80%99%E5%8F%AF%E4%BB%A5%E6%9A%82%E5%81%9C%20WakeNet,%E5%88%87%E6%8D%A2%E5%88%B0%20%E2%80%9CHi%20Lexin%E2%80%9D%20%E5%94%A4%E9%86%92%E8%AF%8D%E3%80%82))
printf("录音结束,等待回复...\n");
}
}
}
上述代码中,audio_record_task
会一直运行:平时 recording
标志为 false 时读入的数据直接丢弃(或可缓冲覆盖);当唤醒发生时回调将 recording
置为 true,于是任务开始真正处理音频数据。
通过 i2s_read
从 I2S采集 BUFFER_SIZE 字节音频,每采集到一块,就用 esp_websocket_client_send_bin
发送二进制数据帧给服务器。这里假定已在别处初始化了 WebSocket 客户端 ws_client
并连接到服务器地址(例如 ws://<服务器IP>:<端口>
)。ESP-IDF 的 WebSocket 客户端是线程安全的,可直接从任务中调用发送函数 (ESP WebSocket Client - - — ESP-IDF Programming Guide v4.1 ...)。
check_silence
是用户定义的函数,用于检查缓冲中是否绝大部分样本都接近零(可设定一个幅值阈值)。如果连续检测到若干帧静音,我们就认为用户停止了讲话。这时通过发送一条特殊的文本帧 "" 通知服务器音频流结束,然后把 recording
置回 false,表示本轮录音完成。还调用 enable_wakenet
恢复唤醒词检测,让设备准备接受下一次唤醒 (以乐鑫语音开发框架为例,系统了解嵌入式设备的语音唤醒和语音识别-RoboticsCV)。
注意: 上述实现相对简单,实际应用中可能需要更健壮的 VAD 算法来准确判断讲话结束。另外,为了防止用户长时间讲话导致数据过多,可以设置最大录制时长,到点后强制结束。本示例省略了错误处理和资源管理(例如在结束录音后可能需要关闭I2S或重置缓冲等)。
4.3 与云端模型的交互并返回文本
当 ESP32 将语音数据发送到服务器后,接下来就轮到服务器端进行处理:把音频转成文本,交给大模型生成回复,再将回复发送回来。因此 ESP32 需要能接收服务器的响应数据。在流式方案中,服务器可能通过 WebSocket 发回文本回复;在简单HTTP方案中,则是 ESP32 主动请求获取结果。我们这里以 WebSocket 双工通信为例说明。
ESP-IDF 的 WebSocket 客户端支持接收服务器推送的消息。可以注册回调或在循环中使用 esp_websocket_client_receive
来获取数据。在本例中,我们假定服务器最终发送回来的内容是纯文本的对话回复(或者为了简便,也可以让服务器直接返回语音合成后的 MP3 URL 等,但初期我们用文本即可)。ESP32 收到文本后,需要在本地加以处理,如在串口打印或者显示在屏幕上,或者通过TTS播放出来。
以下是接收回复并处理的示例代码:
// 简单起见,在主任务里轮询接收服务器消息
char recv_buf[256];
for (;;) {
int32_t rlen = esp_websocket_client_receive(ws_client, recv_buf, sizeof(recv_buf) - 1, portMAX_DELAY);
if (rlen > 0) {
recv_buf[rlen] = 0; // 以NULL结尾形成字符串
if (strcmp(recv_buf, "<ACK>") == 0) {
// 服务器确认收到音频的应答,可忽略或用于统计
continue;
}
printf("云端回复: %s\n", recv_buf);
// 将回复显示到OLED屏幕(伪函数)
display_text_on_screen(recv_buf);
// 可选:调用本地TTS播放语音(此处省略)
}
}
这个循环会一直等待 WebSocket 的消息。esp_websocket_client_receive
阻塞等待服务器发送的数据。收到后,我们检查内容:如果是预定义的控制消息(例如这里假设服务器可能发 "" 表示收到结束标志),那么不做处理;否则将其作为正常回复文本处理。通过串口打印出回复,并调用一个假定的 display_text_on_screen
函数在OLED上显示该文字。实际实现中,您需要用相应的屏幕驱动 API 将文字绘制出来(如使用 SSD1306 驱动或 LVGL 库)。
如果您希望机器人语音回答,可以在ESP32上集成一个简单的本地TTS(ESP-SR 框架本身带有简易的中文语音合成功能 (以乐鑫语音开发框架为例,系统了解嵌入式设备的语音唤醒和语音识别-RoboticsCV))或让服务器直接返回合成后的语音(例如 MP3 文件 URL 或音频流)。初始阶段为了专注核心,我们先不处理TTS。
至此,ESP32 端的主要代码逻辑已经完整:等待唤醒→录音上传→接收回复→输出结果→循环等待下一次唤醒。接下来,我们看看云端服务器该如何部署大模型来配合工作。
5. 云端部署
云端(或本地PC服务器)在整个系统中承担了重型AI计算的任务,包括语音识别和对话生成。根据硬件条件和需求,您可以选择自建服务器或使用云服务。本节将介绍如何连接大语言模型,以及如何搭建服务器端程序来实现智能对话。
连接大语言模型 (DeepSeek/Qwen 等): 大语言模型的选择取决于您的资源。对于中文对话,比较好的开源选择有阿里巴巴的通义千问系列(Qwen)和衍生的DeepSeek模型 (deepseek-ai/DeepSeek-R1-Distill-Qwen-7B · Hugging Face)。如果有足够算力,您可以在服务器上直接运行开源模型实例。例如,使用 Hugging Face Transformers 加载 DeepSeek-R1 Distill (基于 Qwen-7B) 模型,然后在本地执行推理。伪代码如下:
from transformers import AutoModelForCausalLM, AutoTokenizer
tokenizer = AutoTokenizer.from_pretrained("deepseek-ai/DeepSeek-R1-Distill-Qwen-7B")
model = AutoModelForCausalLM.from_pretrained("deepseek-ai/DeepSeek-R1-Distill-Qwen-7B", device_map="auto")
# 假设已经得到用户输入文本 user_text
input_ids = tokenizer.encode(user_text, return_tensors="pt")
output_ids = model.generate(input_ids, max_length=200)
response_text = tokenizer.decode(output_ids[0], skip_special_tokens=True)
上述代码会生成 response_text
作为回复。但需要注意,这对7B模型至少需要十几GB显存或使用CPU耗时较长,因此如果硬件有限,不妨考虑调用在线API服务。
另一种便利的方法是使用模型提供方的云API:例如阿里云的通义千问提供了 DashScope API,可以直接通过 HTTP 请求获得回复,无需本地运行模型 (GitHub - zengjunc/VocaMirror: 语镜VocaMirror——基于sensevoice、cosyvoice和qwen模型实现与“自身声音”对话)。您只需在阿里云申请 API Key,然后按照文档构造请求(通常是一个包含会话历史的 JSON)。以下是使用 DashScope SDK 的伪代码:
from dashscope import Completion
Completion.api_key = "<你的通义API Key>"
response = Completion.create(prompt=user_text, model="qwen-plus")
answer_text = response.completions[0].text
利用在线大模型服务的优势是省去了部署模型的麻烦,而且一般响应速度快、效果好。不过要考虑到接口的使用费用或调用频率限制。
无论采用哪种方式获取对话回复,都应该注意实现上下文管理:即让模型记住之前的对话历史,从而实现连贯的多轮对话。这可以通过每次调用API时在 prompt 中附带前文对话,或使用模型的对话模式接口来实现。在DeepSeek/通义这样的模型中,一般支持多轮对话,只需将历史问答拼接即可(或在 API 参数中传递 conversation_id 等)。
调用语音识别 API: 在让大模型理解用户意图之前,我们需要将音频转成文字。这里我们使用之前准备好的 SenseVoice 模型服务。假设您已在服务器上运行了 SenseVoice 的 API(例如本地 FastAPI 服务,端点URL为 /asr
),ESP32 发送的音频通过 WebSocket 或 HTTP 转发给该服务进行识别。
如果采用我们上节的WebSocket方案,服务器可以在收到 ESP32 的音频帧后,将其拼接或直接送入语音识别引擎进行流式识别。阿里的 SenseVoice 支持长语音转写和多语言,可以逐步输出转写结果 (GitHub - HG-ha/SenseVoice-Api: 阿里SenseVoice的fastpi封装,采用onnx发布,体积更小,附带量化模型,支持GPU。支持从URL文件进行语音识别。)。简化流程如下:
- 服务器收到音频数据帧,缓存至音频缓冲区。
- 如果检测到结束标志"",则将缓冲区音频送入 ASR 引擎。
- 获取到完整的转写文本
user_text
。 - 调用大模型生成
answer_text
。 - 通过 WebSocket 将
answer_text
发回 ESP32。
在实现中,可将步骤1-4集成在服务器的一个事件循环或多线程处理里。您也可以选择不等音频结束就边识别边生成,但那会复杂许多(需要分段、多段上下文管理,这里不深入)。
服务器端搭建 AI 服务: 您可以用任意擅长的语言编写服务器。例如,用 Python 的 websockets
库搭建 WebSocket 服务,收到二进制音频后调用 Python 接口处理。这需要加载 SenseVoice 模型和大模型,可能耗时,建议在服务器启动时就加载好模型到内存,避免每次请求重复初始化。以下是服务器伪代码框架(Python):
import asyncio, websockets
from sensevoice import ASRModel # 假设封装好的识别类
from your_llm_api import LLMAPI # 假设封装的大模型调用
asr_model = ASRModel.load("SenseVoiceSmall") # 加载识别模型
llm_api = LLMAPI(api_key="...") # 初始化大模型API
async def handle_client(websocket, path):
print("客户端已连接")
audio_bytes = b""
async for message in websocket:
if isinstance(message, bytes):
# 累积音频数据
audio_bytes += message
elif message == "<END>":
# 收到音频结束
user_text = asr_model.transcribe(audio_bytes) # 语音转文本
print("识别结果:", user_text)
answer = llm_api.ask(user_text) # 调用大模型获取回复
await websocket.send(answer) # 发送回复给ESP32
audio_bytes = b"" # 清空缓冲,准备下一次对话
else:
# 处理其他控制消息,例如心跳/确认
await websocket.send("<ACK>")
这个简化的服务器逻辑中,我们使用 async for
来持续监听来自某个客户端(ESP32)的消息。如果收到的是二进制数据,则累加到 audio_bytes
缓冲。如果收到文本 "" 则表示一段语音结束,遂调用 ASR 将整个音频转为文字,然后把文字传给大模型接口获得回复,最后通过 websocket.send
将回复发回客户端。然后清空缓冲等待下一轮交流。这里还示范了收/发 "" 之类的控制信息的处理。实际应用中,应考虑异常处理(如网络中断)、识别超时等情况。
通过上述服务器的配合,我们的 ESP32 语音机器人基本功能就完成了:当你对着麦克风说“小智小智”(假定这是唤醒词),ESP32 检测到后开始录音,并把你的话通过网络发送出去;服务器将你的语音识别为文字,再由大模型生成回答“好的,我在呢”(示例),服务器将回答发送回 ESP32;ESP32 接收到后在屏幕上显示出来,并通过扬声器播报出来。整个过程接近实时,并且可以多次轮询,实现连续对话 (Building a fully local LLM voice assistant to control my smart home | Hacker News)。
6. 优化与扩展
在基本功能实现后,我们可以对“小智”机器人进行多方面的优化和扩展,以提升体验和性能:
(1) 提升本地计算能力: 虽然 ESP32 性能有限,但我们可以通过一些技巧优化运行效率。例如,使用缓存机制:针对用户经常询问的问题,可以在ESP32端缓存上一次的回答,若短时间内再次询问相同问题,可以直接回复缓存结果而不必每次都请求云端,从而降低延迟和流量。另外,尽量利用好 ESP32-S3 的硬件加速功能,如采用定点运算或使用 ESP-DSP 库优化音频处理。对于云端的大模型,可以考虑模型蒸馏或量化来加快推理速度。例如将原本大的模型压缩为更小的参数,这也是 DeepSeek 提供蒸馏模型的思路 (deepseek-ai/DeepSeek-R1-Distill-Qwen-7B · Hugging Face)。量化后的模型(如 INT8/INT4)在GPU甚至CPU上的推理速度会有明显提升,使对话响应更快。同时可在服务器端对相似的问答进行缓存,减少重复计算。
(2) 增强离线功能: 目前我们的机器人主要依赖云端AI,但是我们可以赋予它一定的脱机能力。首先,ESP32 上的 MultiNet 命令词识别模块可以识别一些常用指令词离线运行 (以乐鑫语音开发框架为例,系统了解嵌入式设备的语音唤醒和语音识别-RoboticsCV)。我们可以预先定义一组本地指令(例如:“播放音乐”、“停止”、“音量大一点”等)并训练相应模型烧录进ESP32。这使得即使断网,机器人也能执行简单指令控制。此外,可以加入简单的本地问答库或规则,例如对于固定的问题(天气、时间)在ESP32存储预设答案,这样在无网络时也能回答部分问题。更高级的,可以在ESP32上运行一个超小型的语言模型(例如基于 Edge Impulse 或 GPT2-tiny 等),用于应对非常简短的对话。不过由于设备限制,这类模型智能性有限,主要作为备份。在应用中,可以设置策略:优先调用云端AI,若不可用则退而求其次使用本地能力应答。
(3) 显示屏交互扩展: 为了让“小智”更生动,我们可以充分利用 OLED/LCD 显示屏来丰富人机界面。例如,在屏幕上显示对话文字的同时,可以加入表情或头像来指示机器人状态:听的时候显示“耳朵”图标,思考时显示“加载中”动画,说话时显示“嘴巴”或表情变化等。如果使用的是彩色屏幕,可以绘制一个卡通头像,小智在不同状态下的表情(睁眼/闭眼/说话)变化,从而使机器人更具亲和力。技术实现上,可以预先加载几张位图或者使用简单的绘图指令组合出表情。在 ESP-IDF 中可以结合 LVGL 图形库实现精美的 UI 界面,包括文本、图像和动画效果。触摸屏设备还可以扩展触控功能,例如加一个“对话历史”查看或功能菜单。除了屏幕,您还可以增添LED指示灯(比如 RGB LED 显示不同颜色代表正在录音或思考)或者蜂鸣器音效提示用户进行交互提示。
(4) 语音合成输出: 虽然本教程侧重文字输出,但让机器人“开口说话”无疑会提供更直观的互动。在 ESP32 上实现文本转语音(TTS)有几种方案:可以利用 ESP-SR 自带的简易 TTS 模块生成中文语音 (以乐鑫语音开发框架为例,系统了解嵌入式设备的语音唤醒和语音识别-RoboticsCV);或者占用稍多资源,集成一个轻量级的 TTS 引擎(如科大讯飞离线TTS SDK,不过对Flash和RAM要求较高);再或者走云端,由服务器把回复文本经过 TTS 合成后,以音频文件发送给 ESP32 播放。最后一种方式对ESP32 压力最小,只需接收和播放音频即可。在具体实现时,可以在服务器端调用诸如阿里云易音识别、微软Azure TTS等云服务,将回答文本转为语音,然后ESP32通过HTTP获取 MP3文件,使用集成的音频解码器播放。若追求本地纯离线,也可考虑安装一个小型MP3解码库,将服务器传来的MP3数据用 ESP32 解码输出至扬声器(ESP32有足够能力播放中等码率的MP3)。语音合成的声音可以自定义,让小智有独特的音色。
(5) 其他扩展创意: 小智机器人可以结合更多传感器和功能模块,实现丰富的玩法。例如,加入摄像头模块(ESP32-S3支持连接摄像头),配合云端的视觉识别,实现看图对话或人脸识别后的个性化对话;或者集成舵机作为“头部”,让机器人朝向发声方向转动;添加触摸传感器或按钮,让用户可以用触摸来激活对话等。此外,如果追求更强的AI本地能力,可以关注乐鑫新推出的 ESP32-P4 等更高性能芯片或者结合Edge AI加速器来运行部分模型。对于软件层面,也可以将当前方案与其他开源框架集成,例如接入 Home Assistant 平台,实现家居语音控制;或使用 LangChain 框架设计机器人的对话流程和工具调用能力,让小智不仅能聊天,还能帮忙查询资料、控制设备 (I created a Realtime Voice Assistant for my ESP-32, here is my journey - Part 2 : Node, OpenAI, Langchain - DEV Community) (I created a Realtime Voice Assistant for my ESP-32, here is my journey - Part 2 : Node, OpenAI, Langchain - DEV Community)。
通过以上优化,小智 AI 机器人将变得更加实用和智能:在联网时,它是一个功能全面的语音助手,能聊天、查信息、控制家电;离线时,它仍具备基本的指令识别和交流能力。不仅如此,借助ESP32的开源生态,您可以不断为小智添加新功能。在实践过程中,请充分利用社区资源:参考官方示例、查看类似项目源码 (Building a fully local LLM voice assistant to control my smart home | Hacker News) (Building a fully local LLM voice assistant to control my smart home | Hacker News),这将有助于您更深入地理解实现细节并排除开发中的困难。
结语: 恭喜您完成这个从无到有的项目!通过本教程,我们学习了 ESP32 上语音唤醒的原理,搭建了语音对话硬件平台,编写了嵌入式代码将语音与云端AI连接,并了解了如何部署强大的大模型服务来实现智能对答。对于初学者来说,这是迈向物联网 AI 世界的重要一步。当然,受限于时间和篇幅,教程中的很多模块都是简化的,但希望这份详细指南为您理清了思路。在实际开发中,您可能会遇到各种挑战,比如噪声环境下识别率、网络延迟导致的响应速度、不同模块兼容性等,不要气馁,多查阅资料、多尝试调优。借助开源社区的力量,您的“小智”一定可以变得越来越聪明。期待您为它赋予更多有趣的技能!祝您玩得开心,在 AI 创作的道路上不断进步。
参考文献:
- Espressif Systems, ESP-SR 开源语音识别框架 – 模块组成及功能 (以乐鑫语音开发框架为例,系统了解嵌入式设备的语音唤醒和语音识别-RoboticsCV) (esp-box/docs/technical_architecture_cn.md at master · espressif/esp-box · GitHub)
- RoboticsCV, 嵌入式设备语音唤醒和识别原理 – ESP32-S3 AI指令和 PSRAM 优势 (以乐鑫语音开发框架为例,系统了解嵌入式设备的语音唤醒和语音识别-RoboticsCV)
- Espressif GitHub, ESP-BOX 技术架构 – WakeNet 唤醒词模型工作机制 (esp-box/docs/technical_architecture_cn.md at master · espressif/esp-box · GitHub)
- Fabrik, ESP32 实时语音助手项目 – WebSocket 音频流传输架构 (I created a Realtime Voice Assistant for my ESP-32, here is my journey - Part 2 : Node, OpenAI, Langchain - DEV Community) (Building a fully local LLM voice assistant to control my smart home | Hacker News)
- Hacker News, 本地 LLM 语音助手实现讨论 – ESP32 建立 WebSocket 及音频传输流程 (Building a fully local LLM voice assistant to control my smart home | Hacker News) (Building a fully local LLM voice assistant to control my smart home | Hacker News)
- Hacker News, ESP32 语音硬件建议 – 优选 I2S 数字麦克风避免模拟麦克风噪声 (Building a fully local LLM voice assistant to control my smart home | Hacker News)
- 优快云 博客, ESP-Skainet 语音唤醒教程 – ESP32 上唤醒词和命令词识别案例 (esp32学习:语音识别教程esp-skainet库的使用-优快云博客)
- 阿里达摩院, SenseVoice 开源项目 – 多语言语音识别模型简介及能力 (GitHub - HG-ha/SenseVoice-Api: 阿里SenseVoice的fastpi封装,采用onnx发布,体积更小,附带量化模型,支持GPU。支持从URL文件进行语音识别。)
- DeepSeek 项目说明 – 开源大型语言模型性能及开放版本 (deepseek-ai/DeepSeek-R1-Distill-Qwen-7B · Hugging Face)
- VocaMirror 开源项目 – 通义千问(Qwen) API 使用示例