#!/usr/bin/env python3 """teaser · 编辑 case 的「外观编辑 + 动作 / 轨迹控制」编排(maliang venv,CPU)。目标:展示「语义管布局、首帧管外观、按键管动作」 三者可以分开控制。产物都在 outputs/teaser/edit/app/(下称 $A)。 子命令 ------ plan 底子(BASES:A 档首帧 + vscore 高 + 无硬伤的双向 rollout,见 outputs/teaser/roll/)→ $A/bases.json, 外观编辑的首帧任务 → $A/f0/jobs.jsonl(edit_app_f0.py 吃;每个底子 7–8 个变体:时间 / 天气 / 季节整图编辑、 主角换盔甲 / 换英雄、Boss 换造型、地面换材质)。 galleryplan 角色画廊(GALLERY:3 个底子 × 6 种主角、3 个底子 × 6 种 Boss,kind = char 局部编辑)→ $A/f0/gallery.jsonl; rolljobs 会把画好的写进 $A/jobs_extra.jsonl。 rolljobs 已编辑好的首帧 → 双向 rollout 任务 $A/jobs_look.jsonl(rollout.py 吃;ids / prompts = 底子原样,只换首帧; 每个底子另加一条 orig = 原首帧,同一流水线重跑,宫格对照用)。 mirror 镜像 = 动作控制:底子的 ids 与首帧左右镜像(ids[:, :, ::-1],左右部件 id 互换:12↔13、14↔15、17↔18、19↔20 ⇒ 镜像后的 角色仍是解剖上一致的人),按键 a / d 互换、prompts 按新按键重写 ⇒「同一个世界、往反方向走」→ $A/inputs/mirror//; 另做 text 换动作:ids / 首帧不动,只把 21 条 prompts 换成别的动作文本(全 standing still / 反向 / 一直攻击)→ $A/inputs/text//。任务写进 $A/jobs_act.jsonl。 finalize 每条完成的结果目录补齐:first.png(实际首帧)、actions.npy([20,5] w a s d attack;编辑改了动作就是新的)、 prompts.json(21 条)、ids.npz(rollout.py 已写)、meta.json 里加 "edit" 键(编辑类型、编辑帧、编辑参数、底子来源)。 compose $A/showcase/:每个底子「原版 + 各外观变体」宫格(look_.mp4)、全部外观的总宫格、镜像 / 文本对照并排、 帧拼图 jpg($A/showcase/sheets/)。同一世界不同游玩的合成在 edit_sameworld.py compose。 usage($PY = $M/.venv/bin/python + SPEC §0 的 env;在 $M 下运行): $PY scripts/teaser/edit_app.py plan CUDA_VISIBLE_DEVICES=6 $M/envs/img/bin/python scripts/teaser/edit_app_f0.py --jobs outputs/teaser/edit/app/f0/jobs.jsonl $PY scripts/teaser/edit_app.py rolljobs && $PY scripts/teaser/edit_app.py mirror CUDA_VISIBLE_DEVICES=7 nohup $PY scripts/teaser/rollout.py --jobs outputs/teaser/edit/app/jobs_look.jsonl \\ outputs/teaser/edit/app/jobs_act.jsonl --compile --watch 600 > outputs/teaser/logs/edit/roll_g7.log 2>&1 & $PY scripts/teaser/edit_app.py finalize && $PY scripts/teaser/edit_app.py compose """ from __future__ import annotations import argparse import glob import json import os import shutil import sys 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", HERE): if _p not in sys.path: sys.path.insert(0, _p) A = f"{M}/outputs/teaser/edit/app" ROLL = f"{M}/outputs/teaser/roll" # ------------------------------------------------------------------ 编辑变体 ---- GLOBAL = { "night": "Change only the time of day of picture 1 to a dark moonlit night: cool deep-blue moonlight over the whole " "scene, dark soft night shadows, and a few warm orange lights glowing from existing windows, lanterns and " "braziers.", "dusk": "Change only the lighting of picture 1 to a warm sunset at dusk: low golden-orange sunlight from one side, long " "soft shadows and a pink-purple evening glow over the whole scene.", "snow": "Turn picture 1 into deep winter: a thick layer of fresh white snow covers all the open ground, the floors and " "paths, the roofs, the tops of walls, the rocks and the plants; frost and icicles; cold soft winter light.", "autumn": "Turn picture 1 into late autumn: every tree, bush and patch of grass turns red, orange and golden, and fallen " "autumn leaves are scattered over the ground and paths; warm soft autumn light.", "spring": "Turn picture 1 into fresh spring: the grass becomes lush bright green, the trees and bushes are covered in " "pink and white blossoms and fresh green leaves, petals are scattered on the ground; soft bright morning light.", "lava": "Turn picture 1 from ice into fire: every ice surface becomes dark volcanic rock and every frozen water surface " "becomes glowing molten orange lava; the ice walls become black basalt with glowing cracks; hot red-orange light.", } CHAR = { "hero_gold": ("player", "Edit picture 1: the hero warrior in the middle now wears ornate shining golden plate armour " "with a flowing red cape and a golden helmet, still holding the same sword."), "hero_swap": ("player", "Edit picture 1: replace the hero warrior in the middle with a different hero: a battle mage in " "long flowing crimson robes with a pointed hood and glowing blue magic runes, holding a glowing " "blue sword in the same hand."), "boss_swap": ("boss", "Edit picture 1: replace the big monster boss with a different boss: a huge undead skeleton giant " "made of bleached bones, with glowing blue eyes and tattered black cloth, wielding the same kind of " "big weapon."), } # 地面换材质:每个底子各自的 (classes, 提示词) GROUND = { "farm": ((2,), "Change only the grey gravel ground and tracks in picture 1 into bright red brick paving."), "egypt_sc": ((2,), "Change only the large cracked sandstone floor slabs in picture 1 into polished black-and-white " "checkered marble tiles."), "egypt_fl": ((1,), "Change only the golden desert sand in picture 1 into lush green meadow grass."), "tibetan": ((2,), "Change only the light grey stone-paved courtyard floor in picture 1 into a soft green grass lawn."), "greek": ((2,), "Change only the white marble floor slabs in picture 1 into dark red terracotta floor tiles."), "oasis": ((1,), "Change only the golden desert sand in picture 1 into lush green meadow grass."), "samurai": ((2,), "Change only the dark grey stone-paved floor in picture 1 into raked white zen garden gravel."), "harvest": ((1,), "Change only the trampled brown-yellow grass ground in picture 1 into fresh white snow."), "rice": ((1,), "Change only the green terraced rice fields in picture 1 into golden ripe rice fields ready for harvest."), "icecave": ((2,), "Change only the smooth translucent blue ice floor in picture 1 into glowing molten lava rock floor."), } SEED = {"night": 11, "dusk": 12, "snow": 13, "autumn": 14, "spring": 15, "lava": 16, "hero_gold": 21, "hero_swap": 22, "boss_swap": 23, "ground": 31} STD = ["night", "dusk", "snow", "autumn", "hero_gold", "hero_swap", "boss_swap", "ground"] BASES = [ # (短名, 底子 rollout 名, 变体) ("farm", "ruins__P9C_default_s9945__000096__s4002__farm@photoreal", STD), ("egypt_sc", "stonecircle__P9_golden_s7905__001344__s6005__egypt@cinematic", STD), ("egypt_fl", "farmland__R10_s9503__004032__s9006__egypt@photoreal", STD), ("tibetan", "maze__R10_s3505__003552__s6__tibetan@photoreal", STD), ("greek", "colonnade__P9C_default_s10942__001536__s5009__greek@photoreal", [v for v in STD if v != "boss_swap"]), ("oasis", "orchard__R10_s6509__001824__s18__oasis@cinematic", [v for v in STD if v != "boss_swap"]), ("samurai", "maze__P9_golden_s2907__003648__s11__samurai@photoreal", STD), ("harvest", "farmland__R10_s3512__003072__s9012__autumn_harvest@photoreal", ["night", "dusk", "snow", "spring", "hero_gold", "hero_swap", "boss_swap", "ground"]), ("rice", "patchwork__R10_s3505__003552__s6__rice_terraces@cinematic", STD), ("icecave", "arena__P9_golden_s7905__001344__s7000__icecave@photoreal", ["night", "dusk", "lava", "hero_gold", "hero_swap", "boss_swap", "ground"]), ] EDIT_DESC = {"night": "time of day → moonlit night", "dusk": "time of day → sunset", "snow": "weather → snow", "autumn": "season → autumn", "spring": "season → spring", "lava": "material → ice to lava", "hero_gold": "hero → golden armour", "hero_swap": "hero → crimson battle mage", "boss_swap": "boss → skeleton giant", "ground": "ground material", "orig": "original first frame"} EDIT_EN = {"orig": "Original", "night": "Night", "dusk": "Sunset", "snow": "Snow", "autumn": "Autumn", "spring": "Spring", "lava": "Ice to Lava", "hero_gold": "Golden Armour", "hero_swap": "New Hero", "boss_swap": "New Boss", "ground": "New Ground"} SWAP_LR = {12: 13, 13: 12, 14: 15, 15: 14, 17: 18, 18: 17, 19: 20, 20: 19} def rp(p: str) -> str: return p if os.path.isabs(p) else os.path.join(M, p) def base_info(short: str, name: str) -> dict: meta = json.load(open(f"{ROLL}/{name}/meta.json")) j = meta["job"] md = os.path.dirname(j["ids"]) vs = f"{M}/outputs/teaser/vscore/roll/{name}.vscore.json" v = json.load(open(vs)) if os.path.exists(vs) else {} return {"short": short, "name": name, "roll": f"{ROLL}/{name}", "first": j["first"], "ids": j["ids"], "prompts": j["prompts"], "actions": f"{md}/actions.npy", "map_dir": md, "scene": j.get("scene"), "map": j.get("map"), "case": os.path.basename(md), "vscore": v.get("teaser"), "flags": v.get("flags")} def load_bases() -> dict: return {b["short"]: b for b in json.load(open(f"{A}/bases.json"))} # ------------------------------------------------------------------ plan ---- def cmd_plan(a) -> None: os.makedirs(f"{A}/f0", exist_ok=True) bases = [base_info(s, n) | {"variants": vs} for s, n, vs in BASES] json.dump(bases, open(f"{A}/bases.json", "w"), indent=1) rows = [] for b in bases: for v in b["variants"]: j = {"out": f"{A}/f0/{b['short']}/{v}.png", "src": b["first"], "ids": b["ids"], "seed": SEED[v], "base": b["name"], "short": b["short"], "variant": v} if v in GLOBAL: j.update(kind="global", prompt=GLOBAL[v]) elif v in CHAR: j.update(kind="char", target=CHAR[v][0], prompt=CHAR[v][1]) elif v == "ground": cls, p = GROUND[b["short"]] j.update(kind="ground", classes=list(cls), prompt=p) rows.append(j) with open(f"{A}/f0/jobs.jsonl", "w") as fh: for r in rows: fh.write(json.dumps(r, ensure_ascii=False) + "\n") print(f"{len(bases)} 个底子 → {A}/bases.json;{len(rows)} 个首帧编辑任务 → {A}/f0/jobs.jsonl") # ------------------------------------------------------------------ rolljobs ---- def roll_row(name: str, first: str, ids: str, prompts, edit: dict, prio: float = 0, out: str = f"{A}/roll") -> dict: return {"name": name, "first": first, "ids": ids, "prompts": prompts, "out": out, "seed": 0, "prio": prio, "label": edit.get("label"), "scene": edit.get("scene"), "map": edit.get("map"), "edit": edit} # ------------------------------------------------------------------ 角色画廊(同一张图 × 多种主角 / Boss) ---- HEROES = { "h_paladin": "a holy paladin in shining white-and-gold plate armour with a blue tabard", "h_ninja": "a black-clad ninja assassin with a dark hood and a face mask", "h_ranger": "an elven ranger in green leather armour with a hooded green cloak", "h_barbarian": "a muscular barbarian with bare arms, brown fur pelts and a horned helmet", "h_crimson": "a knight in dark crimson plate armour with a long black cape", "h_samurai": "a samurai in red-and-black lacquered armour with a horned kabuto helmet", } BOSSES = { "b_golem": "a huge golem made of mossy grey stone blocks with glowing green cracks", "b_demon": "a huge fire demon with dark red skin, big horns and glowing orange cracks", "b_troll": "a huge ice troll with pale blue skin, white hair and frost on its shoulders", "b_minotaur": "a huge brown-furred minotaur with big curved horns and a nose ring", "b_orc": "a huge green-skinned orc warlord in spiked black iron armour", "b_insect": "a huge armoured insect monster with a glossy purple carapace and many legs", } GALLERY = {"greek": "hero", "samurai": "hero", "tibetan": "hero", "rice": "boss", "farm": "boss", "egypt_sc": "boss"} GAL_EN = {"h_paladin": "Paladin", "h_ninja": "Ninja", "h_ranger": "Elf Ranger", "h_barbarian": "Barbarian", "h_crimson": "Crimson Knight", "h_samurai": "Samurai", "b_golem": "Stone Golem", "b_demon": "Fire Demon", "b_troll": "Ice Troll", "b_minotaur": "Minotaur", "b_orc": "Orc Warlord", "b_insect": "Insect Monster"} EDIT_EN.update(GAL_EN) def gallery_variants(short: str) -> list[str]: return list(HEROES if GALLERY.get(short) == "hero" else BOSSES) if short in GALLERY else [] def cmd_galleryplan(a) -> None: """角色画廊的首帧编辑任务 → $A/f0/gallery.jsonl(edit_app_f0 吃,kind = char)。""" bases = load_bases() rows = [] for s, who in GALLERY.items(): b = bases[s] for i, v in enumerate(gallery_variants(s)): if who == "hero": p = (f"Edit picture 1: replace the hero warrior in the middle with a different hero: {HEROES[v]}, holding " "the same sword in the same hand.") else: p = (f"Edit picture 1: replace the big monster boss with a different boss: {BOSSES[v]}, wielding the same " "kind of big weapon.") rows.append({"out": f"{A}/f0/{s}/{v}.png", "src": b["first"], "ids": b["ids"], "seed": 41 + i, "base": b["name"], "short": s, "variant": v, "kind": "char", "target": "player" if who == "hero" else "boss", "prompt": p}) with open(f"{A}/f0/gallery.jsonl", "w") as fh: for r in rows: fh.write(json.dumps(r, ensure_ascii=False) + "\n") print(f"{len(rows)} 个角色画廊首帧任务 → {A}/f0/gallery.jsonl") def cmd_rolljobs(a) -> None: bases = load_bases() rows = [] for b in bases.values(): common = {"base": b["name"], "base_roll": b["roll"], "map_dir": b["map_dir"], "scene": b["scene"], "map": b["map"]} rows.append(roll_row(f"{b['short']}__orig", b["first"], b["ids"], b["prompts"], common | {"type": "appearance", "variant": "orig", "edit_frame": None, "label": EDIT_EN["orig"], "desc": EDIT_DESC["orig"]}, prio=2)) for v in b["variants"]: png = f"{A}/f0/{b['short']}/{v}.png" if not (os.path.exists(png) and os.path.exists(png[:-4] + ".json")): continue info = json.load(open(png[:-4] + ".json")) j = info["job"] ed = common | {"type": "appearance", "variant": v, "kind": j["kind"], "edit_frame": 0, "first_src": b["first"], "edit_prompt": info["prompt"], "edit_seed": j["seed"], "editor": "Qwen-Image-2.1-Turbo", "target": j.get("target"), "classes": j.get("classes"), "label": EDIT_EN[v], "desc": EDIT_DESC[v], "struct": info.get("struct")} rows.append(roll_row(f"{b['short']}__{v}", png, b["ids"], b["prompts"], ed, prio=1)) with open(f"{A}/jobs_look.jsonl", "w") as fh: for r in rows: fh.write(json.dumps(r, ensure_ascii=False) + "\n") print(f"{len(rows)} 个外观 rollout 任务 → {A}/jobs_look.jsonl") extra = [] # 角色画廊 → jobs_extra.jsonl for s in GALLERY: b = bases[s] common = {"base": b["name"], "base_roll": b["roll"], "map_dir": b["map_dir"], "scene": b["scene"], "map": b["map"]} for v in gallery_variants(s): png = f"{A}/f0/{s}/{v}.png" if not (os.path.exists(png) and os.path.exists(png[:-4] + ".json")): continue info = json.load(open(png[:-4] + ".json")) j = info["job"] ed = common | {"type": "appearance", "variant": v, "kind": "char", "gallery": True, "edit_frame": 0, "first_src": b["first"], "edit_prompt": info["prompt"], "edit_seed": j["seed"], "editor": "Qwen-Image-2.1-Turbo", "target": j["target"], "label": GAL_EN[v], "desc": f"{j['target']} -> {GAL_EN[v]}", "struct": info.get("struct")} extra.append(roll_row(f"{s}__{v}", png, b["ids"], b["prompts"], ed, prio=0.8)) with open(f"{A}/jobs_extra.jsonl", "w") as fh: for r in extra: fh.write(json.dumps(r, ensure_ascii=False) + "\n") print(f"{len(extra)} 个角色画廊 rollout 任务 → {A}/jobs_extra.jsonl") # ------------------------------------------------------------------ mirror / text ---- def mirror_ids(ids: np.ndarray) -> np.ndarray: out = ids[:, :, ::-1].copy() lut = np.arange(256, dtype=np.uint8) for k, v in SWAP_LR.items(): lut[k] = v return lut[out] def blocks_to_prompts(blocks20: np.ndarray) -> list[str]: import text as TXT return TXT.cell_prompts(blocks20) MIRROR_BASES = ["farm", "egypt_sc", "tibetan", "greek", "samurai", "harvest", "rice", "icecave", "oasis", "egypt_fl"] TEXT_BASES = ["farm", "tibetan", "greek", "samurai", "harvest", "icecave"] TEXT_VARIANTS = { # 名字 → blocks20 的变换([20,5] w a s d attack) "still": lambda b: np.zeros_like(b), "reverse": lambda b: b[:, [2, 3, 0, 1, 4]], # w↔s、a↔d(文本说反方向走) "attack": lambda b: np.concatenate([b[:, :4], np.ones_like(b[:, 4:])], 1), } TEXT_EN = {"still": "Text: standing still", "reverse": "Text: opposite direction", "attack": "Text: always attacking"} def cmd_mirror(a) -> None: import cv2 bases = load_bases() rows = [] for s in MIRROR_BASES: b = bases[s] d = f"{A}/inputs/mirror/{s}" os.makedirs(d, exist_ok=True) with np.load(b["ids"]) as z: ids = z["ids"] np.savez_compressed(f"{d}/ids.npz", ids=mirror_ids(ids)) img = cv2.imread(b["first"]) cv2.imwrite(f"{d}/first.png", img[:, ::-1]) blocks = np.load(b["actions"]) mb = blocks[:, [0, 3, 2, 1, 4]].copy() # a↔d np.save(f"{d}/actions.npy", mb) json.dump(blocks_to_prompts(mb), open(f"{d}/prompts.json", "w"), indent=1) common = {"base": b["name"], "base_roll": b["roll"], "map_dir": b["map_dir"], "scene": b["scene"], "map": b["map"]} rows.append(roll_row(f"{s}__mirror", f"{d}/first.png", f"{d}/ids.npz", f"{d}/prompts.json", common | {"type": "action", "variant": "mirror", "edit_frame": 0, "desc": "ids + first frame mirrored left↔right (L/R part ids swapped), keys a↔d", "label": "Mirrored (A/D swapped)", "actions": f"{d}/actions.npy"}, prio=1.5)) # 镜像对照:镜像语义 + 原首帧不镜像?不做(首帧与语义矛盾)。原版 = __orig(jobs_look 里已有) for s in TEXT_BASES: b = bases[s] blocks = np.load(b["actions"]) common = {"base": b["name"], "base_roll": b["roll"], "map_dir": b["map_dir"], "scene": b["scene"], "map": b["map"]} for tv, fn in TEXT_VARIANTS.items(): d = f"{A}/inputs/text/{s}/{tv}" os.makedirs(d, exist_ok=True) nb = fn(blocks).astype(np.uint8) np.save(f"{d}/actions.npy", nb) json.dump(blocks_to_prompts(nb), open(f"{d}/prompts.json", "w"), indent=1) rows.append(roll_row(f"{s}__text_{tv}", b["first"], b["ids"], f"{d}/prompts.json", common | {"type": "action", "variant": f"text_{tv}", "edit_frame": 0, "desc": f"only the 21 action prompts replaced ({tv}); ids and first frame unchanged", "label": TEXT_EN[tv], "actions": f"{d}/actions.npy"}, prio=0.5)) with open(f"{A}/jobs_act.jsonl", "w") as fh: for r in rows: fh.write(json.dumps(r, ensure_ascii=False) + "\n") print(f"{len(rows)} 个镜像 / 文本 rollout 任务 → {A}/jobs_act.jsonl") # ------------------------------------------------------------------ finalize ---- def finalize_dir(od: str) -> bool: """补齐一条结果目录:first.png / actions.npy / prompts.json / meta.json 的 edit 键。返回是否改动。""" mp = f"{od}/meta.json" if not os.path.exists(mp): return False meta = json.load(open(mp)) j = meta.get("job") or {} # edit_long.py 的结果没有 job(自己写全了文件和 edit 键) ed = j.get("edit") or meta.get("edit") or {} changed = False if not os.path.exists(f"{od}/first.png") and j.get("first"): import cv2 img = cv2.imread(rp(j["first"])) cv2.imwrite(f"{od}/first.png", img) changed = True if not os.path.exists(f"{od}/prompts.json"): json.dump(meta["prompts"], open(f"{od}/prompts.json", "w"), indent=1) changed = True if not os.path.exists(f"{od}/actions.npy"): act = ed.get("actions") if not act and ed.get("base_roll"): act = f"{ed['map_dir']}/actions.npy" if ed.get("map_dir") else None if act and os.path.exists(act): shutil.copyfile(act, f"{od}/actions.npy") changed = True if "edit" not in meta: meta["edit"] = ed meta["files"] = {"video": "gen.mp4", "cond_ids": "ids.npz", "actions": "actions.npy (uint8 [20,5]: w a s d attack, " "0.25 s blocks; cell j = block j-1, cell 0 = cell 1)", "prompts": "prompts.json (21 cell prompts)", "first_frame": "first.png", "side_by_side": "side.mp4", "sheet": "sheet.jpg"} tmp = f"{od}/.meta.fin.tmp" json.dump(meta, open(tmp, "w"), ensure_ascii=False, indent=1) os.replace(tmp, mp) changed = True return changed def cmd_finalize(a) -> None: """补齐结果目录 + 写总索引 $A/index.jsonl(每条一行:name、dir、type、variant、label、base / group、文件相对路径)。""" n = k = 0 rows = [] for root in a.roots: for od in sorted(glob.glob(f"{rp(root)}/*/")): od = od.rstrip("/") if os.path.exists(f"{od}/meta.json"): n += 1 k += finalize_dir(od) m = json.load(open(f"{od}/meta.json")) ed = m.get("edit") or {} rows.append({"name": m["name"], "dir": os.path.relpath(od, A), "type": ed.get("type"), "variant": ed.get("variant"), "label": ed.get("label"), "base": ed.get("base"), "group": ed.get("group"), "source_clip": ed.get("source_clip"), "edit_frame": ed.get("edit_frame"), "files": ["gen.mp4", "ids.npz", "actions.npy", "prompts.json", "first.png", "meta.json"]}) with open(f"{A}/index.jsonl", "w") as fh: for r in rows: fh.write(json.dumps(r, ensure_ascii=False) + "\n") print(f"finalize:{n} 条完成的结果,补齐 {k} 条;索引 → {A}/index.jsonl") # ------------------------------------------------------------------ compose ---- def _cases(names: list[str], root: str = f"{A}/roll") -> list[str]: return [f"{root}/{n}" for n in names if os.path.exists(f"{root}/{n}/meta.json")] def sheet(paths: list[str], labels: list[str], out: str, frames=(0, 20, 40, 60, 80), tw: int = 312) -> None: """帧拼图:每行一条结果(标签 + 帧 0/20/40/60/80),jpg。""" import cv2 th = round(tw * 480 / 832) rows = [] for p, lab in zip(paths, labels): z = np.load(f"{p}/frames.npz") idx = list(z["idx"]) tiles = [cv2.resize(z["rgb"][idx.index(f)], (tw, th), interpolation=cv2.INTER_AREA) for f in frames if f in idx] r = np.concatenate(tiles, 1).copy() for t, f in enumerate(frames): cv2.putText(r, f"f{f}", (t * tw + 4, th - 6), cv2.FONT_HERSHEY_SIMPLEX, 0.45, (255, 255, 255), 1, cv2.LINE_AA) cv2.rectangle(r, (0, 0), (min(r.shape[1], 12 + 9 * len(lab)), 20), (0, 0, 0), -1) cv2.putText(r, lab, (4, 15), cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 255), 1, cv2.LINE_AA) rows.append(r) os.makedirs(os.path.dirname(out), exist_ok=True) cv2.imwrite(out, cv2.cvtColor(np.concatenate(rows, 0), cv2.COLOR_RGB2BGR), [cv2.IMWRITE_JPEG_QUALITY, 88]) def cmd_compose(a) -> None: import edit_compose as EC EC.compose_app(a) def main(argv=None) -> None: ap = argparse.ArgumentParser(description=__doc__.split("\n")[0]) sp = ap.add_subparsers(dest="cmd", required=True) sp.add_parser("plan") sp.add_parser("rolljobs") sp.add_parser("galleryplan") sp.add_parser("mirror") f = sp.add_parser("finalize") f.add_argument("--roots", nargs="*", default=[f"{A}/roll", f"{A}/sameworld/roll", f"{A}/long/roll"]) c = sp.add_parser("compose") c.add_argument("--only", nargs="*", default=None, help="只出这些部分:look / act / all_look") a = ap.parse_args(argv) {"plan": cmd_plan, "rolljobs": cmd_rolljobs, "galleryplan": cmd_galleryplan, "mirror": cmd_mirror, "finalize": cmd_finalize, "compose": cmd_compose}[a.cmd](a) if __name__ == "__main__": main()