maliang / code /gc25d /verify_prep.py
teawhite's picture
Add files using upload-large-folder tool
bcdd8fb verified
Raw History Blame Contribute Delete
27.6 kB
#!/usr/bin/env python3
"""prep_v10 产物校验(data_v10.md §8.5)。
python verify_prep.py --root $M/data/gc25d_v10 [--spot 16] [--device cuda:0] [--workers 32]
分辨率 = geometry(env GC_RES,默认 832x480):index/prep_args.json 的 "res" 与每行的 "res"(缺失 = 832x480)必须等于 GC_RES,
下面的 [48,21,30,52] / [81,480,832] 是 832×480 的例子(1280×704:[48,21,44,80] / [81,704,1280])。
A. index:res 一致、clips.jsonl 字段齐、clip_id 唯一、split = SPEC §5 规则、drop = buried 名单、runs.json 一致、每 run 50 条、
text_table_gc25d.pt 与源文件逐字节相同且 19 个 key = text.all_prompts() + NULL_PROMPT、没有残留 .tmp 文件。
B. 全部 clip(多进程):文件齐(cond_ext 当且仅当 has_ext);latent bf16 [48,21,30,52] 有限;actions_block uint8 [21,5],
cell 0 = cell 1,[1:] = 原始 actions_block[s//4 : s//4+20];prompts == text.cell_prompts(actions_block[1:]);cond ids
uint8 [81,480,832] ⊆ parts 词表(无 255);cond_ext ids [28,480,832] / blocks [7,5] = 原始块 / prompts;has_ext 用
frames.jsonl 独立重判;meta 形状 / dtype(F = 81 或 109)、frames 连续、player_px / boss_px == cond 里数出来的像素数。
C. GPU 抽查 N 条:
* VAE 解码(fp32)PSNR vs resample_rgb(GT)(典型 23–26 dB,简单内容 30+;< 18 dB ⇒ FAIL,[PSNR_WARN_LO, 40] 外 ⇒ WARN;
PSNR_WARN_LO = 21(832×480 上定的),其它分辨率 20:GT 高频更多,繁杂 clip 低 ≈ 1 dB,704p mini 实测最低 20.26);
* 官方 vae.encode([x], tiled=False) 重编整段 == 存的 latent(逐位);latent cell 0 == encode(帧 0)(到 bf16 舍入);
* 源 ids.png ⊆ {0..6,10,11};collapse_parts(cond ids) == resample_ids(ids.png)(全部帧,含扩展帧);meta 与 tar json 逐位相同;
从 meta 重跑 label_parts == cond;
* 从 meta 用 capsules.render_capsules 重画角色:v10 无遮挡帧 IoU ≥ 0.98(及 GT ⊆ 重画的覆盖率)。
退出码:有 FAIL ⇒ 1。报告写 <root>/index/verify_report.json。
"""
from __future__ import annotations
import argparse
import collections
import glob
import hashlib
import itertools
import json
import os
import sys
import time
from multiprocessing import Pool
import prep_v10 as P # 先 import:设单线程环境变量
import cv2
import numpy as np
import torch
import capsules as CAP
import geometry as GEO
import model as GM
import text as TX
ROW_KEYS = ("clip_id", "subset", "scene", "run", "start", "end", "split", "drop", "drop_reason", "episode",
"camera_variant", "look_name", "centered", "n_events", "dead_frac", "has_ext", "ortho")
PT_KEYS = ("latent", "prompts", "actions_block", "clip_id", "subset", "scene", "run", "start")
def meta_spec(F: int) -> dict:
return {
"world_to_clip": ("f8", (F, 16)), "world_to_view": ("f8", (F, 16)), "projection": ("f8", (F, 16)),
"orthographic": ("b1", ()), "fov_x_deg": ("f4", (F,)), "ortho_width_cm": ("f4", (F,)),
"frame_offset_cm": ("f4", (F, 3)), "capsules": ("f4", (F, 2, 12, 7)), "actor_pos": ("f4", (F, 2, 3)),
"ground_z": ("f4", (F,)), "player_state": ("U", (F,)), "boss_state": ("U", (F,)),
"player_px": ("i4", (F,)), "boss_px": ("i4", (F,)),
}
def _dtype_ok(arr, code):
return arr.dtype.kind == "U" if code == "U" else arr.dtype == np.dtype(code)
def _px(ids):
"""[F,H,W] parts ids → (player_px, boss_px) int [F]。"""
c = GM.collapse_parts(ids)
return (c == GEO.PLAYER_ID).sum((1, 2)), (c == GEO.BOSS_ID).sum((1, 2))
# ----------------------------------------------------------------------------
# B. 逐 clip 校验(CPU,多进程)
# ----------------------------------------------------------------------------
_TABLE_KEYS: frozenset = frozenset()
_SRC = ""
def _pool_init(table_keys, src):
global _TABLE_KEYS, _SRC
_TABLE_KEYS, _SRC = frozenset(table_keys), src
cv2.setNumThreads(1)
torch.set_num_threads(1)
def check_clip(args) -> tuple[str, list[str], dict]:
row, root = args
cid, s = row["clip_id"], row["start"]
err = []
paths = P.clip_paths(root, cid)
for k in ("latent", "cond", "meta") + (("cond_ext",) if row["has_ext"] else ()):
if not os.path.isfile(paths[k]):
err.append(f"缺 {k}")
if not row["has_ext"] and os.path.isfile(paths["cond_ext"]):
err.append("has_ext=False 却有 cond_ext")
if err:
return cid, err, {}
run_dir = os.path.join(_SRC, row["subset"], row["scene"], row["run"])
AB = np.load(os.path.join(run_dir, "index", "actions_block_0.25s.npy"))
# latent .pt
d = torch.load(paths["latent"], map_location="cpu", weights_only=True)
miss = [k for k in PT_KEYS if k not in d]
if miss:
err.append(f"latent 缺 key {miss}")
else:
lat, ab = d["latent"], d["actions_block"]
if lat.dtype != torch.bfloat16 or tuple(lat.shape) != P.LAT_SHAPE:
err.append(f"latent {lat.dtype} {tuple(lat.shape)}")
elif not torch.isfinite(lat.float()).all():
err.append("latent 有非有限值")
ab = ab.numpy() if torch.is_tensor(ab) else np.asarray(ab)
if ab.dtype != np.uint8 or ab.shape != (21, 5) or not np.isin(ab, (0, 1)).all():
err.append(f"actions_block {ab.dtype} {ab.shape}")
else:
if not (ab[0] == ab[1]).all():
err.append("actions_block cell 0 ≠ cell 1")
if not (ab[1:] == P.clip_blocks(AB, s)).all():
err.append("actions_block[1:] ≠ 原始 actions_block[s//4:s//4+20]")
if list(d["prompts"]) != TX.cell_prompts(ab[1:]):
err.append("prompts ≠ text.cell_prompts(actions_block[1:])")
if not set(d["prompts"]) <= _TABLE_KEYS:
err.append(f"prompt 不在 text table:{set(d['prompts']) - _TABLE_KEYS}")
for k in ("clip_id", "subset", "scene", "run", "start"):
if d[k] != row[k]:
err.append(f"latent[{k}]={d[k]!r} ≠ row {row[k]!r}")
# has_ext 独立重判(index/frames.jsonl,不是 prep 用的 tar json)
with open(os.path.join(run_dir, "index", "frames.jsonl")) as fh:
fr = [json.loads(line) for line in itertools.islice(fh, s, s + P.F_SPAN)]
he = P.ext_valid([j["episode"] for j in fr], [j["camera_variant"] for j in fr], s, len(AB))
if he != row["has_ext"]:
err.append(f"has_ext={row['has_ext']},frames.jsonl 判为 {he}")
# cond / cond_ext
with np.load(paths["cond"], allow_pickle=False) as z:
ids = z["ids"]
if ids.dtype != np.uint8 or ids.shape != (P.F_CLIP, P.DST_H, P.DST_W):
err.append(f"cond ids {ids.dtype} {ids.shape}")
return cid, err, {}
bad = np.nonzero(np.bincount(ids.ravel(), minlength=256) * ~P._VALID_PARTS)[0]
if len(bad):
err.append(f"cond ids 有词表外值 {bad.tolist()}")
if row["has_ext"]:
with np.load(paths["cond_ext"], allow_pickle=False) as z:
eids, eb, ep = z["ids"], z["blocks"], z["prompts"]
if eids.dtype != np.uint8 or eids.shape != (P.F_EXT, P.DST_H, P.DST_W):
err.append(f"cond_ext ids {eids.dtype} {eids.shape}")
else:
bad = np.nonzero(np.bincount(eids.ravel(), minlength=256) * ~P._VALID_PARTS)[0]
if len(bad):
err.append(f"cond_ext ids 有词表外值 {bad.tolist()}")
ids = np.concatenate([ids, eids])
if eb.dtype != np.uint8 or eb.shape != (P.N_EXT_CELLS, 5) or not (eb == P.ext_blocks(AB, s)).all():
err.append("cond_ext blocks ≠ 原始 actions_block[s//4+20:s//4+27]")
elif ep.shape != (P.N_EXT_CELLS,) or ep.tolist() != [TX.block_to_prompt(*r.tolist()) for r in eb]:
err.append("cond_ext prompts ≠ block_to_prompt(blocks)")
# meta
F = P.F_SPAN if row["has_ext"] else P.F_CLIP
with np.load(paths["meta"], allow_pickle=False) as z:
m = {k: z[k] for k in z.files}
for k, (code, shape) in meta_spec(F).items():
if k not in m:
err.append(f"meta 缺 {k}")
elif not _dtype_ok(m[k], code) or m[k].shape != shape:
err.append(f"meta {k}: {m[k].dtype} {m[k].shape},应为 {code} {shape}")
if err:
return cid, err, {}
if bool(m["orthographic"]) != row["ortho"]:
err.append("meta orthographic ≠ row ortho")
if "frames" in m and not (m["frames"] == s + np.arange(F)).all():
err.append("meta frames 不连续")
for k in ("world_to_clip", "world_to_view", "projection", "fov_x_deg", "ortho_width_cm", "frame_offset_cm"):
if not np.isfinite(m[k]).all():
err.append(f"meta {k} 有非有限值")
pp, bp = _px(ids)
if not (pp == m["player_px"]).all() or not (bp == m["boss_px"]).all():
err.append("meta player_px / boss_px ≠ cond 里的像素数")
has_player_caps = ~np.isnan(m["capsules"][:, 0, 0, 0])
if not np.isfinite(m["ground_z"][has_player_caps]).all():
err.append("ground_z 在有玩家胶囊的帧上非有限")
info = {"subset": row["subset"], "player_px_mean": float(pp[:P.F_CLIP].mean()), "boss_px_mean": float(bp[:P.F_CLIP].mean()),
"bytes": {k: os.path.getsize(paths[k]) for k in paths if os.path.isfile(paths[k])}}
return cid, err, info
# ----------------------------------------------------------------------------
# C. GPU 抽查
# ----------------------------------------------------------------------------
def psnr(a: np.ndarray, b: np.ndarray) -> float:
mse = ((a.astype(np.float64) - b.astype(np.float64)) ** 2).mean()
return float(10 * np.log10(255.0 ** 2 / max(mse, 1e-12)))
# 832×480 上定的阈值按分辨率换算(832×480 时与原值相同)
PSNR_WARN_LO = 21.0 if GEO.RES_TAG == GEO.LEGACY_RES else 20.0
VIS_MIN_PX = 50 * GEO.AREA_VS_480 # 「可见帧」的 GT 像素下限(像素数 ∝ 面积)
def render_stats(cond_ids, meta, frames) -> list[dict]:
"""从 meta 重画胶囊(不看 ids)→ 每帧每角色的 IoU / 覆盖率 / 遮挡比例 / 部位一致率。"""
out = []
for i in frames:
caps = CAP.capsules_from_array(meta["capsules"][i])
with np.errstate(invalid="ignore"): # cap_hit 里 inf·0(没命中的射线),无害
rend = CAP.render_capsules(np.zeros((P.DST_H, P.DST_W), np.uint8), meta["world_to_clip"][i], meta["world_to_view"][i], caps)
g, r = GM.collapse_parts(cond_ids[i]), GM.collapse_parts(rend)
for cls in (GEO.PLAYER_ID, GEO.BOSS_ID):
G, Rm = g == cls, r == cls
inter = G & Rm
out.append({"frame": int(i), "cls": cls, "g": int(G.sum()), "r": int(Rm.sum()), "i": int(inter.sum()),
"u": int((G | Rm).sum()), "occl": int((Rm & (cond_ids[i] < GEO.PLAYER_ID)).sum()),
"part_eq": int((rend[inter] == cond_ids[i][inter]).sum())})
return out
def spot_check(row, root, src, pipe, dev, log) -> tuple[list[str], dict]:
cid, s = row["clip_id"], row["start"]
err = []
paths = P.clip_paths(root, cid)
F = P.F_SPAN if row["has_ext"] else P.F_CLIP
R = P.RunTars(os.path.join(src, row["subset"], row["scene"], row["run"]))
Js = [R.json(s + i) for i in range(F)]
gt = np.stack([GEO.resample_rgb(cv2.cvtColor(cv2.imdecode(np.frombuffer(R.get(s + i, "jpg"), np.uint8), cv2.IMREAD_COLOR),
cv2.COLOR_BGR2RGB)) for i in range(P.F_CLIP)])
raws = [cv2.imdecode(np.frombuffer(R.get(s + i, "ids.png"), np.uint8), cv2.IMREAD_UNCHANGED) for i in range(F)]
R.close()
for i, raw in enumerate(raws): # 源 ids ⊆ {0..6,10,11}(全分辨率)
try:
P.check_raw_ids(raw, f"帧 {s + i}")
except AssertionError as e:
err.append(str(e))
break
ids_src = np.stack([GEO.resample_ids(raw) for raw in raws])
del raws
with np.load(paths["cond"]) as z:
cond = z["ids"]
if row["has_ext"]:
with np.load(paths["cond_ext"]) as z:
cond = np.concatenate([cond, z["ids"]])
with np.load(paths["meta"]) as z:
meta = {k: z[k] for k in z.files}
info = {"clip_id": cid, "subset": row["subset"], "drop": row["drop"], "F": F}
# label_parts 一致性
n_bad = int((GM.collapse_parts(cond) != ids_src).sum())
info["collapse_mismatch_px"] = n_bad
if n_bad:
err.append(f"collapse_parts(cond) ≠ resample_ids(ids.png):{n_bad} px")
for k in ("world_to_clip", "world_to_view", "projection"):
if not (meta[k] == np.array([J[k] for J in Js], np.float64)).all():
err.append(f"meta {k} ≠ tar json")
caps_j = np.stack([CAP.capsules_to_array(CAP.capsules_from_json(J)) for J in Js])
if not np.array_equal(meta["capsules"], caps_j, equal_nan=True):
err.append("meta capsules ≠ capsules_to_array(tar json)")
sub = list(range(0, F, 4))
eq_json = eq_meta = tot = 0
for i in sub:
J = Js[i]
a = CAP.label_parts(ids_src[i], J["world_to_clip"], J["world_to_view"], CAP.capsules_from_json(J))
b = CAP.label_parts(ids_src[i], meta["world_to_clip"][i], meta["world_to_view"][i], CAP.capsules_from_array(meta["capsules"][i]))
ch = ids_src[i] >= GEO.PLAYER_ID
eq_json += int((a == cond[i]).all())
eq_meta += int((b[ch] == cond[i][ch]).sum())
tot += int(ch.sum())
info["relabel_json_exact_frames"] = f"{eq_json}/{len(sub)}"
info["relabel_meta_char_px_agree"] = eq_meta / max(tot, 1)
if eq_json != len(sub):
err.append(f"label_parts(tar json) ≠ cond 在 {len(sub) - eq_json}/{len(sub)} 帧")
if info["relabel_meta_char_px_agree"] < 0.999:
err.append(f"label_parts(meta) 与 cond 角色像素一致率 {info['relabel_meta_char_px_agree']:.4f} < 0.999")
# VAE
d = torch.load(paths["latent"], map_location="cpu", weights_only=True)
lat = d["latent"]
with torch.no_grad():
rec = pipe.vae.decode([lat.float()], device=dev, tiled=False)[0] # [3,81,H,W] ∈ [-1,1]
rec = rec.float().add_(1.0).mul_(127.5).round_().clamp_(0, 255).to(torch.uint8).permute(1, 2, 3, 0).cpu().numpy()
info["psnr_db"] = psnr(rec, gt)
info["psnr_frame0_db"] = psnr(rec[:1], gt[:1])
x = torch.from_numpy(gt).permute(3, 0, 1, 2).float().div_(127.5).sub_(1.0) # infer.encode_video 原样(CPU)
z = pipe.vae.encode([x], device=dev, tiled=False)[0].to(torch.bfloat16).cpu()
z0 = pipe.vae.encode([x[:, :1]], device=dev, tiled=False)[0].float().cpu() # [48,1,H/16,W/16] fp32
info["reencode_exact_frac"] = float((z == lat).float().mean())
info["reencode_max_abs"] = float((z.float() - lat.float()).abs().max())
c0 = lat[:, :1].float()
info["cell0_bf16_exact_frac"] = float((z0.to(torch.bfloat16).float() == c0).float().mean())
info["cell0_rel_l2"] = float((z0 - c0).norm() / z0.norm())
if info["psnr_db"] < 18.0: # 错位 / 串 clip 才会这么低;内容简单的 clip 可到 35 dB 以上
err.append(f"VAE 重建 PSNR {info['psnr_db']:.2f} dB < 18")
elif not PSNR_WARN_LO <= info["psnr_db"] <= 40.0:
info["warn"] = f"VAE 重建 PSNR {info['psnr_db']:.2f} dB 不在 [{PSNR_WARN_LO:g}, 40]"
if info["reencode_exact_frac"] < 0.999:
err.append(f"官方 encode 重编 latent 一致率 {info['reencode_exact_frac']:.5f}")
if info["cell0_bf16_exact_frac"] < 0.999 or info["cell0_rel_l2"] > 4e-3:
err.append(f"latent cell 0 ≠ encode(帧 0):bf16 一致率 {info['cell0_bf16_exact_frac']:.5f},rel L2 {info['cell0_rel_l2']:.2e}")
# 胶囊重画(只用 meta)
info["render"] = render_stats(cond, meta, list(range(0, F, 4)))
log(f" {cid} F={F}: PSNR {info['psnr_db']:.2f} dB (f0 {info['psnr_frame0_db']:.2f}); re-encode exact {info['reencode_exact_frac']:.5f} "
f"(max {info['reencode_max_abs']:.2e}); cell0 bf16 exact {info['cell0_bf16_exact_frac']:.5f} rel {info['cell0_rel_l2']:.1e}; "
f"collapse mismatch {n_bad}; relabel json {info['relabel_json_exact_frames']} meta {info['relabel_meta_char_px_agree']:.5f}"
+ (f" ERR {err}" if err else ""))
return err, info
def summarize_render(spots) -> dict:
"""v10 未 drop 的抽查帧:pooled IoU / 覆盖率;无遮挡帧(occl < 1 %,GT ≥ 50 px · AREA_VS_480)的逐帧 IoU。"""
out = {}
for subset in P.SUBSETS:
recs = [r for sp in spots if sp["subset"] == subset and not sp["drop"] for r in sp.get("render", [])]
if not recs:
continue
o = {}
for cls, name in ((GEO.PLAYER_ID, "player"), (GEO.BOSS_ID, "boss")):
rs = [r for r in recs if r["cls"] == cls]
vis = [r for r in rs if r["g"] >= VIS_MIN_PX]
un = [r for r in vis if r["occl"] < 0.01 * max(r["r"], 1)]
ious = [r["i"] / r["u"] for r in un]
o[name] = {
"frames": len(rs), "visible_frames": len(vis), "unoccluded_frames": len(un),
"pooled_iou": sum(r["i"] for r in rs) / max(1, sum(r["u"] for r in rs)),
"coverage_gt_in_render": sum(r["i"] for r in rs) / max(1, sum(r["g"] for r in rs)),
"occluded_frac_of_render": sum(r["occl"] for r in rs) / max(1, sum(r["r"] for r in rs)),
"unoccluded_iou_median": float(np.median(ious)) if ious else None,
"unoccluded_iou_p10": float(np.percentile(ious, 10)) if ious else None,
"unoccluded_pooled_iou": sum(r["i"] for r in un) / max(1, sum(r["u"] for r in un)) if un else None,
"part_agree_in_intersection": sum(r["part_eq"] for r in rs) / max(1, sum(r["i"] for r in rs)),
}
out[subset] = o
return out
# ----------------------------------------------------------------------------
def main(argv=None):
ap = argparse.ArgumentParser(description=__doc__.split("\n")[0])
ap.add_argument("--root", required=True)
ap.add_argument("--src", default=P.SRC_DEFAULT)
ap.add_argument("--spot", type=int, default=8, help="GPU 抽查的 clip 数(0 = 不抽查)")
ap.add_argument("--device", default="cuda:0")
ap.add_argument("--workers", type=int, default=32)
ap.add_argument("--buried", default=P.BURIED_LIST)
ap.add_argument("--text_table", default=P.TEXT_TABLE_SRC)
ap.add_argument("--clips_per_run", type=int, default=50, help="每 run 应有的 clip 数(0 = 不查)")
args = ap.parse_args(argv)
root = os.path.abspath(args.root)
T0 = time.time()
fails, warns = [], []
log = lambda *x: print(*x, flush=True) # noqa: E731
GEO.check_root_res(root, "--root") # prep_args.json 的 res ≠ GC_RES ⇒ 直接报错(形状全对不上,没法往下验)
# ---------------- A. index
rows = [json.loads(line) for line in open(os.path.join(root, "index", "clips.jsonl")) if line.strip()]
ids = [r["clip_id"] for r in rows]
if len(set(ids)) != len(ids):
fails.append("clips.jsonl 有重复 clip_id")
buried = {x.strip() for x in open(args.buried) if x.strip()}
for r in rows:
miss = [k for k in ROW_KEYS if k not in r]
if miss:
fails.append(f"{r.get('clip_id')}: 行缺字段 {miss}")
continue
if r["clip_id"] != P.clip_id_of(r["scene"], r["run"], r["start"]) or r["end"] != r["start"] + P.F_CLIP:
fails.append(f"{r['clip_id']}: clip_id / end 不对")
if r["split"] != P.split_of(r["run"]):
fails.append(f"{r['clip_id']}: split {r['split']} ≠ 规则 {P.split_of(r['run'])}")
if r["drop"] != (r["clip_id"] in buried) or (r["drop_reason"] == P.DROP_REASON) != r["drop"]:
fails.append(f"{r['clip_id']}: drop / drop_reason 与 buried 名单不符")
bad_res = collections.Counter(r.get("res", GEO.LEGACY_RES) for r in rows if r.get("res", GEO.LEGACY_RES) != GEO.RES_TAG)
if bad_res:
fails.append(f"index 行的 res {dict(bad_res)} ≠ {GEO.RES_ENV}={GEO.RES_TAG}")
per_run = collections.Counter(r["run"] for r in rows)
if args.clips_per_run:
badrun = {k: v for k, v in per_run.items() if v != args.clips_per_run}
if badrun:
fails.append(f"这些 run 的 clip 数 ≠ {args.clips_per_run}:{badrun}")
runs = json.load(open(os.path.join(root, "index", "runs.json")))
if "_rule" not in runs or {k: v for k, v in runs.items() if k != "_rule"} != {r["run"]: r["split"] for r in rows}:
fails.append("runs.json 与 clips.jsonl 不一致")
tt = os.path.join(root, "text_table_gc25d.pt")
table_keys = []
if not os.path.isfile(tt):
fails.append("缺 text_table_gc25d.pt")
else:
h = [hashlib.sha256(open(p, "rb").read()).hexdigest() for p in (tt, args.text_table)]
if h[0] != h[1]:
fails.append("text_table_gc25d.pt 与源文件不同")
table = torch.load(tt, map_location="cpu", weights_only=True)
table_keys = sorted(table)
if set(table_keys) != set(TX.all_prompts()) | {TX.NULL_PROMPT}:
fails.append("text table 的 key ≠ 18 条 prompt + NULL_PROMPT")
for k, v in table.items():
if v.dtype != torch.bfloat16 or tuple(v.shape) != (512, 4096):
fails.append(f"text table[{k!r}] {v.dtype} {tuple(v.shape)}")
stray = [p for d in ("latents", "cond", "cond_ext", "meta") for p in glob.glob(os.path.join(root, d, "*.tmp*"))]
if stray:
warns.append(f"{len(stray)} 个残留 .tmp 文件(中断的运行),例如 {stray[:3]}")
on_disk = {os.path.basename(p)[:-3] for p in glob.glob(os.path.join(root, "latents", "*.pt"))}
orphan = on_disk - set(ids)
if orphan:
warns.append(f"{len(orphan)} 个 latent 不在 clips.jsonl 里")
sp = collections.Counter((r["subset"], r["split"], r["drop"], r["has_ext"]) for r in rows)
log(f"A. index: res {GEO.RES_TAG}, {len(rows)} clips, {len(per_run)} runs; " + "; ".join(f"{k}: {v}" for k, v in sorted(sp.items())))
# has_ext 里有多少在 cond_ext 内换 look(v10_real 帧 s+81 的语义图跳变;stage3 是否过滤见 docs/impl/prep.md)
ext = {s: {"has_ext": sum(r["has_ext"] for r in rows if r["subset"] == s),
"ext_look_change": sum(bool(r.get("ext_look_change")) for r in rows if r["subset"] == s),
"dmd_pool_train": sum(r["has_ext"] and not r["drop"] and r["split"] == "train" for r in rows if r["subset"] == s),
"dmd_pool_train_same_look": sum(r["has_ext"] and not r["drop"] and r["split"] == "train" and not r.get("ext_look_change")
for r in rows if r["subset"] == s)}
for s in P.SUBSETS}
log(" ext: " + "; ".join(f"{s} " + " ".join(f"{k}={v}" for k, v in o.items()) for s, o in ext.items()))
# ---------------- B. 逐 clip
t = time.time()
nbad = 0
infos = []
with Pool(min(args.workers, max(1, len(rows))), initializer=_pool_init, initargs=(table_keys, args.src)) as pool:
for cid, err, info in pool.imap_unordered(check_clip, [(r, root) for r in rows], chunksize=1):
if err:
nbad += 1
if nbad <= 50:
fails.append(f"{cid}: {'; '.join(err)}")
if info:
infos.append(info)
if nbad > 50:
fails.append(f"…… 共 {nbad} 个 clip 有错")
pxs = {s: (np.mean([i["player_px_mean"] for i in infos if i["subset"] == s]),
np.mean([i["boss_px_mean"] for i in infos if i["subset"] == s])) for s in P.SUBSETS if any(i["subset"] == s for i in infos)}
size = collections.defaultdict(list)
for i in infos:
for k, v in i["bytes"].items():
size[k].append(v)
sizes = {k: {"mean_MB": round(float(np.mean(v)) / 1e6, 3), "total_GB": round(sum(v) / 1e9, 3)} for k, v in size.items()}
log(f"B. per-clip: {len(rows) - nbad}/{len(rows)} ok ({time.time() - t:.0f}s); player/boss px per frame ({GEO.RES_TAG}) "
+ ", ".join(f"{s} {a:.0f}/{b:.0f}" for s, (a, b) in pxs.items()) + f"; sizes {sizes}")
# ---------------- C. GPU 抽查
spots = []
if args.spot and rows:
order = {s: i for i, s in enumerate(P.SUBSETS)}
cand = sorted(rows, key=lambda r: (order[r["subset"]], r["scene"], r["run"], r["start"]))
pick = sorted(set(np.linspace(0, len(cand) - 1, min(args.spot, len(cand))).round().astype(int).tolist()))
log(f"C. spot check {len(pick)} clips on {args.device}")
pipe = P.load_vae(args.device)
for k in pick:
try:
err, info = spot_check(cand[k], root, args.src, pipe, args.device, log)
except Exception as e: # noqa: BLE001
err, info = [f"抽查异常 {type(e).__name__}: {e}"], {"clip_id": cand[k]["clip_id"], "subset": cand[k]["subset"], "drop": True}
spots.append(info)
if info.get("warn"):
warns.append(f"spot {cand[k]['clip_id']}: {info['warn']}")
fails += [f"spot {cand[k]['clip_id']}: {e}" for e in err]
rs = summarize_render(spots)
for subset, o in rs.items():
for name, v in o.items():
log(f" render {subset:<11} {name:<6} " + " ".join(f"{k}={v[k]:.4f}" if isinstance(v[k], float) else f"{k}={v[k]}" for k in v))
v10 = rs.get("v10_real")
if v10 is None:
warns.append("抽查里没有 v10_real 的未 drop clip,跳过胶囊重画 IoU 判据")
else:
for name, v in v10.items():
if v["visible_frames"] and v["unoccluded_frames"] < 0.5 * v["visible_frames"]:
fails.append(f"render v10 {name}: 无遮挡帧只占 {v['unoccluded_frames']}/{v['visible_frames']}(meta 几何可能错位)")
if v["coverage_gt_in_render"] < 0.995:
fails.append(f"render v10 {name}: GT ⊆ 重画覆盖率 {v['coverage_gt_in_render']:.4f} < 0.995")
if v["unoccluded_iou_median"] is not None and (v["unoccluded_iou_median"] < 0.98 or v["unoccluded_pooled_iou"] < 0.98):
fails.append(f"render v10 {name}: 无遮挡帧 IoU median {v['unoccluded_iou_median']:.4f} / pooled {v['unoccluded_pooled_iou']:.4f} < 0.98")
ps = [s["psnr_db"] for s in spots if "psnr_db" in s]
if ps:
log(f" VAE recon PSNR: mean {np.mean(ps):.2f} dB, min {np.min(ps):.2f}, max {np.max(ps):.2f}")
else:
rs = {}
report = {"root": root, "res": GEO.RES_TAG, "time": time.strftime("%Y-%m-%d %H:%M:%S"), "seconds": round(time.time() - T0, 1),
"n_clips": len(rows), "n_runs": len(per_run), "fails": fails, "warns": warns, "ext": ext,
"px_per_frame": {s: {"player": a, "boss": b} for s, (a, b) in pxs.items()}, "sizes": sizes,
"spots": [{k: v for k, v in s.items() if k != "render"} for s in spots], "render": rs}
with open(os.path.join(root, "index", "verify_report.json"), "w") as fh:
json.dump(report, fh, indent=1, ensure_ascii=False, default=float)
for w in warns:
log("WARN", w)
for f in fails[:80]:
log("FAIL", f)
log(f"verify_prep: {'FAIL' if fails else 'OK'} — {len(rows)} clips, {len(fails)} fails, {len(warns)} warns, {time.time() - T0:.0f}s")
return 1 if fails else 0
if __name__ == "__main__":
sys.exit(main())