"""Phase 2 验收脚本:独立验证 combined_video 抽帧 + 左右切分逻辑。 不需要再跑一次 Trellis(省钱),直接用 generated/ 下已有的 `trellis_combined_*.mp4` 做验证。 用法(在 backend 目录激活 venv 之后): python scripts/test_frame_extract.py python scripts/test_frame_extract.py generated/trellis_combined_211176d3940b.mp4 成功标准: - 退出码 0 - 打印的 color_png / normal_png 均为 /generated/view_xxx.png - 到 backend/generated/ 能看到 6 对 color + normal 文件(12 张)且尺寸接近正方形 - 目视检查:每组 color 图按 0/60/120/180/240/300° 依次环绕建筑一圈 """ 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 video_frame_extractor # noqa: E402 from app.services.image_gen import GENERATED_DIR # noqa: E402 def _pick_default_video() -> str: candidates = sorted( GENERATED_DIR.glob("trellis_combined_*.mp4"), key=lambda p: p.stat().st_mtime, reverse=True, ) if not candidates: raise FileNotFoundError( "generated/ 下找不到 trellis_combined_*.mp4;请先跑一次 Phase 1 " "(scripts/test_trellis.py)或显式传入视频路径" ) return f"/generated/{candidates[0].name}" async def main() -> int: video_arg = sys.argv[1] if len(sys.argv) > 1 else _pick_default_video() # 允许传相对路径或纯文件名 if not video_arg.startswith("/generated/"): if video_arg.startswith("generated/"): video_arg = "/" + video_arg.replace("\\", "/") elif not video_arg.startswith("/") and not Path(video_arg).is_absolute(): # 如果只给了个文件名,假定在 generated/ 下 if (GENERATED_DIR / video_arg).exists(): video_arg = f"/generated/{video_arg}" print(f"[test_frame_extract] 输入视频:{video_arg}") print("[test_frame_extract] 抽取 6 视角:az=[0,60,120,180,240,300],el 全 0") azimuths = [0, 60, 120, 180, 240, 300] elevations = [0] * len(azimuths) frames = await video_frame_extractor.extract_by_azimuth( combined_video_path=video_arg, azimuths=azimuths, elevations=elevations, total_rotation_deg=360, ) print("[test_frame_extract] 抽帧结果:") print(json.dumps(frames, ensure_ascii=False, indent=2)) # 简单完整性校验 all_exist = True for f in frames: for key in ("color_png", "normal_png"): rel = f.get(key) if not rel: print(f"[test_frame_extract] 警告:帧 az={f['az']} 缺少 {key}") all_exist = False continue local = GENERATED_DIR / Path(rel).name size = local.stat().st_size if local.exists() else -1 status = "OK" if size > 0 else "MISSING" print(f" [{status}] az={f['az']:3d} {key}: {rel} ({size} bytes)") if size <= 0: all_exist = False if not all_exist: print("[test_frame_extract] 部分文件缺失,判定失败") return 1 print( f"[test_frame_extract] 成功:共 {len(frames)} 个视角," f"color+normal 合计 {len(frames) * 2} 张已落盘到 {GENERATED_DIR}" ) print( "[test_frame_extract] 请目视检查:打开 generated/ 下任意 view_*_color.png," "确认 az=0 正面、az=180 背面、其余按顺序环绕;normal 帧应为蓝紫调法线图。" ) return 0 if __name__ == "__main__": sys.exit(asyncio.run(main()))