File size: 19,274 Bytes
68e2686
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
#!/usr/bin/env python3
"""teaser · 编辑 case「同一世界、不同游玩」(maliang venv;build 要 GPU,其余 CPU)。产物在 outputs/teaser/edit/app/sameworld/($S)。

两种组(GROUPS)
----------------
* union(任务原意):同一 run(同一游戏世界坐标)里相邻的几条 clip(不同轨迹 / 不同按键),世界坐标原样不动。用它们**足迹的并集**
  搭一个 procmap 世界(procmap.World 的足迹 / 净空 / 视野按所有 clip 的全部帧算 —— 把各 clip 的 meta 沿帧轴拼起来,构造 World
  时临时把 procmap.F 设成总帧数;主题 = themes_ext 的布局函数,原样调用),遮挡控制也对所有 clip 的全部帧做,然后每条 clip 用
  自己的相机 + 胶囊各渲染一份 81 帧 ids ⇒「同一张地图、不同的按键 / 走位」。每条的帧 0 视角不同 ⇒ 首帧各自生成
  (f0gen qwen21 默认配方,提示词 / 场景 / 种子一致)。
* start(加做):同一 run、同一相机档位(camera_variant)的几条 clip,按帧 0 玩家位置**平移**到同一个起点(相机矩阵右乘平移:
  行向量约定 M' = T(−t)·M;胶囊 / actor_pos / ground_z 同步平移),Boss 去掉 ⇒ 帧 0 的相机完全重合、语义只差玩家姿势 ⇒
  **一张首帧**(锚 clip 的首帧)+ 不同的按键 / 轨迹 ⇒ 同一个开局、不同的未来。世界同样按平移后足迹的并集搭。

每条 clip 的地图目录 $S/maps/<group>/<k>_<start>/ = procmap 标准文件(ids.npz / prompts.json / actions.npy / source.json /
report.json / map0.png / map0_shaded.png / depth0.png / contact.jpg),f0gen 地图模式直接能用;组目录另有 plan.png(俯视总图:
所有 clip 的轨迹各一种颜色 + 帧 0 视野框)与 group.json。

子命令
------
build     [--groups g1 g2](GPU):搭世界 + 渲染每条 clip 的 ids。
f0jobs    → $S/f0/jobs.jsonl(f0gen.py --backend qwen21 吃;union 组每条 clip 一张;start 组只出锚 clip 一张)。
f0edits   → $S/f0/edits.jsonl(edit_app_f0.py 吃):start 组锚首帧的整图外观编辑(START_LOOKS = night / snow)
          ⇒ 同一开局 × 外观 × 按键(rolljobs 自动带上:名字后缀 __night / __snow)。
rolljobs  → $S/jobs.jsonl(rollout.py 吃;结果 $S/roll/<group>__<k>_<start>/)。
compose   → $A/showcase/sameworld_<group>.mp4(左 = 俯视总图 + 当前帧各 clip 的位置,右 = 各 clip 的生成视频宫格 + 按键 HUD)
          + $A/showcase/sheets/sameworld_<group>.jpg。

usage(env 同 SPEC §0;在 $M 下):
  CUDA_VISIBLE_DEVICES=6 $PY scripts/teaser/edit_sameworld.py build
  $PY scripts/teaser/edit_sameworld.py f0jobs
  CUDA_VISIBLE_DEVICES=6 $M/envs/img/bin/python scripts/teaser/f0gen.py --backend qwen21 --jobs outputs/teaser/edit/app/sameworld/f0/jobs.jsonl
  $PY scripts/teaser/edit_sameworld.py rolljobs   # 然后 rollout.py --jobs outputs/teaser/edit/app/sameworld/jobs.jsonl
  $PY scripts/teaser/edit_sameworld.py compose
"""
from __future__ import annotations

import argparse
import json
import os
import sys
import time

import numpy as np

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)

