Funpack2-5 基于文心一言的智能聊天机器人
在完成Funpack第2-5期任务一的基础上,经过一番魔改,制作了基于文心一言的智能聊天机器人
标签
Funpack活动
ChatGPT
ESP32-S3-BOX-LITE
枫雪天
更新2023-08-02
4689

任务介绍

    本项目实现了Funpack第2-5期活动的任务一,在ESP32-S3-Box-Lite平台上,使用WiFi和TTS,实现了天气获取与播报,并进一步实现了基于百度文心一言的智能聊天机器人。

硬件平台

    首先介绍本期活动的主角,ESP32-S3-Box-Lite开发板,这是一块集成度很高的嵌入式开发板,主控是我们很熟悉的ESP32微控制器。但和以往不一样的是,它的型号是最新的S3,不仅含有两颗可以达到240M主频的CPU,还集成了AI加速单元,可玩性很强。除了内置SRAM和Flash之外,还外接了16M的Flash以及片外SRAM,量大管饱,可以尽情折腾AI、音视频这种消耗资源的应用。开发板同时配备了屏幕、双麦克风、编解码芯片,所以具备了完整的音频输入输出能力。我很早就想体验一下它的AI和音视频能力,这次正好借着Funpack活动顺利上车。

FoEDIbBjpZWsCH3D56ZkKe3ralSs

任务分析与实现

   这次主办方出的任务,任务一是使用WiFi和TTS功能,实现一个语音播报系统,联网获取一些粉丝、天气之类的信息并播报出来。WiFi联网算是ESP32的基本能力,所以任务的难点在于跑通TTS并驱动IIS语音输出。任务二是做一个带回放功能的录音机,既要有音频的输入输出,还要用上ESP32的声学前端算法,和任务一难度相当。任务三是做一个在线的电子书阅读器,感觉主要难点在于解析和UI显示。可以做的很low,读取文本直接显示。也可以做得很完善,加入目录,字体调整,显示图片等元素,属于起点低上限高的任务。

   因为这块板子的设计定位就是AI和音视频应用,这次我也主要是想体验一下最新的ESP32S3在这方面到底表现如何。所以我选择了任务一,并在它基础上增加难度。做了一个基于ChatGPT的语音聊天机器人。下面我们做个功能拆解,语音聊天方面,需要语音识别、智能聊天、TTS文本转语音三个子流程进行配合。需要做音频输入输出,这就要驱动编解码模块。底层驱动官方已经做好了,我们直接使用就行。

项目的系统功能框图如下

FlZhtTAr_GyqftD24nxTBSVO4ciV

