Download scripts/teaser/edit_showcase.py from teawhite/maliang: direct link, hf CLI and curl.
- Browser
- Download file 10.7 kB
-
https://huggingface.co/teawhite/maliang/resolve/main/scripts/teaser/edit_showcase.py
- Command line
-
hf download hf://teawhite/maliang/scripts/teaser/edit_showcase.py
-
curl -L -o edit_showcase.py https://huggingface.co/teawhite/maliang/resolve/main/scripts/teaser/edit_showcase.py
10.7 kB
| #!/usr/bin/env python3 | |
| """teaser · 世界编辑选片 + 出片(maliang venv,CPU):读 <e81>/cases/*/(推完、有 edit_score.json 的),按编辑类型分组排序,出 | |
| <e81>/showcase/: | |
| picks.jsonl / picks.md 选中的 case(每类前 N 条,同一底子在同一类里只取一条)+ 指标 | |
| side/<rank>_<edit>_<name>.mp4 「原版 | 编辑版」并排(compose_edit side,1920×1080) | |
| grid_<family>.mp4 同一类编辑的宫格(每格「原版 | 编辑版」,3 行 × 2 列) | |
| reel.mp4 精选并排串接(每类轮流取,交叉淡化) | |
| all_index.md / all.jsonl 全部 case 的指标表(含没选中的、生效不明显的) | |
| 排序分 = effect(编辑区域里编辑版 vs 原版的 ΔE − 区域外 ΔE)的类内名次 + 0.3·fit(区域里外观判成编辑后类的比例差) | |
| − 硬伤惩罚(vscore 有 flags 时;没跑 vscore 就不罚)。effect < --min_effect 的不选(= 模型没画出来)。 | |
| 家族(family)= edit + 主变体:add / add-drop / grow / delete / delete-class / swap / spawn-ring / spawn-avenue / trail3d / | |
| paint(地面)/ write-paint / write-raise / trap / boss_del / boss_clone。 | |
| usage:.venv/bin/python scripts/teaser/edit_showcase.py [--root outputs/teaser/edit/e81] [--per 8] [--reel_per 2] [--procs 16] | |
| [--no_video](只出 picks / 索引) | |
| .venv/bin/python scripts/teaser/edit_showcase.py --hero hero.txt # 手挑名单 → showcase/hero/ + hero_reel.mp4 | |
| """ | |
| from __future__ import annotations | |
| import argparse | |
| import collections | |
| import json | |
| import os | |
| import subprocess | |
| import sys | |
| from concurrent.futures import ProcessPoolExecutor | |
| M = "/opt/dlami/nvme/zhiyangdeng/maliang" | |
| HERE = os.path.dirname(os.path.abspath(__file__)) | |
| PY = f"{M}/.venv/bin/python" | |
| HARD = {"scene_lost", "degraded", "broken", "char_melt", "blur"} | |
| TITLE = {"add": "Add an object", "add-drop": "Drop an object from the sky", "grow": "Grow / raise an object", | |
| "delete": "Delete an object", "delete-class": "Delete a whole class", "swap-object": "Swap materials", | |
| "swap-ground": "Swap the ground material", | |
| "spawn-ring": "Spawn a ring of objects", "spawn-avenue": "Objects sprout along the way", | |
| "trail3d": "Things grow in the hero's footsteps", "paint": "Paint the ground", "write-paint": "Write on the ground", | |
| "write-raise": "Raise letters / symbols", "trap": "Trap the boss", "boss_del": "Remove the boss", | |
| "boss_clone": "Clone the boss"} | |
| def family(e: dict) -> str: | |
| ed, p = e["edit"], e.get("params") or {} | |
| if ed == "add": | |
| return "add-drop" if p.get("anim") == "drop" else "add" | |
| if ed == "delete": | |
| return "delete-class" if p.get("mode") == "class" else "delete" | |
| if ed == "spawn": | |
| return "spawn-avenue" if p.get("mode") == "avenue" else "spawn-ring" | |
| if ed == "paint": | |
| return "trail3d" if p.get("mode") == "trail3d" else "paint" | |
| if ed == "write": | |
| return "write-raise" if p.get("mode") == "raise" else "write-paint" | |
| if ed == "swap": | |
| return "swap-ground" if (e.get("resolved") or {}).get("on") == "ground" else "swap-object" | |
| return ed | |
| def load_rows(root: str) -> list[dict]: | |
| rows = [] | |
| cd = os.path.join(root, "cases") | |
| for n in sorted(os.listdir(cd)): | |
| d = os.path.join(cd, n) | |
| sp, ep = os.path.join(d, "edit_score.json"), os.path.join(d, "edit.json") | |
| if not (os.path.isfile(sp) and os.path.isfile(ep) and os.path.isfile(os.path.join(d, "gen.mp4"))): | |
| continue | |
| s, e = json.load(open(sp)), json.load(open(ep)) | |
| vs = f"{M}/outputs/teaser/vscore/edit/{n}.vscore.json" | |
| v = json.load(open(vs)) if os.path.exists(vs) else {} | |
| rows.append(dict(name=n, dir=d, family=family(e), edit=e["edit"], label=e["label"], base=e["base"]["name"], | |
| theme=e["base"]["theme"], scene=e["base"]["scene"], edit_frame=e["edit_frame"], | |
| effect=s.get("effect"), ratio=s.get("ratio"), fit=s.get("fit"), pre_delta=s.get("pre_delta"), | |
| teaser=v.get("teaser"), flags=v.get("flags") or [], weak=e.get("weak"))) | |
| return rows | |
| def rank(rows, min_effect): | |
| by = collections.defaultdict(list) | |
| for r in rows: | |
| by[r["family"]].append(r) | |
| for fam, rs in by.items(): | |
| eff = sorted([r["effect"] or -99 for r in rs]) | |
| for r in rs: | |
| q = (eff.index(r["effect"] or -99) + 1) / len(eff) | |
| hard = set(r["flags"]) & HARD | |
| if fam == "boss_clone": # 两个 Boss 时 vscore 的角色放大框会框到两个 ⇒ char_melt 误报,不罚 | |
| hard -= {"char_melt"} | |
| if fam.startswith("write"): # 地上的字会被 VLM 当成「花屏 / 文字」⇒ broken / degraded 误报,只认 scene_lost | |
| hard &= {"scene_lost"} | |
| pen = 0.25 * len(hard) | |
| r["rank_score"] = round(q + 0.3 * (r["fit"] or 0) - pen + (0.1 * (r["teaser"] or 0)), 4) | |
| r["effective"] = (r["effect"] or -99) >= min_effect | |
| rs.sort(key=lambda r: -r["rank_score"]) | |
| return by | |
| def pick(by, per): | |
| out = {} | |
| for fam, rs in by.items(): | |
| seen, sel = set(), [] | |
| for r in rs: | |
| if not r["effective"] or r["base"] in seen: | |
| continue | |
| sel.append(r) | |
| seen.add(r["base"]) | |
| if len(sel) >= per: | |
| break | |
| out[fam] = sel | |
| return out | |
| def run(cmd): | |
| p = subprocess.run(cmd, capture_output=True, text=True) | |
| if p.returncode: | |
| print("FAIL", " ".join(cmd[:4]), p.stderr[-500:], flush=True) | |
| return p.returncode | |
| def main(argv=None): | |
| ap = argparse.ArgumentParser() | |
| ap.add_argument("--root", default=f"{M}/outputs/teaser/edit/e81") | |
| ap.add_argument("--per", type=int, default=8) | |
| ap.add_argument("--reel_per", type=int, default=2) | |
| ap.add_argument("--min_effect", type=float, default=8.0) | |
| ap.add_argument("--procs", type=int, default=16) | |
| ap.add_argument("--no_video", action="store_true") | |
| ap.add_argument("--hero", default=None, help="手挑的 case 名单(每行一个 case 名或目录)⇒ showcase/hero/NN_<name>.mp4 + hero_reel.mp4") | |
| a = ap.parse_args(argv) | |
| if a.hero: | |
| return hero(a) | |
| sd = os.path.join(a.root, "showcase") | |
| os.makedirs(os.path.join(sd, "side"), exist_ok=True) | |
| rows = load_rows(a.root) | |
| by = rank(rows, a.min_effect) | |
| picks = pick(by, a.per) | |
| with open(os.path.join(sd, "all.jsonl"), "w") as fh: | |
| for fam in sorted(by): | |
| for r in by[fam]: | |
| fh.write(json.dumps(r, ensure_ascii=False) + "\n") | |
| with open(os.path.join(sd, "all_index.md"), "w") as fh: | |
| fh.write(f"# 世界编辑:全部 {len(rows)} 条(按类、类内按排序分)\n\n") | |
| fh.write("| 类 | 条数 | 生效(effect ≥ %.0f) | effect 中位 |\n|---|---|---|---|\n" % a.min_effect) | |
| for fam in sorted(by): | |
| rs = by[fam] | |
| eff = sorted(r["effect"] or -99 for r in rs) | |
| fh.write(f"| {fam} | {len(rs)} | {sum(r['effective'] for r in rs)} | {eff[len(eff) // 2]:.1f} |\n") | |
| for fam in sorted(by): | |
| fh.write(f"\n## {fam}\n\n| 排序分 | effect | fit | pre | teaser | 标签 | case |\n|---|---|---|---|---|---|---|\n") | |
| for r in by[fam]: | |
| fh.write(f"| {r['rank_score']} | {r['effect']} | {r['fit']} | {r['pre_delta']} | {r['teaser']} | {r['label']} | " | |
| f"`{r['name']}` |\n") | |
| sel = [(fam, i, r) for fam in sorted(picks) for i, r in enumerate(picks[fam])] | |
| with open(os.path.join(sd, "picks.jsonl"), "w") as fh: | |
| for fam, i, r in sel: | |
| r["side"] = os.path.join(sd, "side", f"{fam}_{i:02d}__{r['name']}.mp4") | |
| fh.write(json.dumps(r, ensure_ascii=False) + "\n") | |
| with open(os.path.join(sd, "picks.md"), "w") as fh: | |
| fh.write(f"# 世界编辑精选({len(sel)} 条,每类 ≤ {a.per})\n\n") | |
| for fam in sorted(picks): | |
| fh.write(f"\n## {fam} — {TITLE.get(fam, fam)}({len(picks[fam])} / {len(by[fam])})\n\n") | |
| for i, r in enumerate(picks[fam]): | |
| fh.write(f"- {r['label']} · effect {r['effect']} · fit {r['fit']} · `{os.path.basename(r['side'])}`\n") | |
| print(f"{len(rows)} 条 → 精选 {len(sel)};每类:" + ", ".join(f"{f} {len(picks[f])}/{len(by[f])}" for f in sorted(by)), flush=True) | |
| if a.no_video: | |
| return | |
| ce = os.path.join(HERE, "compose_edit.py") | |
| jobs = [[PY, ce, "side", r["dir"], "-o", r["side"]] for _, _, r in sel if not os.path.exists(r["side"])] | |
| for fam in sorted(picks): | |
| if len(picks[fam]) >= 2: | |
| cs = [r["dir"] for r in picks[fam][:6]] | |
| jobs.append([PY, ce, "grid", *cs, "-o", os.path.join(sd, f"grid_{fam}.mp4"), "--cols", "2", "--pairs", | |
| "--title", f"{TITLE.get(fam, fam)} — original | edited"]) | |
| with ProcessPoolExecutor(a.procs) as ex: | |
| list(ex.map(run, jobs)) | |
| reel = [] | |
| for i in range(a.reel_per): | |
| for fam in ("add", "grow", "delete", "delete-class", "swap-object", "spawn-avenue", "paint", "add-drop", "boss_del", | |
| "boss_clone", "trap", "write-raise", "spawn-ring", "trail3d", "write-paint", "swap-ground"): | |
| if fam in picks and i < len(picks[fam]): | |
| reel.append(picks[fam][i]["side"]) | |
| if reel: | |
| run([PY, os.path.join(HERE, "compose.py"), "reel", *reel, "-o", os.path.join(sd, "reel.mp4"), "--fade", "8"]) | |
| print(f"出片 → {sd}", flush=True) | |
| def hero(a): | |
| """手挑名单 → 逐条并排(showcase/hero/NN_<name>.mp4)+ 串接 hero_reel.mp4(交叉淡化 8 帧)。""" | |
| hd = os.path.join(a.root, "showcase", "hero") | |
| os.makedirs(hd, exist_ok=True) | |
| names = [ln.strip() for ln in open(a.hero) if ln.strip() and not ln.startswith("#")] | |
| ce = os.path.join(HERE, "compose_edit.py") | |
| jobs, outs = [], [] | |
| for i, n in enumerate(names): | |
| d = n if os.path.isdir(n) else os.path.join(a.root, "cases", n) | |
| o = os.path.join(hd, f"{i:02d}_{os.path.basename(d.rstrip('/'))}.mp4") | |
| outs.append(o) | |
| if not os.path.exists(o): | |
| jobs.append([PY, ce, "side", d, "-o", o]) | |
| with ProcessPoolExecutor(a.procs) as ex: | |
| list(ex.map(run, jobs)) | |
| run([PY, os.path.join(HERE, "compose.py"), "reel", *outs, "-o", os.path.join(a.root, "showcase", "hero_reel.mp4"), "--fade", "8"]) | |
| print(f"hero:{len(outs)} 条 → {hd},hero_reel.mp4", flush=True) | |
| if __name__ == "__main__": | |
| main() | |