Funpack2-5 基于ESP32-S3的智能语音播报器
这个项目使用了ESP-BOX-Lite作为核心,通过本地服务器向Google Bard查询当前的天气信息和新闻,并使用嵌入式端的TTS功能将查询结果通过扬声器播放出来。
标签
嵌入式系统
Funpack活动
ESP-BOX
智能终端
2x3j
更新2023-08-07
中国海洋大学
439
整体介绍
 
这个项目使用了ESP-BOX-Lite作为核心,通过本地服务器向Google Bard查询当前的天气信息和新闻,并使用嵌入式端的TTS功能将查询结果通过扬声器播放出来。
 
FvQlf-J9zjxzhuN5wxfkUCjJrk-1

ESP32-S3-BOX-Lite 是目前对应的 AIoT 应用开发板,搭载支持 AI 加速的 ESP32-S3 Wi-Fi + Bluetooth 5 (LE) SoC。为用户提供了一个基于语音助手、传感器、红外控制器和智能 Wi-Fi 网关等功能开发和控制智能家居设备的平台。开发板出厂支持离线语音交互功能,用户通过乐鑫丰富的 SDK 和解决方案,能够轻松构建在线和离线语音助手、智能语音设备、HMI 人机交互设备、控制面板、多协议网关等多样的应用。
 
官方连接:https://www.espressif.com.cn/zh-hans/news/ESP32-S3-BOX_video
 
实现过程
 
这个项目的思路和乐鑫官方最近推出的ESP32-S3-BOX直接向ChatGPT提问的Demo是很相近的,但是目前ESP32直接向ChatGPT发起请求面临的问题是耗时较长。具体来说需要先将用户提问的语音转为文本,再将文本发送给ChatGPT的API。AI答复返回给ESP32又需要一定的时间,而且为了让ESP32播放答案又需要用到文本转语音的在线服务,这就导致整个过程需要耗费比较久的时间。为了避免这个问题,我在具体实现时使用了Google Bard作为提问对象。虽然说回答质量可能不够高,但是它的响应速度是比较快的。同时在TTS时,我使用了本地TTS以节约时间。

实现过程大致可分为以下几个步骤:首先是配置ESP IDF环境,之后下载官方的ESP32 box的代码。在编译完基础例子之后学习LVGL图形界面、设备联网和使用乐鑫提供的chinese-tts库实现本地文字转语音的功能。

在对上述功能进行学习实现后,使用Python语言编写一个简单的服务器程序。令ESP32将请求发送到服务器上,服务器根据请求的内容向Google Bard发起具体的问题,并将得到的结果返回给ESP32。
 
具体流程如下图所示:
FmeOt-fWmsNKSE1jurvVk6XlojVb
 
具体介绍

项目整体可分为以下两个部分:一部分代码是在ESP32上面运行的嵌入式代码,另外部分是一个简单的服务器端代码。

首先是这个嵌入式端的代码,weather_report.c文件内部包含了启动代码并对ESP32进行了一些配置。self-lvgl.c文件当中实现了一个简单的GUI界面。界面包含了三个按钮分别是whether news和introduce,分别对应着三个参数,对应天气、新闻查询和一个简单的自我功能介绍。具体实现如下,首先创建横向list,之后逐个添加按钮。
static void menu_display(void)
{
    lv_indev_t *indev = lv_indev_get_next(NULL);

    if (lv_indev_get_type(indev) == LV_INDEV_TYPE_KEYPAD) {
        g_btn_op_group = lv_group_create();
        lv_indev_set_group(indev, g_btn_op_group);
    }

    // 创建横向分布的list,分别显示 Weather News 两个选项
    // 居中显示
    lv_obj_t *list = lv_list_create(lv_scr_act());
    lv_obj_set_size(list, 230, 100);
    lv_obj_set_style_border_width(list, 0, LV_STATE_DEFAULT);
    lv_obj_align(list, LV_ALIGN_LEFT_MID, 50, 0);

    // Weather
    lv_obj_t *btn = lv_list_add_btn(list, NULL, "Weather");
    lv_group_add_obj(g_btn_op_group, btn);
    lv_obj_add_event_cb(btn, btn_event_cb, LV_EVENT_CLICKED, (void *) "Weather");

    // News
    btn = lv_list_add_btn(list, NULL, "News");
    lv_group_add_obj(g_btn_op_group, btn);
    lv_obj_add_event_cb(btn, btn_event_cb, LV_EVENT_CLICKED, (void *) "News");

    // Jokes
    btn = lv_list_add_btn(list, NULL, "Introduce");
    lv_group_add_obj(g_btn_op_group, btn);
    lv_obj_add_event_cb(btn, btn_event_cb, LV_EVENT_CLICKED, (void *)"Jokes");
}
 
