Explorar o código

Generate boss parts from sprite sheets

bang hai 2 semanas
pai
achega
1a4071dddc
Modificáronse 3 ficheiros con 67 adicións e 15 borrados
  1. 60 14
      pipeline.py
  2. 5 1
      server.py
  3. 2 0
      slot_workflow.py

+ 60 - 14
pipeline.py

@@ -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"]

+ 5 - 1
server.py

@@ -276,7 +276,11 @@ def _task_prompt(kind, item):
     if kind == "characters" and item.get("type") == "spine_parts":
         parts = item.get("parts") or []
         part_text = "\n".join([f"- {p.get('id')}: {p.get('prompt', '')}" for p in parts])
-        return (item.get("prompt", "") + ("\n拆件:\n" + part_text if part_text else "")).strip()
+        sheet = item.get("spriteSheet") or {}
+        mode = ""
+        if item.get("partGeneration") == "sprite_sheet" or sheet.get("enabled"):
+            mode = f"\n生成方式:先生成 {sheet.get('cols', 4)}×{sheet.get('rows', 4)} 透明拆件表,再按格子自动切图。"
+        return (item.get("prompt", "") + mode + ("\n拆件:\n" + part_text if part_text else "")).strip()
     return item.get("prompt") or item.get("template") or item.get("preset") or ""
 
 

+ 2 - 0
slot_workflow.py

@@ -406,6 +406,8 @@ def build_manifest(slot_config):
             "id": boss_id,
             "type": "spine_parts",
             "role": "boss",
+            "partGeneration": "sprite_sheet",
+            "spriteSheet": {"enabled": True, "cols": 4, "rows": 4, "size": "1024x1024"},
             "animations": ["idle", "watch", "charge", "coin_throw", "taunt", "stomp", "attack", "hurt", "explode"],
             "prompt": (
                 "a cute stylized mobile game demon lord boss character, full body, oversized dark armor, "