基于 ESP32 的对话机器人实现:整合 Coze 大模型、百度千帆 ASR 与 TTS
语音输入处理:通过麦克风采集语音信号,使用 ASR 服务将语音转换为文本。对话生成:将文本输入到 Coze 大模型中,生成智能响应文本。语音输出:利用 TTS 服务将响应文本转换为语音信号,并通过扬声器输出。整个系统基于 ESP32 实现硬件控制,借助百度千帆云服务处理计算密集型任务(如 ASR 和 TTS),而 Coze 大模型负责对话逻辑。这降低了 ESP32 的资源负担,使其在低功耗环境下高
随着人工智能和物联网技术的快速发展,对话机器人已成为智能家居、客服系统等领域的核心应用。本文将介绍如何利用 ESP32 微控制器、结合 Coze 大模型(用于对话生成)、百度千帆平台提供的自动语音识别(ASR)和文本转语音(TTS)服务,构建一个高效的对话机器人系统。该系统可实现语音输入识别、智能响应生成和语音输出功能,适用于低成本嵌入式设备。
1. 系统概述
对话机器人的核心流程包括三个主要阶段:
- 语音输入处理:通过麦克风采集语音信号,使用 ASR 服务将语音转换为文本。
- 对话生成:将文本输入到 Coze 大模型中,生成智能响应文本。
- 语音输出:利用 TTS 服务将响应文本转换为语音信号,并通过扬声器输出。
整个系统基于 ESP32 实现硬件控制,借助百度千帆云服务处理计算密集型任务(如 ASR 和 TTS),而 Coze 大模型负责对话逻辑。这降低了 ESP32 的资源负担,使其在低功耗环境下高效运行。
2. 硬件组件
ESP32 是一款低成本、低功耗的 Wi-Fi 和蓝牙微控制器,适合物联网应用。本系统所需硬件包括:
- ESP32 开发板:作为主控制器,处理数据通信和控制逻辑。
- 麦克风模块:用于语音输入,常见模块如 INMP441(数字麦克风)或 MAX9814(模拟麦克风放大器),支持 ASR 输入。
- 扬声器模块:用于语音输出,可通过 PWM 或 I2S 接口驱动。
- 辅助电路:如电源管理、Wi-Fi 模块等。
其中,麦克风和扬声器模块的选择取决于具体应用场景。例如,MAX9814 提供高灵敏度,适合嘈杂环境;而 INMP441 则支持数字输出,简化 ESP32 接口。
3. 软件与服务集成
系统依赖于云服务处理 AI 任务,主要组件如下:
- 百度千帆平台:提供强大的 ASR 和 TTS API。ASR 服务可将语音转换为文本,TTS 服务将文本转换为自然语音。百度千帆支持高精度识别和多种语言,适合嵌入式系统通过 HTTP 请求调用。
- Coze 大模型:这是一个先进的对话生成模型,类似于大型语言模型(LLM),可用于生成上下文相关的响应。Coze 模型可通过 API 访问,输入文本后返回生成的对话内容。
- ESP32 固件:使用 Arduino 框架或 MicroPython 开发,负责硬件控制、网络通信和数据转发。
集成时,ESP32 通过 Wi-Fi 连接到互联网,发送语音数据到百度千帆 ASR API,接收文本后转发到 Coze 模型,再将生成的文本发送到百度千帆 TTS API,最后播放语音。
4. 实现步骤
以下是构建系统的关键步骤,确保结构清晰且可复现:
步骤 1: 硬件设置
- 连接麦克风模块到 ESP32 的 ADC 或 I2S 接口。
- 连接扬声器模块到 ESP32 的 DAC 或 PWM 接口。
- 配置 Wi-Fi 模块,确保 ESP32 可以访问互联网。
步骤 2: 云服务配置
- 注册百度千帆账号,创建 ASR 和 TTS 应用,获取 API 密钥和端点 URL。
- 设置 Coze 模型访问:如果 Coze 提供 API,注册并获取密钥;否则,可使用开源替代方案如 GPT 模型。
- 编写 ESP32 代码处理 HTTP 请求。示例伪代码如下:
步骤 3: 优化与测试
- 性能优化:ESP32 资源有限,建议使用流式处理(如分块发送音频数据),并设置超时处理。
- 准确性测试:在安静环境中测试 ASR 识别率,调整麦克风增益;验证 Coze 模型的响应相关性。
- 功耗管理:ESP32 进入低功耗模式当空闲时,延长电池寿命。
5. 应用场景与优势
本系统适用于:
- 智能家居:作为语音助手控制灯光、温度等。
- 教育机器人:提供互动学习体验。
- 工业设备:实现语音控制指令。
优势包括:
- 低成本:ESP32 硬件价格低廉,百度千帆提供免费层服务。
- 高效性:云服务处理复杂 AI 任务,减少本地计算负担。
- 可扩展性:易于集成其他传感器或服务。
6. 挑战与未来展望
当前挑战包括网络延迟(影响实时性)和隐私问题(语音数据上传)。未来可通过边缘计算优化,如使用本地轻量模型处理部分任务,或结合 5G 网络减少延迟。
总之,通过整合 ESP32、Coze 大模型和百度千帆 ASR/TTS,我们可以构建一个功能完善的对话机器人系统。开发者可基于上述框架进一步定制,推动智能嵌入式设备的发展。
相关代码:
#include <Arduino.h>
#include <WiFi.h>
#include <WiFiClientSecure.h>
#include <ArduinoJson.h>
#include "driver/i2s.h"
#include "FS.h"
#include "SD.h"
#include "SPI.h"
#include <base64.h> // 需安装Base64库:搜索"Base64" by Daniel Pawlikowski
/************************ 核心配置(必须修改!)************************/
// WiFi配置
const char* WIFI_SSID = "你的WiFi名称";
const char* WIFI_PASS = "你的WiFi密码";
// Coze API配置
const char* COZE_API_KEY = "pat_DF8e73SOxxxxxxxxxx1VuKKxxxxxxaGwdBqc";
const char* COZE_BOT_ID = "757621xxxxxxx0";
const char* COZE_USER_ID = "123"; // 自定义固定用户ID
const char* COZE_API_DOMAIN = "api.coze.cn";
const int COZE_API_PORT = 443;
// 百度ASR/TTS配置(替换为你的密钥)
const char* BAIDU_API_KEY = "你的百度API Key";
const char* BAIDU_SECRET_KEY = "你的百度Secret Key";
const char* BAIDU_ASR_URL = "https://vop.baidu.com/pro_api"; // 极速版API
const char* BAIDU_TTS_URL = "https://tsn.baidu.com/text2audio";
/************************ 硬件引脚定义 ************************/
// INMP441 录音I2S引脚(I2S_NUM_0)
#define I2S_REC_BCLK 26
#define I2S_REC_LRC 25
#define I2S_REC_DIN 34
// MAX98357A 播放I2S引脚(I2S_NUM_1,与录音区分)
#define I2S_PLAY_BCLK 13
#define I2S_PLAY_LRC 12
#define I2S_PLAY_DOUT 14
// SD卡SPI引脚
#define SD_CS 5
#define SD_SCK 18
#define SD_MISO 19
#define SD_MOSI 23
/************************ 全局配置 ************************/
// 音频参数(与ASR/TTS要求一致)
#define SAMPLE_RATE 16000
#define BITS_PER_SAMPLE I2S_BITS_PER_SAMPLE_16BIT
#define BYTES_PER_SAMPLE (BITS_PER_SAMPLE / 8)
#define RECORD_DURATION 6000 // 6秒录音
#define RECORD_FILE_PATH "/recording.raw" // 录音文件(raw格式)
#define TTS_FILE_PATH "/tts.mp3" // TTS缓存文件
// 状态机(控制流程顺序)
typedef enum {
STATE_IDLE, // 空闲
STATE_RECORDING, // 录音中
STATE_ASR, // 百度ASR识别中
STATE_COZE, // Coze AI对话中
STATE_TTS, // 百度TTS合成中
STATE_PLAYING // 语音播放中
} DeviceState;
DeviceState currentState = STATE_IDLE;
// 全局变量
WiFiClientSecure client; // ESP32 HTTPS客户端
String accessToken = ""; // 百度API Token(有效期30天)
unsigned long tokenExpireTime = 0; // Token过期时间戳
String asrText = ""; // ASR识别结果文本
String cozeReply = ""; // Coze AI回复文本
/************************ 工具函数 ************************/
// 打印带时间戳的日志
void logPrintln(String msg) {
Serial.printf("[%lu] %s\n", millis(), msg.c_str());
}
// 检查WiFi连接(断线重连)
bool checkWiFi() {
if (WiFi.status() != WL_CONNECTED) {
logPrintln("WiFi断线,正在重连...");
WiFi.reconnect();
int retry = 0;
while (WiFi.status() != WL_CONNECTED && retry < 10) {
delay(500);
retry++;
}
if (WiFi.status() == WL_CONNECTED) {
logPrintln("WiFi重连成功!IP:" + WiFi.localIP().toString());
return true;
} else {
logPrintln("WiFi重连失败");
return false;
}
}
return true;
}
// URL编码函数(修复错误2:urlEncode未定义)
String urlEncode(String str) {
String encodedString = "";
char c;
char code0;
char code1;
char code2;
for (int i = 0; i < str.length(); i++) {
c = str.charAt(i);
if (c == ' ') {
encodedString += '+';
} else if (isalnum(c)) {
encodedString += c;
} else {
code1 = (c & 0xf0) >> 4;
code2 = (c & 0x0f);
code0 = 0x25;
encodedString += code0;
encodedString += (code1 < 10) ? (char)(code1 + 48) : (char)(code1 + 55);
encodedString += (code2 < 10) ? (char)(code2 + 48) : (char)(code2 + 55);
}
delayMicroseconds(1);
}
return encodedString;
}
// 获取文件大小(修复错误1和错误5:SD.size()不存在)
uint64_t getFileSize(String filePath) {
if (!SD.exists(filePath)) {
return 0;
}
File file = SD.open(filePath, FILE_READ);
uint64_t size = file.size();
file.close();
return size;
}
/************************ SD卡初始化 ************************/
bool initSDCard() {
SPI.begin(SD_SCK, SD_MISO, SD_MOSI, SD_CS);
if (!SD.begin(SD_CS)) {
logPrintln("❌ SD卡挂载失败!");
return false;
}
uint8_t cardType = SD.cardType();
if (cardType == CARD_NONE) {
logPrintln("❌ 未检测到SD卡!");
return false;
}
logPrintln("✅ SD卡类型:" + String(cardType == CARD_MMC ? "MMC" : (cardType == CARD_SD ? "SDSC" : (cardType == CARD_SDHC ? "SDHC" : "未知"))));
uint64_t cardSize = SD.cardSize() / (1024 * 1024);
uint64_t freeSpace = (SD.totalBytes() - SD.usedBytes()) / (1024 * 1024);
logPrintln("✅ SD卡总容量:" + String(cardSize) + " MB");
logPrintln("✅ SD卡剩余空间:" + String(freeSpace) + " MB");
return true;
}
/************************ I2S录音初始化 ************************/
void initI2SRecord() {
i2s_config_t i2s_config = {
.mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_RX),
.sample_rate = SAMPLE_RATE,
.bits_per_sample = BITS_PER_SAMPLE,
.channel_format = I2S_CHANNEL_FMT_ONLY_LEFT,
.communication_format = I2S_COMM_FORMAT_I2S_MSB,
.intr_alloc_flags = 0,
.dma_buf_count = 2,
.dma_buf_len = 64,
.use_apll = false
};
i2s_driver_install(I2S_NUM_0, &i2s_config, 0, NULL);
i2s_pin_config_t pin_config = {
.bck_io_num = I2S_REC_BCLK,
.ws_io_num = I2S_REC_LRC,
.data_out_num = I2S_PIN_NO_CHANGE,
.data_in_num = I2S_REC_DIN
};
i2s_set_pin(I2S_NUM_0, &pin_config);
logPrintln("✅ I2S录音模块初始化完成");
}
/************************ I2S播放初始化 ************************/
void initI2SPlay() {
i2s_config_t i2s_config = {
.mode = (i2s_mode_t)(I2S_MODE_MASTER | I2S_MODE_TX),
.sample_rate = 16000, // TTS默认16kHz
.bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT,
.channel_format = I2S_CHANNEL_FMT_ONLY_LEFT,
.communication_format = I2S_COMM_FORMAT_I2S_MSB,
.intr_alloc_flags = 0,
.dma_buf_count = 4,
.dma_buf_len = 1024,
.use_apll = false
};
i2s_driver_install(I2S_NUM_1, &i2s_config, 0, NULL);
i2s_pin_config_t pin_config = {
.bck_io_num = I2S_PLAY_BCLK,
.ws_io_num = I2S_PLAY_LRC,
.data_out_num = I2S_PLAY_DOUT,
.data_in_num = I2S_PIN_NO_CHANGE
};
i2s_set_pin(I2S_NUM_1, &pin_config);
i2s_stop(I2S_NUM_1);
logPrintln("✅ I2S播放模块初始化完成");
}
/************************ 录音功能 ************************/
void startRecording() {
if (currentState != STATE_IDLE) {
logPrintln("❌ 当前非空闲状态,无法录音!");
return;
}
if (!checkWiFi()) return;
currentState = STATE_RECORDING;
unsigned long recordStartMillis = millis();
// 删除旧录音文件
if (SD.exists(RECORD_FILE_PATH)) {
SD.remove(RECORD_FILE_PATH);
logPrintln("ℹ️ 删除旧录音文件");
}
// 初始化I2S录音
initI2SRecord();
// 打开文件写入
File recFile = SD.open(RECORD_FILE_PATH, FILE_WRITE);
if (!recFile) {
logPrintln("❌ 打开录音文件失败!");
i2s_driver_uninstall(I2S_NUM_0);
currentState = STATE_IDLE;
return;
}
logPrintln("📢 开始录音(6秒后自动结束)...");
int16_t sampleBuffer[64];
while (currentState == STATE_RECORDING && (millis() - recordStartMillis) < RECORD_DURATION) {
size_t bytesRead;
i2s_read(I2S_NUM_0, sampleBuffer, sizeof(sampleBuffer), &bytesRead, portMAX_DELAY);
if (bytesRead > 0) {
recFile.write((uint8_t*)sampleBuffer, bytesRead);
}
delay(1);
}
// 清理资源
recFile.close();
i2s_driver_uninstall(I2S_NUM_0);
currentState = STATE_IDLE;
logPrintln("🛑 录音结束");
// 检查录音文件
if (SD.exists(RECORD_FILE_PATH)) {
uint64_t fileSize = getFileSize(RECORD_FILE_PATH);
float duration = (float)fileSize / (SAMPLE_RATE * BYTES_PER_SAMPLE);
logPrintln("✅ 录音文件保存成功!大小:" + String(fileSize) + " 字节,时长:" + String(duration, 2) + "秒");
// 录音完成后自动触发ASR识别
currentState = STATE_ASR;
} else {
logPrintln("❌ 录音文件保存失败!");
}
}
/************************ 百度API Token获取 ************************/
bool getBaiduToken() {
// 检查Token是否有效(提前10分钟刷新)
if (accessToken.length() > 0 && millis() < tokenExpireTime - 600000) {
return true;
}
logPrintln("ℹ️ 获取百度API Token...");
String tokenUrl = "https://aip.baidubce.com/oauth/2.0/token?grant_type=client_credentials&client_id=" + String(BAIDU_API_KEY) + "&client_secret=" + String(BAIDU_SECRET_KEY);
if (client.connect("aip.baidubce.com", 443)) {
client.print("GET " + tokenUrl + " HTTP/1.1\r\n");
client.print("Host: aip.baidubce.com\r\n");
client.print("Connection: close\r\n\r\n");
String response = "";
while (client.connected() || client.available()) {
if (client.available()) {
response += client.readString();
}
}
client.stop();
// 解析JSON
int jsonStart = response.indexOf("{");
if (jsonStart != -1) {
String jsonStr = response.substring(jsonStart);
DynamicJsonDocument doc(1024);
DeserializationError error = deserializeJson(doc, jsonStr);
if (!error && doc.containsKey("access_token")) {
accessToken = doc["access_token"].as<String>();
long expireSeconds = doc["expires_in"].as<long>();
tokenExpireTime = millis() + expireSeconds * 1000;
logPrintln("✅ Token获取成功,有效期:" + String(expireSeconds / 3600) + "小时");
return true;
} else {
logPrintln("❌ Token解析失败:" + String(error.c_str()));
logPrintln("响应:" + jsonStr);
}
}
}
logPrintln("❌ Token获取失败");
return false;
}
/************************ 百度ASR识别(极速版)************************/
void baiduASR() {
if (currentState != STATE_ASR) return;
logPrintln("🔊 开始ASR识别...");
asrText = "";
// 检查Token和录音文件
if (!getBaiduToken() || !SD.exists(RECORD_FILE_PATH)) {
currentState = STATE_IDLE;
return;
}
File recFile = SD.open(RECORD_FILE_PATH, FILE_READ);
if (!recFile) {
logPrintln("❌ 打开录音文件失败!");
currentState = STATE_IDLE;
return;
}
// 构造ASR请求参数(分块编码,避免内存溢出)
String requestUrl = BAIDU_ASR_URL + String("?access_token=") + accessToken;
String headers = "Host: vop.baidu.com\r\n";
headers += "Content-Type: application/json\r\n";
headers += "Connection: close\r\n";
// 读取文件并Base64编码(分块处理)
const size_t chunkSize = 4096; // 分块大小(4KB)
uint8_t chunk[chunkSize];
String speechBase64 = "";
while (recFile.available() > 0) {
size_t bytesRead = recFile.read(chunk, chunkSize);
speechBase64 += base64::encode(chunk, bytesRead);
}
recFile.close();
// 构造JSON请求体(修复错误1:替换SD.size为getFileSize)
DynamicJsonDocument reqDoc(4096);
reqDoc["format"] = "raw";
reqDoc["rate"] = SAMPLE_RATE;
reqDoc["dev_pid"] = 1537; // 中文普通话
reqDoc["speech"] = speechBase64;
reqDoc["cuid"] = WiFi.macAddress();
reqDoc["len"] = getFileSize(RECORD_FILE_PATH); // 修复:使用自定义getFileSize函数
String postBody;
serializeJson(reqDoc, postBody);
// 发送HTTPS请求
if (client.connect("vop.baidu.com", 443)) {
client.print("POST " + requestUrl + " HTTP/1.1\r\n");
client.print(headers);
client.print("Content-Length: " + String(postBody.length()) + "\r\n\r\n");
client.print(postBody);
String response = "";
while (client.connected() || client.available()) {
if (client.available()) {
response += client.readString();
}
}
client.stop();
// 解析响应
int jsonStart = response.indexOf("{");
if (jsonStart != -1) {
String jsonStr = response.substring(jsonStart);
DynamicJsonDocument resDoc(1024);
DeserializationError error = deserializeJson(resDoc, jsonStr);
if (!error && resDoc["err_no"].as<int>() == 0) {
asrText = resDoc["result"][0].as<String>();
logPrintln("✅ ASR识别成功:" + asrText);
// ASR完成后自动触发Coze对话
currentState = STATE_COZE;
} else {
logPrintln("❌ ASR识别失败:err_no=" + String(resDoc["err_no"].as<int>()) + ", err_msg=" + resDoc["err_msg"].as<String>());
currentState = STATE_IDLE;
}
} else {
logPrintln("❌ ASR响应无JSON");
currentState = STATE_IDLE;
}
} else {
logPrintln("❌ 连接ASR服务器失败");
currentState = STATE_IDLE;
}
}
/************************ Coze AI对话 ************************/
String processCozeAnswer(DynamicJsonDocument& resDoc) {
if (resDoc["code"].as<int>() != 0) {
return "❌ Coze错误:" + resDoc["msg"].as<String>();
}
JsonArray data = resDoc["data"].as<JsonArray>();
String reply = "无回复";
for (auto item : data) {
if (item["type"].as<String>() == "answer") {
reply = item["content"].as<String>();
break;
}
}
return reply;
}
String getCozeChatResult(String conversationId, String chatId) {
String retrieveUrl = "/v3/chat/retrieve?conversation_id=" + conversationId + "&chat_id=" + chatId;
String msgListUrl = "/v3/chat/message/list?chat_id=" + chatId + "&conversation_id=" + conversationId + "&bot_id=" + String(COZE_BOT_ID) + "&task_id=" + chatId;
int maxRetries = 20;
for (int retry = 0; retry < maxRetries; retry++) {
logPrintln("🤔 Coze轮询中(" + String(retry+1) + "/" + String(maxRetries) + ")");
// 查询状态
if (client.connect(COZE_API_DOMAIN, COZE_API_PORT)) {
client.print("GET " + retrieveUrl + " HTTP/1.1\r\n");
client.print("Host: " + String(COZE_API_DOMAIN) + "\r\n");
client.print("Authorization: Bearer " + String(COZE_API_KEY) + "\r\n");
client.print("Connection: close\r\n\r\n");
String retrieveResp = "";
while (client.connected() || client.available()) {
if (client.available()) retrieveResp += client.readString();
}
client.stop();
int jsonStart = retrieveResp.indexOf("{");
if (jsonStart != -1) {
String jsonStr = retrieveResp.substring(jsonStart);
DynamicJsonDocument resDoc(1024);
DeserializationError error = deserializeJson(resDoc, jsonStr);
if (!error && resDoc["code"].as<int>() == 0) {
String status = resDoc["data"]["status"].as<String>();
if (status == "completed") {
// 获取消息列表
if (client.connect(COZE_API_DOMAIN, COZE_API_PORT)) {
client.print("GET " + msgListUrl + " HTTP/1.1\r\n");
client.print("Host: " + String(COZE_API_DOMAIN) + "\r\n");
client.print("Authorization: Bearer " + String(COZE_API_KEY) + "\r\n");
client.print("Connection: close\r\n\r\n");
String msgResp = "";
while (client.connected() || client.available()) {
if (client.available()) msgResp += client.readString();
}
client.stop();
int msgJsonStart = msgResp.indexOf("{");
if (msgJsonStart != -1) {
String msgJsonStr = msgResp.substring(msgJsonStart);
DynamicJsonDocument msgDoc(2048);
DeserializationError msgError = deserializeJson(msgDoc, msgJsonStr);
if (!msgError) {
return processCozeAnswer(msgDoc);
}
}
}
} else if (status == "failed") {
return "❌ Coze任务失败:" + resDoc["data"]["error_msg"].as<String>();
}
}
}
}
delay(1000);
}
return "❌ Coze轮询超时";
}
void callCozeAI() {
if (currentState != STATE_COZE || asrText.length() == 0) return;
logPrintln("🤖 调用Coze AI:" + asrText);
cozeReply = "";
if (!checkWiFi()) {
currentState = STATE_IDLE;
return;
}
// 构造Coze请求体
DynamicJsonDocument reqDoc(1024);
reqDoc["bot_id"] = COZE_BOT_ID;
reqDoc["user_id"] = COZE_USER_ID;
reqDoc["stream"] = false;
reqDoc["auto_save_history"] = true;
JsonArray messages = reqDoc.createNestedArray("additional_messages");
JsonObject userMsg = messages.createNestedObject();
userMsg["role"] = "user";
userMsg["content"] = asrText;
userMsg["content_type"] = "text";
String postBody;
serializeJson(reqDoc, postBody);
// 发送Coze请求
if (client.connect(COZE_API_DOMAIN, COZE_API_PORT)) {
client.print("POST /v3/chat HTTP/1.1\r\n");
client.print("Host: " + String(COZE_API_DOMAIN) + "\r\n");
client.print("Authorization: Bearer " + String(COZE_API_KEY) + "\r\n");
client.print("Content-Type: application/json\r\n");
client.print("Content-Length: " + String(postBody.length()) + "\r\n");
client.print("Connection: close\r\n\r\n");
client.print(postBody);
String response = "";
while (client.connected() || client.available()) {
if (client.available()) response += client.readString();
}
client.stop();
// 解析对话ID
int jsonStart = response.indexOf("{");
if (jsonStart != -1) {
String jsonStr = response.substring(jsonStart);
DynamicJsonDocument resDoc(1024);
DeserializationError error = deserializeJson(resDoc, jsonStr);
if (!error && resDoc["code"].as<int>() == 0) {
String chatId = resDoc["data"]["id"].as<String>();
String conversationId = resDoc["data"]["conversation_id"].as<String>();
logPrintln("✅ Coze对话创建成功:" + chatId);
// 轮询获取结果
cozeReply = getCozeChatResult(conversationId, chatId);
logPrintln("✅ Coze回复:" + cozeReply);
// Coze完成后自动触发TTS
currentState = STATE_TTS;
} else {
logPrintln("❌ Coze响应解析失败:" + String(error.c_str()));
currentState = STATE_IDLE;
}
} else {
logPrintln("❌ Coze响应无JSON");
currentState = STATE_IDLE;
}
} else {
logPrintln("❌ 连接Coze失败");
currentState = STATE_IDLE;
}
}
/************************ 百度TTS合成+I2S播放 ************************/
void baiduTTSAndPlay() {
if (currentState != STATE_TTS || cozeReply.length() == 0) return;
logPrintln("🎤 开始TTS合成:" + cozeReply);
// 检查Token
if (!getBaiduToken()) {
currentState = STATE_IDLE;
return;
}
// 构造TTS请求参数(修复错误2:使用自定义urlEncode;修复错误3:String拼接)
String encodedText = urlEncode(cozeReply);
String ttsParams = "tex=" + encodedText +
"&lan=zh&cuid=" + WiFi.macAddress() +
"&ctp=1&tok=" + accessToken +
"&spd=5&pit=5&vol=15&per=0";
String requestUrl = String(BAIDU_TTS_URL) + "?" + ttsParams; // 修复:转换为String再拼接
// 下载TTS音频到SD卡
if (SD.exists(TTS_FILE_PATH)) {
SD.remove(TTS_FILE_PATH);
}
File ttsFile = SD.open(TTS_FILE_PATH, FILE_WRITE);
if (!ttsFile) {
logPrintln("❌ 打开TTS文件失败!");
currentState = STATE_IDLE;
return;
}
// 发送TTS请求并保存音频
if (client.connect("tsn.baidu.com", 443)) {
client.print("GET " + requestUrl + " HTTP/1.1\r\n");
client.print("Host: tsn.baidu.com\r\n");
client.print("Connection: close\r\n\r\n");
// 跳过HTTP头部,只保存音频数据
bool headerEnd = false;
while (client.connected() || client.available()) {
if (client.available()) {
String line = client.readStringUntil('\n');
if (headerEnd) {
// 修复错误4:强制类型转换const char* → const uint8_t*
ttsFile.write((const uint8_t*)line.c_str(), line.length());
}
if (line == "\r") {
headerEnd = true; // 头部结束标志
}
}
}
client.stop();
ttsFile.close();
// 播放MP3文件(使用I2S)(修复错误5:替换SD.size为getFileSize)
uint64_t ttsFileSize = getFileSize(TTS_FILE_PATH);
if (SD.exists(TTS_FILE_PATH) && ttsFileSize > 100) {
logPrintln("🎵 开始播放TTS语音(大小:" + String(ttsFileSize) + "字节)...");
currentState = STATE_PLAYING;
File playFile = SD.open(TTS_FILE_PATH, FILE_READ);
if (playFile) {
i2s_start(I2S_NUM_1);
size_t bytesRead;
uint8_t playBuffer[1024];
while (playFile.available() > 0 && currentState == STATE_PLAYING) {
bytesRead = playFile.read(playBuffer, sizeof(playBuffer));
i2s_write(I2S_NUM_1, playBuffer, bytesRead, &bytesRead, portMAX_DELAY);
}
playFile.close();
i2s_stop(I2S_NUM_1);
}
logPrintln("🎵 TTS播放完成");
currentState = STATE_IDLE;
} else {
logPrintln("❌ TTS音频文件无效(大小:" + String(ttsFileSize) + "字节)!");
currentState = STATE_IDLE;
}
} else {
logPrintln("❌ 连接TTS服务器失败");
ttsFile.close();
currentState = STATE_IDLE;
}
}
/************************ 串口指令解析 ************************/
void parseSerialCommand() {
if (Serial.available() > 0) {
String input = Serial.readStringUntil('\n');
input.trim();
if (input.length() == 0) return;
logPrintln("🗣️ 串口输入:" + input);
if (input == "1") {
// 指令1:开始录音(触发语音对话流程)
startRecording();
} else if (input == "3") {
// 指令3:查询录音信息
if (SD.exists(RECORD_FILE_PATH)) {
uint64_t size = getFileSize(RECORD_FILE_PATH);
logPrintln("📋 录音文件信息:大小=" + String(size) + "字节,时长=" + String((float)size/(SAMPLE_RATE*BYTES_PER_SAMPLE),2) + "秒");
} else {
logPrintln("📋 无录音文件");
}
} else if (input == "q") {
// 指令q:退出
logPrintln("❌ 退出程序");
while (1);
} else {
// 其他输入:作为文本对话
if (currentState == STATE_IDLE) {
asrText = input; // 直接复用文本到Coze流程
currentState = STATE_COZE;
} else {
logPrintln("❌ 当前忙碌中,无法处理文本对话!");
}
}
}
}
/************************ 初始化 ************************/
void setup() {
Serial.begin(115200);
delay(1000);
logPrintln("=====================================");
logPrintln(" ESP32 语音AI对话机器人 ");
logPrintln("=====================================");
logPrintln("📋 支持指令:");
logPrintln(" 1 - 开始语音对话(录音6秒→ASR→AI→TTS)");
logPrintln(" 3 - 查询录音文件信息");
logPrintln(" q - 退出程序");
logPrintln(" 其他文本 - 直接文本对话");
logPrintln("=====================================\n");
// 初始化硬件
if (!initSDCard()) {
while (1) {
logPrintln("❌ SD卡初始化失败,程序暂停!");
delay(1000);
}
}
initI2SPlay(); // 初始化播放模块(录音模块按需初始化)
// 连接WiFi
WiFi.begin(WIFI_SSID, WIFI_PASS);
logPrintln("连接WiFi:" + String(WIFI_SSID));
while (WiFi.status() != WL_CONNECTED) {
delay(500);
Serial.print(".");
}
logPrintln("\n✅ WiFi连接成功!IP:" + WiFi.localIP().toString());
// ESP32 HTTPS关闭证书验证(简化开发)
client.setInsecure();
// 初始化百度Token
getBaiduToken();
currentState = STATE_IDLE;
logPrintln("✅ 系统初始化完成,等待指令...");
}
/************************ 主循环 ************************/
void loop() {
// 优先处理串口指令
parseSerialCommand();
// 状态机驱动流程
switch (currentState) {
case STATE_ASR:
baiduASR();
break;
case STATE_COZE:
callCozeAI();
break;
case STATE_TTS:
baiduTTSAndPlay();
break;
default:
// 空闲状态,do nothing
break;
}
delay(100);
}
更多推荐


所有评论(0)