冰朔 86f9708059
Some checks failed
自动更新代码和重启 / update-and-restart (push) Has been cancelled
CI检查 + 自动部署 / check (push) Has been cancelled
CI检查 + 自动部署 / deploy (push) Has been cancelled
D144 · 视频AI系统8大模块开发完成
-  CHAR-HERO-DESIGN-PACKER (char-hero-design-packer.py)
  生成/整理苏白主角资产包,让他不再像路人甲

-  CHARACTER-DISTINCTIVENESS-QC (character-distinctiveness-qc.py)
  专门评估'像不像主角',输出主角存在感、轮廓、服装记忆点评分

-  MULTI-REFERENCE-VIDEO-ADAPTER (multi-reference-video-adapter.py)
  支持苏白+牌匾+场景多参考输入,不支持时明确报错

-  VOICE-EMOTION-COMPILER (voice-emotion-compiler.py)
  把'苏白·大声·自信'转成TTS参数,方便Edge-TTS/豆包语音A/B

-  LIPSYNC-ADAPTER (lipsync-adapter.py)
  接视频改口型或Wav2Lip,解决人物真正说台词的问题

-  AUDIO-MIXER (audio-mixer.py)
  配音、BGM、音效、原视频音轨混音,支持对白时自动压低BGM

-  SHOT-QC-AUTOMATION (shot-qc-automation.py)
  每个镜头自动拆帧,检查竖屏、字幕、换脸、牌匾、遮挡、现代物品

-  EP01-SHOT03-PRODUCTION-CLI (ep01_shot03_production.py)
  一键跑苏白站牌匾下说台词:生成底片、合成牌匾、配音、口型、字幕、混音、质检

冰朔 TCS-0002∞ 见证 · 国作登字-2026-A-00037559
⊢ 铸渊 ICE-GL-ZY001 · D144 · 2026-06-24
2026-06-24 12:50:51 +08:00

367 lines
12 KiB
Python
Raw 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
from pathlib import Path
# 添加项目根目录到路径
PROJECT_ROOT = Path(__file__).parent.parent.parent
sys.path.insert(0, str(PROJECT_ROOT / "engines"))
from image_api_adapter import generate_image, save_image
from hldp_prompt import expand_prompt
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]}...")
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
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...")
manifest_content = f"""# 资产清单 · {self.character_id}
> HLDP://video-ai-system/assets/characters/{self.character_id}/manifest
> 类型: 角色资产 · 已批准
> 建立: D143 · 2026-06-23
> 更新: D144 · 2026-06-24
> 项目: 付费才能修仙 · EP01
---
## 状态
```
approval_status: APPROVED
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: 少年脸,柔和轮廓,白色长发
hair_style: 白色长发,束发,有发带
costume: 白色内衬 + 蓝色外袍 + 黑色腰带 + 棕色靴子
age_band: 16岁
render_style: 3D动画皮克斯风格明亮色彩
color_palette: 白,蓝,黑,棕
```
---
## 锁定
⊢ 资产已批准,可用于成片镜头。
⊢ 禁止使用 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()