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:
Rdzleo 2026-05-13 11:42:30 +08:00
parent ce7a3aad63
commit 7d1c7dc1f0
8 changed files with 474 additions and 1 deletions

View File

@ -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×3606.7MB → 2.3MB,节省 66.0%),处理参数完全一致。
## 3. 显示效果
LCD 360×360GIF 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 切换到 m06PoC 的 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()` 切换接口。

View File

@ -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
- ✅ 三个合计 ≤ 3MBSPIFFS 容量充足)
- ✅ `gifsicle --info` 显示 240×320 + transparent + 256 色
**commit 消息**: `feat(assets): Phase 3 hiyori 三表情 GIFm03/m06/m07240x320 居中裁剪)`
---
### 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 监控期间无内存泄漏
**改回**: 测试完后改回默认 GIFm06 作为 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 的 commitspiffs_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 个)

View File

@ -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

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 KiB

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
View 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()