cang-ying/engines/char-hero-design-packer/char-hero-design-packer.py
Guanghu Domestic Migration a2fa7d57d8 chore: import sanitized domestic snapshot for REPO-005
Source snapshot: 23037fa3a2e9b0597162735755e92fc763bf4d94
2026-07-17 15:55:48 +08:00

395 lines
13 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/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()