|
|
@@ -123,6 +123,33 @@ def run(manifest, out_root, creds=None, log=print, merge_existing=False):
|
|
|
|
|
|
required_failures = []
|
|
|
|
|
|
+ def part_sheet_prompt(c, parts, cols, rows):
|
|
|
+ ordered = ", ".join(f"{idx + 1}. {p['id']} ({p.get('prompt', '')})" for idx, p in enumerate(parts))
|
|
|
+ return ", ".join(x for x in [
|
|
|
+ c.get("prompt", ""),
|
|
|
+ style,
|
|
|
+ (
|
|
|
+ f"create one transparent PNG boss rigging parts sprite sheet, exactly {cols} columns and {rows} rows, "
|
|
|
+ "one isolated part per cell, no labels, no numbers, no grid lines, no text, no shadows, no background, "
|
|
|
+ "parts must not overlap cell borders, all parts from the same character, same lighting and style, "
|
|
|
+ "center each part inside its own cell, leave unused cells fully transparent"
|
|
|
+ ),
|
|
|
+ "cell order left to right, top to bottom: " + ordered,
|
|
|
+ transparent_prompt("sprite sheet of separated rigging parts only")
|
|
|
+ ] if x)
|
|
|
+
|
|
|
+ def split_part_sheet(sheet_img, parts, cols, rows):
|
|
|
+ sheet = sheet_img.convert("RGBA")
|
|
|
+ cell_w = max(1, sheet.width // cols)
|
|
|
+ cell_h = max(1, sheet.height // rows)
|
|
|
+ part_images = {}
|
|
|
+ for idx, part in enumerate(parts):
|
|
|
+ col = idx % cols
|
|
|
+ row = idx // cols
|
|
|
+ box = (col * cell_w, row * cell_h, (col + 1) * cell_w, (row + 1) * cell_h)
|
|
|
+ part_images[part["id"]] = sheet.crop(box)
|
|
|
+ return part_images
|
|
|
+
|
|
|
# ---- A. 角色(Spine)----
|
|
|
for i, c in enumerate(manifest.get("characters", [])):
|
|
|
cid = c.get("id", f"char_{i}")
|
|
|
@@ -132,21 +159,40 @@ def run(manifest, out_root, creds=None, log=print, merge_existing=False):
|
|
|
continue
|
|
|
try:
|
|
|
if c.get("type") == "spine_parts":
|
|
|
- part_images = {}
|
|
|
parts = c.get("parts") or []
|
|
|
- for part in parts:
|
|
|
- part_id = part["id"]
|
|
|
- part_prompt = ", ".join(x for x in [
|
|
|
- c.get("prompt", ""),
|
|
|
- part.get("prompt", ""),
|
|
|
- style,
|
|
|
- transparent_prompt("single separated rigging part only, centered, no text, no other body parts")
|
|
|
- ] if x)
|
|
|
- log(f"🎨 [{cid}/{part_id}] 生成 Boss 拆件…")
|
|
|
- pimg = generate_checked(f"{cid}/{part_id}", part_prompt,
|
|
|
- part.get("size", c.get("size", creds.get("size", "1024x1024"))),
|
|
|
- True)
|
|
|
- part_images[part_id] = pimg
|
|
|
+ part_images = {}
|
|
|
+ sheet_cfg = c.get("spriteSheet") or {}
|
|
|
+ use_sheet = c.get("partGeneration") == "sprite_sheet" or sheet_cfg.get("enabled")
|
|
|
+ if use_sheet:
|
|
|
+ cols = int(sheet_cfg.get("cols") or 4)
|
|
|
+ rows = int(sheet_cfg.get("rows") or 4)
|
|
|
+ if len(parts) > cols * rows:
|
|
|
+ raise RuntimeError(f"Boss 拆件数 {len(parts)} 超过 sprite sheet 容量 {cols}x{rows}")
|
|
|
+ try:
|
|
|
+ log(f"🎨 [{cid}] 生成 Boss 拆件表 {cols}×{rows}…")
|
|
|
+ sheet = generate_checked(f"{cid}/parts_sheet",
|
|
|
+ part_sheet_prompt(c, parts, cols, rows),
|
|
|
+ sheet_cfg.get("size", c.get("size", "1024x1024")),
|
|
|
+ True)
|
|
|
+ part_images = split_part_sheet(sheet, parts, cols, rows)
|
|
|
+ log(f"✂️ [{cid}] 已按固定网格切出 {len(parts)} 个 Boss 拆件")
|
|
|
+ except Exception as e:
|
|
|
+ log(f"⚠️ [{cid}] 拆件表生成/切图失败,回退逐部件生成:{e}")
|
|
|
+ part_images = {}
|
|
|
+ if not part_images:
|
|
|
+ for part in parts:
|
|
|
+ part_id = part["id"]
|
|
|
+ part_prompt = ", ".join(x for x in [
|
|
|
+ c.get("prompt", ""),
|
|
|
+ part.get("prompt", ""),
|
|
|
+ style,
|
|
|
+ transparent_prompt("single separated rigging part only, centered, no text, no other body parts")
|
|
|
+ ] if x)
|
|
|
+ log(f"🎨 [{cid}/{part_id}] 生成 Boss 拆件…")
|
|
|
+ pimg = generate_checked(f"{cid}/{part_id}", part_prompt,
|
|
|
+ part.get("size", c.get("size", creds.get("size", "1024x1024"))),
|
|
|
+ True)
|
|
|
+ part_images[part_id] = pimg
|
|
|
spine_builder.build_parts_character(cid, part_images, chars_out, anims, parts)
|
|
|
w, h = 1000, 1000
|
|
|
files = [f"characters/{cid}.json", f"characters/{cid}.atlas", f"characters/{cid}.png"]
|