按 GSD 框架 .planning/milestones/digital_human_rtc/phases/phase_04_emotion_mapping/
规划完成 Phase 4 情绪映射 + 运行时切换。
## 核心变更
### bg_gif_demo 新增运行时切换接口
```c
esp_err_t bg_gif_demo_switch_gif(const char *new_gif_path);
```
实现要点:
- 先 heap_caps_free(g_gif_data) 释放旧 PSRAM,再加载新 GIF,单峰仅一份
- 内部 static last_gif_path 去重(相同路径直接返回 ESP_OK)
- 切换后立即 lv_timer_set_period(timer, 20) 防止 lv_gif_set_src 重建为默认 10ms
- LVGL 锁保护 200ms 超时
### 22 情绪 → 3 hiyori GIF 映射表
定义在 main/dzbj/ai_chat_ui.c USE_BG_GIF_POC 包裹内:
| GIF | 情绪标签 (个数) |
|-----|----------------|
| m06 (默认/积极) | neutral, happy, laughing, funny, loving, relaxed, delicious, kissy, confident, silly, blink, curious (12) |
| m07 (思考/疲倦) | sleepy, thinking, confused, embarrassed, dizzy (5) |
| m03 (负面/严肃) | sad, crying, angry, surprised, shocked (5) |
22 个标准情绪 100% 覆盖,未映射的情绪默认 fallback 到 m06。
### ai_chat_set_emotion 改造
PoC 模式下优先切换数字人大图(不再切隐藏的 emoji 200x89):
```c
#ifdef USE_BG_GIF_POC
if (bg_gif_demo_is_running()) {
const char *path = find_hiyori_gif(emotion);
bg_gif_demo_switch_gif(path);
return;
}
#endif
// 非 PoC 模式 fallback emoji 路径保留
```
## 调用链(已与现有 RTC 字幕解析对接,无需改 application.cc)
RTC 字幕 → application.cc:1419 display->SetEmotion(emo)
→ AiChatDisplay::SetEmotion (display/ai_chat_display.cc)
→ ai_chat_set_emotion (dzbj/ai_chat_ui.c)
→ bg_gif_demo_switch_gif (PoC 模式)
→ 数字人 hiyori GIF 切换
## 烧录验证(用户实测)
监控 60s 期间捕获情绪切换:
- neutral / happy → m06(已在播,去重)
- thinking → m07 切换成功 (590ms 延迟)
- confused → m07 去重跳过
- AI 回复结束 → 自动回到 neutral
- 无 abort / 重启
m06 ↔ m07 切换屏幕可视确认,m03 走相同代码路径无需重复测试。
## GSD 文档(同时提交)
- .planning/milestones/digital_human_rtc/phases/phase_04_emotion_mapping/PLAN.md
- .planning/milestones/digital_human_rtc/phases/phase_04_emotion_mapping/EMOTION_REPORT.md
407 lines
15 KiB
C
407 lines
15 KiB
C
#include "ai_chat_ui.h"
|
||
#include "lvgl.h"
|
||
#include "esp_lvgl_port.h"
|
||
#include "esp_log.h"
|
||
#include <string.h>
|
||
|
||
// ====================================================================
|
||
// PoC 开关(验证不同显示方案)
|
||
// USE_SPRITE_POC — sprite_test.bin(RGB565 raw)方案(已弃用)
|
||
// USE_DUAL_GIF_POC — 两个 GIF 循环播放(已弃用)
|
||
// USE_BG_GIF_POC — 背景图 + 透明 GIF 叠加(方案 C,当前启用 ⭐)
|
||
// 都注释:恢复原 GIF 眨眼显示
|
||
// ====================================================================
|
||
// #define USE_SPRITE_POC
|
||
// #define USE_DUAL_GIF_POC
|
||
#define USE_BG_GIF_POC
|
||
|
||
#ifdef USE_SPRITE_POC
|
||
#include "sprite_demo.h"
|
||
#endif
|
||
#ifdef USE_DUAL_GIF_POC
|
||
#include "dual_gif_demo.h"
|
||
#endif
|
||
#ifdef USE_BG_GIF_POC
|
||
#include "bg_gif_demo.h"
|
||
#endif
|
||
|
||
// lv_gif API 通过 lvgl.h 自动包含(LV_USE_GIF=y 时)
|
||
|
||
static const char *TAG = "AI_CHAT_UI";
|
||
|
||
// 声明阿里巴巴普惠体 20px 4bpp(GB2312 简体中文+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
|
||
|
||
#ifdef USE_BG_GIF_POC
|
||
// Phase 4: 数字人 hiyori GIF 路径映射表(22 情绪 → 3 GIF)
|
||
typedef struct {
|
||
const char *emotion;
|
||
const char *hiyori_gif_path;
|
||
} hiyori_emotion_map_t;
|
||
|
||
static const hiyori_emotion_map_t hiyori_emotion_map[] = {
|
||
// 默认/积极 → m06
|
||
{"neutral", "/spiflash/hiyori_m06.gif"},
|
||
{"happy", "/spiflash/hiyori_m06.gif"},
|
||
{"laughing", "/spiflash/hiyori_m06.gif"},
|
||
{"funny", "/spiflash/hiyori_m06.gif"},
|
||
{"loving", "/spiflash/hiyori_m06.gif"},
|
||
{"relaxed", "/spiflash/hiyori_m06.gif"},
|
||
{"delicious", "/spiflash/hiyori_m06.gif"},
|
||
{"kissy", "/spiflash/hiyori_m06.gif"},
|
||
{"confident", "/spiflash/hiyori_m06.gif"},
|
||
{"silly", "/spiflash/hiyori_m06.gif"},
|
||
{"blink", "/spiflash/hiyori_m06.gif"},
|
||
{"curious", "/spiflash/hiyori_m06.gif"},
|
||
// 思考/疲倦 → m07
|
||
{"sleepy", "/spiflash/hiyori_m07.gif"},
|
||
{"thinking", "/spiflash/hiyori_m07.gif"},
|
||
{"confused", "/spiflash/hiyori_m07.gif"},
|
||
{"embarrassed", "/spiflash/hiyori_m07.gif"},
|
||
{"dizzy", "/spiflash/hiyori_m07.gif"},
|
||
// 负面/严肃 → m03
|
||
{"sad", "/spiflash/hiyori_m03.gif"},
|
||
{"crying", "/spiflash/hiyori_m03.gif"},
|
||
{"angry", "/spiflash/hiyori_m03.gif"},
|
||
{"surprised", "/spiflash/hiyori_m03.gif"},
|
||
{"shocked", "/spiflash/hiyori_m03.gif"},
|
||
};
|
||
#define HIYORI_EMOTION_MAP_SIZE (sizeof(hiyori_emotion_map) / sizeof(hiyori_emotion_map[0]))
|
||
|
||
static const char* find_hiyori_gif(const char *emotion) {
|
||
for (int i = 0; i < HIYORI_EMOTION_MAP_SIZE; i++) {
|
||
if (strcmp(emotion, hiyori_emotion_map[i].emotion) == 0) {
|
||
return hiyori_emotion_map[i].hiyori_gif_path;
|
||
}
|
||
}
|
||
return "/spiflash/hiyori_m06.gif"; // 默认/未映射 → m06
|
||
}
|
||
#endif // USE_BG_GIF_POC
|
||
|
||
// 表情→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.5,icon高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);
|
||
|
||
#ifdef USE_SPRITE_POC
|
||
// === Sprite Sheet PoC:用 RGB565 raw 替代 GIF 显示 ===
|
||
// 隐藏原有 GIF 表情对象(避免 LVGL 与 DMA 同时刷新同一区域)
|
||
#if LV_USE_GIF
|
||
if (gif_emotion) {
|
||
lv_obj_add_flag(gif_emotion, LV_OBJ_FLAG_HIDDEN);
|
||
}
|
||
if (gif_icon) {
|
||
lv_obj_add_flag(gif_icon, LV_OBJ_FLAG_HIDDEN);
|
||
}
|
||
#endif
|
||
// 启动 sprite sheet 播放(从 SPIFFS 加载到 PSRAM,DMA 直接写 LCD)
|
||
esp_err_t ret = sprite_demo_start("/spiflash/sprite_test.bin");
|
||
if (ret == ESP_OK) {
|
||
ESP_LOGI(TAG, "Sprite PoC 启动成功");
|
||
} else {
|
||
ESP_LOGW(TAG, "Sprite PoC 启动失败: %s,回退到 LVGL", esp_err_to_name(ret));
|
||
// 失败时显示原 GIF
|
||
#if LV_USE_GIF
|
||
if (gif_emotion) lv_obj_clear_flag(gif_emotion, LV_OBJ_FLAG_HIDDEN);
|
||
#endif
|
||
}
|
||
#endif // USE_SPRITE_POC
|
||
|
||
#ifdef USE_DUAL_GIF_POC
|
||
// === 双 GIF 循环播放 PoC ===
|
||
// 隐藏原有 GIF 表情控件,由 dual_gif_demo 创建新的 lv_gif 控件接管
|
||
#if LV_USE_GIF
|
||
if (gif_emotion) {
|
||
lv_obj_add_flag(gif_emotion, LV_OBJ_FLAG_HIDDEN);
|
||
}
|
||
if (gif_icon) {
|
||
lv_obj_add_flag(gif_icon, LV_OBJ_FLAG_HIDDEN);
|
||
}
|
||
#endif
|
||
// GIF 时长(由 Python 脚本预先测得):
|
||
// dance_0_gesture.gif: 58 帧 = 2900 ms
|
||
// tapbody_0_greet.gif: 100 帧 = 5000 ms
|
||
esp_err_t dgret = dual_gif_demo_start(
|
||
"/spiflash/dance_0_gesture.gif", 2900,
|
||
"/spiflash/tapbody_0_greet.gif", 5000);
|
||
if (dgret == ESP_OK) {
|
||
ESP_LOGI(TAG, "Dual GIF PoC 启动成功");
|
||
} else {
|
||
ESP_LOGW(TAG, "Dual GIF PoC 启动失败: %s", esp_err_to_name(dgret));
|
||
#if LV_USE_GIF
|
||
if (gif_emotion) lv_obj_clear_flag(gif_emotion, LV_OBJ_FLAG_HIDDEN);
|
||
#endif
|
||
}
|
||
#endif // USE_DUAL_GIF_POC
|
||
|
||
#ifdef USE_BG_GIF_POC
|
||
// === 背景图 + 透明 Hiyori GIF 叠加(方案 C)===
|
||
// 隐藏原有 GIF 表情控件,由 bg_gif_demo 创建新的图层
|
||
#if LV_USE_GIF
|
||
if (gif_emotion) {
|
||
lv_obj_add_flag(gif_emotion, LV_OBJ_FLAG_HIDDEN);
|
||
}
|
||
if (gif_icon) {
|
||
lv_obj_add_flag(gif_icon, LV_OBJ_FLAG_HIDDEN);
|
||
}
|
||
#endif
|
||
esp_err_t bgret = bg_gif_demo_start(
|
||
"/spiflash/Background_360x360.jpg",
|
||
"/spiflash/hiyori_m06.gif"); // Phase 3: m06 默认表情(neutral/积极),240x320 居中
|
||
if (bgret == ESP_OK) {
|
||
ESP_LOGI(TAG, "BG+GIF PoC 启动成功");
|
||
} else {
|
||
ESP_LOGW(TAG, "BG+GIF PoC 启动失败: %s", esp_err_to_name(bgret));
|
||
#if LV_USE_GIF
|
||
if (gif_emotion) lv_obj_clear_flag(gif_emotion, LV_OBJ_FLAG_HIDDEN);
|
||
#endif
|
||
}
|
||
#endif // USE_BG_GIF_POC
|
||
}
|
||
|
||
void ai_chat_set_status(const char* status) {
|
||
if (!status_label) return;
|
||
#if LV_USE_GIF
|
||
// GIF 模式下隐藏状态文字,仅记录日志
|
||
ESP_LOGD(TAG, "状态: %s(GIF模式不显示)", 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;
|
||
|
||
ESP_LOGI(TAG, "GIF表情切换: %s", emotion);
|
||
|
||
#ifdef USE_BG_GIF_POC
|
||
// PoC 模式:bg_gif_demo 在运行时优先切换数字人 hiyori GIF
|
||
if (bg_gif_demo_is_running()) {
|
||
const char *path = find_hiyori_gif(emotion);
|
||
esp_err_t r = bg_gif_demo_switch_gif(path);
|
||
if (r != ESP_OK) {
|
||
ESP_LOGW(TAG, "hiyori 切换失败: %s, ret=%s", path, esp_err_to_name(r));
|
||
}
|
||
return; // 不再走 emoji 路径(emoji 控件已被隐藏)
|
||
}
|
||
#endif
|
||
|
||
#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
|
||
}
|
||
|
||
// 切换表情 GIF(lv_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;
|
||
}
|