The e2e asset check now completes: PASS on all six steps - empty maps 9 s, demo build 10 s, review build 9 s, captures 80 s, judge 388 s, review file. Four breakages from the folder move had to be fixed first: - ue_placement and ue_common are siblings of the scripts that import them, but the rewrite put only the tools root on sys.path, so every Unreal commandlet died with ModuleNotFoundError. Both paths are inserted now. - render_screenshots.sh resolved the repository root one level up, so commandlet paths became Tools/Tools/... and the capture step failed in 0 s. - the driver treated a result file left by an earlier run as success, which is how a ten-second 'demo build' passed without building. Result files are unlinked before each step and child output streams to a per-step log file rather than a pipe. - two editors on one project hang instead of failing, so the driver refuses to start while one is running. Also corrects ue_recreate_maps.py: LevelEditorSubsystem.new_level() refuses a path whose package is still in memory and delete_asset() leaves the file on disk, measured create->delete->create returning False. The step now loads each map, destroys every actor, saves, and reports the count it left (0), which is the property the builders actually need. Open question recorded in TODO.md: the rebuilt map holds 409 actors and 613 KB against 600 and 886 KB before, and the ~190 missing ones look like the facade-kit and theatre-trim placements that run as separate steps.
121 lines
5.4 KiB
Python
121 lines
5.4 KiB
Python
#!/usr/bin/env python3
|
|
"""Generate character blueprints with the local ComfyUI instance.
|
|
|
|
A "blueprint" here is an art-direction sheet for one character: a front, side and back
|
|
view, plus a head detail, generated from a fixed prompt and seed set so the sheet can
|
|
be regenerated and compared against later. It is the reference the modelled character
|
|
is evaluated against — see `Tools/blender/character_match.py`.
|
|
|
|
python3 Tools/blender/comfyui_character_blueprint.py \
|
|
--character "GigLine walker" \
|
|
--desc "lean adult pedestrian, short dark hair, olive skin, plain navy t-shirt, dark jeans, worn sneakers" \
|
|
--out Art/characters/blueprints
|
|
|
|
Writes one PNG per view plus `contact-sheet.png` and `blueprint.json`, which records
|
|
the prompt, model, seeds and the ComfyUI prompt ids. Everything is generated locally
|
|
by `Tools/gen/comfyui_generate.py`; no network service and no credential is involved.
|
|
|
|
The sheet is generated art, not photography, and it stays out of the reference photo
|
|
folders on purpose: AGENTS.md forbids committing reference photography, and a
|
|
generated concept should not blur that line.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import json
|
|
import os
|
|
import sys
|
|
|
|
_HERE = os.path.dirname(os.path.abspath(__file__))
|
|
sys.path.insert(0, os.path.dirname(_HERE)) # tools root, for _common
|
|
sys.path.insert(0, _HERE) # siblings in this folder (ue_common, ue_placement)
|
|
import comfyui_generate as comfy # noqa: E402
|
|
|
|
from PIL import Image # noqa: E402
|
|
|
|
VIEWS = (
|
|
# name, prompt suffix, seed
|
|
("front", "full body front view, neutral standing pose, arms at sides, "
|
|
"even studio light, plain light grey background, character sheet", 101),
|
|
("side", "full body side profile view, neutral standing pose, arms at sides, "
|
|
"even studio light, plain light grey background, character sheet", 202),
|
|
("back", "full body back view, neutral standing pose, arms at sides, "
|
|
"even studio light, plain light grey background, character sheet", 303),
|
|
("head", "head and shoulders portrait, front view, neutral expression, "
|
|
"even studio light, plain light grey background", 404),
|
|
)
|
|
NEGATIVE = ("multiple people, crowd, text, watermark, signature, blurry, low quality, "
|
|
"cropped limbs, extra limbs, deformed hands, cartoon, anime, logo")
|
|
|
|
|
|
def contact_sheet(paths, out_path, columns=4):
|
|
images = [Image.open(p).convert("RGB") for p in paths if os.path.exists(p)]
|
|
if not images:
|
|
return None
|
|
width = max(i.width for i in images)
|
|
height = max(i.height for i in images)
|
|
sheet = Image.new("RGB", (width * columns, height), (24, 24, 24))
|
|
for index, image in enumerate(images[:columns]):
|
|
sheet.paste(image.resize((width, height)), (index * width, 0))
|
|
sheet.save(out_path)
|
|
return out_path
|
|
|
|
|
|
def main(argv=None):
|
|
ap = argparse.ArgumentParser(description=__doc__,
|
|
formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
ap.add_argument("--character", default="GigLine walker")
|
|
ap.add_argument("--desc", required=True,
|
|
help="what the character wears and looks like; drives every view")
|
|
ap.add_argument("--out", default="Art/characters/blueprints")
|
|
ap.add_argument("--model", default=comfy.DEFAULT_MODEL)
|
|
ap.add_argument("--size", default="832x1216")
|
|
ap.add_argument("--steps", type=int, default=30)
|
|
ap.add_argument("--cfg", type=float, default=5.0)
|
|
args = ap.parse_args(argv)
|
|
|
|
if not os.path.isdir(comfy.HOST) and "://" not in comfy.HOST:
|
|
print(f"error: bad COMFYUI_HOST {comfy.HOST}", file=sys.stderr)
|
|
return 2
|
|
|
|
os.makedirs(args.out, exist_ok=True)
|
|
width, height = (int(v) for v in args.size.lower().split("x"))
|
|
|
|
record = {"character": args.character, "description": args.desc,
|
|
"model": args.model, "views": {}}
|
|
written = []
|
|
for name, suffix, seed in VIEWS:
|
|
prompt = f"{args.character}, {args.desc}, {suffix}"
|
|
out = os.path.join(args.out, f"{name}.png")
|
|
graph = comfy.workflow(prompt, NEGATIVE, args.model, width, height, seed,
|
|
args.steps, args.cfg, "dpmpp_2m", "karras")
|
|
queued = comfy.post("/prompt", {"prompt": graph})
|
|
prompt_id = queued.get("prompt_id")
|
|
if not prompt_id:
|
|
print(f"error: ComfyUI rejected the {name} prompt: {queued}", file=sys.stderr)
|
|
return 1
|
|
print(f"{name}: queued {prompt_id} (seed {seed})")
|
|
image = comfy.wait_for_image(prompt_id, timeout=600.0)
|
|
if image is None:
|
|
print(f"error: no image for {name}", file=sys.stderr)
|
|
return 1
|
|
with open(out, "wb") as handle:
|
|
handle.write(image)
|
|
record["views"][name] = {"file": out, "seed": seed, "prompt": prompt,
|
|
"negative": NEGATIVE, "prompt_id": prompt_id}
|
|
written.append(out)
|
|
|
|
sheet = contact_sheet(written, os.path.join(args.out, "contact-sheet.png"))
|
|
record["contact_sheet"] = sheet
|
|
record["note"] = ("generated locally by Tools/blender/comfyui_character_blueprint.py; "
|
|
"record the model licence in THIRD_PARTY_NOTICES.md before shipping")
|
|
with open(os.path.join(args.out, "blueprint.json"), "w") as handle:
|
|
json.dump(record, handle, indent=2)
|
|
print(f"wrote {len(written)} views + contact sheet -> {args.out}")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|