A = f"{M}/outputs/teaser/edit/app"
S = f"{A}/sameworld"
F = 81
COLORS = [(255, 64, 200), (40, 220, 255), (255, 200, 40), (120, 255, 90), (255, 110, 60), (170, 120, 255)]   # RGB

GROUPS = {
    # 选组:outputs/teaser/edit/app/sameworld/scan.json(全部 clip 的帧 0 视野中心 / 位移 / Boss 可见率)。第一版用过 R10_s8506 /
    # P9C_default_s1944(帧 0 视野宽只有 9–11 m,画面里几乎只有一种地面,看不出「同一张地图」)⇒ 换成视野 14–27 m 的组。
    # 帧 0 视野重叠的 3 条(同一片地方出发、往不同方向;视野 14 m),有 Boss;v10 透视
    "shrine_R10_s5510": dict(mode="union", run="R10_s5510", scene_src="05_Japanese_Spring_Sanctuary",
                             starts=[3840, 3936, 4128], theme="graveyard", seed=5, f0_scene="greek@photoreal"),
    # 帧 0 视野重叠的 3 条(视野 23 m),Boss 可见率 0.71
    "temple_R10_s6507": dict(mode="union", run="R10_s6507", scene_src="06_Overgrown_Tropical_Water_Temple",
                             starts=[2304, 2688, 2784], theme="maze", seed=7, f0_scene="tibetan@photoreal"),
    # 同一 run 的 4 条,往 4 个方向(−123° / 60° / 148° / −20°;视野 27 m),无 Boss
    "court_R10_s1512": dict(mode="union", run="R10_s1512", scene_src="01_Sunlit_Mediterranean_Courtyard",
                            starts=[1824, 2112, 2208, 2400], theme="orchard", seed=3, f0_scene="farm@photoreal"),
    # 同一组 clip 平移到同一起点 ⇒ 一张首帧、4 种按键
    "court_R10_s1512_start": dict(mode="start", run="R10_s1512", scene_src="01_Sunlit_Mediterranean_Courtyard",
                                  starts=[1824, 2112, 2208, 2400], theme="orchard", seed=3, f0_scene="farm@photoreal"),
    # R10_s6507 的 4 条,往 4 个方向(159° / −16° / −121° / 61°;视野 24 m),平移到同一起点
    "temple_R10_s6507_start": dict(mode="start", run="R10_s6507", scene_src="06_Overgrown_Tropical_Water_Temple",
                                   starts=[2304, 2496, 2880, 2976], theme="maze", seed=7, f0_scene="tibetan@photoreal"),
}


def clip_id(g: dict, start: int) -> str:
    return f"{g['scene_src']}__{g['run']}__{start:06d}"


def translate_meta(meta: dict, t: np.ndarray) -> dict:
    """世界整体平移 t(cm,xyz):相机矩阵 M' = T(−t)·M(行向量 [X,1]·M),胶囊端点 / actor_pos / ground_z 加 t。"""
    T = np.eye(4)
    T[3, :3] = -t
    out = dict(meta)
    for k in ("world_to_clip", "world_to_view"):
        m = np.asarray(meta[k], np.float64).reshape(-1, 4, 4)
        out[k] = np.einsum("ij,fjk->fik", T, m).reshape(-1, 16)
    caps = np.array(meta["capsules"], np.float64)
    caps[..., 0:3] += t
    caps[..., 3:6] += t
    out["capsules"] = caps.astype(np.float32)
    out["actor_pos"] = (np.asarray(meta["actor_pos"], np.float64) + t).astype(np.float32)
    out["ground_z"] = (np.asarray(meta["ground_z"], np.float64) + t[2]).astype(np.float32)
    return out


def drop_boss(meta: dict) -> dict:
    out = dict(meta)
    caps = np.array(meta["capsules"], np.float32)
    caps[:, 1] = np.nan
    out["capsules"] = caps
    ap = np.array(meta["actor_pos"], np.float32)
    ap[:, 1] = np.nan
    out["actor_pos"] = ap
    return out


