feat(rtc-only): Phase 3 - 数字人 GIF 资源准备(hiyori m03/m06/m07,209x360)
按 GSD 框架 .planning/milestones/digital_human_rtc/phases/phase_03_gif_resources/ 规划完成 Phase 3 数字人表情 GIF 资源处理。 ## 处理方式(与 PoC 阶段 hiyori_m05.gif 一致) ```bash gifsicle --resize _x360 -O3 input.gif -o output.gif ``` - 高度 = LCD 360px,宽度按原比例自动算 → 209px - 不裁剪(保持源 GIF 完整人物) - 不加 --lossy / --colors(保留 256 色,画质优先) - 只用 -O3 优化文件大小 ## 处理结果 | GIF | 用途 | 源 | 处理后 | 节省 | |-----|------|-----|--------|------| | m03 | 负面/严肃 | 407×700 3.3MB | 209×360 1.15MB | 66% | | m06 | 默认/积极 | 407×700 1.3MB | 209×360 0.44MB | 66% | | m07 | 思考/疲倦 | 407×700 1.2MB | 209×360 0.40MB | 66% | | 合计 | — | 5.7MB | 1.94MB | 66% | ## 决策过程(避免后续重复犯错) Phase 3 初稿曾尝试裁剪到 240×320 + PIL 全帧 bbox 居中裁剪, 用户烧录后反馈"视觉感官不好"——角色被横向压扁(240×320 纵横比 0.75 vs 源 407×700 纵横比 0.583)。回归 PoC 等比例缩放方式后效果与 PoC 一致。 PoC 处理标准已写入用户级 feedback memory(feedback_hiyori_gif_processing.md), 后续 hiyori GIF 处理一律用本方式,除非用户主动要求修改。 ## 显示效果(用户已目视确认) LCD 360×360 居中显示 209×360 GIF: - 垂直方向: 360 = 360,完全充满 - 横向: 209 < 360,左右各 75.5px 留边显示背景图 - 角色比例: 完整保留源 GIF 的 407:700 = 0.582 纵横比,人物细高自然 ## 删除项 - spiffs_image/hiyori_m05.gif (2.3MB) 已删除 - 被 m06/m07/m03 替代 文件历史保留在 git,可通过 git show eb96130:spiffs_image/hiyori_m05.gif 恢复 ## 默认表情切换 main/dzbj/ai_chat_ui.c:234: - PoC: bg_gif_demo_start(..., "/spiflash/hiyori_m05.gif") - Phase 3: bg_gif_demo_start(..., "/spiflash/hiyori_m06.gif") ## 烧录运行时验证 - 烧录后 0 次重启(连续监控 18 秒) - BG_GIF: GIF 已加载到 PSRAM: /spiflash/hiyori_m06.gif (441.8 KB) - AudioCodec: Audio codec started(首次冷启动直接成功) - 用户目视确认显示效果良好 ## GSD 文档(同时提交) - .planning/milestones/digital_human_rtc/phases/phase_03_gif_resources/PLAN.md - .planning/milestones/digital_human_rtc/phases/phase_03_gif_resources/GIF_REPORT.md ## SPIFFS 容量 新 SPIFFS 4.94MB 当前实际占用 ~2MB(40%),余量 ~2.94MB 充足。
This commit is contained in:
parent
ce7a3aad63
commit
7d1c7dc1f0
@ -0,0 +1,130 @@
|
|||||||
|
# GIF_REPORT — Phase 3 数字人 GIF 资源处理报告
|
||||||
|
|
||||||
|
> 阶段: `phase_03_gif_resources`
|
||||||
|
> 日期: 2026-05-13
|
||||||
|
> 状态: ✅ **完成**
|
||||||
|
|
||||||
|
## 1. 处理方式(与 PoC 阶段 hiyori_m05.gif 一致)
|
||||||
|
|
||||||
|
3 个 hiyori 表情 GIF 经 `tools/prepare_hiyori_gifs.py` 处理,**等比例缩小到高 360px,不裁剪**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gifsicle --resize _x360 -O3 input.gif -o output.gif
|
||||||
|
```
|
||||||
|
|
||||||
|
- `--resize _x360`: 高度 = LCD 360px,宽度按原比例自动算 → 209px
|
||||||
|
- `-O3`: 优化压缩
|
||||||
|
- **不加** `--lossy`(避免锯齿)
|
||||||
|
- **不加** `--colors`(保留 256 色,画质优先)
|
||||||
|
- **不裁剪**(保持源 GIF 完整人物)
|
||||||
|
|
||||||
|
## 2. 处理结果对比
|
||||||
|
|
||||||
|
| GIF | 用途 | 源尺寸 | 源大小 | 输出尺寸 | 输出大小 | 节省 |
|
||||||
|
|-----|------|--------|--------|---------|----------|------|
|
||||||
|
| m03 | 负面/严肃 | 407×700 | 3,376 KB | **209×360** | **1,149 KB** | **66.0%** ⬇ |
|
||||||
|
| m06 | 默认/积极 | 407×700 | 1,303 KB | **209×360** | **442 KB** | **66.1%** ⬇ |
|
||||||
|
| m07 | 思考/疲倦 | 407×700 | 1,173 KB | **209×360** | **399 KB** | **66.0%** ⬇ |
|
||||||
|
| **合计** | — | — | **5,852 KB (5.7 MB)** | — | **1,990 KB (1.94 MB)** | **66.0%** ⬇ |
|
||||||
|
|
||||||
|
PoC 阶段的 `hiyori_m05.gif` 也是 209×360(6.7MB → 2.3MB,节省 66.0%),处理参数完全一致。
|
||||||
|
|
||||||
|
## 3. 显示效果
|
||||||
|
|
||||||
|
LCD 360×360,GIF 209×360 居中显示:
|
||||||
|
- **垂直方向**: 360 = 360,完全充满 LCD 高度
|
||||||
|
- **横向**: 209 < 360,左右各 75.5px 留边显示背景图
|
||||||
|
- **角色比例**: 完整保留源 GIF 的 407:700 = 0.582 纵横比,人物细高自然
|
||||||
|
|
||||||
|
LVGL 代码(不变):
|
||||||
|
```c
|
||||||
|
bg_gif_demo_start(
|
||||||
|
"/spiflash/Background_360x360.jpg",
|
||||||
|
"/spiflash/hiyori_m06.gif"
|
||||||
|
);
|
||||||
|
// 内部 lv_obj_align(LV_ALIGN_CENTER, 0, 0) 自动居中
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. 决策过程(避免后续重复犯错)
|
||||||
|
|
||||||
|
Phase 3 初稿曾尝试**裁剪到 240×320**(PIL 全帧 bbox 居中裁剪),用户烧录后反馈视觉感官差。
|
||||||
|
原因分析:
|
||||||
|
- 240×320 的纵横比 0.75,源 407×700 的纵横比 0.583
|
||||||
|
- 强制裁剪后角色被"横向压扁",与原 Live2D 细高人物比例不符
|
||||||
|
- 视觉上看起来"角色变粗",违反 PoC 阶段已验证的良好效果
|
||||||
|
|
||||||
|
最终决策:**回归 PoC 等比例缩小方式**,处理标准已写入用户级 feedback memory(`feedback_hiyori_gif_processing.md`),后续除非用户主动修改,否则一律用本方式。
|
||||||
|
|
||||||
|
## 5. SPIFFS 容量使用
|
||||||
|
|
||||||
|
```
|
||||||
|
SPIFFS 容量: 4.94 MB (0x4F0000)
|
||||||
|
|
||||||
|
实际占用:
|
||||||
|
├── Background_360x360.jpg 20 KB
|
||||||
|
├── hiyori_m03.gif 1,149 KB
|
||||||
|
├── hiyori_m06.gif 442 KB
|
||||||
|
├── hiyori_m07.gif 399 KB
|
||||||
|
├── 02.jpg 20 KB (历史)
|
||||||
|
└── default.jpg 9 KB (历史)
|
||||||
|
合计: ~2.0 MB (40% 占用,~2.94 MB 余量)
|
||||||
|
```
|
||||||
|
|
||||||
|
`hiyori_m05.gif` (2.27 MB) **已删除**——被 m06/m07/m03 替代,文件历史保留在 git。
|
||||||
|
|
||||||
|
## 6. 烧录运行时验证
|
||||||
|
|
||||||
|
烧录后启动监控 18 秒(0 次重启):
|
||||||
|
|
||||||
|
```log
|
||||||
|
✅ I (1099) BG_GIF: 背景图已解码: 360x360 (253.1 KB RGB565)
|
||||||
|
✅ I (1489) BG_GIF: GIF 已加载到 PSRAM: /spiflash/hiyori_m06.gif (441.8 KB)
|
||||||
|
✅ I (1549) BG_GIF: ✓ 背景 + GIF 叠加显示启动
|
||||||
|
✅ I (1699) Airhub1: 🤖 AI对话模式启动
|
||||||
|
✅ I (3159) AudioCodec: Audio codec started ← 首次冷启动直接成功
|
||||||
|
```
|
||||||
|
|
||||||
|
用户目视确认:**显示效果与 PoC 一致,角色细高比例自然**。
|
||||||
|
|
||||||
|
## 7. 默认表情切换
|
||||||
|
|
||||||
|
`main/dzbj/ai_chat_ui.c:234`:
|
||||||
|
|
||||||
|
```c
|
||||||
|
esp_err_t bgret = bg_gif_demo_start(
|
||||||
|
"/spiflash/Background_360x360.jpg",
|
||||||
|
"/spiflash/hiyori_m06.gif"); // Phase 3: m06 默认(neutral/积极)
|
||||||
|
```
|
||||||
|
|
||||||
|
PoC 用 `hiyori_m05.gif` → Phase 3 切换到 m06(PoC 的 m05 已删除)。
|
||||||
|
|
||||||
|
## 8. 未在 Phase 3 验证的项目
|
||||||
|
|
||||||
|
| 项 | 推迟到 |
|
||||||
|
|----|--------|
|
||||||
|
| `m03` GIF 显示效果(负面情绪) | Phase 4(情绪映射时切换测试) |
|
||||||
|
| `m07` GIF 显示效果(睡眠) | Phase 4(情绪映射时切换测试) |
|
||||||
|
| GIF 切换内存泄漏(连续切 50 次 PSRAM 不减少) | Phase 4 实现 `bg_gif_demo_switch_gif()` 接口后 |
|
||||||
|
| 24h 长时间稳定性 | Phase 7 集成测试 |
|
||||||
|
|
||||||
|
3 个 GIF 用同一脚本同一参数处理,单独测试 m03/m07 价值不高;Phase 4 实现切换接口后自然会触发。
|
||||||
|
|
||||||
|
## 9. 风险事项
|
||||||
|
|
||||||
|
| 风险 | 实际发生 | 处置 |
|
||||||
|
|------|---------|------|
|
||||||
|
| SPIFFS 装不下(旧版 5.4 MB > 4.94 MB) | ✅ 已发生(240×320 版本时) | 删除 m05.gif 释放 2.3 MB;切换到 209×360 后总占用降到 2 MB,不再紧张 |
|
||||||
|
| 默认表情失效(指向不存在的 m05) | ✅ 已发生 | 改 `ai_chat_ui.c` 默认指向 m06 |
|
||||||
|
| gifsicle brew link 失败 | ✅ 已规避 | 脚本用绝对路径 |
|
||||||
|
| 240×320 强制裁剪导致角色压扁 | ✅ 已发生 | 回归 PoC 的 `_x360` 等比例缩放方式,已写入 memory 防再犯 |
|
||||||
|
|
||||||
|
## 10. Phase 3 验收结论
|
||||||
|
|
||||||
|
- ✅ Task 3.1: `tools/prepare_hiyori_gifs.py` (等比例缩小版本) 已提交
|
||||||
|
- ✅ Task 3.2: 3 个 GIF (209×360) 生成 + 提交到 `spiffs_image/`
|
||||||
|
- ✅ Task 3.3: m06 烧录验证通过(首次启动 0 重启,用户目视确认)
|
||||||
|
- ✅ Task 3.4: 本报告生成
|
||||||
|
- ✅ 移除 `hiyori_m05.gif`,默认表情更新为 m06
|
||||||
|
- ✅ PoC 处理标准写入 user-scope memory(`feedback_hiyori_gif_processing.md`)
|
||||||
|
|
||||||
|
**下一步**: Phase 4 — 实现"情绪标签 → 3 GIF 映射" + `bg_gif_demo_switch_gif()` 切换接口。
|
||||||
@ -0,0 +1,229 @@
|
|||||||
|
# Phase 3 PLAN — GIF 资源准备
|
||||||
|
|
||||||
|
> 里程碑: `digital_human_rtc`
|
||||||
|
> 阶段目标: 准备 m03/m06/m07 三个 hiyori 表情 GIF,裁剪到 240×320 显示上半身,居中显示在 360×360 LCD 屏幕上。
|
||||||
|
|
||||||
|
## 0. 调研结论
|
||||||
|
|
||||||
|
### 0.1 源 GIF 现状
|
||||||
|
|
||||||
|
所有 4 个 GIF 均为 **407×700 / 256 色 / 全帧透明**(Cubism Editor 直接导出):
|
||||||
|
|
||||||
|
| GIF | 帧数 | 大小 | 状态 |
|
||||||
|
|-----|------|------|------|
|
||||||
|
| m05 | 136 | 6.7MB | 当前 PoC 使用中 |
|
||||||
|
| m03 | 66 | 3.3MB | **Phase 3 待处理(负面/严肃)** |
|
||||||
|
| m06 | 25 | 1.3MB | **Phase 3 待处理(默认/积极)** |
|
||||||
|
| m07 | 22 | 1.1MB | **Phase 3 待处理(思考/疲倦)** |
|
||||||
|
|
||||||
|
### 0.2 工具状态
|
||||||
|
|
||||||
|
- **gifsicle 1.96**: 已安装在 brew cellar,**未 link** 到 `/opt/homebrew/bin/`
|
||||||
|
- 绝对路径: `/opt/homebrew/var/homebrew/tmp/.cellar/gifsicle/1.96/bin/gifsicle`
|
||||||
|
- 脚本中直接用绝对路径(不依赖 brew link)
|
||||||
|
- **PIL 12.2.0**: 可用
|
||||||
|
|
||||||
|
### 0.3 目标尺寸
|
||||||
|
|
||||||
|
- LCD: 360×360
|
||||||
|
- GIF: 240×320 → 居中后留边:
|
||||||
|
- 左右各 60px (`(360-240)/2`)
|
||||||
|
- 上下各 20px (`(360-320)/2`)
|
||||||
|
- 背景图 360×360 占满屏幕,GIF 在中央显示
|
||||||
|
|
||||||
|
### 0.4 容量预算(vs PARTITION_REPORT.md 第 6 节)
|
||||||
|
|
||||||
|
裁剪到 240×320 后像素数量是 407×700 的 27%,预期文件大小同比例缩小:
|
||||||
|
|
||||||
|
| GIF | 原始 | 裁剪+优化后预期 |
|
||||||
|
|-----|------|---------------|
|
||||||
|
| m03 | 3.3MB | ~900KB |
|
||||||
|
| m06 | 1.3MB | ~350KB |
|
||||||
|
| m07 | 1.1MB | ~300KB |
|
||||||
|
| **合计** | **5.7MB** | **~1.55MB** |
|
||||||
|
|
||||||
|
加上背景图 20KB,总 SPIFFS 占用约 **1.6MB**,远小于 SPIFFS 4.9MB 容量。
|
||||||
|
|
||||||
|
## 1. 任务清单
|
||||||
|
|
||||||
|
### Task 3.1: 写 Python 处理脚本
|
||||||
|
|
||||||
|
**文件**: `tools/prepare_hiyori_gifs.py`(新建)
|
||||||
|
|
||||||
|
**逻辑**:
|
||||||
|
1. 用 PIL `ImageSequence.Iterator` 遍历所有帧
|
||||||
|
2. 对每帧 `.convert('RGBA').getbbox()` 拿到非透明像素边界
|
||||||
|
3. 计算**所有帧**的最大 bbox(避免某帧角色位置偏移导致裁剪偏右——CLAUDE.md 经验)
|
||||||
|
4. 从最大 bbox 计算 240×320 裁剪窗口:
|
||||||
|
- 宽度:使用 bbox 横向中心点 ± 120px
|
||||||
|
- 高度:从角色顶部(bbox top)向下 320px(自动裁掉膝盖以下)
|
||||||
|
5. 调用 gifsicle 做实际裁剪+重采样:
|
||||||
|
```bash
|
||||||
|
gifsicle --crop X,Y+WxH --resize 240x320 -O3 input.gif -o output.gif
|
||||||
|
```
|
||||||
|
- **不加 `--lossy`**(会产生锯齿,CLAUDE.md 经验)
|
||||||
|
- **不加 `--colors`**(默认保留 256 色,画质优先)
|
||||||
|
- `-O3` 优化压缩
|
||||||
|
6. 用 gifsicle `--info` 验证输出
|
||||||
|
7. 输出路径:`spiffs_image/hiyori_m{03,06,07}.gif`
|
||||||
|
|
||||||
|
**关键代码片段**:
|
||||||
|
|
||||||
|
```python
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Phase 3: 准备 hiyori 表情 GIF(裁剪 + 居中 + 优化)"""
|
||||||
|
import subprocess, sys
|
||||||
|
from pathlib import Path
|
||||||
|
from PIL import Image, ImageSequence
|
||||||
|
|
||||||
|
GIFSICLE = "/opt/homebrew/var/homebrew/tmp/.cellar/gifsicle/1.96/bin/gifsicle"
|
||||||
|
OUT_W, OUT_H = 240, 320
|
||||||
|
|
||||||
|
def find_max_bbox(gif_path):
|
||||||
|
"""遍历所有帧,找最大 bbox(兼容角色动作偏移)"""
|
||||||
|
img = Image.open(gif_path)
|
||||||
|
min_x, min_y, max_x, max_y = img.width, img.height, 0, 0
|
||||||
|
for frame in ImageSequence.Iterator(img):
|
||||||
|
bbox = frame.convert('RGBA').getbbox()
|
||||||
|
if bbox:
|
||||||
|
min_x = min(min_x, bbox[0])
|
||||||
|
min_y = min(min_y, bbox[1])
|
||||||
|
max_x = max(max_x, bbox[2])
|
||||||
|
max_y = max(max_y, bbox[3])
|
||||||
|
return (min_x, min_y, max_x, max_y), img.width, img.height
|
||||||
|
|
||||||
|
def compute_crop_box(bbox, src_w, src_h):
|
||||||
|
"""从最大 bbox 计算 240×320 裁剪窗口"""
|
||||||
|
min_x, min_y, max_x, max_y = bbox
|
||||||
|
cx = (min_x + max_x) // 2 # 角色横向中心
|
||||||
|
crop_x = max(0, min(cx - OUT_W // 2, src_w - OUT_W))
|
||||||
|
crop_y = min_y # 从角色头顶开始
|
||||||
|
if crop_y + OUT_H > src_h:
|
||||||
|
crop_y = src_h - OUT_H # 越界 fallback
|
||||||
|
return crop_x, crop_y
|
||||||
|
|
||||||
|
def process_gif(src, dst):
|
||||||
|
bbox, sw, sh = find_max_bbox(src)
|
||||||
|
cx, cy = compute_crop_box(bbox, sw, sh)
|
||||||
|
print(f" 全帧 bbox: {bbox}, 源 {sw}x{sh}")
|
||||||
|
print(f" 裁剪起点: ({cx}, {cy}), 大小: {OUT_W}x{OUT_H}")
|
||||||
|
subprocess.run([
|
||||||
|
GIFSICLE,
|
||||||
|
"--crop", f"{cx},{cy}+{OUT_W}x{OUT_H}",
|
||||||
|
"--resize", f"{OUT_W}x{OUT_H}",
|
||||||
|
"-O3",
|
||||||
|
src, "-o", dst
|
||||||
|
], check=True)
|
||||||
|
# 验证
|
||||||
|
info = subprocess.check_output([GIFSICLE, "--info", dst], text=True)
|
||||||
|
print(f" 输出大小: {Path(dst).stat().st_size / 1024:.1f} KB")
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
src_base = Path("docs/Rtc_AIavatar/Resources/hiyori_free_zh/Export")
|
||||||
|
dst_base = Path("spiffs_image")
|
||||||
|
for name in ["m03", "m06", "m07"]:
|
||||||
|
src = src_base / name / f"hiyori_{name}.gif"
|
||||||
|
dst = dst_base / f"hiyori_{name}.gif"
|
||||||
|
print(f"处理 {name}...")
|
||||||
|
process_gif(str(src), str(dst))
|
||||||
|
```
|
||||||
|
|
||||||
|
**验证**:
|
||||||
|
- 脚本运行无错误
|
||||||
|
- 生成 3 个 `spiffs_image/hiyori_m{03,06,07}.gif`
|
||||||
|
|
||||||
|
**commit 消息**: `tools(phase03): 新增 hiyori GIF 处理脚本(PIL bbox + gifsicle 裁剪)`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 3.2: 执行脚本生成 3 个 GIF + 验证
|
||||||
|
|
||||||
|
**步骤**:
|
||||||
|
```bash
|
||||||
|
cd /Users/rdzleo/Desktop/Baji_Rtc_Toy
|
||||||
|
python3 tools/prepare_hiyori_gifs.py
|
||||||
|
ls -la spiffs_image/hiyori_*.gif
|
||||||
|
```
|
||||||
|
|
||||||
|
**验证标准**:
|
||||||
|
- ✅ 3 个 GIF 都存在
|
||||||
|
- ✅ 每个 ≤ 1.5MB
|
||||||
|
- ✅ 三个合计 ≤ 3MB(SPIFFS 容量充足)
|
||||||
|
- ✅ `gifsicle --info` 显示 240×320 + transparent + 256 色
|
||||||
|
|
||||||
|
**commit 消息**: `feat(assets): Phase 3 hiyori 三表情 GIF(m03/m06/m07,240x320 居中裁剪)`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 3.3: 烧录验证(逐个 GIF)
|
||||||
|
|
||||||
|
**目标**: 验证三个新 GIF 在设备上的显示效果(位置、画质、动画流畅度)。
|
||||||
|
|
||||||
|
**策略**: 临时修改 `main/dzbj/ai_chat_ui.c` 中的 `USE_BG_GIF_POC` 调用,让它依次加载 m03 / m06 / m07 测试,每次烧录 + 监控 ~10 秒。
|
||||||
|
|
||||||
|
**步骤(每个 GIF 重复一次)**:
|
||||||
|
|
||||||
|
1. 临时改 `bg_gif_demo_start()` 调用参数为目标 GIF 路径
|
||||||
|
2. `idf.py build`(增量,快)
|
||||||
|
3. `idf.py -p /dev/cu.usbmodem834401 flash`
|
||||||
|
4. Python 串口监控 10 秒,看是否:
|
||||||
|
- 无 abort / 重启循环
|
||||||
|
- `BG_GIF` 日志正确加载(无 LZW 解码错误)
|
||||||
|
- PSRAM 余量充足
|
||||||
|
|
||||||
|
**验证标准(每个 GIF)**:
|
||||||
|
- ✅ 设备烧录后无重启循环(仅冷启动 codec 1 次失败属 Phase 0 已知问题)
|
||||||
|
- ✅ GIF 加载日志显示正确大小
|
||||||
|
- ✅ 30s 监控期间无内存泄漏
|
||||||
|
|
||||||
|
**改回**: 测试完后改回默认 GIF(m06 作为 neutral 默认表情),方便后续 Phase 4 测试。
|
||||||
|
|
||||||
|
**不产出 commit**(仅验证步骤)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 3.4: 生成 GIF_REPORT.md
|
||||||
|
|
||||||
|
**文件**: `.planning/milestones/digital_human_rtc/phases/phase_03_gif_resources/GIF_REPORT.md`
|
||||||
|
|
||||||
|
**内容**:
|
||||||
|
- 处理前后大小对比
|
||||||
|
- 实际全帧 bbox + 裁剪窗口
|
||||||
|
- 设备显示效果(位置、画质、流畅度)
|
||||||
|
- Phase 4 SPIFFS 总占用(背景图 + 3 GIF + 旧 m05 是否保留?)
|
||||||
|
|
||||||
|
**commit 消息**: `docs(phase03): GIF 资源处理报告(GIF_REPORT.md)`
|
||||||
|
|
||||||
|
## 2. 任务依赖顺序
|
||||||
|
|
||||||
|
```
|
||||||
|
Task 3.1 (写脚本) → Task 3.2 (执行生成 GIF) → Task 3.3 (烧录验证) → Task 3.4 (报告)
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. 风险与回滚
|
||||||
|
|
||||||
|
| 风险 | 缓解 |
|
||||||
|
|------|------|
|
||||||
|
| PIL bbox 找不到非透明像素(背景全空) | 脚本内 `if bbox is None` 警告,fallback 用全图 |
|
||||||
|
| gifsicle 绝对路径失效(brew 更新) | 脚本顶部 `if not Path(GIFSICLE).exists()` 检查 + 降级到 PATH 查找 |
|
||||||
|
| 裁剪后人物头部被裁 | bbox 起点是非透明像素顶部,理论上头部不会被裁;如果出现,调整 crop_y |
|
||||||
|
| GIF 文件大小超 1.5MB | 加 `--lossy=30 --colors 128`,CLAUDE.md 经验显示锯齿可接受 |
|
||||||
|
| 烧录后 GIF 不显示/花屏 | 回滚 SPIFFS 镜像,检查 gifdec 解码错误 |
|
||||||
|
|
||||||
|
**回滚**: 每个 Task 独立 commit。如果 GIF 显示问题,单独 revert Task 3.2 的 commit,spiffs_image/ 恢复到 PoC 阶段(只有 m05)。
|
||||||
|
|
||||||
|
## 4. Phase 3 完成验收清单
|
||||||
|
|
||||||
|
- [ ] Task 3.1 commit 完成(tools/prepare_hiyori_gifs.py)
|
||||||
|
- [ ] Task 3.2 commit 完成(spiffs_image/hiyori_m{03,06,07}.gif)
|
||||||
|
- [ ] Task 3.3 烧录验证 3 个 GIF 都能在设备显示
|
||||||
|
- [ ] Task 3.4 GIF_REPORT.md commit
|
||||||
|
- [ ] 整个 Phase 3 合并为 1 个大 commit 推送 gitea + GitHub
|
||||||
|
|
||||||
|
## 5. Phase 3 不做的事
|
||||||
|
|
||||||
|
- ❌ Phase 4 的情绪映射代码(22 情绪 → 3 GIF 映射表)
|
||||||
|
- ❌ Phase 5 的字幕显示恢复
|
||||||
|
- ❌ 删除 PoC 阶段的 `spiffs_image/hiyori_m05.gif`(保留作历史参考;Phase 4 可决定是否移除)
|
||||||
|
- ❌ 修改 `bg_gif_demo.c` API(仅 Phase 3 测试时临时改路径,不动接口)
|
||||||
|
- ❌ 处理 m01/m02/m04/m05/m08 其他表情(本里程碑只用 3 个)
|
||||||
@ -231,7 +231,7 @@ void ai_chat_screen_init(void) {
|
|||||||
#endif
|
#endif
|
||||||
esp_err_t bgret = bg_gif_demo_start(
|
esp_err_t bgret = bg_gif_demo_start(
|
||||||
"/spiflash/Background_360x360.jpg",
|
"/spiflash/Background_360x360.jpg",
|
||||||
"/spiflash/hiyori_m05.gif");
|
"/spiflash/hiyori_m06.gif"); // Phase 3: m06 默认表情(neutral/积极),240x320 居中
|
||||||
if (bgret == ESP_OK) {
|
if (bgret == ESP_OK) {
|
||||||
ESP_LOGI(TAG, "BG+GIF PoC 启动成功");
|
ESP_LOGI(TAG, "BG+GIF PoC 启动成功");
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
BIN
spiffs_image/hiyori_m03.gif
Normal file
BIN
spiffs_image/hiyori_m03.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.1 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 2.3 MiB |
BIN
spiffs_image/hiyori_m06.gif
Normal file
BIN
spiffs_image/hiyori_m06.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 442 KiB |
BIN
spiffs_image/hiyori_m07.gif
Normal file
BIN
spiffs_image/hiyori_m07.gif
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 398 KiB |
114
tools/prepare_hiyori_gifs.py
Executable file
114
tools/prepare_hiyori_gifs.py
Executable file
@ -0,0 +1,114 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Phase 3: 准备 hiyori 表情 GIF(等比例缩小到高 360px,与 PoC 保持一致)
|
||||||
|
|
||||||
|
输入: docs/Rtc_AIavatar/Resources/hiyori_free_zh/Export/m{03,06,07}/hiyori_m*.gif
|
||||||
|
输出: spiffs_image/hiyori_m{03,06,07}.gif
|
||||||
|
|
||||||
|
处理策略(与 PoC 阶段 hiyori_m05.gif 一致,复现 209×360 等比例缩放):
|
||||||
|
- 不裁剪,保持源 GIF 的人物全身完整(407×700 原始比例)
|
||||||
|
- 用 gifsicle --resize _x360 让高度 = LCD 360px,宽度按比例自动算 → 约 209px
|
||||||
|
- LVGL lv_obj_align(LV_ALIGN_CENTER) 居中显示:垂直充满 LCD,左右各 ~75px 留边
|
||||||
|
- 不加 --lossy / --colors(保留 256 色,画质优先;CLAUDE.md 经验)
|
||||||
|
- 只用 -O3 优化文件大小
|
||||||
|
|
||||||
|
LCD 360×360,目标显示效果:人物充满垂直方向、横向居中、左右留边显示背景图
|
||||||
|
"""
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
GIFSICLE = "/opt/homebrew/var/homebrew/tmp/.cellar/gifsicle/1.96/bin/gifsicle"
|
||||||
|
TARGET_HEIGHT = 360 # = LCD 高度,与 PoC 阶段 m05.gif 一致
|
||||||
|
|
||||||
|
SRC_BASE = Path("docs/Rtc_AIavatar/Resources/hiyori_free_zh/Export")
|
||||||
|
DST_BASE = Path("spiffs_image")
|
||||||
|
TARGETS = ["m03", "m06", "m07"]
|
||||||
|
|
||||||
|
|
||||||
|
def check_tools():
|
||||||
|
if not Path(GIFSICLE).exists():
|
||||||
|
import shutil
|
||||||
|
alt = shutil.which("gifsicle")
|
||||||
|
if alt:
|
||||||
|
globals()["GIFSICLE"] = alt
|
||||||
|
print(f"使用 PATH 中的 gifsicle: {alt}")
|
||||||
|
else:
|
||||||
|
print(f"ERROR: gifsicle 不存在于 {GIFSICLE},且 PATH 中找不到", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def process_gif(src_path, dst_path):
|
||||||
|
"""处理单个 GIF: gifsicle 等比例缩小到高 360px"""
|
||||||
|
src_size_kb = Path(src_path).stat().st_size / 1024
|
||||||
|
|
||||||
|
# 读源尺寸(仅日志用)
|
||||||
|
info = subprocess.check_output(
|
||||||
|
[GIFSICLE, "--info", str(src_path)], text=True
|
||||||
|
)
|
||||||
|
# 解析 "logical screen WxH"
|
||||||
|
src_w, src_h, frame_count = "?", "?", "?"
|
||||||
|
for line in info.splitlines():
|
||||||
|
if "logical screen" in line:
|
||||||
|
wh = line.split("logical screen")[1].strip().split("x")
|
||||||
|
src_w, src_h = wh[0], wh[1]
|
||||||
|
if "images" in line and line.startswith("*"):
|
||||||
|
frame_count = line.split()[-2]
|
||||||
|
|
||||||
|
# gifsicle: 高度 = 360,宽度按比例自动算
|
||||||
|
cmd = [
|
||||||
|
GIFSICLE,
|
||||||
|
"--resize", f"_x{TARGET_HEIGHT}",
|
||||||
|
"-O3",
|
||||||
|
str(src_path),
|
||||||
|
"-o", str(dst_path),
|
||||||
|
]
|
||||||
|
|
||||||
|
print(f" 源: {src_w}x{src_h}, {frame_count} 帧, {src_size_kb:.1f} KB")
|
||||||
|
subprocess.run(cmd, check=True)
|
||||||
|
|
||||||
|
# 验证输出尺寸
|
||||||
|
dst_info = subprocess.check_output([GIFSICLE, "--info", str(dst_path)], text=True)
|
||||||
|
for line in dst_info.splitlines():
|
||||||
|
if "logical screen" in line:
|
||||||
|
dst_wh = line.split("logical screen")[1].strip()
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
dst_wh = "?"
|
||||||
|
|
||||||
|
dst_size_kb = Path(dst_path).stat().st_size / 1024
|
||||||
|
reduction = (1 - dst_size_kb / src_size_kb) * 100
|
||||||
|
print(f" 输出: {dst_wh}, {dst_size_kb:.1f} KB (节省 {reduction:.1f}%)")
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
check_tools()
|
||||||
|
|
||||||
|
if not DST_BASE.exists():
|
||||||
|
print(f"ERROR: 目标目录 {DST_BASE} 不存在", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
print(f"=== Phase 3: 处理 hiyori GIF (等比例缩小到高 {TARGET_HEIGHT}px) ===\n")
|
||||||
|
|
||||||
|
total_kb = 0
|
||||||
|
for name in TARGETS:
|
||||||
|
src = SRC_BASE / name / f"hiyori_{name}.gif"
|
||||||
|
dst = DST_BASE / f"hiyori_{name}.gif"
|
||||||
|
|
||||||
|
if not src.exists():
|
||||||
|
print(f"SKIP {name}: 源文件不存在 {src}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
print(f"处理 {name}...")
|
||||||
|
try:
|
||||||
|
process_gif(src, dst)
|
||||||
|
total_kb += Path(dst).stat().st_size / 1024
|
||||||
|
except subprocess.CalledProcessError as e:
|
||||||
|
print(f" ERROR: gifsicle 失败: {e}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
print()
|
||||||
|
|
||||||
|
print(f"=== 合计输出: {total_kb:.1f} KB ({total_kb / 1024:.2f} MB) ===")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Loading…
x
Reference in New Issue
Block a user