让AI知识触手可及

首页 » 【项目实战】基于esp32开发板+大模型实现的语音助手-软件篇

前言

在上一篇【项目实战】基于esp32开发板+大模型实现的陪伴助手-模型篇中,我们完成了唤醒词训练、评估,并将唤醒词识别集成至DSP32开发板。

本章,我们将重点梳理项目中涉及到的软件部分,包括前端和后端实现内容。

回顾

正如上图所示,项目整体架构分为前端和后端两部分。

  • 前端(DSP32开发板):负责用户交互,实现监听唤醒词,进行音频录制、音频播放等功能。
  • 后端(百度API部分):通过HTTP请求,实现语音识别(ASR)、语音合成(TTS)等功能。
  • 后端(Dify部分):通过HTTP请求,实现大模型对话功能。

接下来,我们分别梳理前端和后端实现内容。

1. 前端(DSP32开发板)

1.1 基础知识

因为Arduino 程序运行的基本框架是有两个函数构成:

  • setup():初始化设置,设备启动时,只运行一次。
  • loop():主循环,持续循环运行该函数。

以Arduino预置的Blink示例代码为例:

// 初始化设置
void setup() {
  Serial.begin(115200);  // 初始化串口

  // 初始化LED引脚
  pinMode(LED_BUILTIN, OUTPUT);
}

// 主循环
void loop() {
  digitalWrite(LED_BUILTIN, HIGH);  // 点亮LED
  delay(1000);                      // 延时1秒
  digitalWrite(LED_BUILTIN, LOW);   // 熄灭LED
  delay(1000);                      // 延时1秒

  Serial.println("this is blink test");  // 串口打印日志
}

1.2 工作流程

基于上述基础知识,我们可以将DSP32开发板的工作流程绘制如下:

graph TD
    A[设备启动] --> B[初始化设置]
    B --> C[WiFi连接]
    B --> D[I2S初始化]
    B --> E[创建mainChat线程]

    %% 唤醒词检测流程
    F[循环监听] --> G{检测唤醒词}
    G -->|未检测到| F
    G -->|检测到| H[LED指示灯亮起]
    H --> I[播放应答音]
    I --> J[设置record_status为false]

    %% 主对话流程
    E --> K[mainChat线程]
    K --> L{检查record_status}
    L -->|true| K
    L -->|false| M[录音处理]

    %% 录音和识别流程
    M --> N[分配内存]
    N --> O[开始录音]
    O --> P{检测声音}
    P -->|无声音超时| Q[结束录音]
    P -->|有声音| O
    Q --> R{是否有效录音}
    R -->|无效| S[LED关闭]
    S --> T[释放内存]
    T --> K

    %% 语音处理流程
    R -->|有效| U[音频转文本STT]
    U --> V[调用AI对话]
    V --> W[文本转语音TTS]
    W --> X[播放回复]
    X --> T

说明:

  • 设备启动时:
      1. 进行初始化设置:包括WiFi连接、I2S初始化、创建mainChat线程。
      1. mainChat线程:持续循环运行,检查record_status状态,若为false,则进行录音处理以及后续的应答;若为true,则返回mainChat线程。
      1. 监听唤醒词线程:循环监听唤醒词,若检测到唤醒词,则点亮LED指示灯,播放应答音,并设置record_status为false(以便触发mainChat线程的工作)。

1.3 关键代码

1.3.1 初始化设置

代码文件:esp32s3-ai-chat\esp32s3-ai-chat.ino

#include <wakeup_voice_inferencing.h>
#include <WiFi.h>
#include <HTTPClient.h>
#include <ArduinoJson.h>
#include <driver/i2s.h>
#include <UrlEncode.h>
#include <base64.hpp>

#include "config.h"  // 包含配置头文件

// Audio recording settings
#define SAMPLE_RATE 16000
#define RECORD_TIME_SECONDS 15
#define BUFFER_SIZE (SAMPLE_RATE * RECORD_TIME_SECONDS)

static bool debug_nn = false;  // Set this to true to see e.g. features generated from the raw signal
static bool record_status = true;

