Files
hao-ai-lab-FastVideo/scripts/eval/score_folder.py
T
shaoxiongduan 5cc337fdb6 [feat] eval: add basic_ltx2_eval.py + score_folder.py scripts
Two new entrypoints showcasing the post-refactor eval surface:

- examples/inference/eval/basic_ltx2_eval.py — generate one LTX2
  video using the same parameters as basic_ltx2.py (same prompt,
  model, 1088×1920×121), then score it with the prompt-aware VBench
  subset. Builds Evaluator directly, calls evaluate(**kwargs) per
  the single-sample API; no runner, no argparse.

- scripts/eval/score_folder.py — bulk-score every video in a folder
  with prompt-free VBench metrics. Optional --prompts-json maps
  filenames to prompts to enable prompt-aware metrics. Uses
  Evaluator.evaluate(samples=[...]) for multi-GPU fan-out.

Tested on fs-mbz-gpu-085:

- score_folder.py: 3 duplicate mp4s × 4 metrics → 3 identical score
  sets, summary aggregated, scores.json written. Clean run.

- basic_ltx2_eval.py: generation succeeded; scoring path verified
  for 7 of 8 metrics on the LTX2 output (aesthetic_quality,
  subject_consistency, background_consistency, imaging_quality,
  temporal_flickering, dynamic_degree, overall_consistency).
  vbench.motion_smoothness OOMs at 1088×1920 on a shared GPU because
  VBench's AMT memory autoscale reads total_memory rather than
  mem_get_info() free memory, so it underestimates the required
  resolution scale-down when another process holds 45 GB. Documented
  in the script docstring; not refactor-related (same behavior at
  HEAD pre-refactor). Drop motion_smoothness from METRICS or run on
  a dedicated GPU to score it.

Added a torch.cuda.empty_cache() between generator.shutdown() and
evaluator construction to free residual generation memory.
2026-05-06 01:46:29 +00:00

132 lines
4.6 KiB
Python

"""Score every video in a folder with VBench metrics (no prompts).
Multi-GPU bulk eval over a flat folder of mp4s. The default metric
set is the prompt-free VBench subset — anything that needs a source
prompt or aux info is excluded by default.
python scripts/eval/score_folder.py \\
--videos-dir runs/my_outputs \\
--num-gpus 4 \\
--fps 24 \\
--results-json runs/my_outputs/scores.json
Pass ``--metrics`` to override the default set; pass a custom JSON
mapping ``filename → prompt`` via ``--prompts-json`` to enable
prompt-aware metrics.
"""
from __future__ import annotations
import argparse
import json
import logging
from pathlib import Path
logging.basicConfig(level=logging.INFO,
format="%(asctime)s %(levelname)s %(name)s: %(message)s")
log = logging.getLogger("score-folder")
# Metrics that need only the generated frames (and optionally fps).
DEFAULT_METRICS = [
"vbench.aesthetic_quality",
"vbench.subject_consistency",
"vbench.background_consistency",
"vbench.imaging_quality",
"vbench.temporal_flickering",
"vbench.motion_smoothness",
"vbench.dynamic_degree",
]
VIDEO_EXTS = {".mp4", ".mov", ".avi", ".webm", ".mkv"}
def parse_args() -> argparse.Namespace:
ap = argparse.ArgumentParser(
__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("--videos-dir", required=True, type=Path)
ap.add_argument("--metrics", default=",".join(DEFAULT_METRICS),
help="Comma-separated metric names, or 'all', or a "
"group prefix like 'vbench'.")
ap.add_argument("--num-gpus", type=int, default=1)
ap.add_argument("--fps", type=float, default=24.0)
ap.add_argument("--prompts-json", type=Path, default=None,
help="Optional path to a JSON {filename: prompt} mapping. "
"If given, prompts attach to each sample as "
"text_prompt (enables prompt-aware metrics).")
ap.add_argument("--results-json", type=Path, default=None)
return ap.parse_args()
def parse_metrics(spec: str) -> str | list[str]:
spec = spec.strip()
if spec == "all":
return "all"
if "," in spec:
return [m.strip() for m in spec.split(",") if m.strip()]
return spec
def main() -> None:
args = parse_args()
from fastvideo.eval import Evaluator
from fastvideo.eval.io import build_eval_kwargs
from fastvideo.eval.types import EvalResult
videos_dir: Path = args.videos_dir
if not videos_dir.is_dir():
raise SystemExit(f"--videos-dir {videos_dir} is not a directory")
paths = sorted(p for p in videos_dir.iterdir()
if p.suffix.lower() in VIDEO_EXTS)
if not paths:
raise SystemExit(f"no videos found in {videos_dir} "
f"(looked for {sorted(VIDEO_EXTS)})")
log.info("[score] %d videos found in %s", len(paths), videos_dir)
prompts: dict[str, str] = {}
if args.prompts_json:
prompts = json.loads(args.prompts_json.read_text())
log.info("[score] loaded %d prompts from %s",
len(prompts), args.prompts_json)
samples: list[dict] = []
metas: list[dict] = []
for vp in paths:
row: dict = {}
if vp.name in prompts:
row["prompt"] = prompts[vp.name]
samples.append(build_eval_kwargs(row, vp, fps=args.fps))
metas.append({
"video": str(vp),
"prompt": row.get("prompt"),
})
log.info("[score] building evaluator: %s (num_gpus=%d)",
args.metrics, args.num_gpus)
evaluator = Evaluator(metrics=parse_metrics(args.metrics),
num_gpus=args.num_gpus)
log.info("[score] dispatching %d samples...", len(samples))
results = evaluator.evaluate(samples)
by_metric: dict[str, list[float]] = {}
per_video: list[dict] = []
for meta, res in zip(metas, results, strict=True):
scores: dict[str, float] = {}
for name, mr in res.items():
if mr is not None and mr.score is not None:
scores[name] = float(mr.score)
by_metric.setdefault(name, []).append(scores[name])
per_video.append({**meta, "scores": scores})
log.info("[score] %s -> %s", meta["video"],
{k: round(v, 4) for k, v in scores.items()})
log.info("[score] done. videos_scored=%d", len(per_video))
result = EvalResult.from_raw(by_metric, per_video)
result.print()
if args.results_json:
result.save(args.results_json)
log.info("[score] wrote %s", args.results_json)
if __name__ == "__main__":
main()