Baji_Rtc_Toy/main/dzbj/ai_chat_ui.c
Rdzleo 919bf8f28f feat: GIF动画表情系统 + 情绪映射增强 + HTTPS音频中止修复
一、新增功能:
1、新增8种GIF动画表情(200x89) + 3种叠加图标(45x45),实现22种情绪标签到GIF的映射表;
2、新增30+组英文近义词情绪fallback映射(如worried→sad),防止AI使用非标准标签时GIF无法切换;
3、新增HTTPS中止后诊断日志,自动追踪前20帧音频处理流程便于定位无声问题;

二、Bug修复:
4、修复HTTPS播放中止后RTC音频解码参数未恢复(16000/60→8000/20),通过background_task_串行化恢复;
5、修复AbortHttpsPlayback解码器竞态崩溃,将重置/恢复/DMA flush全部串行化执行;
6、修复LVGL gifdec不支持无全局颜色表GIF的问题,支持仅使用局部颜色表的压缩GIF;
7、修复GIF透明区域显示黑色方块,canvas初始alpha改为0x00;
8、修复lv_gif定时器gif对象为NULL时的空指针崩溃;

三、优化:
9、情绪标签从等待is_final改为第一条字幕即时触发GIF切换,新增去重和回复结束自动恢复neutral;
10、对话状态表情映射优化:THINKING→thinking、ANSWERING→happy、INTERRUPTED→surprised;
11、CPU核心绑定:LVGL任务Core0,音频循环Core1,避免GIF解码与音频争抢;
12、中文情绪词映射扩展,新增担心/心疼/着急等映射;

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-19 15:28:14 +08:00

