maliang / scripts /teaser /canvas_batch.py
teawhite's picture
scripts/teaser: teaser / edit / bossswap / infinite / cover code
68e2686 verified
Raw History Blame Contribute Delete
23.2 kB
#!/usr/bin/env python3
"""teaser · canvas(白纸作画)量产调度:长地图 × 模板(canvas_edits.TEMPLATES)× 首帧 → 编辑版语义 → DMD 任务(rollout_dmd,
共享队列 outputs/teaser/edit/canvas/jobs.jsonl)→ 结果目录 cases/<name>/ + index.jsonl → 打分 → showcase。
每个 (长地图, 首帧) 的对照:
orig 原版(完整世界,不编辑):live 已经跑过的直接复用(outputs/teaser/edit/live/roll/<地图>__<场景>__orig),没有的新跑;
B0 白纸对照(帧 0 看不见的东西全删 + 1 s 时清空,之后什么都不画):白纸类编辑版(W / R / WV / RV / atk)逐笔效果的参照
(同首帧同种子同按键 ⇒ 第一笔之前逐帧一致,之后只差这一笔一笔)。
usage(env:source scripts/teaser/live_env.sh):
$PY scripts/teaser/canvas_batch.py tl --gpus 3,4,5 --procs 12 [--only V]
$PY scripts/teaser/canvas_batch.py jobs # 追加 / 重写 jobs.jsonl(workers 自己会读到新行)
$PY scripts/teaser/canvas_batch.py status
$PY scripts/teaser/canvas_batch.py cases --procs 16 # cases/<name>/ + index.jsonl(mask.npz 在这一步算)
$PY scripts/teaser/canvas_batch.py score --procs 24 # 逐事件效果分 → showcase/scores.json + score_summary.json
"""
from __future__ import annotations
import argparse
import json
import os
import shutil
import subprocess
import sys
import time
M = "/opt/dlami/nvme/zhiyangdeng/maliang"
HERE = os.path.dirname(os.path.abspath(__file__))
for _p in (f"{M}/code/gc25d", f"{M}/code/arope", HERE):
if _p not in sys.path:
sys.path.insert(0, _p)
CV = f"{M}/outputs/teaser/edit/canvas"
LV = f"{M}/outputs/teaser/edit/live"
F0 = f"{M}/outputs/teaser/f0"
LF0 = f"{LV}/f0"
LOG = f"{M}/outputs/teaser/logs/edit2"
P8 = "P9C_default_s8944__001056"
MAPS = { # 短名 → 长地图目录
"patchwork_p9c": f"{LV}/maps_v2/patchwork__{P8}__s10__L181",
"pillarhall_p9c": f"{LV}/maps_v2/pillarhall__{P8}__s20006__L181",
"mountainpass_p9c": f"{LV}/maps_v2/mountainpass__{P8}__s11003__L181",
"farmland_p9c": f"{LV}/maps_v2/farmland__{P8}__s9013__L181",
"patchwork_v10": f"{LV}/maps/patchwork__R10_s1512__001440__s31__L241",
"orchard_v10": f"{LV}/maps/orchard__R10_s2505__001248__s37__L241",
"village_v10": f"{LV}/maps/village__R10_s2505__001248__s37__L241",
}
SCENES = { # 短名 → [(场景, 首帧 png)];第一个 = live 已有原版的那个
"patchwork_p9c": [("icecave@photoreal", f"{F0}/patchwork__{P8}__s10/icecave@photoreal/qwen21_s0.png"),
("salt_flats@cartoon", f"{F0}/patchwork__{P8}__s10/salt_flats@cartoon/klein9b_s0.png"),
("honey_hive@photoreal", f"{F0}/patchwork__{P8}__s10/honey_hive@photoreal/qwen21_s0.png"),
("greek@voxel", f"{F0}/patchwork__{P8}__s10/greek@voxel/klein9b_s0.png")],
"pillarhall_p9c": [("icecave@cartoon", f"{F0}/pillarhall__{P8}__s20006/icecave@cartoon/qwen21_s0.png")],
"mountainpass_p9c": [("dunes@cartoon", f"{F0}/mountainpass__{P8}__s11003/dunes@cartoon/qwen21_s0.png")],
"farmland_p9c": [("synthwave@cartoon", f"{F0}/farmland__{P8}__s9013/synthwave@cartoon/qwen21_s0.png")],
"patchwork_v10": [("snow_forest@photoreal", f"{LF0}/patchwork__R10_s1512__001440__s31/snow_forest@photoreal/qwen21_s0.png"),
("alien@photoreal", f"{LF0}/patchwork__R10_s1512__001440__s31/alien@photoreal/qwen21_s1.png"),
("lava@photoreal", f"{LF0}/patchwork__R10_s1512__001440__s31/lava@photoreal/qwen21_s0.png")],
"orchard_v10": [("icecave@photoreal", f"{LF0}/orchard__R10_s2505__001248__s37/icecave@photoreal/qwen21_s1.png"),
("alien@photoreal", f"{LF0}/orchard__R10_s2505__001248__s37/alien@photoreal/qwen21_s1.png")],
"village_v10": [("snow_forest@photoreal", f"{LF0}/village__R10_s2505__001248__s37/snow_forest@photoreal/qwen21_s0.png"),
("dunes@photoreal", f"{LF0}/village__R10_s2505__001248__s37/dunes@photoreal/qwen21_s0.png")],
}
# (地图短名, 模板, blank, attack, 场景下标表)
PLAN = [
# 1 冒烟:植被地面(不白纸,对照 = 原版)
("patchwork_p9c", "V", False, False, [0]),
("pillarhall_p9c", "V", False, False, [0]),
("patchwork_v10", "V", False, False, [0]),
# 白纸对照
("patchwork_p9c", "B0", True, False, [0, 1, 2, 3]),
("patchwork_v10", "B0", True, False, [0, 1, 2]),
("pillarhall_p9c", "B0", True, False, [0]),
("mountainpass_p9c", "B0", True, False, [0]),
("orchard_v10", "B0", True, False, [0]),
("village_v10", "B0", True, False, [0]),
# 2 白纸 → 世界;3 同一时间线、多个世界(patchwork_p9c × 4、patchwork_v10 × 3)
("patchwork_p9c", "W", True, False, [0, 1, 2, 3]),
("patchwork_v10", "W", True, False, [0, 1, 2]),
("pillarhall_p9c", "W", True, False, [0]),
("mountainpass_p9c", "W", True, False, [0]),
("orchard_v10", "W", True, False, [0]),
("village_v10", "W", True, False, [0]),
# 擦掉重画
("patchwork_p9c", "R", True, False, [0, 1]),
("patchwork_v10", "R", True, False, [0, 1]),
("village_v10", "R", True, False, [0]),
("mountainpass_p9c", "R", True, False, [0]),
# 4 攻击键建造(白纸 + 每次攻击长出一样东西)
("orchard_v10", "none", True, True, [0]),
("village_v10", "none", True, True, [0]),
]
EXTRA = [] # 冒烟之后追加(WV / RV / 新长地图的攻击版),见 canvas_plan2.json
if os.path.exists(f"{CV}/canvas_plan2.json"):
_p2 = json.load(open(f"{CV}/canvas_plan2.json"))
for k, v in _p2.get("maps", {}).items():
MAPS[k] = v
for k, v in _p2.get("scenes", {}).items():
SCENES[k] = [tuple(x) for x in v]
EXTRA = [tuple(x) for x in _p2.get("plan", [])]
def tl_name(mshort, tl, blank, attack):
return f"{mshort}__{tl}{'_blank' if blank and tl not in ('B0',) else ''}{'_atk' if attack else ''}"
def tag_of(tl, blank, attack):
return ("atk" if attack else tl) if tl != "none" else "atk"
def roll_name(mshort, scene, tl, blank, attack):
return f"{mshort}__{scene}__{tag_of(tl, blank, attack)}"
def env():
e = dict(os.environ)
e.update(DIFFSYNTH_SKIP_DOWNLOAD="True", PYTHONPATH=f"{M}/code/gc25d:{M}/code/arope:{HERE}",
HF_HOME="/opt/dlami/nvme/zhiyangdeng/.cache/huggingface", TMPDIR=f"{M}/.tmp", HF_HUB_OFFLINE="1",
PYTHONUNBUFFERED="1", OMP_NUM_THREADS="6", MKL_NUM_THREADS="6", OPENBLAS_NUM_THREADS="6")
return e
def run_pool(cmds, gpus, procs, tag):
os.makedirs(LOG, exist_ok=True)
running, k = [], 0
todo = list(cmds)
while todo or running:
while todo and len(running) < procs:
name, argv = todo.pop(0)
e = env()
e["CUDA_VISIBLE_DEVICES"] = gpus[k % len(gpus)]
k += 1
lf = open(os.path.join(LOG, f"canvas_{tag}_{name}.log"), "w")
running.append((name, subprocess.Popen(argv, env=e, stdout=lf, stderr=subprocess.STDOUT), time.time()))
print(f"[start] {name}", flush=True)
for r in list(running):
if r[1].poll() is not None:
running.remove(r)
print(f"[done] {r[0]} rc={r[1].returncode} {time.time() - r[2]:.0f}s", flush=True)
time.sleep(2)
def all_plan():
return PLAN + EXTRA
def cmd_tl(a):
cmds, seen = [], set()
for mshort, tl, blank, attack, _ in all_plan():
if a.only and tl not in a.only.split(","):
continue
nm = tl_name(mshort, tl, blank, attack)
if nm in seen:
continue
seen.add(nm)
if os.path.exists(f"{CV}/tl/{nm}/events.json") and not a.redo:
continue
argv = [sys.executable, f"{HERE}/canvas_edits.py", "run", "--map", MAPS[mshort], "--tl", tl, "--name", nm]
if blank:
argv.append("--blank")
if attack:
argv.append("--attack")
cmds.append((nm, argv))
print(f"{len(cmds)} 条时间线要渲染")
run_pool(cmds, a.gpus.split(","), a.procs, "tl")
def live_orig(mdir, scene):
lname = os.path.basename(mdir)
d = f"{LV}/roll/{lname}__{scene}__orig"
return d if os.path.exists(f"{d}/meta.json") else None
def orig_dir(mshort, scene):
d = live_orig(MAPS[mshort], scene)
return d or f"{CV}/roll/{mshort}__{scene}__orig"
def cmd_jobs(a):
rows, seen = [], set()
for mshort, tl, blank, attack, sidx in all_plan():
md = MAPS[mshort]
nm = tl_name(mshort, tl, blank, attack)
td = f"{CV}/tl/{nm}"
for si in sidx:
scene, png = SCENES[mshort][si]
if not os.path.exists(png):
print(f"[skip] 没有首帧 {png}")
continue
common = dict(first=png, prompts=f"{md}/prompts.json", actions=f"{md}/actions.npy", out=f"{CV}/roll", seed=0,
labels=dict(scene=scene, map=mshort, long_map=os.path.basename(md)))
on = f"{mshort}__{scene}__orig"
if on not in seen and live_orig(md, scene) is None:
seen.add(on)
rows.append(dict(name=on, ids=f"{md}/ids.npz", prio=2, meta=dict(edit="none", long_map=md, scene=scene), **common))
if not os.path.exists(f"{td}/events.json"):
continue
ev = json.load(open(f"{td}/events.json"))
rn = roll_name(mshort, scene, tl, blank, attack)
sf = sorted({min(max(e["f"] + d, 0), ev["n_frames"] - 1) for e in ev["events"] for d in (-1, 24)})
pr = 3 if tl == "V" else (1.5 if tl == "B0" else 1)
rows.append(dict(name=rn, ids=f"{td}/ids.npz", prio=pr, sheet_frames=sf[:16] or None,
meta=dict(edit="canvas", timeline=tl, blank=blank, attack=attack, tl_dir=td, long_map=md, scene=scene,
events=ev["events"], f_first_edit=ev["f_first_edit"]), **common))
out = f"{CV}/jobs.jsonl"
tmp = out + ".tmp"
with open(tmp, "w") as fh:
for r in rows:
fh.write(json.dumps(r, ensure_ascii=False, default=float) + "\n")
os.replace(tmp, out)
print(f"→ {out}:{len(rows)} 个任务")
def cmd_status(a):
subprocess.run([sys.executable, f"{HERE}/rollout_dmd.py", "--jobs", f"{CV}/jobs.jsonl", "--status"], env=env())
# ----------------------------------------------------------------------------
# 结果目录
# ----------------------------------------------------------------------------
def pairs():
"""→ [(edited roll dir, orig dir, blank-control dir 或 None, mshort, scene, tl, blank, attack)]。"""
out = []
for mshort, tl, blank, attack, sidx in all_plan():
for si in sidx:
scene, _ = SCENES[mshort][si]
ed = f"{CV}/roll/{roll_name(mshort, scene, tl, blank, attack)}"
ctl = f"{CV}/roll/{mshort}__{scene}__B0" if blank and tl != "B0" else None
out.append((ed, orig_dir(mshort, scene), ctl, mshort, scene, tl, blank, attack))
return out
def link(src, dst):
if os.path.exists(dst):
os.remove(dst)
try:
os.link(src, dst)
except OSError:
shutil.copyfile(src, dst)
EDIT_DESC = {
"V": "veg ground smoke: paint a green path, write MALIANG / 马良 in grass, grow a lawn (world otherwise unchanged)",
"B0": "blank-canvas control: objects not visible at frame 0 removed, visible ones erased at 0.75 s, nothing drawn afterwards",
"W": "blank canvas → world: clear, then houses / trees / towers / wall / stone circle / MALIANG stone letters added step by step",
"R": "erase and redraw: blank canvas → village (houses, wall, tower) → erase everything → forest + stone circle + obelisk + MALIANG",
"WV": "blank canvas → world with vegetation ground painting (green path, MALIANG / 马良 in grass)",
"RV": "erase and redraw with vegetation ground painting",
"atk": "attack-triggered building on a blank canvas: every attack press spawns an object in front of the player",
"WF": "blank canvas → world with grass painting, then a finale burst (one stroke every 1.5 s from 30 s) so the last frames are crowded",
}
def _case_one(args):
import numpy as np
ed, od, ctl, mshort, scene, tl, blank, attack = args
name = os.path.basename(ed)
cd = f"{CV}/cases/{name}"
if not os.path.exists(f"{ed}/meta.json") or not os.path.exists(f"{od}/meta.json"):
return None
os.makedirs(cd, exist_ok=True)
m = json.load(open(f"{ed}/meta.json"))
for fn in ("gen.mp4", "ids.npz", "first.png", "actions.npy", "prompts.json", "sheet.jpg"):
if os.path.exists(f"{ed}/{fn}"):
link(f"{ed}/{fn}", f"{cd}/{fn}")
ids = np.load(f"{ed}/ids.npz")["ids"]
ids_o = np.load(f"{od}/ids.npz")["ids"]
mk = dict(mask=ids != ids_o)
if ctl and os.path.exists(f"{ctl}/ids.npz"):
mk["mask_brush"] = ids != np.load(f"{ctl}/ids.npz")["ids"]
np.savez_compressed(f"{cd}/mask.npz", **mk)
evs = (m.get("meta") or {}).get("events") or []
tag = tag_of(tl, blank, attack)
edit = dict(edit_type=f"canvas_{tag}", description=EDIT_DESC.get(tag, tag), timeline=tl, blank_canvas=blank, attack_trigger=attack,
trigger="attack key onsets (cell k attack=1, k-1=0; held ⇒ every 3 cells)" if attack else "timeline (fixed frames)",
n_events=len(evs), frames=[e["f"] for e in evs],
events=[dict(type=e["type"], frame=e["f"], t_sec=e["t_sec"], cell=e["cell"], label=e["label"], label_zh=e["label_zh"],
params={k: v for k, v in e.items() if k not in ("items", "type", "f", "t_sec", "cell", "label", "label_zh",
"names")}) for e in evs],
base_source=dict(long_map=(m.get("meta") or {}).get("long_map"), tl_dir=(m.get("meta") or {}).get("tl_dir"),
first_frame=m["job"]["first"]),
paired_original=od, paired_original_case=f"{CV}/cases/{os.path.basename(od)}",
blank_control=ctl, mask_keys=dict(mask="ids != paired original ids", mask_brush="ids != blank-control ids (brush strokes only)"
if "mask_brush" in mk else None))
json.dump(edit, open(f"{cd}/edit.json", "w"), indent=1, ensure_ascii=False, default=float)
meta = dict(name=name, kind="canvas_edit", model="gc25d v10 causal DMD student (stage3 EMA step 500), 4 steps/cell, sink 3 / window 17",
ckpt=m["ckpt"], seed=m["seed"], fps=16, n_frames=m["n_frames"], cells=m["cells"], res="832x480", decode=m["decode"],
scene=scene, map=mshort, timing=m["timing"], actions="actions.npy uint8 [N,5] = cell 1..N keys (w,a,s,d,attack); cell k = frames 4k-3..4k",
prompts="prompts.json N+1 per-cell strings (cell 0 = cell 1)", roll_dir=ed)
json.dump(meta, open(f"{cd}/meta.json", "w"), indent=1, ensure_ascii=False, default=float)
# 配对原版 / 白纸对照也放成 case(硬链接)
for src, kind in ((od, "original"), (ctl, "blank_control")):
if not src or not os.path.exists(f"{src}/meta.json"):
continue
sd = f"{CV}/cases/{os.path.basename(src)}"
os.makedirs(sd, exist_ok=True)
for fn in ("gen.mp4", "ids.npz", "first.png", "actions.npy", "prompts.json", "sheet.jpg"):
if os.path.exists(f"{src}/{fn}") and not os.path.exists(f"{sd}/{fn}"):
link(f"{src}/{fn}", f"{sd}/{fn}")
mm = json.load(open(f"{src}/meta.json"))
json.dump(dict(edit_type="none" if kind == "original" else "canvas_B0", role=kind, source=src,
description="unedited long-map rollout (full world)" if kind == "original" else EDIT_DESC["B0"]),
open(f"{sd}/edit.json", "w"), indent=1)
json.dump(dict(name=os.path.basename(src), kind=f"canvas_{kind}", ckpt=mm["ckpt"], seed=mm["seed"], fps=16,
n_frames=mm["n_frames"], cells=mm["cells"], scene=scene, map=mshort, roll_dir=src),
open(f"{sd}/meta.json", "w"), indent=1)
return dict(name=name, case_dir=cd, kind=f"canvas_{tag}", map=mshort, scene=scene, timeline=tl, blank=blank, attack=attack,
n_frames=m["n_frames"], sec=round(m["n_frames"] / 16, 1), n_events=len(evs),
events=" → ".join(f"{e['t_sec']:.0f}s {e['label']}" for e in evs), paired_original=os.path.basename(od),
blank_control=os.path.basename(ctl) if ctl else None)
def cmd_cases(a):
from multiprocessing import Pool
todo = [p for p in pairs() if os.path.exists(f"{p[0]}/meta.json")]
with Pool(a.procs) as pool:
rows = [r for r in pool.map(_case_one, todo) if r]
sc = {}
if os.path.exists(f"{CV}/showcase/scores.json"):
sc = json.load(open(f"{CV}/showcase/scores.json"))
seen = set()
with open(f"{CV}/index.jsonl", "w") as fh:
for r in rows:
s = sc.get(r["name"])
if isinstance(s, list):
v = [x.get("resp@12") for x in s if x.get("resp@12") is not None and x["type"] != "clear"]
r["effect_mean@12"] = round(sum(v) / len(v), 1) if v else None
fh.write(json.dumps(r, ensure_ascii=False) + "\n")
seen.add(r["name"])
for r in rows:
for k in ("paired_original", "blank_control"):
n = r.get(k)
if n and n not in seen:
seen.add(n)
fh.write(json.dumps(dict(name=n, case_dir=f"{CV}/cases/{n}", kind="canvas_original" if k == "paired_original"
else "canvas_B0_control", map=r["map"], scene=r["scene"], n_frames=r["n_frames"]),
ensure_ascii=False) + "\n")
print(f"{len(rows)} 条编辑版 → {CV}/cases + index.jsonl(共 {len(seen)} 行)")
def self_erase_scores(ed, ev, offs=(12, 48)):
"""清空事件的「自身前后」分:R = 帧 f−1 是物体(3..6)、帧 f+o 变成地面(1 / 2)的像素;resp = R 内 f−1 → f+o 的色差 − 其余
语义没变处的色差(白纸对照里也清空了、原版又是另一个世界时用)。"""
import cv2
import numpy as np
import compose as CP
ids = np.load(os.path.join(ed, "ids.npz"))["ids"]
f0 = max(ev["f"] - 1, 0)
want = {f0} | {min(ev["f"] + o, len(ids) - 1) for o in offs}
fr = {}
for k, x in enumerate(CP.iter_video(os.path.join(ed, "gen.mp4"))):
if k in want:
fr[k] = x.astype(np.int16)
if k > max(want):
break
r = dict(type=ev["type"], f=ev["f"], label=ev["label"], metric="self")
for o in offs:
f = min(ev["f"] + o, len(ids) - 1)
R = np.isin(ids[f0], (3, 4, 5, 6)) & np.isin(ids[f], (1, 2))
R = cv2.erode(R.astype(np.uint8), np.ones((5, 5), np.uint8)) > 0
d = np.abs(fr[f] - fr[f0]).mean(-1)
out_m = ~cv2.dilate(R.astype(np.uint8), np.ones((15, 15), np.uint8)).astype(bool) & (ids[f] == ids[f0])
r[f"frac@{o}"] = round(float(R.mean()), 4)
r[f"resp@{o}"] = round(float(d[R].mean() - d[out_m].mean()), 2) if R.sum() > 200 and out_m.any() else None
return r
def _score_one(args):
import live_compose as LC
ed, od, ctl, mshort, scene, tl, blank, attack = args
ref = ctl if ctl and os.path.exists(f"{ctl}/meta.json") else od
try:
evs = [e for e in LC.load_events(ed)[0]]
main = [e for e in evs if e["type"] != "clear"]
sc = {(r["f"], r["type"]): r for r in (LC.event_scores(ref, ed, main) if main else [])}
for e in evs:
if e["type"] != "clear":
continue
if e["f"] <= 24: # 开头的清空:对原版(原版里这些东西还在)
r = LC.event_scores(od, ed, [e])[0]
r["metric"] = "vs_orig"
else:
r = self_erase_scores(ed, e)
sc[(e["f"], e["type"])] = r
out = [sc[(e["f"], e["type"])] for e in evs]
return os.path.basename(ed), dict(ref=os.path.basename(ref), events=out)
except Exception as e: # noqa: BLE001
import traceback
return os.path.basename(ed), traceback.format_exc()
def cmd_score(a):
"""逐事件效果分(live_compose.event_scores:R = 帧 f+o 编辑版 / 参照语义不同、帧 f−1 相同的像素;resp = R 内两版色差 − R 外色差)。
参照:白纸类 = 白纸对照 B0(只差这一笔一笔),其余 = 原版。"""
from multiprocessing import Pool
import numpy as np
todo = [p for p in pairs() if os.path.exists(f"{p[0]}/meta.json") and p[5] != "B0"]
with Pool(a.procs) as pool:
res = dict(pool.map(_score_one, todo))
os.makedirs(f"{CV}/showcase", exist_ok=True)
flat = {k: (v["events"] if isinstance(v, dict) else v) for k, v in res.items()}
json.dump(res, open(f"{CV}/showcase/scores_full.json", "w"), indent=1, ensure_ascii=False)
json.dump(flat, open(f"{CV}/showcase/scores.json", "w"), indent=1, ensure_ascii=False)
by = {}
for k, evs in flat.items():
if isinstance(evs, str):
continue
for r in evs:
key = r["type"]
if r["type"] in ("text3d", "vtext"):
key += " (cjk)" if "马良" in r["label"] else " (latin)"
if r["type"] == "spawn":
key += f" ({r['label'].split('→')[-1].strip()})"
by.setdefault(key, []).append((r.get("resp@12"), r.get("resp@48")))
summ = {}
for k, v in sorted(by.items()):
v12 = [x for x, _ in v if x is not None]
v48 = [y for _, y in v if y is not None]
summ[k] = dict(n=len(v), n_scored=len(v12), median12=round(float(np.median(v12)), 1) if v12 else None,
eff_rate12=round(float(np.mean(np.array(v12) >= 15)), 2) if v12 else None,
median48=round(float(np.median(v48)), 1) if v48 else None,
eff_rate48=round(float(np.mean(np.array(v48) >= 15)), 2) if v48 else None)
rank = sorted(((float(np.mean([r["resp@12"] or 0 for r in evs if r["type"] != "clear"] or [0])), k) for k, evs in flat.items()
if not isinstance(evs, str)), reverse=True)
json.dump(dict(per_type=summ, rank=rank), open(f"{CV}/showcase/score_summary.json", "w"), indent=1, ensure_ascii=False)
print(json.dumps(summ, ensure_ascii=False, indent=1))
for s_, k in rank:
print(f"{s_:6.1f} {k}")
def main(argv=None):
ap = argparse.ArgumentParser()
ap.add_argument("cmd", choices=["tl", "jobs", "status", "cases", "score"])
ap.add_argument("--gpus", default="3,4,5")
ap.add_argument("--procs", type=int, default=12)
ap.add_argument("--only", default=None)
ap.add_argument("--redo", action="store_true")
a = ap.parse_args(argv)
dict(tl=cmd_tl, jobs=cmd_jobs, status=cmd_status, cases=cmd_cases, score=cmd_score)[a.cmd](a)
if __name__ == "__main__":
main()