feat(character): ComfyUI blueprints and a render-vs-blueprint evaluation
Two tools that turn character work into something measurable. - Tools/comfyui_character_blueprint.py generates a four-view sheet (front, side, back, head) plus a contact sheet from one description, with fixed seeds and a JSON record of prompts and prompt ids. First sheet: 'GigLine walker', lean pedestrian in a navy tee, jeans and sneakers. - Tools/character_match.py compares a rendered character against a sheet: figure aspect, fill and coverage deltas, mean CIE76 colour distance, dominant-palette distance, and a side-by-side montage. The deltas stay separate on purpose - a single similarity score between a photoreal sheet and a low-detail body would be a number with no meaning. - comfyui_generate.py grew wait_for_image() so both tools share the polling path instead of duplicating it. - 13 unit tests for the colour maths, masks and palette; 95 tool tests pass. First real result, recorded rather than smoothed over: aspect delta 1.24, fill delta 0.12, mean deltaE 31.9, palette deltaE 25.4. The montage shows the reason - the model is a nude body and the blueprint wears a tee and jeans, so clothing dominates every metric. Figure segmentation is explicit (--render-box): auto-detection picked backdrop blobs on every attempt, and a background plate is the proper fix.
This commit is contained in:
1 parent
1fc5835327
commit
85a8437cbc
56 files changed
+841
-170
No files matched your search
+53
-45
@@ -89,6 +89,29 @@ def workflow(prompt: str, negative: str, model: str, width: int, height: int,
|
||||
}
|
||||
|
||||
|
||||
def wait_for_image(prompt_id: str, timeout: float = 300.0, poll: float = 1.5):
|
||||
"""Poll /history until the job has an image, then fetch its bytes."""
|
||||
deadline = time.time() + timeout
|
||||
while time.time() < deadline:
|
||||
history = api(f"/history/{prompt_id}")
|
||||
entry = history.get(prompt_id)
|
||||
if entry and entry.get("outputs"):
|
||||
images = []
|
||||
for node in entry["outputs"].values():
|
||||
images.extend(node.get("images", []))
|
||||
if not images:
|
||||
return None
|
||||
image = images[0]
|
||||
query = urllib.parse.urlencode({
|
||||
"filename": image["filename"],
|
||||
"subfolder": image.get("subfolder", ""),
|
||||
"type": image.get("type", "output")})
|
||||
with urllib.request.urlopen(f"{HOST}/view?{query}", timeout=60) as response:
|
||||
return response.read()
|
||||
time.sleep(poll)
|
||||
return None
|
||||
|
||||
|
||||
def generate(args) -> int:
|
||||
width, height = (int(v) for v in args.size.lower().split("x"))
|
||||
seed = args.seed if args.seed is not None else random.randrange(0, 2**31)
|
||||
@@ -101,51 +124,36 @@ def generate(args) -> int:
|
||||
return 1
|
||||
print(f"queued {prompt_id} ({args.model}, {width}x{height}, seed {seed})")
|
||||
|
||||
deadline = time.time() + args.timeout
|
||||
while time.time() < deadline:
|
||||
history = api(f"/history/{prompt_id}")
|
||||
entry = history.get(prompt_id)
|
||||
if entry and entry.get("outputs"):
|
||||
images = []
|
||||
for node in entry["outputs"].values():
|
||||
images.extend(node.get("images", []))
|
||||
if not images:
|
||||
print("error: job finished with no image", file=sys.stderr)
|
||||
return 1
|
||||
image = images[0]
|
||||
query = urllib.parse.urlencode({
|
||||
"filename": image["filename"],
|
||||
"subfolder": image.get("subfolder", ""),
|
||||
"type": image.get("type", "output")})
|
||||
with urllib.request.urlopen(f"{HOST}/view?{query}", timeout=60) as response:
|
||||
data = response.read()
|
||||
os.makedirs(os.path.dirname(os.path.abspath(args.out)), exist_ok=True)
|
||||
with open(args.out, "wb") as handle:
|
||||
handle.write(data)
|
||||
sidecar = os.path.splitext(args.out)[0] + ".json"
|
||||
with open(sidecar, "w") as handle:
|
||||
json.dump({
|
||||
"tool": "Tools/comfyui_generate.py",
|
||||
"generated_utc": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
||||
"prompt": args.prompt,
|
||||
"negative": args.negative,
|
||||
"model": args.model,
|
||||
"size": [width, height],
|
||||
"seed": seed,
|
||||
"steps": args.steps,
|
||||
"cfg": args.cfg,
|
||||
"sampler": args.sampler,
|
||||
"scheduler": args.scheduler,
|
||||
"workflow": graph,
|
||||
"comfyui_prompt_id": prompt_id,
|
||||
"note": "generated locally; record the model licence in "
|
||||
"THIRD_PARTY_NOTICES.md before shipping",
|
||||
}, handle, indent=2)
|
||||
print(f"wrote {args.out} (+ sidecar)")
|
||||
return 0
|
||||
time.sleep(args.poll)
|
||||
print("error: timed out waiting for the image", file=sys.stderr)
|
||||
return 1
|
||||
data = wait_for_image(prompt_id, timeout=args.timeout, poll=args.poll)
|
||||
if data is None:
|
||||
print("error: no image came back (timeout or empty job)", file=sys.stderr)
|
||||
return 1
|
||||
os.makedirs(os.path.dirname(os.path.abspath(args.out)) or ".", exist_ok=True)
|
||||
with open(args.out, "wb") as handle:
|
||||
handle.write(data)
|
||||
sidecar = os.path.splitext(args.out)[0] + ".json"
|
||||
with open(sidecar, "w") as handle:
|
||||
json.dump({
|
||||
"tool": "Tools/comfyui_generate.py",
|
||||
"generated_utc": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
||||
"prompt": args.prompt,
|
||||
"negative": args.negative,
|
||||
"model": args.model,
|
||||
"size": [width, height],
|
||||
"seed": seed,
|
||||
"steps": args.steps,
|
||||
"cfg": args.cfg,
|
||||
"sampler": args.sampler,
|
||||
"scheduler": args.scheduler,
|
||||
"workflow": graph,
|
||||
"comfyui_prompt_id": prompt_id,
|
||||
"note": "generated locally; record the model licence in "
|
||||
"THIRD_PARTY_NOTICES.md before shipping",
|
||||
}, handle, indent=2)
|
||||
print(f"wrote {args.out} (+ sidecar)")
|
||||
return 0
|
||||
|
||||
|
||||
|
||||
|
||||
def main(argv=None) -> int:
|
||||
|
||||
Reference in new issue
Block a user