File size: 12,388 Bytes
eafbe80 | 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 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 | #!/usr/bin/env python3
from __future__ import annotations
import argparse
import base64
import csv
import json
import re
import time
from collections import defaultdict
from concurrent.futures import ThreadPoolExecutor, as_completed
from pathlib import Path
from typing import Any
import requests
SYSTEM = "You are a strict visual memory evaluator. Return only valid JSON."
USER_PROMPT = """Evaluate whether the revisit tail frames preserve the same object and scene as the first frame.
The most important target is the toy bear if it appears in the first frame. Judge its appearance consistency strongly:
- bear identity, color, shape, face, pose, clothing/accessories if visible
- whether the final revisit still depicts the same bear after camera motion
Scene/layout consistency is secondary. Give lower weight to background unless it contradicts the target.
Return JSON with:
{
"bear_appearance_score": 0-5,
"bear_presence_score": 0-5,
"scene_consistency_score": 0-5,
"view_revisit_score": 0-5,
"overall_score": 0-100,
"verdict": "pass|partial|fail",
"reason": "short evidence"
}
Weights for overall_score: bear_appearance 45%, bear_presence 25%, view_revisit 20%, scene_consistency 10%.
"""
def load_json(path: Path) -> dict[str, Any]:
return json.loads(path.read_text(encoding="utf-8"))
def encode_image(path: str | Path) -> str:
return base64.b64encode(Path(path).read_bytes()).decode("ascii")
def parse_json(text: str) -> dict[str, Any]:
text = re.sub(r"<think>.*?</think>", "", text, flags=re.DOTALL).strip().strip("`")
match = re.search(r"\{.*\}", text, flags=re.DOTALL)
if not match:
raise ValueError(f"no JSON in VLM response: {text[:300]}")
return json.loads(match.group(0))
def wait_ready(api_base: str, timeout: int) -> None:
url = f"{api_base.rstrip('/')}/models"
deadline = time.time() + timeout
while time.time() < deadline:
try:
if requests.get(url, timeout=5).status_code == 200:
return
except Exception:
pass
time.sleep(2)
raise RuntimeError(f"VLM server not ready at {api_base}")
def score_one(metrics_path: Path, api_base: str, model: str, timeout: int, force: bool) -> Path:
out_path = metrics_path.parent / "vlm_score.json"
if out_path.is_file() and not force:
return out_path
metrics = load_json(metrics_path)
images = list(metrics.get("first_frame_paths") or []) + list(metrics.get("revisit_tail_paths") or [])
content: list[dict[str, Any]] = [{"type": "text", "text": USER_PROMPT}]
for image_path in images:
content.append({"type": "image_url", "image_url": {"url": f"data:image/png;base64,{encode_image(image_path)}"}})
payload = {
"model": model,
"messages": [
{"role": "system", "content": [{"type": "text", "text": SYSTEM}]},
{"role": "user", "content": content},
],
"temperature": 0.0,
"max_tokens": 800,
"chat_template_kwargs": {"enable_thinking": False},
}
response = requests.post(f"{api_base.rstrip('/')}/chat/completions", json=payload, timeout=timeout)
response.raise_for_status()
raw = response.json()["choices"][0]["message"]["content"]
vlm = parse_json(raw)
out = {
"metrics_path": str(metrics_path),
"run_id": metrics.get("run_id"),
"domain": (metrics.get("sample") or {}).get("domain"),
"sample_id": (metrics.get("sample") or {}).get("sample_id"),
"mode": metrics.get("mode"),
"traditional": {
"closure_psnr": metrics.get("closure_psnr"),
"closure_ssim": metrics.get("closure_ssim"),
"closure_mse": metrics.get("closure_mse"),
},
"vlm": vlm,
}
out_path.write_text(json.dumps(out, ensure_ascii=False, indent=2), encoding="utf-8")
return out_path
def maybe_score_vlm(metrics_paths: list[Path], args: argparse.Namespace) -> None:
if not args.score_vlm:
return
wait_ready(args.vlm_api_base, args.startup_wait_sec)
todo = [p for p in metrics_paths if args.force_vlm or not (p.parent / "vlm_score.json").is_file()]
print(f"[vlm] scoring {len(todo)} / {len(metrics_paths)} cases with workers={args.vlm_workers}")
if not todo:
return
with ThreadPoolExecutor(max_workers=args.vlm_workers) as pool:
futures = [pool.submit(score_one, p, args.vlm_api_base, args.vlm_model, args.vlm_timeout, args.force_vlm) for p in todo]
for i, fut in enumerate(as_completed(futures), 1):
path = fut.result()
print(f"[vlm] {i}/{len(futures)} {path}")
def flat_row(metrics_path: Path, eval_root: Path) -> dict[str, Any]:
metrics = load_json(metrics_path)
vlm_path = metrics_path.parent / "vlm_score.json"
vlm = load_json(vlm_path).get("vlm", {}) if vlm_path.is_file() else {}
sample = metrics.get("sample") or {}
align = metrics.get("alignment") or {}
init_ctx = align.get("initial_context") or {}
key_ev = align.get("ckpt_key_evidence") or {}
return {
"eval_root": eval_root.name,
"run_id": metrics.get("run_id"),
"ckpt": metrics.get("ckpt"),
"domain": sample.get("domain"),
"sample_id": sample.get("sample_id"),
"video_name": sample.get("video_name"),
"start_frame": sample.get("start_frame"),
"mode": metrics.get("mode"),
"num_chunks": metrics.get("num_chunks"),
"context_frames": metrics.get("context_frames"),
"closure_psnr": metrics.get("closure_psnr"),
"closure_ssim": metrics.get("closure_ssim"),
"closure_mse": metrics.get("closure_mse"),
"vlm_overall_score": vlm.get("overall_score"),
"vlm_bear_appearance_score": vlm.get("bear_appearance_score"),
"vlm_bear_presence_score": vlm.get("bear_presence_score"),
"vlm_scene_consistency_score": vlm.get("scene_consistency_score"),
"vlm_view_revisit_score": vlm.get("view_revisit_score"),
"vlm_verdict": vlm.get("verdict"),
"vlm_reason": vlm.get("reason"),
"memory_profile": align.get("memory_profile"),
"memory_profile_matched": align.get("memory_profile_matched"),
"action_injection_impl": align.get("action_injection_impl"),
"camera_inject_mode_effective": align.get("camera_inject_mode_effective"),
"rt_encoding_effective": align.get("rt_encoding_effective"),
"use_framepack_memory": align.get("use_framepack_memory"),
"use_framepack_length_compress": align.get("use_framepack_length_compress"),
"framepack_ratio": align.get("framepack_ratio"),
"use_spatial_memory": align.get("use_spatial_memory"),
"use_spatial_memory_legacy": align.get("use_spatial_memory_legacy"),
"spatial_memory_tokens": align.get("spatial_memory_tokens"),
"spatial_memory_inject_mode": align.get("spatial_memory_inject_mode"),
"ckpt_has_camera_encoder_keys": key_ev.get("has_camera_encoder_keys"),
"ckpt_has_spatial_memory_module_keys": key_ev.get("has_spatial_memory_module_keys"),
"ckpt_has_block_wise_ssm_keys": key_ev.get("has_block_wise_ssm_keys"),
"ckpt_has_videossm_hybrid_keys": key_ev.get("has_videossm_hybrid_keys"),
"ckpt_has_ssm_keys": key_ev.get("has_ssm_keys"),
"initial_context_source": init_ctx.get("training_memory_source"),
"initial_context_detail": init_ctx.get("context_source_detail"),
"initial_context_frame_count": init_ctx.get("context_frame_count"),
"stage1_metrics_path": str(metrics_path),
"vlm_score_path": str(vlm_path) if vlm_path.is_file() else "",
"video_path": metrics.get("video_path"),
}
def mean(values: list[Any]) -> float | None:
xs = []
for value in values:
try:
if value is not None and value != "":
xs.append(float(value))
except (TypeError, ValueError):
pass
return sum(xs) / len(xs) if xs else None
def aggregate(rows: list[dict[str, Any]]) -> list[dict[str, Any]]:
groups: dict[tuple[Any, ...], list[dict[str, Any]]] = defaultdict(list)
for row in rows:
groups[(row["run_id"], row["domain"], row["mode"])].append(row)
out = []
for (run_id, domain, mode), group in sorted(groups.items()):
out.append(
{
"run_id": run_id,
"domain": domain,
"mode": mode,
"num_cases": len(group),
"mean_closure_psnr": mean([r["closure_psnr"] for r in group]),
"mean_closure_ssim": mean([r["closure_ssim"] for r in group]),
"mean_closure_mse": mean([r["closure_mse"] for r in group]),
"mean_vlm_overall_score": mean([r["vlm_overall_score"] for r in group]),
"mean_vlm_bear_appearance_score": mean([r["vlm_bear_appearance_score"] for r in group]),
"mean_vlm_bear_presence_score": mean([r["vlm_bear_presence_score"] for r in group]),
"mean_vlm_scene_consistency_score": mean([r["vlm_scene_consistency_score"] for r in group]),
"mean_vlm_view_revisit_score": mean([r["vlm_view_revisit_score"] for r in group]),
"memory_profile": group[0]["memory_profile"],
"context_frames": group[0]["context_frames"],
"use_framepack_memory": group[0]["use_framepack_memory"],
"use_framepack_length_compress": group[0]["use_framepack_length_compress"],
"framepack_ratio": group[0]["framepack_ratio"],
"use_spatial_memory": group[0]["use_spatial_memory"],
"use_spatial_memory_legacy": group[0]["use_spatial_memory_legacy"],
"spatial_memory_tokens": group[0]["spatial_memory_tokens"],
"spatial_memory_inject_mode": group[0]["spatial_memory_inject_mode"],
"eval_roots": "|".join(sorted({str(r["eval_root"]) for r in group})),
}
)
return out
def write_csv(path: Path, rows: list[dict[str, Any]]) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
if not rows:
path.write_text("", encoding="utf-8")
return
fieldnames = list(rows[0].keys())
with path.open("w", encoding="utf-8", newline="") as f:
writer = csv.DictWriter(f, fieldnames=fieldnames)
writer.writeheader()
writer.writerows(rows)
def main() -> None:
ap = argparse.ArgumentParser()
ap.add_argument("--eval-root", action="append", required=True)
ap.add_argument("--out-dir", default="revisit_materials")
ap.add_argument("--prefix", default="ep0_revisit")
ap.add_argument("--score-vlm", action="store_true")
ap.add_argument("--force-vlm", action="store_true")
ap.add_argument("--vlm-workers", type=int, default=8)
ap.add_argument("--vlm-api-base", default="http://127.0.0.1:8000/v1")
ap.add_argument("--vlm-model", default="Qwen/Qwen2.5-VL-72B-Instruct")
ap.add_argument("--vlm-timeout", type=int, default=180)
ap.add_argument("--startup-wait-sec", type=int, default=900)
args = ap.parse_args()
eval_roots = [Path(p) for p in args.eval_root]
metrics_paths: list[Path] = []
for root in eval_roots:
stage1 = root / "stage1"
found = sorted(stage1.rglob("stage1_metrics.json"))
print(f"[collect] {root}: {len(found)} stage1 metrics")
metrics_paths.extend(found)
maybe_score_vlm(metrics_paths, args)
rows = []
for root in eval_roots:
for path in sorted((root / "stage1").rglob("stage1_metrics.json")):
rows.append(flat_row(path, root))
agg_rows = aggregate(rows)
out_dir = Path(args.out_dir)
cases_csv = out_dir / f"{args.prefix}_cases.csv"
aggregate_csv = out_dir / f"{args.prefix}_aggregate_by_model_domain_mode.csv"
write_csv(cases_csv, rows)
write_csv(aggregate_csv, agg_rows)
(out_dir / f"{args.prefix}_cases.json").write_text(json.dumps(rows, ensure_ascii=False, indent=2), encoding="utf-8")
(out_dir / f"{args.prefix}_aggregate_by_model_domain_mode.json").write_text(json.dumps(agg_rows, ensure_ascii=False, indent=2), encoding="utf-8")
print(f"[export] cases={len(rows)} -> {cases_csv}")
print(f"[export] aggregates={len(agg_rows)} -> {aggregate_csv}")
if __name__ == "__main__":
main()
|