def plan_image(w, srcs: list[dict], dev, max_px: int = 1100):
    """俯视总图(procmap.plan 的画法)+ 每条 clip 的轨迹(各一色)+ 帧 0 视野框;返回 (BGR 图, 世界→图像素的函数)。"""
    import cv2
    import procmap as PM
    res = max(w.nx, w.ny) * PM.GRID_CM / max_px
    nx, ny = int(w.nx * PM.GRID_CM / res), int(w.ny * PM.GRID_CM / res)
    yy, xx = np.mgrid[0:ny, 0:nx]
    xy = np.stack([w.x0 + (xx.ravel() + 0.5) * res, w.y0 + (yy.ravel() + 0.5) * res], 1)
    ro = np.c_[xy, np.full(len(xy), w.z0 + 5000.0)]
    rd = np.tile(np.array([3e-4, 2e-4, -1.0]) / np.linalg.norm([3e-4, 2e-4, -1.0]), (len(xy), 1))
    prims = w.prims()
    t, k = PM.Caster(prims, dev).cast(ro, rd)
    cls = w.ground.classify(xy, dev)
    hit = k >= 0
    cls[hit] = [prims[i]["cls"] for i in k[hit]]
    hgt = np.where(hit, 5000.0 - t, 0)
    import geometry as GEO
    img = GEO.PALETTE[cls].astype(np.float32) * (0.75 + 0.25 * np.clip(hgt / 800, 0, 1))[:, None]
    img = img.reshape(ny, nx, 3).clip(0, 255).astype(np.uint8)[..., ::-1].copy()
    sc = PM.GRID_CM / res

    def q(P):
        return ((w.px(np.asarray(P, float)) + 0.5) * sc).astype(np.float32)
    return img, q, res


