rsigame-page / _build /build_media.py
WenyiWU0111's picture
Add the build scripts and docs; one index.html for every host
f12c058 verified
Raw History Blame Contribute Delete
9.2 kB
"""Build the page's media + data from the r0 baseline scoring runs.
For every (arm, task) it writes, under MEDIA_OUT/baseline/<arm>/<task>/:
highlight.mp4 ~10 s montage of the most active window of up to 4 demos (640x360, no audio)
poster.webp the busiest frame, for the card before the video loads
demos/<id>.mp4 every pass-1 demo, remuxed with faststart so it streams
and into the page repo:
static/data/games.json one row per game (what the gallery needs)
static/data/details/<arm>/<task>.json per-requirement scores + judge rationales (loaded on open)
Scores come from pass 1 of r0_scores (judge qwen38-27b); reward/M/D/V/A are the CSV means.
python _build/build_media.py --arms godot_gpt phaser_gpt [--tasks a b] [--jobs 16] [--data-only]
"""
import argparse, csv, json, re, subprocess, sys
from concurrent.futures import ProcessPoolExecutor
from pathlib import Path
import numpy as np
from PIL import Image
SCORES = Path("/storage/admin/wenyi/r0_scores")
MEDIA_OUT = Path("/storage/admin/wenyi/evogame-page-media")
PAGE = Path(__file__).resolve().parents[1]
TASKS = {"godot": Path("/home/admin/wenyi/gamecraft-bench/tasks"),
"phaser": Path("/home/admin/wenyi/gamecraft-bench-web/tasks-web")}
ARMS = {
"godot_gpt": {"engine": "Godot 4", "model": "GPT-5.5", "harness": "Codex CLI"},
"phaser_gpt": {"engine": "Phaser", "model": "GPT-5.5", "harness": "OpenGame"},
"godot_qwen": {"engine": "Godot 4", "model": "Qwen3.8-27B", "harness": ""},
"phaser_qwen": {"engine": "Phaser", "model": "Qwen3.8-27B", "harness": ""},
"godot_kimi": {"engine": "Godot 4", "model": "Kimi-K2.6", "harness": ""},
"godot_glm": {"engine": "Godot 4", "model": "GLM-5.3-Flash", "harness": ""},
}
CATS = {"M": "Core Mechanics", "D": "Content Depth", "V": "Functional Visuals", "A": "Presentation & Art"}
SEG_S, MAX_SEGS, FRAME_STEP_S = 2.5, 4, 0.5
def run(cmd):
subprocess.run(cmd, check=True, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE)
def instruction_meta(engine_key, task):
md = (TASKS[engine_key] / task / "instruction.md").read_text()
title = re.search(r"^# (.+)$", md, re.M).group(1).strip()
m = re.search(r"## Core Vision\s+(.+?)(?:\n## |\Z)", md, re.S)
vision = re.sub(r"\s+", " ", m.group(1)).strip() if m else ""
toml = (TASKS[engine_key] / task / "task.toml").read_text()
d = re.search(r'^description\s*=\s*"(.*)"', toml, re.M)
return title, (d.group(1) if d else ""), vision
def padded_from(task_dir):
"""demo_id -> first padded frame index (frozen tail we should not show)."""
p = task_dir / "tail_padding.json"
if not p.exists():
return {}
return {x["demo"]: x["from"] for x in json.loads(p.read_text()).get("padded", [])}
def activity(frames):
"""Per-step visual change between consecutive 0.5 s frames."""
thumbs = [np.asarray(Image.open(f).convert("L").resize((64, 36)), dtype=np.float32) for f in frames]
return [float(np.abs(a - b).mean()) for a, b in zip(thumbs, thumbs[1:])]
def best_window(demo):
"""(start_s, score, busiest_frame) of the most active SEG_S window in the unpadded part."""
frames = sorted((demo["dir"] / "frames").glob("frame_*.png"))
usable = demo["dur"] - 0.3
if demo["pad_from_frame"] is not None:
usable = min(usable, demo["pad_from_frame"] / 30.0)
n = int(usable / FRAME_STEP_S)
frames = frames[: max(n, 2)]
if len(frames) < 2:
return 0.0, 0.0, frames[0] if frames else None
act = activity(frames)
w = max(1, int(SEG_S / FRAME_STEP_S))
sums = [sum(act[i:i + w]) for i in range(max(1, len(act) - w + 1))]
i = int(np.argmax(sums))
start = min(i * FRAME_STEP_S, max(0.0, usable - SEG_S))
busiest = frames[i + 1 + int(np.argmax(act[i:i + w]))] if act[i:i + w] else frames[i]
return start, sums[i], busiest
def build_game(arm, task, data_only=False):
tdir = SCORES / arm / task
p1 = tdir / "p1"
out = MEDIA_OUT / "baseline" / arm / task
pads = padded_from(tdir)
demos = []
if (p1 / "demos").is_dir():
for d in sorted((p1 / "demos").iterdir()):
mp4 = d / f"{d.name}.mp4"
if not mp4.exists():
continue
dur = float(subprocess.run(["ffprobe", "-v", "error", "-show_entries", "format=duration",
"-of", "csv=p=0", str(mp4)], capture_output=True, text=True).stdout or 0)
demos.append({"id": d.name, "dir": d, "mp4": mp4, "dur": dur, "pad_from_frame": pads.get(d.name)})
has_media = bool(demos)
if has_media and not (data_only and (out / "highlight.mp4").exists()):
(out / "demos").mkdir(parents=True, exist_ok=True)
for d in demos:
run(["ffmpeg", "-y", "-i", str(d["mp4"]), "-c", "copy", "-an", "-movflags", "+faststart",
str(out / "demos" / f"{d['id']}.mp4")])
wins = [(d, *best_window(d)) for d in demos]
# the MAX_SEGS most active demos, shown in their original order
picked = sorted(sorted(wins, key=lambda x: -x[2])[:MAX_SEGS], key=lambda x: x[0]["id"])
cmd, fl = ["ffmpeg", "-y"], []
for k, (d, start, _, _) in enumerate(picked):
cmd += ["-ss", f"{start:.2f}", "-t", str(SEG_S), "-i", str(d["mp4"])]
fl.append(f"[{k}:v]scale=640:360:force_original_aspect_ratio=decrease,pad=640:360:(ow-iw)/2:(oh-ih)/2,"
f"fps=24,setsar=1,format=yuv420p[v{k}]")
fl.append("".join(f"[v{k}]" for k in range(len(picked))) + f"concat=n={len(picked)}:v=1:a=0[out]")
cmd += ["-filter_complex", ";".join(fl), "-map", "[out]", "-c:v", "libx264", "-preset", "slow",
"-crf", "30", "-movflags", "+faststart", "-an", str(out / "highlight.mp4")]
run(cmd)
busiest = max(wins, key=lambda x: x[2])[3]
Image.open(busiest).convert("RGB").resize((640, 360)).save(out / "poster.webp", "WEBP", quality=72)
return arm, task, [{"id": d["id"], "dur": round(d["dur"], 1)} for d in demos], has_media
def details(arm, task):
p1 = SCORES / arm / task / "p1"
bpath = p1 / "breakdown.json"
if not bpath.exists():
return None
b = json.loads(bpath.read_text())
rationale = {}
jl = p1 / "judge_log.json"
if jl.exists():
for r in json.loads(jl.read_text()):
rationale.setdefault(r["requirement_id"], {})[r["demo_id"]] = r.get("rationale", "")
return {
"reward_p1": b.get("reward"), "build_ok": b.get("build_ok"), "formula": b.get("formula"),
"judge": b.get("judge", {}).get("model"),
"requirements": [{
"id": r["id"], "cat": CATS.get(r["id"][0], ""), "description": r["description"],
"score": r.get("aggregated"), "agg": r.get("agg"),
"per_demo": r.get("per_demo", {}), "rationale": rationale.get(r["id"], {}),
} for r in b.get("requirements", [])],
}
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--arms", nargs="+", default=["godot_gpt", "phaser_gpt"])
ap.add_argument("--tasks", nargs="*")
ap.add_argument("--jobs", type=int, default=16)
ap.add_argument("--data-only", action="store_true", help="skip media that already exists")
a = ap.parse_args()
rows = {}
for arm in a.arms:
for r in csv.DictReader(open(SCORES / f"{arm}.csv")):
if not a.tasks or r["task"] in a.tasks:
rows[(arm, r["task"])] = r
media = {}
with ProcessPoolExecutor(a.jobs) as ex:
futs = [ex.submit(build_game, arm, task, a.data_only) for arm, task in rows]
for f in futs:
try:
arm, task, demos, has_media = f.result()
media[(arm, task)] = (demos, has_media)
print("ok", arm, task, len(demos), flush=True)
except subprocess.CalledProcessError as e:
print("FAIL", e.cmd[-1], e.stderr.decode()[-400:], file=sys.stderr, flush=True)
data = PAGE / "static" / "data"
games = []
for (arm, task), r in rows.items():
if (arm, task) not in media:
continue
engine_key = arm.split("_")[0]
title, blurb, vision = instruction_meta(engine_key, task)
demos, has_media = media[(arm, task)]
det = details(arm, task)
if det:
(data / "details" / arm).mkdir(parents=True, exist_ok=True)
(data / "details" / arm / f"{task}.json").write_text(json.dumps(det, ensure_ascii=False))
f = lambda k: round(float(r[k]), 4)
games.append({
"arm": arm, "task": task, **ARMS[arm], "genre": task.split("-")[0],
"title": title, "blurb": blurb, "vision": vision,
"reward": f("reward"), "build": f("BUILD"), "M": f("M"), "D": f("D"), "V": f("V"), "A": f("A"),
"media": f"baseline/{arm}/{task}" if has_media else None, "demos": demos,
"repaired": None,
})
games.sort(key=lambda g: (g["arm"], -g["reward"]))
data.mkdir(parents=True, exist_ok=True)
(data / "games.json").write_text(json.dumps(games, ensure_ascii=False, indent=0))
print(f"wrote {len(games)} games")
if __name__ == "__main__":
main()