#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ CHAR-HERO-DESIGN-PACKER 生成/整理苏白主角资产包,让他不再像路人甲。 功能: 1. 读取候选参考图 2. 用 Seedance/Kling 生成多视角变体 3. 输出完整资产包到 approved/ 目录 4. 更新 manifest.hdlp 用法: python char-hero-design-packer.py --character CHAR-003-SuBai --generate-all python char-hero-design-packer.py --character CHAR-003-SuBai --view front_half_body """ import os import sys import json import argparse import shutil from pathlib import Path # 添加项目根目录到路径 PROJECT_ROOT = Path(__file__).parent.parent.parent sys.path.insert(0, str(PROJECT_ROOT / "engines")) try: from image_api_adapter import generate_image except Exception as exc: print(f"⚠️ 图片生成适配器未接通: {exc}") print("💡 桥接需要 Node.js: image-api-bridge.js + image-api-adapter.js") generate_image = None try: from hldp_prompt import expand_prompt except Exception: expand_prompt = None class CharHeroDesignPacker: """主角资产包生成器""" def __init__(self, character_id, assets_root=None): self.character_id = character_id self.assets_root = Path(assets_root or PROJECT_ROOT / "assets" / "characters" / character_id) self.manifest_path = self.assets_root / "manifest.hdlp" self.approved_dir = self.assets_root / "approved" self.candidates_dir = self.assets_root / "candidates" self.rejected_dir = self.assets_root / "rejected" self.turnarounds_dir = self.assets_root / "turnarounds" self.voice_dir = self.assets_root / "voice" # 确保目录存在 for d in [self.approved_dir, self.candidates_dir, self.rejected_dir, self.turnarounds_dir, self.voice_dir]: d.mkdir(parents=True, exist_ok=True) # 读取 manifest self.manifest = self._read_manifest() # 读取角色描述 self.character_desc = self._load_character_description() def _read_manifest(self): """读取 manifest.hdlp""" if not self.manifest_path.exists(): return {"asset_id": self.character_id, "approval_status": "DRAFT"} # 简单解析 HLDP 文件 manifest = {} with open(self.manifest_path, "r", encoding="utf-8") as f: for line in f: line = line.strip() if line.startswith("asset_id:") or line.startswith("canonical_id:"): manifest["asset_id"] = line.split(":", 1)[1].strip() elif line.startswith("approval_status:"): manifest["approval_status"] = line.split(":", 1)[1].strip() elif line.startswith("candidate_front_half_body:"): manifest["candidate_front_half_body"] = line.split(":", 1)[1].strip() return manifest def _load_character_description(self): """从 data/characters-v2.hdlp 加载角色描述""" char_file = PROJECT_ROOT / "data" / "characters-v2.hdlp" if not char_file.exists(): return None # 简单解析 description = {} with open(char_file, "r", encoding="utf-8") as f: content = f.read() # 找 CHAR-003 段落 if "CHAR-003" in content: lines = content.split("\n") in_char = False for line in lines: if "CHAR-003" in line: in_char = True elif in_char: if line.strip().startswith("---"): break if ":" in line: key, _, val = line.partition(":") description[key.strip()] = val.strip() return description def generate_front_half_body(self, output_name="front_half_body.png"): """ 生成正面半身批准图 使用候选图作为参考,用 Seedance/Kling 图生图生成稳定版本 """ print(f"[1/4] 生成正面半身图: {output_name}") candidate = self.manifest.get("candidate_front_half_body") if not candidate or not Path(candidate).exists(): print(f" ❌ 候选图不存在: {candidate}") print(f" 💡 请先准备候选图,放入 candidates/ 目录") return None # 构建提示词 prompt = self._build_prompt("front_half_body") # 调用图像生成 API (图生图) print(f" 参考图: {candidate}") print(f" 提示词: {prompt[:100]}...") if generate_image is None: print(" ⚠️ 当前仓库只有 JS 图片适配器 image-api-adapter.js,未提供 Python generate_image。") print(" ⚠️ 暂不能生成新主角资产;仅将候选图复制到 candidates/ 供人工/后续工具处理。") fallback_path = self.candidates_dir / output_name shutil.copy2(candidate, fallback_path) print(f" 🟡 已复制候选图: {fallback_path}") return None try: result_path = generate_image( prompt=prompt, reference_image=candidate, output_dir=str(self.approved_dir), output_name=output_name ) print(f" ✅ 已生成: {result_path}") return result_path except Exception as e: print(f" ❌ 生成失败: {e}") return None def generate_side_face(self, output_name="side_face.png"): """生成侧脸批准图""" print(f"[2/4] 生成侧脸图: {output_name}") prompt = self._build_prompt("side_face") candidate = self.approved_dir / "front_half_body.png" if not candidate.exists(): print(f" ❌ 请先生成正面半身图") return None try: result_path = generate_image( prompt=prompt, reference_image=str(candidate), output_dir=str(self.approved_dir), output_name=output_name ) print(f" ✅ 已生成: {result_path}") return result_path except Exception as e: print(f" ❌ 生成失败: {e}") return None def generate_full_body_costume(self, output_name="full_body_costume.png"): """生成全身服装图""" print(f"[3/4] 生成全身服装图: {output_name}") prompt = self._build_prompt("full_body_costume") candidate = self.approved_dir / "front_half_body.png" if not candidate.exists(): print(f" ❌ 请先生成正面半身图") return None try: result_path = generate_image( prompt=prompt, reference_image=str(candidate), output_dir=str(self.approved_dir), output_name=output_name ) print(f" ✅ 已生成: {result_path}") return result_path except Exception as e: print(f" ❌ 生成失败: {e}") return None def generate_expression_sheet(self, output_name="expression_sheet.png"): """生成表情表""" print(f"[4/4] 生成表情表: {output_name}") prompt = self._build_prompt("expression_sheet") candidate = self.approved_dir / "front_half_body.png" if not candidate.exists(): print(f" ❌ 请先生成正面半身图") return None try: result_path = generate_image( prompt=prompt, reference_image=str(candidate), output_dir=str(self.approved_dir), output_name=output_name ) print(f" ✅ 已生成: {result_path}") return result_path except Exception as e: print(f" ❌ 生成失败: {e}") return None def _build_prompt(self, view_type): """构建特定视角的提示词""" base_desc = self.character_desc or {} prompts = { "front_half_body": f""" {base_desc.get('visual_description', '中国古代修仙少年,16岁,主角辨识度强')} 正面半身像,胸部以上,面部清晰,眼神坚定, 3D国风漫剧风格,统一渲染风格, 高清,8K,最佳质量 """.strip(), "side_face": f""" {base_desc.get('visual_description', '中国古代修仙少年')} 侧脸45度,能看到面部轮廓和发型, 3D国风漫剧风格,统一渲染风格, 高清,8K,最佳质量 """.strip(), "full_body_costume": f""" {base_desc.get('visual_description', '中国古代修仙少年')} 全身像,站立姿势,完整展示服装细节, 服装细节必须来自角色资产清单,不得自由改色, 3D国风漫剧风格,统一渲染风格, 高清,8K,最佳质量 """.strip(), "expression_sheet": f""" {base_desc.get('visual_description', '中国古代修仙少年')} 表情表,网格布局,包含: 平静,微笑,大笑,生气,惊讶,悲伤, 每个表情单独一格,统一光照和背景, 3D国风漫剧风格, 高清,8K,最佳质量 """.strip(), } return prompts.get(view_type, prompts["front_half_body"]) def generate_all(self): """生成所有资产""" print(f"\n🎬 开始生成 {self.character_id} 主角资产包") print(f"=" * 60) results = {} # 1. 正面半身 path = self.generate_front_half_body() if path: results["front_half_body"] = path # 2. 侧脸 path = self.generate_side_face() if path: results["side_face"] = path # 3. 全身服装 path = self.generate_full_body_costume() if path: results["full_body_costume"] = path # 4. 表情表 path = self.generate_expression_sheet() if path: results["expression_sheet"] = path # 更新 manifest。只有四项资产都真实生成时才可进入 APPROVED。 self._update_manifest(results) print(f"\n✅ 资产包生成完成!") print(f" 已生成: {len(results)}/4 个资产") print(f" 位置: {self.approved_dir}") return results def _update_manifest(self, results): """更新 manifest.hdlp""" print(f"\n📝 更新 manifest.hdlp...") required_assets = {"front_half_body", "side_face", "full_body_costume", "expression_sheet"} is_complete = required_assets.issubset(results.keys()) approval_status = "APPROVED" if is_complete else "DRAFT_GENERATED" type_label = "已批准" if is_complete else "待质检" lock_note = ( "⊢ 资产已批准,可用于成片镜头。" if is_complete else "⊢ 资产包尚未完整生成/质检,禁止作为成片锁定资产。" ) manifest_content = f"""# 资产清单 · {self.character_id} > HLDP://video-ai-system/assets/characters/{self.character_id}/manifest > 类型: 角色资产 · {type_label} > 建立: D143 · 2026-06-23 > 更新: D144 · 2026-06-24 > 项目: 付费才能修仙 · EP01 --- ## 状态 ``` approval_status: {approval_status} asset_type: character canonical_id: {self.character_id} canonical_name: 苏白 ``` --- ## 批准资产 ``` front_half_body: {results.get("front_half_body", "NOT_GENERATED")} side_face: {results.get("side_face", "NOT_GENERATED")} full_body_costume: {results.get("full_body_costume", "NOT_GENERATED")} expression_sheet: {results.get("expression_sheet", "NOT_GENERATED")} ``` --- ## 视觉锁 ``` face_shape: TODO_FROM_APPROVED_ASSET hair_style: TODO_FROM_APPROVED_ASSET costume: TODO_FROM_APPROVED_ASSET age_band: 16岁 render_style: 3D国风漫剧 color_palette: TODO_FROM_APPROVED_ASSET ``` --- ## 锁定 {lock_note} ⊢ 禁止使用 candidates/ 或 rejected/ 中的图片作为最终资产。 """ with open(self.manifest_path, "w", encoding="utf-8") as f: f.write(manifest_content) print(f" ✅ 已更新: {self.manifest_path}") def main(): parser = argparse.ArgumentParser(description="CHAR-HERO-DESIGN-PACKER") parser.add_argument("--character", type=str, default="CHAR-003-SuBai", help="角色ID (默认: CHAR-003-SuBai)") parser.add_argument("--generate-all", action="store_true", help="生成所有资产") parser.add_argument("--view", type=str, choices=["front_half_body", "side_face", "full_body_costume", "expression_sheet"], help="生成特定视角的资产") args = parser.parse_args() packer = CharHeroDesignPacker(args.character) if args.generate_all: packer.generate_all() elif args.view: method_map = { "front_half_body": packer.generate_front_half_body, "side_face": packer.generate_side_face, "full_body_costume": packer.generate_full_body_costume, "expression_sheet": packer.generate_expression_sheet, } method_map[args.view]() else: print("请指定 --generate-all 或 --view ") parser.print_help() if __name__ == "__main__": main()