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