任务有三个难点:
  1. 第一个难点是要识别有人在说话。我们不能让他一直录音,给语音识别接口发数据,这样费电、废流量、还费钱。网上许多程序是用按键来控制,按下去就开始录音,松手就停止。这样还是有点笨,很不方便。这次还好声学前端里实现了VAD算法,也就是语音活动检测,我们可以用算法来自动控制录音的开关,在esp-ADF里有相关的例程。在程序中,我首先使用Wakenet他网络做了一个唤醒检测,当机器唤醒后,使用VAD算法进行语音活动检测,VAD算法监测到我们在说话后,就给录音线程发信号,启动录音。我们停止说话后,VAD发信号停止录音。我们说的话就被完整地录下来,发送给你百度语音识别接口,第一步语音识别就完成了。
    static esp_err_t rec_engine_cb(audio_rec_evt_t type, void *user_data)
    {
        if (AUDIO_REC_WAKEUP_START == type) {
            ESP_LOGI(TAG, "rec_engine_cb - AUDIO_REC_WAKEUP_START");
            duer_dcs_audio_sync_play_tone("spiffs://spiffs/dingding.wav");
        } else if (AUDIO_REC_VAD_START == type) {
            ESP_LOGI(TAG, "rec_engine_cb - AUDIO_REC_VAD_START");
            xEventGroupSetBits(duer_evt, TASK_REC_READING);
        } else if (AUDIO_REC_VAD_END == type) {
            xEventGroupClearBits(duer_evt, TASK_REC_READING);
            xEventGroupSetBits(duer_evt, TASK_UPLOAD_START);
            ESP_LOGI(TAG, "rec_engine_cb - AUDIO_REC_VAD_STOP, state");
        } else if (AUDIO_REC_WAKEUP_END == type) {
            ESP_LOGI(TAG, "rec_engine_cb - AUDIO_REC_WAKEUP_END");
        }
    
        return ESP_OK;
    }

     

    在上面这段代码中,esp-adf提供了rec_engine_cb回调函数。在该函数中,通过语音检测的事件类型,ESP32可以先通过语音唤醒进入VAD检测状态,随后由VAD检测控制录音线程的启停。
    下图是语音唤醒与识别的过程。
    FrYGWab357lit0ECQsrAk6v-jiC4
  2. 第二个难点是TTS功能的实现,我们既可以用ESP32官方写的TTS库,也可以用第三方的接口,比如微软的Edge-TTS、百度TTS。这里我先试了一下ESP32官方的TTS,毕竟ESP32能自己完成的,就最好别依赖第三方接口。我用官方的skainet工程体验了一下,感觉只能说是勉强能用。首先是合成的语音有时候断句不准确、对符号的发音有缺失,最致命的是输入句子还不能太长。所以只适合做一些例如收付款、家电控制这种固定的短句场景。而在我们的ChatGPT聊天助手的场景中,因为你不知道AI要回答什么,回答的长度也不固定,只能通过GPT的Prompt提示来做个概略的限制。所以我们需要TTS的能力更强一些。因此我最后选择了调用网络服务的方式。
    from fastapi import FastAPI
    from fastapi.responses import FileResponse
    from fastapi.middleware.cors import CORSMiddleware
    import uvicorn
    import edge_tts
    
    OUTPUT_FILE = "C:/Files/Codes/Python/tts/test.mp3"
    
    app = FastAPI()
    app.add_middleware(
        CORSMiddleware,
        allow_origins=["*"],
        allow_credentials=True,
        allow_methods=["*"],
        allow_headers=["*"],
    )
    
    async def get_file(text):
        communicate = edge_tts.Communicate(text, 'zh-CN-YunxiNeural', rate="-10%")
        await communicate.save(OUTPUT_FILE)
    
    @app.get('/tts')
    async def tts(text: str = '不好意思,请重新问一遍'):
        await get_file(text)
        return FileResponse(OUTPUT_FILE, media_type='audio/mp3')
    
    if __name__ == '__main__':
        uvicorn.run(app, host="0.0.0.0", port=5000)
    在上述代码中我们使用edge-tts的Python库,使用FastAPI框架搭建了一个简易的Edge-TTS服务。服务中首先使用传入的文字生成对应的mp3文件,随后将该文件传输给请求方,ESP-BOX-LITE收到该文件后,esp-adf将会调用自身的功能,边接收mp3流,边解码播放。
  3. 任务第三个难点是语音识别、智能聊天、TTS文本转语音这三个网络接口的适配,我使用的是百度语音识别、文心一言聊天接口、和微软的Egde-TTS。这三种接口反应最快的是Edge-TTS,因为它搭建在局域网我的笔记本电脑上。其次是百度语音识别,最差的就是聊天接口,ChatGPT、Claude这些接口需要魔法,延迟也很高。另外我的ChatGPT的APIKey到期了。所以最后选择了百度文心一言的接口,至少延迟方面有保证。最后要提一点,我现在的程序版本中,这三个接口都是阻塞式调用的。网络传输的延迟加上接口本身的响应延迟还是比较明显。在后续的改进版本中,最好是改成基于WebSocket协议的流式调用,边聊天边传输。那些成熟的产品都是这样的方案。
    下面是本项目三个核心线程的代码,可以看到它们内部的主要功能就是发起网络请求,并将请求结果填入相应的缓冲区。其中百度ASR(即语音识别服务)需要请求百度的服务器,而TTS和GPT服务则部署在局域网的其他电脑上。

     

    #define TTS_BASE_URL "http://192.168.1.7:5000/tts?text=%s"
    #define GPT_BASE_URL "http://192.168.1.7:6000/chat"
    #define BAIDU_ASR_URL "http://vop.baidu.com/server_api?dev_pid=1537&cuid=ESP32&token="
    
    static void chatgpt_task(void *args)
    {
        bool runing = true;
    
        while (runing) {
            EventBits_t bits = xEventGroupWaitBits(duer_evt, TASK_CHATGPT_START, false, true, portMAX_DELAY);
            if (bits & TASK_CHATGPT_START) {
                ESP_LOGI("chatgpt_task", LOG_BOLD(LOG_COLOR_BLUE) "TASK_CHATGPT_START  url:%s\tload:%s", GPT_BASE_URL, gpt_post_load);
                http_request(CHATGPT_REQUEST);
                xEventGroupClearBits(duer_evt, TASK_CHATGPT_START);
                ESP_LOGI("chatgpt_task", LOG_BOLD(LOG_COLOR_BLUE) "TASK_CHATGPT_START FINISHED");
            }
        }
    
        xEventGroupClearBits(duer_evt, TASK_CHATGPT_START);
        vTaskDelete(NULL);
    }
    
    static void player_task(void *args)
    {
        char *tts_url = audio_calloc(1, 512);
        bool runing = true;
    
        while (runing) {
            EventBits_t bits = xEventGroupWaitBits(duer_evt, TASK_PLAYER_START, false, true, portMAX_DELAY);
            if (bits & TASK_PLAYER_START) {
                memset(tts_url, 0, 512);
                sprintf(tts_url, TTS_BASE_URL, tts_post_load);
                ESP_LOGI("player_task", LOG_BOLD(LOG_COLOR_BLUE) "TASK_PLAYER_START  url:%s", tts_url);
                esp_audio_play(player, AUDIO_CODEC_TYPE_DECODER, (const char *)tts_url, 0);
                xEventGroupClearBits(duer_evt, TASK_PLAYER_START);
                ESP_LOGI("player_task", LOG_BOLD(LOG_COLOR_BLUE) "TASK_PLAYER_START FINISHED");
            }
        }
        
        free(tts_url);
        xEventGroupClearBits(duer_evt, TASK_PLAYER_START);
        vTaskDelete(NULL);
    }
    
    static void baidu_asr_task(void *args)
    {
        bool runing = true;
    
        while (runing) {
            EventBits_t bits = xEventGroupWaitBits(duer_evt, TASK_UPLOAD_START, false, true, portMAX_DELAY);
            if (bits & TASK_UPLOAD_START) {
                ESP_LOGI("baidu_asr_task", "TASK_UPLOAD_START");
                http_request(BAIDU_VOICE_REQUEST);
                xEventGroupClearBits(duer_evt, TASK_UPLOAD_START);
                ESP_LOGI("baidu_asr_task", "TASK_UPLOAD_START END");
            }
        }
    
        xEventGroupClearBits(duer_evt, TASK_UPLOAD_START);
        free(voiceData);
        vTaskDelete(NULL);
    }
    
    static void http_request(request_type_t type)
    {
        if (type == BAIDU_VOICE_REQUEST) {
            get_access_token();
    
            esp_http_client_config_t config = {
                .url = baidu_asr_url,
                .method = HTTP_METHOD_POST,
                .timeout_ms = 5000,
                .event_handler = _http_baidu_voice_event_handler,
            };
    
            esp_http_client_handle_t client = esp_http_client_init(&config);
            esp_http_client_set_header(client, "Content-Type", "audio/pcm;rate=16000");
            esp_http_client_set_post_field(client, (const char *)voiceData, voiceDataLoc);
            esp_err_t err = esp_http_client_perform(client);
            if (err == ESP_OK) {
                ESP_LOGI(TAG, "HTTP POST Status = %d, content_length = %d",
                        esp_http_client_get_status_code(client),
                        esp_http_client_get_content_length(client));
            } else {
                ESP_LOGE(TAG, "HTTP POST request failed: %s", esp_err_to_name(err));
            }
            esp_http_client_cleanup(client);
            voiceDataLoc = 0;
        } else if (type == CHATGPT_REQUEST) {
            esp_http_client_config_t config = {
                .url = GPT_BASE_URL,
                .method = HTTP_METHOD_POST,
                .timeout_ms = 5000,
                .event_handler = _http_chatgpt_event_handler,
            };
            esp_http_client_handle_t client = esp_http_client_init(&config);
                ESP_LOGI(TAG, "[gpt_post_load]: %s, len: %d", gpt_post_load, strlen(gpt_post_load));
            esp_http_client_set_header(client, "Content-Type", "application/json");
            esp_http_client_set_post_field(client, (const char *)gpt_post_load, strlen(gpt_post_load));
            esp_err_t err = esp_http_client_perform(client);
            if (err == ESP_OK) {
                ESP_LOGI(TAG, "HTTP POST Status = %d, content_length = %d",
                        esp_http_client_get_status_code(client),
                        esp_http_client_get_content_length(client));
            } else {
                ESP_LOGE(TAG, "HTTP POST request failed: %s", esp_err_to_name(err));
            }
            esp_http_client_cleanup(client);
        } else {
            ESP_LOGE(TAG, "http_request type error");
            return;
        }
    }
    

   因为平时不停地接触各种项目,我越来越发现一个好的UI界面,对于项目是非常加分的。但是我没有正式地学过UI设计,也没成体系地练习过PS、AI这种设计工具。所以在UI界面上,我使用了X-TRACK框架,它是基于MVC架构设计的。作者的代码能力很强,还实现了页面生命周期管理、消息发布订阅、资源中心,框架层、应用层和HAL硬件抽象层的分离,整套代码就是一个字:漂亮。我对它的页面做了一些魔改,主页显示各种天气信息。

FtV3FP_2o9-Xt5YY8h74KhJIb_rV

效果展示

FhHWcD0q-M0aTNaPkPDcTkYh97z3

FoNqmOWb5V_28u9Gczq-mZSMFthe

活动感想

   在这次活动中,确实学到了非常多的东西,首先是通过ESP32音视频开发,接触了ESP-idf、adf、skainet这几个开发框架。为了实现智能聊天,开发过程中还接触了百度语音识别、ChatGPT、Claude、文心一言、Edge-TTS这一系列接口,其中一些接口不是直接开放的,我不得不用Python搭建局域网服务代理,写了一些Python程序。最后是UI界面方面,我整体地看了X-TRACK的UI框架源码,学习了各种的机制是怎么实现的,并在此基础上做了一些魔改,可以说是收获颇丰。在交流群里也认识了很多志同道合的小伙伴,很期待能够在接下来地活动中继续和大家见面。

  最后,感谢硬禾学堂和得捷电子联合举办的Funpack活动,祝硬禾的活动越办越好!

附件下载
chatbox.zip
主程序
gpt-server.zip
文心一言服务端
tts-server.zip
edge-tts服务端
团队介绍
个人
团队成员
枫雪天
评论
0 / 100
查看更多
目录
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2024 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号