void setup() {
  // 设置串口波特率
  Serial.begin(115200);

  // 设置LED输出模式,并初始化设置为低
  pinMode(LED_BUILTIN, OUTPUT);
  digitalWrite(LED_BUILTIN, LOW);  //Turn off

  // Connect to WiFi
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(1000);
    Serial.println("[INFO][Setup]Connecting to WiFi...");
  }
  Serial.println("[INFO][Setup]Connected to WiFi");

  // 初始化I2S
  initI2S();
  Serial.println("[INFO][Setup]I2S initialized");

  // 开启对话主流程
  Serial.println("[INFO][Setup]Starting mainChat task.");
  xTaskCreate(mainChat, "mainChat", 1024 * 32, NULL, 10, NULL);

  // summary of inferencing settings (from model_metadata.h)
  ei_printf("[INFO][Setup]Inferencing settings:\n");
  ei_printf("\tInterval: ");
  ei_printf_float((float)EI_CLASSIFIER_INTERVAL_MS);
  ei_printf(" ms.\n");
  ei_printf("\tFrame size: %d\n", EI_CLASSIFIER_DSP_INPUT_FRAME_SIZE);
  ei_printf("\tSample length: %d ms.\n", EI_CLASSIFIER_RAW_SAMPLE_COUNT / 16);
  ei_printf("\tNo. of classes: %d\n", sizeof(ei_classifier_inferencing_categories) / sizeof(ei_classifier_inferencing_categories[0]));

  ei_printf("\n[INFO][Setup]Starting continious inference in 2 seconds...\n");
  ei_sleep(2000);

  if (microphone_inference_start(EI_CLASSIFIER_RAW_SAMPLE_COUNT) == false) {
    ei_printf("[ERR][Setup]Could not allocate audio buffer (size %d), this could be due to the window length of your model\r\n", EI_CLASSIFIER_RAW_SAMPLE_COUNT);
    return;
  }

  ei_printf("[INFO][Setup]Recording...\n");
}

说明:

  • config.h:包含WiFi连接、I2S初始化、创建mainChat线程等配置信息。
  • wakeup_voice_inferencing.h:是由训练好的模型导入后的库文件。
  • initI2S()对应esp32s3-ai-chat/audio_inference.ino定义的初始化I2S函数。
  • microphone_inference_start()对应esp32s3-ai-chat/audio_inference.ino定义的开始录音函数。
  • 为了方便调试,我们通过Serial.println打印日志,[INFO]表示重点步骤的信息,[ERR]表示错误信息。

1.3.2 mainChat线程

代码文件:esp32s3-ai-chat/esp32s3-ai-chat.ino

void mainChat(void* arg) {
  //获取access token
  String baidu_access_token = "";
  String qianfan_access_token = "";

  baidu_access_token = getAccessToken(baidu_api_key, baidu_secret_key);

  #if USE_QIANFAN_MODEL
    qianfan_access_token = getAccessToken(qianfan_api_key, qianfan_secret_key);
    Serial.println("[INFO][mainChat]Qianfan access token retrieved.");
  #endif

  Serial.println("[INFO][mainChat]Access tokens retrieved.");

  while (1) {
    if (!record_status) {
      // Record audio from INMP441
      // 分配内存
      uint8_t* pcm_data = (uint8_t*)ps_malloc(BUFFER_SIZE);
      if (!pcm_data) {
        Serial.println("[ERR][mainChat]Failed to allocate memory for pcm_data");
        return;
      }
      Serial.println("[INFO][mainChat]Memory allocated for pcm_data.");  // 添加调试日志

      Serial.println("[INFO][mainChat]i2s_read");
      // 开始循环录音,将录制结果保存在pcm_data中
      size_t bytes_read = 0, recordingSize = 0, ttsSize = 0;
      int16_t data[512];
      size_t noVoicePre = 0, noVoiceCur = 0, noVoiceTotal = 0, VoiceCnt = 0;
      bool recording = true;

      while (1) {
        // 记录刚开始的时间
        noVoicePre = millis();

        // 使用封装的函数替代原始的I2S读取代码
        bytes_read = readAudioData(data, pcm_data, recordingSize);
        recordingSize += bytes_read;
        // Serial.printf("%x recordingSize: %d bytes_read :%d\n", pcm_data + recordingSize, recordingSize, bytes_read);

        // 计算平均值
        uint32_t sum_data = 0;
        for (int i = 0; i < bytes_read / 2; i++) {
          sum_data += abs(data[i]);
        }
        sum_data = sum_data / bytes_read;
        Serial.printf("[INFO][mainChat]Average sound level: %d\n", sum_data);

        // 判断当没有说话时间超过一定时间时就退出录音
        noVoiceCur = millis();
        if (sum_data < 15) {
          noVoiceTotal += noVoiceCur - noVoicePre;
        } else {
          noVoiceTotal = 0;
          VoiceCnt += 1;
        }
        Serial.printf("[INFO][mainChat]No voice duration: %d ms\n", noVoiceTotal);

        if (noVoiceTotal > 1000) {
          recording = false;
        }

        if (!recording || (recordingSize >= BUFFER_SIZE - bytes_read)) {
          Serial.printf("[INFO][mainChat]Recording done: %d\n", recordingSize);
          break;
        }
      }

      // 设置唤醒录音状态为true,此后可以唤醒
      record_status = true;

      // 此时一直没有说话,则退出被唤醒状态
      if (VoiceCnt == 0) {
        digitalWrite(LED_BUILTIN, LOW);  //Turn off
        Serial.println("[INFO][mainChat]No voice detected, turning off LED.");

        recordingSize = 0;

        // 释放内存
        free(pcm_data);
        continue;
      }

      if (recordingSize > 0) {
        // 音频转文本(语音识别API访问)
        String recognizedText = baiduSTT_Send(baidu_access_token, pcm_data, recordingSize);
        Serial.println("[INFO][mainChat]Recognized text: " + recognizedText);

        // 根据宏定义选择使用的大模型
        String aiResponse;
        #if USE_QIANFAN_MODEL
          aiResponse = baiduErnieBot_Get(qianfan_access_token, recognizedText.c_str());
          Serial.println("[INFO][mainChat]Qianfan Bot response: " + aiResponse);
        #else
          aiResponse = difyChat_Send(recognizedText);
          Serial.println("[INFO][mainChat]Dify Bot response: " + aiResponse);
        #endif

        // 文本转音频tts并通过MAX98357A输出(语音合成API访问)
        baiduTTS_Send(baidu_access_token, aiResponse);
        Serial.println("[INFO][mainChat]TTS processing completed."); 
      }

      // 释放内存
      free(pcm_data);
      Serial.println("[INFO][mainChat]Memory for pcm_data freed."); 

      // 设置唤醒录音状态为false,此后继续录音对话
      record_status = true;   // 设置为true看看问题是不是在这里
    }
    delay(1000);
  }
}

