cang-ying/engines/char-hero-design-packer/char-hero-design-packer.py

395 lines
13 KiB
Python
Raw Normal View History

#!/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 <view_type>")
parser.print_help()
if __name__ == "__main__":
main()