其次是self-wifi.c文件,这个文件当中实现了具体的联网代码,并且封装了一个简单的request请求,可以接受按钮参数。这部分代码的核心是下面的回调函数,里面对不同的网络事件进行了处理,比较关键的是HTTP_EVENT_ON_DATA,内部详细定义了响应数据的接收方式。数据最终被存储在output_buffer,以供语音播放。
static char* output_buffer;
static int output_len;
static esp_err_t _http_event_handle(esp_http_client_event_t* evt) {
    switch (evt->event_id) {
        case HTTP_EVENT_ERROR:
            ESP_LOGI(TAG, "HTTP_EVENT_ERROR");
            free(output_buffer);
            output_buffer = NULL;
            output_len = 0;
            break;
        case HTTP_EVENT_ON_CONNECTED:
            ESP_LOGI(TAG, "HTTP_EVENT_ON_CONNECTED");
            break;
        case HTTP_EVENT_HEADER_SENT:
            ESP_LOGI(TAG, "HTTP_EVENT_HEADER_SENT");
            break;
        case HTTP_EVENT_ON_HEADER:
            ESP_LOGI(TAG, "HTTP_EVENT_ON_HEADER");
            printf("%.*s", evt->data_len, (char*)evt->data);
            break;
        case HTTP_EVENT_ON_DATA:
            ESP_LOGI(TAG, "HTTP_EVENT_ON_DATA, len=%d", evt->data_len);
            // if (!esp_http_client_is_chunked_response(evt->client)) {
            //     printf("%.*s", evt->data_len, (char*)evt->data);
            // }
            if(evt->user_data){
                memcpy(evt->user_data + output_len, evt->data, evt->data_len);
            } else {
                if (output_buffer == NULL) {
                    output_buffer = (char*)malloc(
                        esp_http_client_get_content_length(evt->client));
                    output_len = 0;
                    if (output_buffer == NULL) {
                        ESP_LOGE(TAG,
                                 "Failed to allocate memory for output buffer");
                        return ESP_FAIL;
                    }
                }
                memcpy(output_buffer + output_len, evt->data, evt->data_len);
            }
            output_len += evt->data_len;

            break;
        case HTTP_EVENT_ON_FINISH:
            ESP_LOGI(TAG, "HTTP_EVENT_ON_FINISH");
            break;
        case HTTP_EVENT_DISCONNECTED:
            ESP_LOGI(TAG, "HTTP_EVENT_DISCONNECTED");
            break;
    }
    return ESP_OK;
}
最后一个文件是self-tts.c,这个文件基本上修改自官方的Chinese TTS代码,可以实现将中文文本实时播放到嵌入式设备的喇叭上。相对关键的部分是play_word函数,其使用内置tts功能播放传入的中文答复。函数的主体是循环调用esp_tts_stream_play函数,获得对应的语音,之后将其通过I2S播放。
void play_words(char *words) 
{
    printf("words: %s\n", words);
    if (esp_tts_parse_chinese(tts_handle, words))  // 文字解析成拼音
    {
        int len[1] = {0};
        size_t i2s_bytes_write = 0;
        do {
            short *pcm_data =
                esp_tts_stream_play(tts_handle, len, 0);  // 拼音转换成pcm音频
            i2s_write(I2S_NUM_0, pcm_data, len[0] * 2, &i2s_bytes_write,
                      portMAX_DELAY);  // 播放pcm音频
        } while (len[0] > 0);
    }
    esp_tts_stream_reset(tts_handle);  // 重置 tts 流并清除 TTS 实例的所有缓存
}

