Spaces:
Sleeping
Sleeping
File size: 6,116 Bytes
6ec7f56 7966bbd 6ec7f56 7a86a2b 7966bbd 7a86a2b 7966bbd 6ec7f56 | 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 | #!/usr/bin/env python3
"""Package recorded agent episodes into the Space's replay-player format.
Input: run directories produced by an arena runner (uitars-webtask-eval's
run_arena.py or this repo's harness) containing frames/, transcript.txt, and
result.json. Frames are assumed to be saved 8x per step (video padding);
one frame per step is kept, downscaled to JPEG.
python3 scripts/build_replays.py --agent "UI-TARS-1.5-7B (4-bit)" RUN_DIR...
Output: server/static/replays/<task>/step_NNN.jpg + replay.json, and a
top-level manifest.json the landing/replay/play pages read.
"""
from __future__ import annotations
import argparse
import json
import re
import sys
from pathlib import Path
from PIL import Image
OUT_ROOT = Path(__file__).resolve().parent.parent / "server" / "static" / "replays"
FRAME_DUP = 8 # frames saved per step by the runners
JPEG_WIDTH = 880
JPEG_QUALITY = 68
_STEP = re.compile(r"^\[step (\d+)\] (\w+) point=(.*?) end=.*?content=(.*)$")
DIAGNOSIS = {
"checkout-form": "Clean run: precise field clicks, correct typing, "
"three wizard steps, submit.",
"email-triage": "State-tracking failure: archives worked, but the model never "
"perceived rows disappearing — it re-clicked fixed coordinates "
"while the list reflowed underneath, archiving 7 extra emails "
"while blaming 'an unresponsive system'.",
"settings-panel": "Environment finding: headless chromium renders native "
"<select> dropdowns outside the page — the timezone options "
"are invisible to any screenshot agent. Dark Mode itself was "
"correctly enabled and saved.",
"causal-gate": "Causal reasoning: correct. Grounding: a near-miss. The model "
"opened the dropdown, selected “Friend referral,” watched the "
"referral field appear, and typed the right code — it got the "
"causal precondition exactly right. Its final click aimed at "
"“Save changes” landed about 12px above the button, on the "
"divider line just above it, so the click never registered "
"and nothing was saved.",
"flash-offer": "Same 'distrusts its own successful action' pattern as "
"email-triage, reproduced in an unrelated task. The model "
"correctly named the live code at each glance as it rotated, "
"but its first click genuinely focused the input — a subtle "
"focus-ring change it misread as 'the page didn't respond,' "
"so it re-clicked the same point instead of typing, until the "
"harness's repeat-action guard aborted the episode.",
}
def parse_transcript(path: Path) -> list[dict]:
steps, current = [], None
for line in path.read_text().splitlines():
m = _STEP.match(line)
if m:
current = {"step": int(m.group(1)), "action": m.group(2),
"point": m.group(3), "content": m.group(4), "thought": ""}
steps.append(current)
elif current is not None and line.strip().startswith("thought:"):
current["thought"] = line.split("thought:", 1)[1].strip()
return steps
def action_label(s: dict) -> str:
label = s["action"]
if s["point"] not in ("None", ""):
label += f" @ {s['point']}"
if s["content"] not in ("None", "''", '""'):
label += f" {s['content']}"
return label
def build_one(run_dir: Path, agent: str) -> dict:
result = json.loads((run_dir / "result.json").read_text())
task = result["task"]
steps = parse_transcript(run_dir / "transcript.txt")
frames = sorted((run_dir / "frames").glob("frame_*.png"))
out_dir = OUT_ROOT / task
out_dir.mkdir(parents=True, exist_ok=True)
step_entries = []
for s in steps:
idx = s["step"] * FRAME_DUP
if idx >= len(frames):
break
img = Image.open(frames[idx]).convert("RGB")
img = img.resize((JPEG_WIDTH, round(img.height * JPEG_WIDTH / img.width)))
name = f"step_{s['step']:03d}.jpg"
img.save(out_dir / name, quality=JPEG_QUALITY, optimize=True)
step_entries.append({"img": name, "action": action_label(s),
"thought": s["thought"]})
# trailing "final state" frame, if recorded beyond the last step
final_idx = len(steps) * FRAME_DUP
if final_idx < len(frames):
img = Image.open(frames[final_idx]).convert("RGB")
img = img.resize((JPEG_WIDTH, round(img.height * JPEG_WIDTH / img.width)))
img.save(out_dir / "final.jpg", quality=JPEG_QUALITY, optimize=True)
step_entries.append({"img": "final.jpg", "action": "final state",
"thought": ""})
entry = {
"task": task,
"agent": agent,
"seed": result["seed"],
"instruction": result["instruction"],
"success": result["success"],
"subgoals": result["subgoals"],
"diagnosis": DIAGNOSIS.get(task, ""),
"steps": step_entries,
}
(out_dir / "replay.json").write_text(json.dumps(entry, indent=1))
return entry
def main() -> int:
ap = argparse.ArgumentParser(description=__doc__)
ap.add_argument("run_dirs", nargs="+", type=Path)
ap.add_argument("--agent", default="UI-TARS-1.5-7B (4-bit)")
args = ap.parse_args()
manifest = [build_one(d, args.agent) for d in args.run_dirs]
OUT_ROOT.mkdir(parents=True, exist_ok=True)
# manifest omits per-step data to keep the landing page light
slim = [{k: v for k, v in e.items() if k != "steps"} | {"n_steps": len(e["steps"])}
for e in manifest]
(OUT_ROOT / "manifest.json").write_text(json.dumps(slim, indent=1))
total = sum(f.stat().st_size for f in OUT_ROOT.rglob("*") if f.is_file())
print(f"built {len(manifest)} replays -> {OUT_ROOT} ({total/1e6:.1f} MB)")
return 0
if __name__ == "__main__":
sys.exit(main())
|