说明:

  • baiduSTT_Send() 对应esp32s3-ai-chat/server_api.ino 中定义的百度语音识别API。
  • baiduErnieBot_Get() 对应esp32s3-ai-chat/server_api.ino 中定义的百度大模型API。
  • difyChat_Send() 对应esp32s3-ai-chat/server_api.ino 中定义的Dify大模型API。
  • baiduTTS_Send() 对应esp32s3-ai-chat/server_api.ino 中定义的百度语音合成API。

1.3.3 监听唤醒词

代码文件:esp32s3-ai-chat/esp32s3-ai-chat.ino

void loop() {
  bool m = microphone_inference_record();
  if (!m) {
    ei_printf("[ERR][Loop]Failed to record audio...\n");
    return;
  }
  ei_printf("[INFO][Loop]Audio recorded successfully.\n");  // 添加调试日志

  signal_t signal;
  signal.total_length = EI_CLASSIFIER_RAW_SAMPLE_COUNT;
  signal.get_data = &microphone_audio_signal_get_data;
  ei_impulse_result_t result = { 0 };

  EI_IMPULSE_ERROR r = run_classifier(&signal, &result, debug_nn);
  if (r != EI_IMPULSE_OK) {
    ei_printf("[ERR][Loop]Failed to run classifier (%d)\n", r);
    return;
  }
  ei_printf("[INFO][Loop]Classifier run successfully.\n");  // 添加调试日志

  int pred_index = -1;                      // Initialize pred_index
  float pred_value = PRED_VALUE_THRESHOLD;  // Initialize pred_value

  // print the predictions
  ei_printf("[INFO][Loop]Predictions ");
  ei_printf("(DSP: %d ms., Classification: %d ms., Anomaly: %d ms.)",
            result.timing.dsp, result.timing.classification, result.timing.anomaly);
  ei_printf(": \n");
  for (size_t ix = 0; ix < EI_CLASSIFIER_LABEL_COUNT; ix++) {
    ei_printf("    %s: ", result.classification[ix].label);
    ei_printf_float(result.classification[ix].value);
    ei_printf("\n");

    // 唤醒词在第一位,此时判断classification[0]位置大于阈值表示唤醒,如果你的唤醒词在第2位,则需要判断classification[1],第3位就是classification[2]
    if (result.classification[0].value > pred_value) {
      pred_index = 0;
      ei_printf("[INFO][Loop]Wake word detected.\n");
    }
  }
  // Display inference result
  if (pred_index == 0) {
    digitalWrite(LED_BUILTIN, HIGH);  //Turn on
    ei_printf("[INFO][Loop]LED turned ON for wake word.\n"); 

    Serial.println("[INFO][Loop]playAudio_Zai");
    playAudio_Zai();
    record_status = false;
  }

#if EI_CLASSIFIER_HAS_ANOMALY == 1
  ei_printf("    anomaly score: ");
  ei_printf_float(result.anomaly);
  ei_printf("\n");
#endif
}

