本项目是使用OpenAI API和ESP-Box来创建一个语音的聊天机器人。ESP-Box是一种集成了ESP32-S3微控制器的设备或系统。此实现的目的是使用户能够使用口语与聊天机器人进行通信。整个流程首先是通过音频传感器捕获音频输入,然后将其发送到OpenAI API进行处理,并接收响应,最后将其转换为语音并播放给用户。
ESP-BOX 简介
ESP-BOX 是乐鑫信息科技发布的新一代 AIoT 应用开发平台。ESP32-S3-BOX 和 ESP32-S3-BOX-Lite 是目前对应的 AIoT 应用开发板,搭载支持 AI 加速的 ESP32-S3 Wi-Fi + Bluetooth 5 (LE) SoC。他们为用户提供了一个基于语音助手 + 触摸屏控制、传感器、红外控制器和智能 Wi-Fi 网关等功能,开发和控制智能家居设备的平台。开发板出厂支持离线语音交互功能,用户通过乐鑫丰富的 SDK 和解决方案,能够轻松构建在线和离线语音助手、智能语音设备、HMI 人机交互设备、控制面板、多协议网关等多样的应用。
ESP32-S3 是一款集成 2.4 GHz Wi-Fi 和 Bluetooth 5 (LE) 的 MCU 芯片,支持远距离模式 (Long Range)。ESP32-S3 搭载 Xtensa® 32 位 LX7 双核处理器,主频高达 240 MHz,内置 512 KB SRAM (TCM),具有 45 个可编程 GPIO 管脚和丰富的通信接口。ESP32-S3 支持更大容量的高速 Octal SPI flash 和片外 RAM,支持用户配置数据缓存与指令缓存。
- Xtensa® 32 位 LX7 双核处理器,主频高达 240 MHz
- 内置 512 KB SRAM、384 KB ROM 存储空间,并支持多个外部 SPI、Dual SPI、 Quad SPI、Octal SPI、QPI、OPI flash 和片外 RAM
- 额外增加用于加速神经网络计算和信号处理等工作的向量指令 (vector instructions)
- 45 个可编程 GPIO,支持常用外设接口如 SPI、I2S、I2C、PWM、RMT、ADC、UART、SD/MMC 主机控制器和 TWAITM 控制器等
- 基于 AES-XTS 算法的 Flash 加密和基于 RSA 算法的安全启动,数字签名和 HMAC 模块,“世界控制器 (World Controller)”模块
项目设计思路
关于本项目的实现思路主要分为以下几个步骤,首先是完成ESP-BSP的软硬件环境的了解和学习,包括硬件特性与结构的了解学习以及阅读 ESP-IDF(release/v4.4 或者 release/v5.0) 环境搭建指引,一步一步完成开发环境搭建;然后需要分析chatgbt-demo程序的结构组成及实现并尝试 构建并烧录一个新的示例程序来进行阶段性验证。最后是根据实现目标调整示例工程并解决适配ChatGPT API的网络问题。
开发环境搭建
开发环境的搭建主要参照 ESP-IDF环境搭建指引官方文档一步一步完成的开发环境搭建,官方在windows Linux 和 macOS平台都有对应的说明文档,我这边在windows和linux平台都搭建了开发环境,个人感觉整体上对比来看linux平台环境配置稍微麻烦一些,但是编译效率高一些。windows端因为提供了离线按照工具所以配置上相对比较简单。
环境搭建完成后可以按照ESP-IDF Programming Guide )对helloworld工程进行编译测试,如果没有问题的话就可以进行下一步了。
ChatGPT工程构建流程
在ChatGPT_demo项目中还有另一个名为factory_nvs的项目。用来存储wifi账户和OpenAi Key等信息。因此需要分别构建factory_nvs和ChatGPT_demo。
1.克隆Github仓库
git clone https://github.com/espressif/esp-box
2. 更改工作目录到factory_nvs
cd examples/chatgpt_demo/factory_nvs
3. 编译 factory_nvs
idf.py build
4. 更改工作目录到chatgpt_demo/
cd examples/chatgpt_demo/
5. 编译 chatgpt_demo
idf.py build
6. 烧写到Flash
python -m esptool -p /dev/ttyACM0 --chip esp32s3 -b 460800 --before default_reset --after hard_reset write_flash --flash_mode dio --flash_size 16MB --flash_freq 80m 0x0 build/bootloader/bootloader.bin 0x8000 build/partition_table/partition-table.bin 0xd000 build/ota_data_initial.bin 0x10000 build/chatgpt_demo.bin 0x900000 build/storage.bin 0xb00000 build/srmodels/srmodels.bin 0x700000 factory_nvs/build/factory_nvs.bin
在编译 factory_nvs项目的时候可以通过menuconfig对一些存储信息进行修改。
调整完后发现修改的信息并未存储成功,后来通过修改代码增加强制擦除操作进行更新(应该是有更优雅的解决方法的)。
// Initialize NVS
ESP_ERROR_CHECK(nvs_flash_erase());//执行一次后注释掉
err = nvs_flash_init();
if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) {
ESP_ERROR_CHECK(nvs_flash_erase());
err = nvs_flash_init();
}
ESP_ERROR_CHECK(err);
如故成功更改后,会在串口的接受信息中看到如下更改信息:
在chatgpt目录可以menuconfig对一些语音识别相关配置信息进行修改,也可以修改唤醒词等相关参数。
软件流程图
主要功能代码
主函数部分主要是由一些初始化函数和任务创建函数组成,包括基本的板级相关外设初始胡以及显示 网络和语音识别等相关任务的创建。
void app_main()
{
//Initialize NVS
esp_err_t ret = nvs_flash_init();
if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
ESP_ERROR_CHECK(nvs_flash_erase());
ret = nvs_flash_init();
}
ESP_ERROR_CHECK(ret);
ESP_ERROR_CHECK(settings_read_parameter_from_nvs());
sys_param = settings_get_parameter();
bsp_spiffs_mount();
bsp_i2c_init();
bsp_display_start();
bsp_board_init();
ESP_LOGI(TAG, "Display LVGL demo");
bsp_display_backlight_on();
ui_ctrl_init();
app_network_start();
ESP_LOGI(TAG, "speech recognition start");
app_sr_start(false);
audio_register_play_finish_cb(audio_play_finish_cb);
while (true) {
ESP_LOGD(TAG, "\tDescription\tInternal\tSPIRAM");
ESP_LOGD(TAG, "Current Free Memory\t%d\t\t%d",
heap_caps_get_free_size(MALLOC_CAP_8BIT | MALLOC_CAP_INTERNAL),
heap_caps_get_free_size(MALLOC_CAP_SPIRAM));
ESP_LOGD(TAG, "Min. Ever Free Size\t%d\t\t%d",
heap_caps_get_minimum_free_size(MALLOC_CAP_8BIT | MALLOC_CAP_INTERNAL),
heap_caps_get_minimum_free_size(MALLOC_CAP_SPIRAM));
vTaskDelay(pdMS_TO_TICKS(5 * 1000));
}
}
识别到唤醒词后,系统对语音数据进行录制后在进行语音识别并将识别结果转换成文本信息,接着通过ChatGPT API将文本进行传入 ,最终再将将语音数据通过扬声器播放出来(由于OpenAI API本质上只是文本语言的交互,所以想实现语音的交互还需要文本到语音的转换支持,这部分功能使用外部API来满足此需求。使用TalkingGenie提供的文本转语音功能)。
/* program flow. This function is called in app_audio.c */
esp_err_t start_openai(uint8_t *audio, int audio_len)
{
static OpenAI_t *openai = NULL;
static OpenAI_AudioTranscription_t *audioTranscription = NULL;
static OpenAI_ChatCompletion_t *chatCompletion = NULL;
if (openai == NULL) {
openai = OpenAICreate(sys_param->key);
audioTranscription = openai->audioTranscriptionCreate(openai);
chatCompletion = openai->chatCreate(openai);
audioTranscription->setResponseFormat(audioTranscription, OPENAI_AUDIO_RESPONSE_FORMAT_JSON);
audioTranscription->setLanguage(audioTranscription,"zh");
audioTranscription->setTemperature(audioTranscription, 0.2);
chatCompletion->setModel(chatCompletion, "gpt-3.5-turbo");
chatCompletion->setSystem(chatCompletion, "Code geek");
chatCompletion->setMaxTokens(chatCompletion, CONFIG_MAX_TOKEN);
chatCompletion->setTemperature(chatCompletion, 0.2);
chatCompletion->setStop(chatCompletion, "\r");
chatCompletion->setPresencePenalty(chatCompletion, 0);
chatCompletion->setFrequencyPenalty(chatCompletion, 0);
chatCompletion->setUser(chatCompletion, "OpenAI-ESP32");
}
ui_ctrl_show_panel(UI_CTRL_PANEL_GET, 0);
char *text = audioTranscription->file(audioTranscription, (uint8_t *)audio, audio_len, OPENAI_AUDIO_INPUT_FORMAT_WAV); // Calling transcript api
if (text == NULL){
ui_ctrl_label_show_text(UI_CTRL_LABEL_LISTEN_SPEAK, "API Key is not valid");
return ESP_FAIL;
}
if (strcmp(text, "invalid_request_error") == 0 || strcmp(text, "server_error") == 0) {
ui_ctrl_label_show_text(UI_CTRL_LABEL_LISTEN_SPEAK, "Sorry, I can't understand.");
ui_ctrl_show_panel(UI_CTRL_PANEL_SLEEP, 2000);
return ESP_FAIL;
}
// UI listen success
ui_ctrl_label_show_text(UI_CTRL_LABEL_REPLY_QUESTION, text);
ui_ctrl_label_show_text(UI_CTRL_LABEL_LISTEN_SPEAK, text);
OpenAI_StringResponse_t *result = chatCompletion->message(chatCompletion, text, false); //Calling Chat completion api
char *response = result->getData(result, 0);
if (response != NULL && (strcmp(response, "invalid_request_error") == 0 || strcmp(response, "server_error") == 0)) {
// UI listen fail
ui_ctrl_label_show_text(UI_CTRL_LABEL_LISTEN_SPEAK, "Sorry, I can't understand.");
ui_ctrl_show_panel(UI_CTRL_PANEL_SLEEP, 2000);
return ESP_FAIL;
}
// UI listen success
ui_ctrl_label_show_text(UI_CTRL_LABEL_REPLY_QUESTION, text);
ui_ctrl_label_show_text(UI_CTRL_LABEL_LISTEN_SPEAK, response);
if (strcmp(response, "invalid_request_error") == 0) {
ui_ctrl_label_show_text(UI_CTRL_LABEL_LISTEN_SPEAK, "Sorry, I can't understand.");
ui_ctrl_show_panel(UI_CTRL_PANEL_SLEEP, 2000);
return ESP_FAIL;
}
ui_ctrl_label_show_text(UI_CTRL_LABEL_REPLY_CONTENT, response);
ui_ctrl_show_panel(UI_CTRL_PANEL_REPLY, 0);
esp_err_t status = text_to_speech_request(response, AUDIO_CODECS_MP3);
if (status != ESP_OK) {
ESP_LOGE(TAG, "Error creating ChatGPT request: %s\n", esp_err_to_name(status));
// UI reply audio fail
ui_ctrl_show_panel(UI_CTRL_PANEL_SLEEP, 0);
} else {
// Wait a moment before starting to scroll the reply content
vTaskDelay(pdMS_TO_TICKS(SCROLL_START_DELAY_S * 1000));
ui_ctrl_reply_set_audio_start_flag(true);
}
// Clearing resources
result->delete(result);
free(text);
return ESP_OK;
}
网络配置
关于此项目有以下两点问题需要解决:
-
您需要OpenAI API密钥
-
连接的WIFI要能够访问OpenAI
其中第1个问题可以参考网络上的关于如何注册ChatGPT以及申请API的教程,这里就不再赘述了。
关于第二个问题由于ESP32本身并不支持手动配置魔法,所以要就需要实现让ESP32连接Wifi具有直接能访问OpenAI的能力。在网上搜索后大概了解到有软路由配置、手机热点配置、电脑热点配置等几种方法(魔法中继功能)。
第一种方式相对麻烦一些,需要可以刷机的路由器。一开始选择的是第二种方式找到一些支持该功能的App,但是都必须要开启Root权限。最终是通过第3种方式实现使用Clash软件开启TUN Mode和移动热点的方式来解决这个问题的。
心得体会
通过这次活动了解和学习了ESP32S3这款芯片以及乐鑫官方的IDF开发环境,整体使用上还是比较方便的。在环境搭建上一开始也遇到许多坑,不过本质原因还是网络问题和版本问题,这部分要仔细安装官方文档进行操作才能更快速完成开发环境的搭建。