| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202 |
- """可被网站/命令行复用的生成管线。
- 读 manifest -> 生成各类资产 -> 写 library.json(供网站可视化列出与预览)。
- """
- import json
- import os
- import providers
- import spine_builder
- import particle_builder
- import tween_builder
- HERE = os.path.dirname(os.path.abspath(__file__))
- def run(manifest, out_root, creds=None, log=print):
- """manifest: dict; out_root: 输出根目录; creds: {provider,api_key,base_url,model,size}
- 返回 (library_dict, base_out)。"""
- creds = creds or {}
- game = manifest.get("game", "game")
- base_out = os.path.join(out_root, game)
- chars_out = os.path.join(base_out, "characters")
- vfx_out = os.path.join(base_out, "vfx")
- ui_out = os.path.join(base_out, "ui")
- style = manifest.get("style", "")
- library = {
- "game": game,
- "slot_config": manifest.get("slot_config", {}),
- "characters": [],
- "vfx": [],
- "ui": [],
- "ui_art": [],
- }
- total_steps = len(manifest.get("characters", [])) + len(manifest.get("ui_art", [])) + len(manifest.get("vfx", [])) + (1 if manifest.get("ui", []) else 0)
- done_steps = 0
- def progress(label):
- nonlocal done_steps
- done_steps += 1
- log(f"进度 {done_steps}/{max(1, total_steps)} · {label}")
- def transparent_prompt(extra):
- return ", ".join([
- extra,
- "生成纯透明背景 PNG,真实 Alpha 通道,不要棋盘格,不要白底,不要阴影。",
- ])
- def alpha_report(img):
- img = img.convert("RGBA")
- alpha = img.getchannel("A")
- mn, mx = alpha.getextrema()
- transparent = sum(1 for v in alpha.getdata() if v == 0)
- ratio = transparent / max(1, img.width * img.height)
- return mn, mx, ratio
- def has_alpha(img):
- return alpha_report(img)[0] == 0
- def log_alpha(label, img, required):
- if not required:
- return
- mn, mx, ratio = alpha_report(img)
- if mn == 0:
- log(f"✅ [{label}] Alpha 透明通道有效:透明像素 {ratio:.1%}")
- else:
- log(f"⚠️ [{label}] 模型返回 PNG 但没有透明 Alpha:alpha={mn}-{mx},请重新生成或换支持透明输出的图像模型")
- def generate_checked(label, prompt, size, require_alpha):
- retry_suffixes = [
- "",
- "这不是真透明背景。请重新生成:\n背景必须是 Alpha 透明通道,不是白色、灰色或棋盘格。\n去掉所有背景、阴影、光晕和底板,只保留主体,输出 PNG。",
- "这不是真透明背景。请重新生成:\n背景必须是 Alpha 透明通道,不是白色、灰色或棋盘格。\n去掉所有背景、阴影、光晕和底板,只保留主体,输出 PNG。",
- ]
- last = None
- for attempt, suffix in enumerate(retry_suffixes, start=1):
- attempt_prompt = ", ".join(x for x in [prompt, suffix] if x)
- if attempt > 1:
- log(f"🔁 [{label}] Alpha 不合格,重新生成透明 PNG(第 {attempt}/{len(retry_suffixes)} 次)…")
- img = providers.generate(creds["provider"], attempt_prompt, creds["api_key"],
- creds.get("base_url", "https://api.openai.com/v1"),
- creds.get("model", "gpt-image-2"),
- size)
- last = img
- log_alpha(label, img, require_alpha)
- if not require_alpha or has_alpha(img):
- return img
- raise RuntimeError(f"模型连续 {len(retry_suffixes)} 次没有返回真实 Alpha 透明通道;请换支持透明输出的图像模型或稍后重试")
- # ---- A. 角色(Spine)----
- for i, c in enumerate(manifest.get("characters", [])):
- cid = c.get("id", f"char_{i}")
- anims = c.get("animations", ["idle"])
- if not creds.get("api_key"):
- log(f"⚠️ 未填 key,跳过角色 {cid}")
- 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
- 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"]
- else:
- full_prompt = ", ".join(x for x in [
- c.get("prompt", ""), style,
- transparent_prompt("single game icon character or slot symbol, centered, full body in frame, no text, not a boss, not a demon lord, not dark armor"),
- ] if x)
- log(f"🎨 [{cid}] 生成角色图…")
- img = generate_checked(cid, full_prompt, c.get("size", creds.get("size", "1024x1024")), True)
- spine_builder.build_character(cid, img, chars_out, anims)
- w, h = spine_builder.trim_to_content(img).size
- files = [f"characters/{cid}.json", f"characters/{cid}.atlas", f"characters/{cid}.png"]
- library["characters"].append({
- "id": cid,
- "png": f"characters/{cid}.png",
- "w": w, "h": h,
- "type": c.get("type", "spine"),
- "role": c.get("role", ""),
- "animations": spine_builder.anim_data(anims),
- "files": files,
- })
- log(f"✅ [{cid}] 完成 ({anims})")
- progress(f"{cid}")
- except Exception as e:
- log(f"❌ [{cid}] 失败: {e}")
- progress(f"{cid}")
- # ---- A2. UI 美术(背景 / Logo / 卷轴框 / 按钮 等整图)----
- ui_art_out = os.path.join(base_out, "ui_art")
- for a in manifest.get("ui_art", []):
- aid = a.get("id", "art")
- if not creds.get("api_key"):
- log(f"⚠️ 未填 key,跳过 UI 美术 {aid}")
- continue
- try:
- transparent = a.get("transparent", True)
- extra = (transparent_prompt("single clean UI element, no text")
- if transparent
- else "full-bleed illustration, no text, no UI elements")
- full_prompt = ", ".join(x for x in [a.get("prompt", ""), style if a.get("use_style") else "", extra] if x)
- log(f"🖼 [{aid}] 生成 UI 美术…")
- img = generate_checked(aid, full_prompt, a.get("size", creds.get("size", "1024x1024")), transparent)
- os.makedirs(ui_art_out, exist_ok=True)
- img.save(os.path.join(ui_art_out, f"{aid}.png"))
- library["ui_art"].append({"id": aid, "file": f"ui_art/{aid}.png",
- "w": img.width, "h": img.height,
- "transparent": transparent})
- log(f"✅ [{aid}] UI 美术完成")
- progress(f"{aid}")
- except Exception as e:
- log(f"❌ [{aid}] UI 美术失败: {e}")
- progress(f"{aid}")
- # ---- B. 粒子 VFX ----
- for v in manifest.get("vfx", []):
- vid = v.get("id", "vfx")
- try:
- path = particle_builder.build_particle(
- vid, v.get("template", "burst"), v.get("color", [255, 255, 255]), vfx_out)
- cfg = json.load(open(path, encoding="utf-8"))
- library["vfx"].append({"id": vid, "template": v.get("template"),
- "file": f"vfx/{vid}.particle.json", "config": cfg})
- log(f"✨ [{vid}] 粒子配置完成")
- progress(f"{vid}")
- except Exception as e:
- log(f"❌ [{vid}] 粒子失败: {e}")
- progress(f"{vid}")
- # ---- C. UI Tween ----
- ui = manifest.get("ui", [])
- if ui:
- used = [u.get("preset") for u in ui if u.get("preset")]
- try:
- tween_builder.build_tweens(used, ui_out)
- for u in ui:
- library["ui"].append({"id": u.get("id"), "preset": u.get("preset"),
- "params": u.get("params", {})})
- log(f"🎛 TweenPresets.ts 完成 ({used})")
- progress("TweenPresets")
- except Exception as e:
- log(f"❌ Tween 失败: {e}")
- progress("TweenPresets")
- os.makedirs(base_out, exist_ok=True)
- with open(os.path.join(base_out, "library.json"), "w", encoding="utf-8") as f:
- json.dump(library, f, ensure_ascii=False, indent=2)
- log("—— 完成 ——")
- return library, base_out
|