说明:

  • PRED_VALUE_THRESHOLD 对应唤醒词的阈值,默认设置为0.8;当result.classification[0].value大于阈值时,表示检测到唤醒词。
  • microphone_inference_record() 对应esp32s3-ai-chat/audio_inference.ino 中定义的录音函数。
  • playAudio_Zai() 对应esp32s3-ai-chat/audio_data.ino 中定义的播放唤醒词音频函数,它是一段base64编码的音频,预置在程序中以便快速读取调用。

2. 后端(百度API部分)

2.1 注册百度语音处理API并获取API-KEY

操作步骤:

  1. 访问百度智能云官网并注册登录。https://cloud.baidu.com/

  2. 选择短音频

  3. 点击立即使用

  4. 创建应用,输入相关应用信息后,勾选短语音识别

  5. 填写应用信息,应用归属 个人,填写 应用描述,点击 立即创建

  6. 创建成功后,可以在应用列表查看到相关信息。

  7. 开通语音服务。

  8. 选择开通内容:点击 按量后付费->语音识别->短语音识别-中文普通话->勾选服务协议->确认开通。

备注:

  • 开启语音合成方式与语音识别类似,不再赘述。
  • 百度语音识别和语音合成可以使用同一个API-KEY,但需要分别开通。

2.3 调试API接口

操作步骤:

  1. 进入控制台后,点击 文档->示例代码

  2. 示例代码选择全部产品->语音技术

2.3.1 获取鉴权信息

操作步骤:

  1. 获取AccessToken
    点击 鉴权认证机制->获取AccessToken->立即前往

  2. 鉴权参数页面点击确定

  3. 回到获取AccessToken页面,点击调试

2.3.2 测试语音识别

操作步骤:

  1. 点击 语音识别->短语音识别标准版,点击 上传文件

  2. 上传一段音频文件后,点击 调试

说明:

  • 在测试页面中,上传的音频文件会被base64编码,所以我们在DSP开发板中,也需要实现将录制的音频文件进行base64编码。
  1. 调试成功后,音频会显示识别结果

2.3.3 测试语音合成

操作步骤:

  1. 点击 语音合成->短文本在线合成

  2. 在编辑框输入要合成的文本,选择音色、语速、音量以及格式后,点击 合成

  3. 合成后,可以通过 播放按钮 查看生成的音频是否正确。

  4. 点击示例代码,可以查看各类语言调用API接口的示例。

3. 后端(Dify部分)

3.1 部署Dify服务

