File size: 6,557 Bytes
f70ac4f | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 | #!/usr/bin/env python
"""Score one strategy on the eight protocol VBench dimensions.
Standard VBench 0.1.5 metric code is used unmodified. What is built here is only
the evaluation metadata file, because the protocol needs two things the stock
`vbench_standard` mode cannot express:
* `prompt_en` must be the *extended* prompt actually fed to the generator
(section 6.1), not the short canonical one;
* `scene` must keep the canonical `auxiliary_info` keywords.
The file has exactly the shape `build_full_info_json` produces, and each
`compute_<dimension>` selects the rows that list it, so per-suite scoping
(72 / 93 / 86 videos) falls out of the mapping rather than being hard-coded.
python eval/run_vbench.py --strategy sf_ffff --out-root eval_out
"""
import argparse
import json
import os
import sys
import time
ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
sys.path.insert(0, ROOT)
MAPPING = os.path.join(ROOT, "assets/vbench8_extended_subset_mapping.json")
SUITE_DIMENSIONS = {
"subject_consistency": ["subject_consistency", "motion_smoothness", "dynamic_degree"],
"overall_consistency": ["overall_consistency", "aesthetic_quality", "imaging_quality"],
"scene": ["scene", "background_consistency"],
}
DIMENSIONS = [d for dims in SUITE_DIMENSIONS.values() for d in dims]
def parse_args():
p = argparse.ArgumentParser()
p.add_argument("--strategy", required=True)
p.add_argument("--out-root", default="eval_out")
p.add_argument("--dimensions", default=None,
help="Comma-separated subset (default: all eight)")
p.add_argument("--allow-partial", action="store_true",
help="Score even if fewer than 251 videos exist (smoke tests only)")
p.add_argument("--overwrite", action="store_true",
help="Recompute dimensions whose raw result file already exists")
p.add_argument("--threads", type=int, default=None,
help="torch/OpenMP CPU threads for this process (scoring is CPU-bound; "
"several scorers on one node oversubscribe the cores)")
return p.parse_args()
def main():
args = parse_args()
out_root = (args.out_root if os.path.isabs(args.out_root)
else os.path.join(ROOT, args.out_root))
dims = args.dimensions.split(",") if args.dimensions else list(DIMENSIONS)
with open(MAPPING) as f:
mapping = json.load(f)
video_root = os.path.join(out_root, "generated_videos", args.strategy)
rows, missing = [], []
for r in mapping["rows"]:
vp = os.path.join(video_root, r["prompt_suite"], f"{r['suite_index']:03d}.mp4")
if not os.path.exists(vp):
missing.append(vp)
continue
entry = {
# The extended prompt is what the video was generated from, so it is
# what overall_consistency and scene must be scored against.
"prompt_en": r["extended_prompt"],
"dimension": [d for d in SUITE_DIMENSIONS[r["prompt_suite"]]],
"video_list": [vp],
"global_index": r["global_index"],
"prompt_suite": r["prompt_suite"],
"suite_index": r["suite_index"],
}
if "auxiliary_info" in r:
entry["auxiliary_info"] = r["auxiliary_info"]
rows.append(entry)
if missing and not args.allow_partial:
print(f"ERROR: {len(missing)} of {len(mapping['rows'])} videos missing for "
f"{args.strategy}; refusing to score an incomplete strategy.")
for m in missing[:5]:
print(" ", m)
return 1
print(f"{args.strategy}: {len(rows)} videos, dimensions={dims}", flush=True)
scores_dir = os.path.join(out_root, "vbench", "raw_results", args.strategy)
os.makedirs(scores_dir, exist_ok=True)
info_path = os.path.join(scores_dir, "full_info.json")
# Atomic: several per-dimension processes may start on the same strategy.
with open(info_path + f".{os.getpid()}", "w") as f:
json.dump(rows, f, indent=2)
os.replace(info_path + f".{os.getpid()}", info_path)
import importlib
import torch
if args.threads:
torch.set_num_threads(args.threads)
from vbench.utils import init_submodules
device = torch.device("cuda")
def raw_path(d):
return os.path.join(scores_dir, f"{d}.json")
# A dimension already scored (its raw file exists) is reused, so dimensions can
# be spread over several processes and a killed run resumes where it stopped.
todo = [d for d in dims if args.overwrite or not os.path.exists(raw_path(d))]
submodules = init_submodules(todo, local=True, read_frame=False) if todo else {}
for d in todo:
t0 = time.time()
module = importlib.import_module(f"vbench.{d}")
fn = getattr(module, f"compute_{d}")
score, per_video = fn(info_path, device, submodules[d])
tmp = raw_path(d) + ".tmp"
with open(tmp, "w") as f:
json.dump({"dimension": d, "score": score, "video_results": per_video,
"seconds": time.time() - t0}, f, indent=2)
os.replace(tmp, raw_path(d))
print(f" {d:24s} {score:.6f} ({len(per_video)} videos, "
f"{time.time() - t0:.0f}s)", flush=True)
torch.cuda.empty_cache()
# The strategy's score file is written only once all eight dimensions exist.
missing_dims = [d for d in DIMENSIONS if not os.path.exists(raw_path(d))]
if missing_dims:
print(f"{args.strategy}: {len(DIMENSIONS) - len(missing_dims)}/{len(DIMENSIONS)} "
f"dimensions scored; missing {missing_dims}", flush=True)
return 0
results = {}
for d in DIMENSIONS:
with open(raw_path(d)) as f:
raw = json.load(f)
results[d] = {"score": raw["score"], "num_videos": len(raw["video_results"]),
"seconds": raw.get("seconds")}
out_path = os.path.join(out_root, "vbench", "scores", f"{args.strategy}.json")
os.makedirs(os.path.dirname(out_path), exist_ok=True)
payload = {
"strategy": args.strategy,
"vbench_long": False,
"num_videos": len(rows),
"raw": {d: results[d]["score"] for d in DIMENSIONS},
"detail": results,
"full_info": info_path,
}
tmp = out_path + ".tmp"
with open(tmp, "w") as f:
json.dump(payload, f, indent=2)
os.replace(tmp, out_path)
print(f"wrote {out_path}", flush=True)
return 0
if __name__ == "__main__":
sys.exit(main())
|