Baji_Rtc_Toy/tools/prepare_hiyori_gifs.py
Rdzleo 7d1c7dc1f0 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 充足。
2026-05-13 11:42:30 +08:00

115 lines
3.8 KiB
Python
Executable File
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.

#!/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()