服务器端代码采用Flask框架编写,并且安装了从GitHub上面找到bardapi库。根据用户token值去模拟web端向bard发起提问,在一个比较短时间内就可以得到回复。由于Bard得到回复通常情况下是比较长的文本,并且可能包含一些强调字符等各种各样情况所以在具体实现这个函数时我对结果加了一个简单的过滤,删除了多余的符号让这个结果变得更加简洁,同时为了避免新闻这样的提问返回的内容过长,在提问时直接限制Bard将这个答案长度限制在一百个字符。
@app.route('/ask', methods=['GET'])
def ask():
    esp_question = request.args.get('q')
    if esp_question:
        real_question = ""
        if esp_question == 'Weather':
            real_question = "今天青岛的天气"
        elif esp_question == 'News':
            real_question = "今天的中国新闻,答复限制在100字"
        else:
            return "我是智能语音助手,你可以问我天气,新闻信息"
        return get_bard_answer(real_question)
    else:
        return 'question is empty'


if __name__ == '__main__':
    # 局域网运行
    app.run(host='0.0.0.0', port=5000, debug=True)
 
总结
 
最后,对这个项目做一个简单的总结。由于这是我首次接触ESP-IDF的编程,发现它还是比较困难的。官方的ESP-Box仓库实际上支持了ESP32和ESP-Box两套设备,但是最新的代码其实只支持ESP-Box,所以在具体安装框架的时候需要优先安装一个比较旧的版本才能够正常使用。我在实践的时候一开始安装了最新的IDF 5.0,发现不能编译ESP-Box-Lite的程序,之后又多次切换,最后使用了4.4版本。这个问题说明官方的支持整体还是比较乱,对于我这样的新手来说用起来还是有点吃力。

在这个项目当中,本地播放TTS结果时还有一定的问题。由于这个TTS代码其实是官方闭源的,他只是提供了API接口,我不能够直接去修改内部的一些参数。我在直接播放之后发现本地的TTS会有一个声音过于尖锐的问题,导致播放时很难听清楚。查看官方文档之后发现频率应该是16KHz,在初始化内部音频编解码芯片时频率也是16KHz。两边虽然相等,但是最终结果就会变得特别尖锐,跟官方提供的示例音频差很多。我在具体实现时去修改了初始化音频编解码芯片时的频率,将频率调整成了11KHz,听起来就会比较好了,就能够比较清楚地辨别。经过这次编程测试也发现了本地TTS面临的一个问题,就是不能够正确理解语义。当遇到文本中的多音字时就会出现不能够正确识别读音的情况,导致语音连贯性不够好。

目前最终做出来的效果还是主要为了保障TTS功能的正确性,所以花费了大部分时间在调整这一套流程上面。当这套流程变得比较完整之后,其实可以扩展一些更多的功能,比方说利用ESP-Box内部的GPIO接一些人体存在传感器,当检测到有人之后去发送一些固定的问题,并且播放答案,可以实现智能音箱的效果。同时ESP32官方也提供了实时唤醒词检测这样的功能,也可以使用这个功能综合性地去做一个比较不错的项目。但目前AI模型方面也有一些问题,就是不管是Google API还是Bing API或者Chat-GPT API,他们目前的框架还是比较不稳定的,一个请求需要花费比较长的时间,对于嵌入式设备来说,这样的体验是比较难受的。
附件下载
weather-report.zip
团队介绍
个人团队
团队成员
2x3j
评论
0 / 100
查看更多
目录
硬禾服务号
关注最新动态
0512-67862536
info@eetree.cn
江苏省苏州市苏州工业园区新平街388号腾飞创新园A2幢815室
苏州硬禾信息科技有限公司
Copyright © 2023 苏州硬禾信息科技有限公司 All Rights Reserved 苏ICP备19040198号