"""可被网站/命令行复用的生成管线。 读 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, "no shadow", "no background", "transparent background", "isolated subject", "clean PNG asset", "输出 PNG,背景必须是真实 Alpha 透明通道,不是白底,不是棋盘格。主体居中,边缘干净,适合用于 App、网页和海报叠加", ]) # ---- 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 = providers.generate(creds["provider"], part_prompt, creds["api_key"], creds.get("base_url", "https://api.openai.com/v1"), creds.get("model", "gpt-image-2"), part.get("size", c.get("size", creds.get("size", "1024x1024")))) 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, centered, full body in frame, no text"), ] if x) log(f"🎨 [{cid}] 生成角色图…") img = providers.generate(creds["provider"], full_prompt, creds["api_key"], creds.get("base_url", "https://api.openai.com/v1"), creds.get("model", "gpt-image-2"), c.get("size", creds.get("size", "1024x1024"))) 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 = providers.generate(creds["provider"], full_prompt, creds["api_key"], creds.get("base_url", "https://api.openai.com/v1"), creds.get("model", "gpt-image-2"), a.get("size", creds.get("size", "1024x1024"))) 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