部署步骤:

  1. 准备一台服务器
  2. 安装Docker
  3. 安装Docker-Compose
  4. 配置镜像源
  5. 拉取Dify代码(https://github.com/langgenius)
  6. 配置Dify
  7. 启动Dify镜像

以上详细步骤在网上可找到详细资料,也可参考【产品体验】使用dify部署微信群聊天机器人,本章不再赘述。

3.2 创建Dify项目

3.2.1 创建空白项目

  1. 登录Dify平台后,点击 创建空白应用

  2. 配置应用名称和描述信息

  3. 配置prompt

说明:
Dify平台提供了辅助生成prompt的功能,点击右上角的生成,输入需求后,平台会自动生成prompt。

  1. 配置大模型:点击个人头像,进入设置页面->模型提供商,可以配置不同的大模型。

说明:

  • Dify平台默认是GPT3.5,需要有API-KEY;
  • 如果需要使用其他大模型,可以在模型列表中选择对应模型并配置API-KEY。
  • 本章示例中配置的是硅基(硅基平台集成了多种模型并统一管理,免去了去各个模型平台注册账号的麻烦,具体方法可参考【产品体验】使用dify部署微信群聊天机器人)

3.3 创建API-KEY

由于DSP后续需要调用该API,Dify服务部署在公网(部署在本地或局域网不涉及此问题),为了安全起见,我们需要给应用创建一个API-KEY,以便进行API访问的鉴权。

操作步骤:

  1. 切换至左侧的API
  2. 点击 创建API-KEY

说明:

  • 右上角的http://dify.17aitech.com/v1是API地址,其中dify.17aitech.com是Dify服务器地址,请读者根据实际情况修改为对应的IP地址或者域名。

4. 前后端联调

4.1 配置DSP中的地址和API-KEY

代码路径:esp32s3-ai-chat/config.cpp

// config.cpp
#include "config.h"

// WiFi credentials
const char* ssid = "HUAWEI_E2D1";
const char* password = "xxxxx";

// Baidu API credentials
const char* baidu_api_key = "Ulxxxx";
const char* baidu_secret_key = "0rjxxxxxx";

// Dify API credentials
const char* DIFY_API_KEY = "app-xxxxxx";

说明:

  • ssidpassword 对应 WiFi名称和密码。
  • baidu_api_keybaidu_secret_key 对应 2.1 中的API-KEY。
  • DIFY_API_KEY 对应 3.3 中的API-KEY。

代码路径: esp32s3-ai-chat/server_api.ino

// server_api.ino

// API URLs
const char* BAIDU_TOKEN_URL = "https://aip.baidubce.com/oauth/2.0/token";
const char* BAIDU_STT_URL = "http://vop.baidu.com/server_api";
const char* BAIDU_TTS_URL = "https://tsn.baidu.com/text2audio";

const char* DIFY_API_URL = "http://dify.17aitech.com/v1/chat-messages";

说明:

  • BAIDU_TOKEN_URL 对应 2.3.1 中的API地址。
  • BAIDU_STT_URL 对应 2.3.2 中的API地址。
  • BAIDU_TTS_URL 对应 2.3.3 中的API地址。
  • DIFY_API_URL 对应 3.2 中的API地址,其中dify.17aitech.com是Dify服务器地址,请读者根据实际情况修改为对应的IP地址或者域名。

4.2 编译并上传程序

  1. 编译程序

  2. 上传至DSP开发板

  3. 打开串口程序查看日志

  4. 对着DSP呼叫"蛋仔",收到"在"的回复后,进行对话

  5. 可以看到DSP会识别用户的说话内容并给出回复内容

  6. 查看Dify平台上的日志记录,可以看到Dify平台收到的DSP请求和回复内容

不足之处

  • 响应速度问题。该项目中的语音识别、大模型响应、语音合成需要发起3次HTTP请求,导致响应速度较慢,无法达到实时语音沟通。后续可以考虑将语音识别和语音合成集成到DSP中,以减少对网络的依赖。
  • 对话体验问题。该项目中目前的对话方式类似于小爱音箱,需要通过唤醒词触发对话,无法做到多轮对话以及实时对话,需要后续调研实时对话API。
  • 多用户管理问题。该项目目前仍然只实现了一个DSP与DIFY服务的对话,还无法做到多个用户与DIFY服务的内容隔离,需要后续增加用户管理功能。
  • 唤醒词问题。该项目中目前的唤醒词准确率仍然较低,需要后续继续优化唤醒词的神经网络。

总结

  • 本项目实现了基于DSP32开发板+Dify服务+百度API的语音助手,基本实现了从硬件→后端服务的基本流程。
  • 硬件部分:
    • 通过DSP32开发板实现了语音识别、大模型响应、语音合成。
    • 通过MAX98357A实现了音频输出。
    • 通过麦克风实现了音频输入。
  • 后端部分:
    • 通过Dify服务实现了对话内容。
    • 通过百度API实现了语音识别、语音合成。
  • 模型部分:
    • 将音频采集并且预处理后的信号数据,进行特征提取
    • 通过多层神经网络进行模型训练,实现了唤醒词识别

该系列其他文章

欢迎关注公众号以获得最新的文章和新闻

声明:一起AI技术所有文章,如无特殊说明或标注,均为本站作者原创发布。任何个人或组织,在未征得作者同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。
原创不易,内容版权受保护,感谢您的尊重与支持。
0 0 投票数
Article Rating
订阅评论
提醒
guest的头像
0 评论
内联反馈
查看所有评论
Picture of Dongming
Dongming
见天地,见众生,见自己。

分类文章

推荐活动

推荐文章

【产品体验】DeepSeek的三种接入使用方法
【论文简读】DeepSeek LLM:以长远主义拓展开源语言模型
【产品体验】趋动云上使用LLaMaFactory进行模型微调的流程体验
【课程总结】day24(上):大模型三阶段训练方法(LLaMa Factory)
【项目实战】通过LLaMaFactory+Qwen2-VL-2B微调一个多模态医疗大模型
【模型训练】在AutoDL上使用LLamaFactory进行模型训练
【产品体验】豆包大模型实时语音本地化部署及功能体验
【项目实战】基于esp32开发板+大模型实现的陪伴助手-硬件篇
【模型部署】在趋动云上使用vllm部署模型
【模型部署】在趋动云上使用xinference部署模型
0
希望看到您的想法,请您发表评论x
滚动至顶部