def build_group(name: str, g: dict, dev: str = "cuda") -> dict:
    import cv2
    import procmap as PM
    import themes_ext  # noqa: F401  注册主题
    import geometry as GEO
    import model as GM
    t0 = time.time()
    gd = f"{S}/maps/{name}"
    os.makedirs(gd, exist_ok=True)
    srcs = [PM.load_source(clip_id(g, s)) for s in g["starts"]]
    if g["mode"] == "start":
        m0 = srcs[0]["meta"]
        p0 = np.r_[m0["actor_pos"][0, 0, :2], m0["ground_z"][0]].astype(np.float64)
        for s in srcs:
            m = s["meta"]
            t = p0 - np.r_[m["actor_pos"][0, 0, :2], m["ground_z"][0]].astype(np.float64)
            s["shift_cm"] = t.tolist()
            s["meta"] = drop_boss(translate_meta(m, t))
        d = np.abs(np.asarray(srcs[1]["meta"]["world_to_clip"][0]) - np.asarray(m0["world_to_clip"][0])).max()
        print(f"[{name}] 平移后帧 0 相机矩阵最大差 {d:.3g}", flush=True)
    keys = ("world_to_clip", "world_to_view", "capsules", "actor_pos", "ground_z")
    merged = dict(srcs[0])
    merged["meta"] = {k: np.concatenate([np.asarray(s["meta"][k]) for s in srcs], 0) for k in keys}
    merged["meta"]["ortho"] = srcs[0]["meta"]["ortho"]
    n_tot = F * len(srcs)
    PM.F = n_tot                                     # World 的足迹 / 视野 + 遮挡控制按全部 clip 的全部帧
    try:
        w = PM.World(merged, g["seed"])
        PM.THEME_FN[g["theme"]](w)
        cache = [PM.char_rays(merged["meta"], f) for f in range(n_tot)]
        occ = PM.control_occlusion(w, dev, cache)
    finally:
        PM.F = F
    print(f"[{name}] 世界 {w.nx * PM.GRID_CM / 100:.0f}×{w.ny * PM.GRID_CM / 100:.0f} m · {len(w.prims())} 图元 · "
          f"遮挡控制删 {len(occ['removed'])} 个 · {time.time() - t0:.0f}s", flush=True)
    plan, q, res = plan_image(w, srcs, dev)
    rows = []
    for k, (s, start) in enumerate(zip(srcs, g["starts"])):
        w.src = s
        ids, keep = PM.render(w, dev)
        assert set(np.unique(ids).tolist()) <= GM.valid_raw_ids("parts")
        od = f"{gd}/{k}_{start:06d}"
        os.makedirs(od, exist_ok=True)
        coll = GM.collapse_parts(ids)
        ppx = [int((coll[f] == GEO.PLAYER_ID).sum()) for f in range(F)]
        np.savez_compressed(f"{od}/ids.npz", ids=ids)
        json.dump(s["prompts"], open(f"{od}/prompts.json", "w"), indent=1)
        np.save(f"{od}/actions.npy", s["blocks"][1:])
        row = s["row"]
        json.dump(dict(clip_id=s["clip_id"], subset=row["subset"], scene=row["scene"], run=row["run"], start=row["start"],
                       theme=g["theme"], theme_desc=PM.THEME_DESC[g["theme"]], seed=g["seed"], ortho=s["meta"]["ortho"],
                       z_ground_cm=w.z0, group=name, group_mode=g["mode"], group_clips=[clip_id(g, x) for x in g["starts"]],
                       shift_cm=s.get("shift_cm"), boss_dropped=g["mode"] == "start", actions_block=s["blocks"].tolist(),
                       world_origin_cm=[float(w.x0), float(w.y0)], generator="scripts/teaser/edit_sameworld.py"),
                  open(f"{od}/source.json", "w"), indent=1)
        rep = dict(case=f"{name}/{k}_{start:06d}", clip_id=s["clip_id"], player_px_f0=ppx[0], player_px_min=min(ppx),
                   boss_px_f0=int((coll[0] == GEO.BOSS_ID).sum()), n_prims=len(w.prims()), occlusion_control=occ,
                   frame0={c: round(float((coll[0] == i).mean()), 4) for c, i in
                           (("terrain", 1), ("paving", 2), ("architecture", 3), ("rock", 4), ("vegetation", 5),
                            ("prop", 6), ("sky", 0))}, checks_ok=bool(ppx[0] >= 500), sec=round(time.time() - t0, 1))
        json.dump(rep, open(f"{od}/report.json", "w"), indent=1)
        cv2.imwrite(f"{od}/map0.png", PM.frame_rgb(w, ids, 0)[..., ::-1])
        cv2.imwrite(f"{od}/map0_shaded.png", PM.shaded0(w, ids, keep[0])[..., ::-1])
        cv2.imwrite(f"{od}/depth0.png", PM.depth0(w, ids, keep[0]))
        tiles = [PM.label(cv2.resize(PM.frame_rgb(w, ids, f), (PM.W // 2, PM.H // 2), interpolation=cv2.INTER_AREA),
                          f"f{f}") for f in (0, 20, 40, 60, 80)]
        cv2.imwrite(f"{od}/contact.jpg", np.concatenate(tiles, 1)[..., ::-1], [cv2.IMWRITE_JPEG_QUALITY, 90])
        P = s["meta"]["actor_pos"][:F, 0, :2]
        P = P[np.isfinite(P).all(1)]
        foot0 = PM.frame_ground_poly(s["meta"]["world_to_clip"][0], s["meta"]["world_to_view"][0], w.z0)
        rows.append(dict(k=k, start=start, clip_id=s["clip_id"], dir=od, path_px=q(P).tolist(), foot0_px=q(foot0).tolist(),
                         disp_cm=(P[-1] - P[0]).tolist(), player_px_f0=ppx[0], shift_cm=s.get("shift_cm")))
        print(f"[{name}] clip {k} {s['clip_id']}: 帧 0 玩家 {ppx[0]} px · 最少 {min(ppx)} px", flush=True)
    for r in rows:                                    # plan:帧 0 视野框 + 轨迹
        col = COLORS[r["k"] % len(COLORS)][::-1]
        cv2.polylines(plan, [np.asarray(r["foot0_px"], np.float32).round().astype(np.int32)], True, col, 1, cv2.LINE_AA)
    for r in rows:
        col = COLORS[r["k"] % len(COLORS)][::-1]
        pp = np.asarray(r["path_px"], np.float32).round().astype(np.int32)
        cv2.polylines(plan, [pp], False, (0, 0, 0), 5, cv2.LINE_AA)
        cv2.polylines(plan, [pp], False, col, 3, cv2.LINE_AA)
        cv2.circle(plan, tuple(pp[0]), 6, col, -1, cv2.LINE_AA)
        cv2.circle(plan, tuple(pp[0]), 6, (0, 0, 0), 1, cv2.LINE_AA)
    cv2.imwrite(f"{gd}/plan.png", plan)
    info = dict(name=name, **g, plan=f"{gd}/plan.png", plan_res_cm_per_px=res, clips=rows,
                world_extent_m=[round(w.nx * PM.GRID_CM / 100, 1), round(w.ny * PM.GRID_CM / 100, 1)],
                n_prims=len(w.prims()), occlusion_control=occ, sec=round(time.time() - t0, 1))
    json.dump(info, open(f"{gd}/group.json", "w"), indent=1)
    return info


def cmd_build(a) -> None:
    for name in a.groups or list(GROUPS):
        build_group(name, GROUPS[name])


def cmd_f0jobs(a) -> None:
    os.makedirs(f"{S}/f0", exist_ok=True)
    rows = []
    for name, g in GROUPS.items():
        gj = f"{S}/maps/{name}/group.json"
        if not os.path.exists(gj):
            continue
        info = json.load(open(gj))
        clips = info["clips"] if g["mode"] == "union" else info["clips"][:1]
        for c in clips:
            for seed in a.seeds:
                rows.append({"case": f"{name}__{c['k']}", "map_dir": c["dir"], "scene": g["f0_scene"], "seed": seed,
                             "out": f"{S}/f0/{name}/{c['k']}_{c['start']:06d}/qwen21_s{seed}.png"})
    with open(f"{S}/f0/jobs.jsonl", "w") as fh:
        for r in rows:
            fh.write(json.dumps(r) + "\n")
    print(f"{len(rows)} 个首帧任务 → {S}/f0/jobs.jsonl")


START_LOOKS = ("night", "snow")      # start 组的锚首帧再做外观编辑(edit_app_f0 整图编辑)⇒ 同一开局 × 外观 × 按键


def cmd_f0edits(a) -> None:
    """start 组锚 clip 的首帧 → edit_app_f0 的整图编辑任务(night / snow),写 $S/f0/edits.jsonl。"""
    import edit_app as E
    rows = []
    for name, g in GROUPS.items():
        gj = f"{S}/maps/{name}/group.json"
        if g["mode"] != "start" or not os.path.exists(gj):
            continue
        c = json.load(open(gj))["clips"][0]
        src = f"{S}/f0/{name}/{c['k']}_{c['start']:06d}/qwen21_s{a.seed}.png"
        if not os.path.exists(src):
            continue
        for v in START_LOOKS:
            rows.append({"out": src[:-4] + f"__{v}.png", "src": src, "ids": f"{c['dir']}/ids.npz", "kind": "global",
                         "prompt": E.GLOBAL[v], "seed": E.SEED[v], "variant": v, "group": name})
    with open(f"{S}/f0/edits.jsonl", "w") as fh:
        for r in rows:
            fh.write(json.dumps(r) + "\n")
    print(f"{len(rows)} 个锚首帧外观编辑任务 → {S}/f0/edits.jsonl")


def cmd_rolljobs(a) -> None:
    rows = []
    for name, g in GROUPS.items():
        gj = f"{S}/maps/{name}/group.json"
        if not os.path.exists(gj):
            continue
        info = json.load(open(gj))
        anchor = info["clips"][0]
        looks = [None] + (list(START_LOOKS) if g["mode"] == "start" else [])
        for c, look in [(c, lk) for lk in looks for c in info["clips"]]:
            fc = c if g["mode"] == "union" else anchor
            first = f"{S}/f0/{name}/{fc['k']}_{fc['start']:06d}/qwen21_s{a.seed}" + (f"__{look}" if look else "") + ".png"
            if not os.path.exists(first):
                continue
            ed = {"type": "sameworld", "variant": g["mode"], "group": name, "group_mode": g["mode"], "clip_index": c["k"],
                  "source_clip": c["clip_id"], "group_clips": [x["clip_id"] for x in info["clips"]], "theme": g["theme"],
                  "world_seed": g["seed"], "f0_scene": g["f0_scene"], "f0_seed": a.seed, "f0_backend": "qwen21 shp_afix_rs",
                  "shift_cm": c.get("shift_cm"), "boss_dropped": g["mode"] == "start", "edit_frame": None,
                  "map_dir": c["dir"], "actions": f"{c['dir']}/actions.npy", "plan": info["plan"],
                  "look_edit": look, "label": f"Play {c['k'] + 1}" + (f" ({look})" if look else ""),
                  "scene": g["f0_scene"], "map": g["theme"],
                  "desc": ("same procmap world (union of the clips' footprints), each clip's own camera + capsules + keys"
                           if g["mode"] == "union" else
                           "clips translated to one common start (boss removed): same first frame, different keys")}
            if look:
                ed["first_edit"] = {"editor": "Qwen-Image-2.1-Turbo", "kind": "global", "variant": look,
                                    "src": first.replace(f"__{look}.png", ".png")}
            rows.append({"name": f"{name}__{c['k']}_{c['start']:06d}" + (f"__{look}" if look else ""), "first": first, "ids": f"{c['dir']}/ids.npz",
                         "prompts": f"{c['dir']}/prompts.json", "out": f"{S}/roll", "seed": 0, "prio": 1,
                         "scene": g["f0_scene"], "map": g["theme"], "label": ed["label"], "edit": ed})
    with open(f"{S}/jobs.jsonl", "w") as fh:
        for r in rows:
            fh.write(json.dumps(r, ensure_ascii=False) + "\n")
    print(f"{len(rows)} 个 rollout 任务 → {S}/jobs.jsonl")


def cmd_compose(a) -> None:
    import edit_compose as EC
    for name in a.groups or list(GROUPS):
        if os.path.exists(f"{S}/maps/{name}/group.json"):
            EC.compose_sameworld(name, a)
            if GROUPS[name]["mode"] == "start":
                EC.compose_sameworld_looks(name, START_LOOKS)


def main(argv=None) -> None:
    ap = argparse.ArgumentParser(description=__doc__.split("\n")[0])
    sp = ap.add_subparsers(dest="cmd", required=True)
    b = sp.add_parser("build")
    b.add_argument("--groups", nargs="*", default=None)
    f = sp.add_parser("f0jobs")
    f.add_argument("--seeds", type=int, nargs="+", default=[0])
    e = sp.add_parser("f0edits")
    e.add_argument("--seed", type=int, default=0)
    r = sp.add_parser("rolljobs")
    r.add_argument("--seed", type=int, default=0, help="用哪个首帧种子")
    c = sp.add_parser("compose")
    c.add_argument("--groups", nargs="*", default=None)
    a = ap.parse_args(argv)
    {"build": cmd_build, "f0jobs": cmd_f0jobs, "f0edits": cmd_f0edits, "rolljobs": cmd_rolljobs,
     "compose": cmd_compose}[a.cmd](a)


if __name__ == "__main__":
    main()