←新闻详情

21ic电子网更新于 10月30日 14:41

在裸 ESP32 上构建 AI 语音助手:接入 Whisper、GPT 与 TTS

构建一个基于ESP32的AI语音助手

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

来源

21ic电子网