maliang / scripts /teaser /edit_plan.py
teawhite's picture
scripts/teaser: teaser / edit / bossswap / infinite / cover code
68e2686 verified
Raw History Blame Contribute Delete
10.6 kB
#!/usr/bin/env python3
"""teaser · 世界编辑的底子挑选 + 编辑计划(CPU)→ <out>/bases.jsonl、<out>/plan.jsonl(edits.py run 的输入)。
底子 = 已有 rollout(outputs/teaser/roll/<name>/)里:首帧 A 档(prod/picks.jsonl)、vscore 无硬伤(flicker 不算)、变体 ∈ {原首帧, rc}、
teaser 分 ≥ --min_score;贪心按 teaser 分取 --n 个,同一张地图(map case)只取一条、每个主题 ≤ --per_theme、每种画风 ≤ --per_look、
同一个场景 ≤ 2 ⇒ 主题 / 场景 / 画风都分散。另算每条底子的「Boss 显眼度」(帧 24..80 Boss 像素中位)和「玩家走了多远」(帧 16..80),
决定给它排哪些编辑:
* 人人都有:add ×2(自动选反差大的物体,k 32 / 40)、grow(k 24,dur 16)、delete(pop / sink 交替,k 32)、swap(wave 16,k 28)、
write(马良 / MALIANG / ★ / ♥ 轮换,k 20,dur 32;每第 3 条用 raise 立体字)、spawn(树 / 柱 / 立石 / 水晶轮换,k 24)、
delete class(k 36,每第 2 条);
* 玩家走得够远(≥ 3 m):paint(k 16;每第 2 条 lead 16 = 路铺在前面);
* Boss 显眼(像素中位 ≥ 1500):boss_del(k 32)、boss_clone(k 32)、trap(k 32)。
usage:.venv/bin/python scripts/teaser/edit_plan.py [--n 48] [--out outputs/teaser/edit/e81] [--exclude a,b]
.venv/bin/python scripts/teaser/edit_plan.py --wave2 outputs/teaser/edit/e81/plan2.jsonl # 第二波(只做立体物体类编辑)
.venv/bin/python scripts/teaser/edit_plan.py --wave3 outputs/teaser/edit/e81/plan3.jsonl # 第三波(vegetation 地面图层)
"""
from __future__ import annotations
import argparse
import collections
import json
import os
import numpy as np
M = "/opt/dlami/nvme/zhiyangdeng/maliang"
T = f"{M}/outputs/teaser"
HARD = {"scene_lost", "degraded", "broken", "char_melt", "blur"}
def candidates(min_score: float) -> list[dict]:
tier = {}
for ln in open(f"{T}/prod/picks.jsonl"):
r = json.loads(ln)
tier[r["case"] + "__" + r["scene"]] = r.get("tier")
rows = []
for f in os.listdir(f"{T}/vscore/roll"):
name = f[:-len(".vscore.json")]
parts = name.split("__")
var = parts[5] if len(parts) > 5 else ""
if var not in ("", "rc"):
continue
base = "__".join(parts[:5])
if tier.get(base) != "A":
continue
v = json.load(open(f"{T}/vscore/roll/{f}"))
if set(v.get("flags") or []) & HARD or v["teaser"] < min_score:
continue
if not os.path.exists(f"{T}/roll/{name}/meta.json"):
continue
rows.append(dict(name=name, case=base.rsplit("__", 1)[0], theme=parts[0], scene=parts[4].split("@")[0],
look=parts[4].split("@")[1], var=var, teaser=v["teaser"], flags=v.get("flags") or []))
return sorted(rows, key=lambda r: -r["teaser"])
def pick(rows, n, per_theme, per_look, exclude=()):
out, seen_case = [], set()
th, lk, sc = collections.Counter(), collections.Counter(), collections.Counter()
for r in rows:
if r["name"] in exclude or r["case"] in seen_case:
continue
if th[r["theme"]] >= per_theme or lk[r["look"]] >= per_look or sc[r["scene"]] >= 2:
continue
out.append(r)
seen_case.add(r["case"])
th[r["theme"]] += 1
lk[r["look"]] += 1
sc[r["scene"]] += 1
if len(out) >= n:
break
return out
def stats(r):
import sys
sys.path.insert(0, f"{M}/code/gc25d")
ids = np.load(f"{T}/roll/{r['name']}/ids.npz")["ids"]
boss = ((ids >= 17) & (ids <= 21)) | (ids == 11)
bpx = boss[24:].sum((1, 2))
meta = json.load(open(f"{T}/roll/{r['name']}/meta.json"))
src = json.load(open(os.path.join(os.path.dirname(meta["job"]["ids"]) if os.path.isabs(meta["job"]["ids"])
else os.path.join(M, os.path.dirname(meta["job"]["ids"])), "source.json")))
m = np.load(f"{M}/data/gc25d_v10/meta/{src['clip_id']}.npz")
P = m["actor_pos"][16:81, 0, :2]
walk = float(np.linalg.norm(np.diff(P, axis=0), axis=1).sum()) if np.isfinite(P).all() else 0.0
return dict(boss_px_med=int(np.median(bpx)), walk_cm=round(walk, 1), ortho=bool(src["ortho"]), clip_id=src["clip_id"])
TEXTS = ["马良", "MALIANG", "star", "马良", "heart", "HELLO", "马良", "GG"]
SPAWN = ["tree", "pillar", "stone", "crystal"]
def plan_rows(bases):
rows = []
for i, b in enumerate(bases):
nm = b["name"]
add = lambda e, k, p=None, prio=0: rows.append(dict(base=nm, edit=e, k=k, params=p or {}, prio=prio))
add("add", 32, {}, 2)
add("add", 40, {"size": 1.15}, 1)
add("grow", 24, {"dur": 16}, 2)
add("delete", 32, {"mode": "pop" if i % 2 == 0 else "sink"}, 1)
add("swap", 28, {"wave": 16}, 2)
t = TEXTS[i % len(TEXTS)]
add("write", 20, {"text": t, "dur": 32, "mode": "raise" if i % 3 == 2 else "paint"}, 2)
add("spawn", 24, {"obj": SPAWN[i % len(SPAWN)]}, 1)
if i % 2 == 1:
add("delete", 36, {"mode": "class"}, 0)
if b["walk_cm"] >= 300:
add("paint", 16, {"lead": 16} if i % 2 else {}, 2)
if b["boss_px_med"] >= 1500:
add("boss_del", 32, {}, 1)
add("boss_clone", 32, {}, 1)
add("trap", 32, {}, 1)
return rows
ADD2 = ["tower", "house", "statue", "crystal", "tree", "wall", "pillar", "stone"]
GROW2 = ["wall", "crystal", "tree", "stone", "tower"]
DROP = ["props", "boulder"]
TRAIL = ["bush", "crystal", "stone", "pillar"]
SWAP2 = ["veg2rock", "arch2veg", "rock2veg", "arch2rock", "veg2arch"]
RAISE = [("star", "arch"), ("HI", "veg"), ("heart", "veg"), ("GO", "arch"), ("arrow", "veg"), ("WIN", "arch")]
def plan_rows2(bases):
"""第二波(只做立体物体类的编辑:第一波看下来地面图层类 —— paint / write paint / 地面 swap —— 模型基本不画)。"""
rows = []
for i, b in enumerate(bases):
nm = b["name"]
add = lambda e, k, p=None, prio=0: rows.append(dict(base=nm, edit=e, k=k, params=p or {}, prio=prio))
add("add", 24, {"obj": ADD2[i % len(ADD2)]}, 1)
add("add", 48, {"obj": ADD2[(i + 3) % len(ADD2)]}, 0)
add("add", 32 + 8 * (i % 2), {"anim": "drop", "obj": DROP[i % 2]}, 1)
add("grow", 32, {"obj": GROW2[i % len(GROW2)], "dur": 20}, 1)
add("delete", 32 if i % 2 == 0 else 40, {"mode": "sink" if i % 2 == 0 else "pop"}, 0)
if i % 2 == 0:
add("delete", 36, {"mode": "class"}, 1)
for j in range(2):
add("swap", 28, {"pair": SWAP2[(i + j) % len(SWAP2)], "wave": 16}, 1)
add("swap", 36, {"pair": SWAP2[(i + 2) % len(SWAP2)]}, 0)
add("spawn", 20, {"obj": TRAIL[i % len(TRAIL)] if TRAIL[i % len(TRAIL)] != "bush" else "tree", "mode": "avenue"}, 1)
add("spawn", 24, {"obj": ["tree", "pillar", "stone", "crystal"][(i + 1) % 4], "loose": 1}, 0)
add("paint", 16, {"mode": "trail3d", "obj": TRAIL[(i + 1) % len(TRAIL)]}, 1)
t, c = RAISE[i % len(RAISE)]
add("write", 20, {"text": t, "mode": "raise", "cls": c, "big": 1.3, "shrink": 0.7, "cell": 10, "max_run": 1,
"bold": 0.6 if t.isalpha() and t.isupper() else 0.0}, 1)
if b["boss_px_med"] >= 1500:
add("trap", 32, {"min_keep": 0.4, "cls": ["arch", "veg", "rock"][i % 3]}, 1)
return rows
def plan_rows3(bases):
"""第三波:地面图层里只有 vegetation 画得出来(铺装上的草 / 花径)⇒ 画路 / 写字 / 地面 swap 都改用 vegetation。"""
rows = []
texts = ["马良", "star", "heart", "MALIANG", "马良", "HI"]
for i, b in enumerate(bases):
nm = b["name"]
add = lambda e, k, p=None, prio=0: rows.append(dict(base=nm, edit=e, k=k, params=p or {}, prio=prio))
if b["walk_cm"] >= 300:
add("paint", 16, {"cls": "veg", "lead": 16}, 2)
add("paint", 20, {"cls": "veg"}, 1)
add("write", 20, {"text": texts[i % len(texts)], "cls": "veg", "big": 1.2, "dur": 32}, 2)
add("swap", 28, {"pair": "pave2veg", "wave": 20}, 1)
add("swap", 28, {"pair": "terrain2veg", "wave": 20}, 1)
return rows
def main(argv=None):
ap = argparse.ArgumentParser()
ap.add_argument("--n", type=int, default=48)
ap.add_argument("--min_score", type=float, default=0.8)
ap.add_argument("--per_theme", type=int, default=3)
ap.add_argument("--per_look", type=int, default=12)
ap.add_argument("--out", default=f"{T}/edit/e81")
ap.add_argument("--exclude", default="")
ap.add_argument("--append", action="store_true", help="追加到已有 plan(新底子)")
ap.add_argument("--wave2", default="", help="给 <out>/bases.jsonl 里的底子出第二波计划 → 这个路径")
ap.add_argument("--wave3", default="", help="第三波(vegetation 地面图层)→ 这个路径")
a = ap.parse_args(argv)
if a.wave2 or a.wave3:
bases = [json.loads(l) for l in open(f"{a.out}/bases.jsonl")]
pr = plan_rows2(bases) if a.wave2 else plan_rows3(bases)
with open(a.wave2 or a.wave3, "w") as fh:
for r in pr:
fh.write(json.dumps(r, ensure_ascii=False) + "\n")
print(f"{len(bases)} 个底子 · {len(pr)} 行 → {a.wave2 or a.wave3}")
print("编辑", dict(collections.Counter(r["edit"] for r in pr)))
return
rows = candidates(a.min_score)
ex = set(x for x in a.exclude.split(",") if x)
if a.append and os.path.exists(f"{a.out}/bases.jsonl"):
ex |= {json.loads(l)["name"] for l in open(f"{a.out}/bases.jsonl")}
bases = pick(rows, a.n, a.per_theme, a.per_look, ex)
for b in bases:
b.update(stats(b))
os.makedirs(a.out, exist_ok=True)
mode = "a" if a.append else "w"
with open(f"{a.out}/bases.jsonl", mode) as fh:
for b in bases:
fh.write(json.dumps(b, ensure_ascii=False) + "\n")
pr = plan_rows(bases)
with open(f"{a.out}/plan.jsonl", mode) as fh:
for r in pr:
fh.write(json.dumps(r, ensure_ascii=False) + "\n")
print(f"{len(rows)} 个候选 → {len(bases)} 个底子 · {len(pr)} 行计划")
print("主题", dict(collections.Counter(b["theme"] for b in bases)))
print("画风", dict(collections.Counter(b["look"] for b in bases)))
print("编辑", dict(collections.Counter(r["edit"] for r in pr)))
if __name__ == "__main__":
main()