251 lines
9.0 KiB
C
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#include "ai_chat_ui.h"
#include "lvgl.h"
#include "esp_lvgl_port.h"
#include "esp_log.h"
#include <string.h>
// lv_gif API 通过 lvgl.h 自动包含LV_USE_GIF=y 时)
static const char *TAG = "AI_CHAT_UI";
// 声明阿里巴巴普惠体 20px 4bppGB2312 简体中文+ASCII
LV_FONT_DECLARE(font_puhui_20_4);
#if LV_USE_GIF
// 需要访问 lv_gif_t 内部结构(暂停/恢复定时器)
#include "extra/libs/gif/lv_gif.h"
// 声明 GIF 表情资源压缩优化200x89
LV_IMG_DECLARE(emotion_angry_200_89);
LV_IMG_DECLARE(emotion_blink1_200_89);
LV_IMG_DECLARE(emotion_blink_fast_200_89);
LV_IMG_DECLARE(emotion_blink_slow_200_89);
LV_IMG_DECLARE(emotion_dizzy_200_89);
LV_IMG_DECLARE(emotion_happy_200_89);
LV_IMG_DECLARE(emotion_sad_200_89);
LV_IMG_DECLARE(emotion_sleep_200_89);
// 声明 GIF 图标资源压缩优化45x45
LV_IMG_DECLARE(icon_emotion_confused_45);
LV_IMG_DECLARE(icon_emotion_sleep_45);
LV_IMG_DECLARE(icon_emotion_thinking_45);
#endif
// 声明旧 PNG 表情资源LV_USE_GIF 未启用时的回退)
LV_IMG_DECLARE(ui_img_neutral_png);
LV_IMG_DECLARE(ui_img_happy_png);
LV_IMG_DECLARE(ui_img_sad_png);
LV_IMG_DECLARE(ui_img_angry_png);
LV_IMG_DECLARE(ui_img_crying_png);
LV_IMG_DECLARE(ui_img_funny_png);
LV_IMG_DECLARE(ui_img_laughing_png);
// 屏幕和控件
static lv_obj_t *ai_screen = NULL;
static lv_obj_t *status_label = NULL;
static lv_obj_t *chat_label = NULL;
#if LV_USE_GIF
static lv_obj_t *gif_emotion = NULL; // GIF 表情对象
static lv_obj_t *gif_icon = NULL; // GIF 图标对象(叠加在表情右上角)
static bool gif_animation_paused = false;
#else
static lv_obj_t *emoji_img = NULL; // 旧 PNG 表情
#endif
// 背景色(纯黑,与 GIF 背景一致避免色差)
#define BG_COLOR 0x000000
// 表情→GIF 映射表
typedef struct {
const char *name;
const lv_img_dsc_t *emotion_gif;
const lv_img_dsc_t *icon_gif; // NULL 表示无叠加图标
} emotion_gif_entry_t;
#if LV_USE_GIF
static const emotion_gif_entry_t emotion_gif_map[] = {
{"neutral", &emotion_blink_slow_200_89, NULL},
{"happy", &emotion_happy_200_89, NULL},
{"laughing", &emotion_happy_200_89, NULL},
{"funny", &emotion_happy_200_89, NULL},
{"loving", &emotion_happy_200_89, NULL},
{"relaxed", &emotion_happy_200_89, NULL},
{"delicious", &emotion_happy_200_89, NULL},
{"kissy", &emotion_happy_200_89, NULL},
{"confident", &emotion_happy_200_89, NULL},
{"sad", &emotion_sad_200_89, NULL},
{"crying", &emotion_sad_200_89, NULL},
{"angry", &emotion_angry_200_89, NULL},
{"surprised", &emotion_blink_fast_200_89, NULL},
{"shocked", &emotion_blink_fast_200_89, NULL},
{"silly", &emotion_blink_fast_200_89, NULL},
{"embarrassed", &emotion_blink_fast_200_89, &icon_emotion_thinking_45},
{"thinking", &emotion_blink_fast_200_89, &icon_emotion_thinking_45},
{"confused", &emotion_blink_fast_200_89, &icon_emotion_confused_45},
{"curious", &emotion_blink_fast_200_89, &icon_emotion_confused_45},
{"dizzy", &emotion_dizzy_200_89, NULL},
{"blink", &emotion_blink1_200_89, NULL},
{"sleepy", &emotion_sleep_200_89, &icon_emotion_sleep_45},
};
#define EMOTION_GIF_MAP_SIZE (sizeof(emotion_gif_map) / sizeof(emotion_gif_map[0]))
#endif
void ai_chat_screen_init(void) {
// 创建 AI 对话屏幕
ai_screen = lv_obj_create(NULL);
lv_obj_set_style_bg_color(ai_screen, lv_color_hex(BG_COLOR), 0);
lv_obj_set_style_bg_opa(ai_screen, 255, 0);
lv_obj_clear_flag(ai_screen, LV_OBJ_FLAG_SCROLLABLE);
#if LV_USE_GIF
// GIF 表情屏幕正中央200x89
gif_emotion = lv_gif_create(ai_screen);
lv_gif_set_src(gif_emotion, &emotion_blink_slow_200_89);
lv_obj_align(gif_emotion, LV_ALIGN_CENTER, 0, 0);
// 降低 GIF 定时器频率10ms→20ms平衡动画流畅度与 CPU 占用
lv_gif_t *gifobj = (lv_gif_t *)gif_emotion;
lv_timer_set_period(gifobj->timer, 20);
// GIF 图标表情上方居中45x45
// 表情高89顶边y=-44.5icon高45中心再上移几像素避免重叠
gif_icon = lv_gif_create(ai_screen);
lv_obj_align(gif_icon, LV_ALIGN_CENTER, 0, -70);
lv_obj_add_flag(gif_icon, LV_OBJ_FLAG_HIDDEN);
#else
// 旧 PNG 表情(回退方案)
emoji_img = lv_img_create(ai_screen);
lv_img_set_src(emoji_img, &ui_img_neutral_png);
lv_obj_align(emoji_img, LV_ALIGN_CENTER, 0, 0);
#endif
// 状态文本暂时隐藏GIF 模式下不需要文字)
status_label = lv_label_create(ai_screen);
lv_obj_set_style_text_font(status_label, &font_puhui_20_4, 0);
lv_obj_set_style_text_color(status_label, lv_color_white(), 0);
lv_obj_set_width(status_label, 300);
lv_obj_set_style_text_align(status_label, LV_TEXT_ALIGN_CENTER, 0);
lv_label_set_text(status_label, "");
lv_obj_align(status_label, LV_ALIGN_CENTER, 0, 90);
#if LV_USE_GIF
lv_obj_add_flag(status_label, LV_OBJ_FLAG_HIDDEN);
#endif
// 聊天消息文本(暂时隐藏,不显示字幕)
chat_label = lv_label_create(ai_screen);
lv_obj_set_style_text_font(chat_label, &font_puhui_20_4, 0);
lv_obj_set_style_text_color(chat_label, lv_color_hex(0xAAAAAA), 0);
lv_obj_set_width(chat_label, 300);
lv_obj_set_style_text_align(chat_label, LV_TEXT_ALIGN_LEFT, 0);
lv_label_set_text(chat_label, "");
lv_obj_align(chat_label, LV_ALIGN_CENTER, 0, 50);
lv_label_set_long_mode(chat_label, LV_LABEL_LONG_WRAP);
lv_obj_add_flag(chat_label, LV_OBJ_FLAG_HIDDEN);
// 加载屏幕
lv_disp_load_scr(ai_screen);
}
void ai_chat_set_status(const char* status) {
if (!status_label) return;
#if LV_USE_GIF
// GIF 模式下隐藏状态文字,仅记录日志
ESP_LOGD(TAG, "状态: %sGIF模式不显示", status);
(void)status;
#else
lvgl_port_lock(50);
lv_label_set_text(status_label, status);
lvgl_port_unlock();
#endif
}
void ai_chat_resume_animation(void) {
#if LV_USE_GIF
if (!gif_emotion || !gif_animation_paused) return;
if (!lvgl_port_lock(200)) return;
lv_gif_t *gifobj = (lv_gif_t *)gif_emotion;
lv_timer_resume(gifobj->timer);
gif_animation_paused = false;
ESP_LOGI(TAG, "GIF动画已恢复播放");
lvgl_port_unlock();
#endif
}
void ai_chat_set_emotion(const char* emotion) {
if (!emotion) return;
#if LV_USE_GIF
if (!gif_emotion) return;
if (!lvgl_port_lock(200)) {
ESP_LOGW(TAG, "LVGL锁超时跳过表情切换: %s", emotion);
return;
}
// 查找映射表
const emotion_gif_entry_t *entry = NULL;
for (int i = 0; i < EMOTION_GIF_MAP_SIZE; i++) {
if (strcmp(emotion, emotion_gif_map[i].name) == 0) {
entry = &emotion_gif_map[i];
break;
}
}
// 未找到映射,默认使用 neutral
if (!entry) {
ESP_LOGW(TAG, "未映射的GIF表情: %s, 使用 neutral", emotion);
entry = &emotion_gif_map[0]; // neutral
}
// 切换表情 GIFlv_gif_set_src 内部已自动启动播放)
lv_gif_set_src(gif_emotion, entry->emotion_gif);
// set_src 内部会重建 10ms 定时器,重新设置为 50ms 降低 CPU 占用
lv_gif_t *gifobj = (lv_gif_t *)gif_emotion;
lv_timer_set_period(gifobj->timer, 20);
gif_animation_paused = false;
// 处理叠加图标
if (entry->icon_gif) {
lv_gif_set_src(gif_icon, entry->icon_gif);
lv_gif_t *icon_gifobj = (lv_gif_t *)gif_icon;
lv_timer_set_period(icon_gifobj->timer, 20);
lv_obj_clear_flag(gif_icon, LV_OBJ_FLAG_HIDDEN);
} else {
// 隐藏图标时暂停其定时器,避免空跑浪费 CPU
lv_gif_t *icon_gifobj = (lv_gif_t *)gif_icon;
if (icon_gifobj->gif) {
lv_timer_pause(icon_gifobj->timer);
}
lv_obj_add_flag(gif_icon, LV_OBJ_FLAG_HIDDEN);
}
ESP_LOGI(TAG, "GIF表情切换: %s%s", emotion, entry->icon_gif ? " +图标" : "");
lvgl_port_unlock();
#else
// 旧 PNG 回退逻辑
if (!emoji_img) return;
lvgl_port_lock(50);
const lv_img_dsc_t *img = &ui_img_neutral_png;
if (strcmp(emotion, "neutral") == 0) img = &ui_img_neutral_png;
else if (strcmp(emotion, "happy") == 0) img = &ui_img_happy_png;
else if (strcmp(emotion, "sad") == 0) img = &ui_img_sad_png;
else if (strcmp(emotion, "angry") == 0) img = &ui_img_angry_png;
else if (strcmp(emotion, "surprised") == 0) img = &ui_img_funny_png;
else if (strcmp(emotion, "crying") == 0) img = &ui_img_crying_png;
else if (strcmp(emotion, "laughing") == 0) img = &ui_img_laughing_png;
lv_img_set_src(emoji_img, img);
lvgl_port_unlock();
#endif
}
void ai_chat_set_chat_message(const char* role, const char* content) {
if (!chat_label) return;
// 字幕暂时隐藏,不更新内容
// 后续恢复时去掉 return 和 ai_chat_screen_init 中的 LV_OBJ_FLAG_HIDDEN
(void)role;
(void)content;
}