Files
EPEEAIKit/art-agent/backend/scripts/test_style_restore.py
2026-04-20 21:52:35 +08:00

168 lines
5.9 KiB
Python
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.
"""Phase 3 / Checkpoint 3 验收脚本:独立验证 Stage 3 风格还原链路。
复用 Phase 2 已抽出的 normal 帧,无需再跑 Trellis省 $0.041)。
默认会挑最新一组 view_*_el0_normal.png 作为结构参考,并让用户指定原图。
用法(在 backend 目录激活 venv 后):
# 1. 全 6 帧批量重绘(~6 × $0.07 ≈ $0.42
python scripts/test_style_restore.py <原图路径>
# 2. 只跑一帧做快速冒烟(便宜 ~$0.07
python scripts/test_style_restore.py <原图路径> --single
# 3. 手动指定 normal 帧前缀(不指定时用最新一组)
python scripts/test_style_restore.py <原图路径> --prefix view_xxxxxxxx
成功标准:
- 退出码 0
- 打印的每一帧都返回 success=True 且 url 以 /generated/restyled_ 开头
- 打开 generated/restyled_* 图片,目视检查:风格与原图一致、视角符合 az 标注
"""
from __future__ import annotations
import argparse
import asyncio
import json
import sys
from pathlib import Path
_BACKEND_ROOT = Path(__file__).resolve().parent.parent
if str(_BACKEND_ROOT) not in sys.path:
sys.path.insert(0, str(_BACKEND_ROOT))
from dotenv import load_dotenv # noqa: E402
load_dotenv(_BACKEND_ROOT / ".env", override=True)
from app.services import style_restorer # noqa: E402
from app.services.image_gen import GENERATED_DIR # noqa: E402
def _pick_latest_normal_group() -> tuple[str, list[dict]]:
"""在 generated/ 下挑一组最新的 view_<prefix>_az*_el0_normal.png按 az 排好序。
返回 (prefix, frames)frames = [{az, el, normal_png, color_png}, ...]
"""
all_normals = sorted(
GENERATED_DIR.glob("view_*_el0_normal.png"),
key=lambda p: p.stat().st_mtime,
reverse=True,
)
if not all_normals:
raise FileNotFoundError(
"generated/ 下找不到 view_*_el0_normal.png请先跑 scripts/test_frame_extract.py"
)
# 最新文件的 prefixview_<8hex>_az...
latest_name = all_normals[0].name
# 形如 view_63731b77_az0_el0_normal.png → prefix = view_63731b77
parts = latest_name.split("_")
prefix = "_".join(parts[:2])
return prefix, _load_group_by_prefix(prefix)
def _load_group_by_prefix(prefix: str) -> list[dict]:
normals = sorted(GENERATED_DIR.glob(f"{prefix}_az*_el0_normal.png"))
if not normals:
raise FileNotFoundError(f"找不到前缀为 {prefix} 的 normal 帧")
frames = []
for n in normals:
# 从文件名解析 azview_63731b77_az120_el0_normal.png → az=120
tokens = n.stem.split("_")
az = 0
el = 0
for t in tokens:
if t.startswith("az"):
try:
az = int(t[2:])
except ValueError:
pass
elif t.startswith("el"):
try:
el = int(t[2:])
except ValueError:
pass
color = n.with_name(n.name.replace("_normal.png", "_color.png"))
frames.append({
"az": az,
"el": el,
"normal_png": f"/generated/{n.name}",
"color_png": f"/generated/{color.name}" if color.exists() else None,
"frame_idx": -1,
})
frames.sort(key=lambda f: f["az"])
return frames
async def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("original", help="用户原图路径(本地相对/绝对路径均可)")
parser.add_argument("--prefix", default=None, help="normal 帧的文件名前缀(如 view_63731b77")
parser.add_argument("--single", action="store_true", help="只跑第一帧做快速冒烟")
parser.add_argument("--scale", type=float, default=None, help="覆盖 IP-Adapter 权重0-1")
parser.add_argument("--cn-scale", type=float, default=None, help="覆盖 ControlNet 权重0-1")
args = parser.parse_args()
original = args.original
original_path = Path(original)
if not original_path.is_absolute():
original_path = (_BACKEND_ROOT / original).resolve()
if not original_path.exists():
print(f"[test_style_restore] 原图不存在:{original_path}")
return 2
if args.prefix:
frames = _load_group_by_prefix(args.prefix)
prefix = args.prefix
else:
prefix, frames = _pick_latest_normal_group()
if args.single:
frames = frames[:1]
override_params = {}
if args.scale is not None:
override_params["scale"] = args.scale
if args.cn_scale is not None:
override_params["controlnet_conditioning_scale"] = args.cn_scale
print(f"[test_style_restore] 原图:{original_path}")
print(f"[test_style_restore] normal 帧组 prefix={prefix},共 {len(frames)}")
print(f"[test_style_restore] 角度列表:{[f['az'] for f in frames]}")
if override_params:
print(f"[test_style_restore] 覆盖参数:{override_params}")
print(
f"[test_style_restore] 预计耗时 ~{72}s × {len(frames)}"
f"成本 ~${0.07 * len(frames):.2f}(并发 3 实际会更快)"
)
results = await style_restorer.restore_batch(
original_image=str(original_path),
frames=frames,
params=override_params or None,
concurrency=3,
)
print("[test_style_restore] 重绘结果:")
print(json.dumps(results, ensure_ascii=False, indent=2))
ok = sum(1 for r in results if r.get("success"))
print(f"[test_style_restore] 成功 {ok}/{len(results)}")
if ok == 0:
print("[test_style_restore] 全军覆没,判定失败")
return 1
if ok < len(results):
print("[test_style_restore] 部分帧失败,视为部分通过;请检查上方错误信息")
return 0
print(
"[test_style_restore] 全部成功;请目视检查 generated/restyled_* 图片:"
"风格应贴近原图、视角按 az 顺序环绕。"
)
return 0
if __name__ == "__main__":
sys.exit(asyncio.run(main()))