在裸 ESP32 上构建 AI 语音助手:接入 Whisper、GPT 与 TTS
构建一个基于ESP32的AI语音助手
Alex 是一个完全 DIY 的 AI 语音助手,仅用一块无 PSRAM 的 ESP32-D0WD-V3 开发板配合 INMP441 麦克风、MAX98357A 功放和 SSD1306 OLED,就实现了语音采集、Whisper 转录、GPT 生成回答和 TTS 流式播放的实时闭环。项目把 OLED 动画放在第二个核心的独立 FreeRTOS 任务上,避免每次 1 至 3 秒的 HTTP 调用卡住画面。调试中还解决了 TLS 握手连续内存不足、SSD1306_EXTERNALVCC 供电模式错误和 TTS 语速过快等问题。
正文
Alex:在裸露的ESP32上构建AI语音助手
Alex是一款完全DIY的AI语音助手,基于普通的ESP32开发板构建而成——无需云音箱中转服务,无需专用硬件,也无需树莓派。它通过I2S麦克风进行声音采集,使用OpenAI的Whisper API转录语音,借助GPT生成真实回答,并通过有线放大器和扬声器将回复大声播放出来。整个过程实时完成,搭配一块小型OLED显示屏,可动态呈现生动的“小狗眼”,随着助手聆听、思考和说话时做出相应反应。
本文详细介绍了该项目从零开始的构建过程,涵盖每个环节的实现细节,包括实际调试经历——因为项目的困难程度与最终成果同样重要。
所用硬件:
- ESP32开发板(ESP32-D0WD-V3,标准开发模块,无PSRAM)
- INMP441 — I2S MEMS麦克风
- MAX98357A — I2S 3W Class D功放扩展板
- 3W扬声器
- SSD1306 OLED显示屏 — 0.96英寸,128x64分辨率,I2C接口
接线配置
注意:为放大器专门提供5V电源(而非ESP32自身的5V供电)是刻意做出的选择——与WiFi当前的瞬时电流峰值共享电源会导致录音初期出现音频异常。
第一部分:核心语音处理流程
该项目的基础是一个五步循环:
•监听 — ESP32持续采样I2S麦克风,检测是否有持续超过阈值的音量突增。需要连续几段响亮的声音片段才能触发,因此避免了短暂的噪音尖峰(如门关闭、椅子吱呀声)误触发录音。
•录制 — 触发后开始录音,直到检测到自然静止,然后停止。
•转录 — 捕获的音频片段被打包成WAV文件(手动构建,包含完整头部信息),并发送至OpenAI的Whisper API。
•思考 — 转录后的文本通过Chat Completions API发送给GPT,以获得真实且对话式的回答。
•语音输出 — 回答由OpenAI的TTS API生成,并将原始PCM音频直接通过I2S流式传输至MAX98357A放大器,无需先等待完整响应下载。
已过滤掉一些已知的Whisper“幻觉”短语(例如“感谢观看”),因为几乎无声或仅含噪音的片段往往会导致这些内容持续出现,而非真正不输出任何文字。
第二部分:添加OLED“面部”
为了让助手看起来更生动,而不仅仅是一个对音频做出反应的黑箱,我们加入了SSD1306 OLED显示屏,通过动态的狗狗眼睛动画来表现助手的行为状态:
关键设计决策:OLED独立运行于自己的FreeRTOS任务中,并固定绑定到ESP32的第二个核心。这一点至关重要,因为Whisper、GPT和TTS的调用都会阻塞HTTP请求,每个请求可能耗时1至3秒。如果屏幕更新与这些调用在同一任务中进行,动画就会在调用过程中卡住,无法流畅播放。在主管道运行于核心1的同时,将子程序独立运行于核心0,可确保整个周期内面部保持活跃,并通过一个受小范围互斥保护的共享状态进行通信。
遇到的问题及解决方法
1. 编译错误:setBufferSizes 不是 WiFiClientSecure 的成员
早期尝试修复内存问题(见下文)时使用了 client.setBufferSizes(1024, 512) 方法——该方法在较旧版本的 Arduino-ESP32 核心中存在,但在 3.x 版本核心中已被移除。3.x 版本用新的 mbedtls 基于实现(NetworkClientSecure)取代了原有的基于 BearSSL 的 WiFiClientSecure,而后者不再提供运行时 TLS 缓冲区控制功能。修复方案:完全移除了相关调用,并以另一种方式解决了底层的内存问题(见下文)。
2. “直接连接失败”——TLS握手偶尔失败
当天首次发起的HTTPS请求(启动时的独立TTS测试)会失败,尽管Wi-Fi本身连接正常。诊断结果显示ESP32总可用堆栈空间充足,但连续可用堆栈不足——TLS握手需要一个大而连续的内存块,而其他分配(如麦克风采样缓冲区、JSON解析、Wi-Fi栈缓冲区)导致的碎片化使得可用连续内存不足40KB,正好处于mbedtls所需内存的边缘。解决方案:将MAX_RECORD_SECONDS从3秒减至2秒,缩小了音频上传缓冲区,并释放了约32KB的连续堆栈空间。仅此一项就足以使TLS连接变得可靠。
3. OLED完全无显示
这是项目中调试过程最长的一次,经历了多个误判后才找到真正原因:
•首先怀疑线路问题——但当心跳打印确认显示任务正常运行,且display.begin()返回成功(即真实的I2C握手)后,该问题被排除。
•接着怀疑是错误的OLED控制器(SH1106与SSD1306混淆,廉价模块中常见匹配错误)——但经确认模块为真正的0.96英寸SSD1306后,问题依然存在。
•然后怀疑是GPIO引脚损坏——通过物理连通性检查确认线路本身端到端连接良好。
•最终发现真正原因是:OLED模块需要使用SSD1306_EXTERNALVCC供电模式,而不是display.begin()函数中默认传递的SSD1306_SWITCHCAPVCC电源模式。由于模式设置错误,I2C握手及所有初始化命令均能成功执行,但显示屏始终无法接收到正确的内部驱动电压以实现点亮。解决方法:将电源模式常量更改为SSD1306_EXTERNALVCC后,问题彻底解决。
4. 在测试OLED接线时,尝试将GPIO 22用作I2C引脚,但该引脚已在组装好的PCB上物理焊接至放大器的DIN线路,导致两个外设会争夺同一根线路。解决方法:将OLED的I2C时钟线移至一个真正空闲的引脚(GPIO 19)上。
5. TTS播放声音显得过快
当管道端到端完全正常运行后,AI的语音回复听起来像是以正常音调播放得过快——这排除了采样率不匹配的可能性,反而指向了TTS语音的默认语速问题。解决方法:在TTS API请求中添加了“speed”参数0.85,使语音速度放缓,更符合自然语速。
6. 必须非常靠近麦克风说话
INMP441 麦克风灵敏度较高,但原始样本值在处理时未经过放大,而扬声器路径已预先设置了增益。问题修复:在触发检测和保存/转录音频之前,为每个麦克风样本添加了软件增益(MIC_GAIN),从而提升灵敏度,确保正常说话距离下的声音能够可靠采集。
本项目展示的内容
除了最终成果之外,这个构建还涉及多个嵌入式系统的基本原理,这些内容在高级教程中容易被忽略:
- 手动配置 I2S 接口,用于录音(麦克风)和流媒体播放(扬声器),包括原始 WAV 头部的构建
- 流式传输 HTTP 响应(分块传输编码),并实时播放音频,而非缓冲整个文件
- 区分堆碎片化与总可用堆空间,明确它们是两个不同的问题
- 双核 FreeRTOS 任务设计,使用户界面在主任务因慢速网络调用而阻塞时仍保持响应
- 系统化的硬件调试方法——逐个隔离变量(线路、软件、电源或元件缺陷),而非猜测
注意:在上传代码之前,请前往“工具”->“分区方案”,并将其更新为下方所示的选项。
本文编译自hackster.io