跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客GitHub 精选镜像工具UI配色美学隐私政策关于联系
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
C++AI

ESP32 对话机器人:整合 Coze 大模型与百度千帆 ASR/TTS

介绍基于 ESP32 微控制器构建对话机器人的方案,整合 Coze 大模型进行对话生成,利用百度千帆平台提供 ASR(语音转文本)和 TTS(文本转语音)服务。系统通过 Wi-Fi 连接云端,实现语音输入识别、智能响应及语音输出全流程。文章涵盖硬件选型、云服务配置、核心代码逻辑及优化建议,适用于低成本嵌入式物联网设备开发。

古灵精怪发布于 2026/4/5更新于 2026/5/2830 浏览

随着人工智能和物联网技术的快速发展,对话机器人已成为智能家居、客服系统等领域的核心应用。本文将介绍如何利用 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>

/************************ 核心配置(必须修改!)************************/
// 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;
String accessToken; // 百度 API Token(有效期 30 天)
unsigned long tokenExpireTime = 0; // Token 过期时间戳
String asrText;   // ASR 识别结果文本
String cozeReply; // Coze AI 回复文本
String speechBase64;
String response;
String retrieveResp;
String msgResp;

/************************ 工具函数 ************************/
// 打印带时间戳的日志
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 编码函数
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;
}

// 获取文件大小
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");
        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 识别...");
    // 检查 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];
    while (recFile.available() > 0) {
        size_t bytesRead = recFile.read(chunk, chunkSize);
        speechBase64 += base64::encode(chunk, bytesRead);
    }
    recFile.close();
    // 构造 JSON 请求体
    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);
    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);
        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");
            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");
                            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);
    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);
        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 请求参数
    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;
    // 下载 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;
                line = client.readStringUntil('\n');
                if (headerEnd) {
                    ttsFile.write((const uint8_t*)line.c_str(), line.length());
                }
                if (line == "\r") {
                    headerEnd = true; // 头部结束标志
                }
            }
        }
        client.stop();
        ttsFile.close();
        // 播放 MP3 文件(使用 I2S)
        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);
}

目录

  1. 1. 系统概述
  2. 2. 硬件组件
  3. 3. 软件与服务集成
  4. 4. 实现步骤
  5. 步骤 1: 硬件设置
  6. 步骤 2: 云服务配置
  7. 步骤 3: 优化与测试
  8. 5. 应用场景与优势
  9. 6. 挑战与未来展望
  10. 相关代码
  • 💰 8折买阿里云服务器限时8折了解详情
  • Magick API 一键接入全球大模型注册送1000万token查看
  • 🤖 一键搭建Deepseek满血版了解详情
  • 一键打造专属AI 智能体了解详情
极客日志微信公众号二维码

微信扫一扫,关注极客日志

微信公众号「极客日志V2」,在微信中扫描左侧二维码关注。展示文案:极客日志V2 zeeklog

更多推荐文章

查看全部
  • DeepSeek 各版本演进历程与核心特性对比
  • MySQL 常用命令速查表
  • 字节跳动大模型工程师日常与 Top Seed 计划深度解析
  • 民用无人机新规 2026 年 5 月实施:实名登记与激活要求详解
  • Retinaface+CurricularFace 人脸识别服务 Kubernetes StatefulSet 部署实战
  • 2023 年电赛 H 题信号分离装置 FPGA+STM32 解法
  • Qwen3-4B-Instruct-2507 智能写作助手部署与优化
  • 玩客云边缘 AI 模型本地部署:llama.cpp 与 Qwen
  • 前端 WebSocket 实时通信:替代轮询的最佳实践
  • Android 开发常用快速开发框架与第三方库精选指南
  • 2025 前端技术复盘:框架收敛、AI 赋能与生态趋势
  • OpenClaw Skills 合集开源,收录超 700 个本地 AI Agent 技能
  • C++ 标准库:map 与 set 容器详解与实战
  • Seedance 2.0 多模态 AI 视频创作操作手册
  • AI 绘画提示词大全:从入门到精通的创意指南
  • FPGA实现UART串口通信
  • GLM-4.6V-Flash-WEB 部署实战:弹性计费降低成本方案
  • Rust 与 WebAssembly 实战:在浏览器与 Node.js 运行高性能代码
  • Jetson Orin NX 部署 Ollama 与 Llama 3.2 实战指南
  • OpenClaw 部署方式对比:云端、WSL、Mac 及 Ubuntu 虚拟机

相关免费在线工具

  • RSA密钥对生成器

    生成新的随机RSA私钥和公钥pem证书。 在线工具,RSA密钥对生成器在线工具,online

  • Mermaid 预览与可视化编辑

    基于 Mermaid.js 实时预览流程图、时序图等图表,支持源码编辑与即时渲染。 在线工具,Mermaid 预览与可视化编辑在线工具,online

  • 随机西班牙地址生成器

    随机生成西班牙地址(支持马德里、加泰罗尼亚、安达卢西亚、瓦伦西亚筛选),支持数量快捷选择、显示全部与下载。 在线工具,随机西班牙地址生成器在线工具,online

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

  • Base64 文件转换器

    将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online

  • Markdown转HTML

    将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online