cang-ying/video-ai-system/tools/generate_assets_local.py

244 lines
9.5 KiB
Python
Raw Normal View History

#!/usr/bin/env python3
"""
generate_assets_local.py 本地ComfyUI批量生成角色/场景/道具资产图
替代线上Seedream API¥0.15-1/ 本地ComfyUI¥0电费
用法:
python tools/generate_assets_local.py <项目目录> [--comfy-url URL]
python tools/generate_assets_local.py <项目目录> --list # 列出待生成的资产
"""
import sys, os, json, argparse, subprocess
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
# 默认资产生成prompt模板
ASSET_PROMPTS = {
"CHAR": {
"template": "{name}, {description}, {style}, full body character design, character sheet, consistent clothing, clean background, high quality, detailed",
"default_style": "digital art, anime style, vibrant colors"
},
"ENV": {
"template": "{name}, {description}, {style}, wide angle view, environmental design, detailed scene, high quality",
"default_style": "digital painting, cinematic lighting, detailed environment"
},
"PROP": {
"template": "{name}, {description}, {style}, isolated object, clean background, detailed, high quality",
"default_style": "product photography, sharp focus, detailed texture"
}
}
def generate_workflow(prompt, asset_type, asset_name, output_prefix="asset"):
"""生成ComfyUI API调用JSON"""
workflow = {
"3": {
"class_type": "KSampler",
"inputs": {
"seed": abs(hash(asset_name)) % 1000000,
"steps": 25,
"cfg": 7,
"sampler_name": "euler",
"scheduler": "normal",
"denoise": 1,
"model": ["4", 0],
"positive": ["6", 0],
"negative": ["7", 0],
"latent_image": ["5", 0]
}
},
"4": {
"class_type": "CheckpointLoaderSimple",
"inputs": {"ckpt_name": "realvisxlV40_v40BDFPonNoobVae.safetensors"}
},
"5": {
"class_type": "EmptyLatentImage",
"inputs": {"width": 1024, "height": 1024, "batch_size": 1}
},
"6": {
"class_type": "CLIPTextEncode",
"inputs": {"text": prompt, "clip": ["4", 1]}
},
"7": {
"class_type": "CLIPTextEncode",
"inputs": {"text": "worst quality, low quality, blurry, deformed", "clip": ["4", 1]}
},
"8": {
"class_type": "VAEDecode",
"inputs": {"samples": ["3", 0], "vae": ["4", 2]}
},
"9": {
"class_type": "SaveImage",
"inputs": {"filename_prefix": f"{output_prefix}_{asset_name}", "images": ["8", 0]}
}
}
return workflow
def load_asset_list(project_dir):
"""加载项目资产清单"""
# 检查是否有 ASSET-LIST 文件
for pattern in ['ASSET-LIST*', 'assets*.json', 'CHARACTERS*']:
import glob
matches = glob.glob(os.path.join(project_dir, pattern))
if matches:
with open(matches[0], 'r', encoding='utf-8') as f:
return json.load(f)
# 检查项目protocols目录
protocols_dir = os.path.join(project_dir, 'protocols')
if os.path.isdir(protocols_dir):
for fname in os.listdir(protocols_dir):
if 'ASSET' in fname.upper() or 'CHAR' in fname.upper():
with open(os.path.join(protocols_dir, fname), 'r', encoding='utf-8') as f:
content = f.read()
try:
return json.loads(content)
except:
pass
return None
def scan_project_assets(project_dir):
"""扫描项目目录,识别需要生成的资产"""
assets = []
# 检查 characters/ envs/ props/ 目录
for asset_type, subdir in [('CHAR', 'characters'), ('ENV', 'envs'), ('PROP', 'props')]:
asset_dir = os.path.join(project_dir, 'assets', subdir)
if os.path.isdir(asset_dir):
for f in os.listdir(asset_dir):
if f.endswith(('.json', '.hdlp', '.txt')):
with open(os.path.join(asset_dir, f), 'r', encoding='utf-8') as fh:
content = fh.read()
assets.append({
'type': asset_type,
'name': os.path.splitext(f)[0],
'description': content[:200],
'file': os.path.join(asset_dir, f)
})
# 检查分镜JSON中出现的角色/场景
for root, dirs, files in os.walk(project_dir):
for f in files:
if f.startswith('STORYBOARD') and f.endswith('.json'):
try:
with open(os.path.join(root, f), 'r', encoding='utf-8') as fh:
sb = json.load(fh)
for shot in sb.get('shots', []):
for char in shot.get('characters', []):
if not any(a['name'] == char for a in assets):
assets.append({
'type': 'CHAR',
'name': char,
'description': f'角色 {char}',
'source': f
})
for scene in shot.get('scenes', []):
if not any(a['name'] == scene for a in assets):
assets.append({
'type': 'ENV',
'name': scene,
'description': f'场景 {scene}',
'source': f
})
except:
pass
return assets
def main():
parser = argparse.ArgumentParser(description='本地ComfyUI批量生成资产图')
parser.add_argument('project_dir', help='项目目录')
parser.add_argument('--comfy-url', default='http://127.0.0.1:8188', help='ComfyUI API地址')
parser.add_argument('--style', default='', help='全局风格')
parser.add_argument('--list', action='store_true', help='只列出待生成资产')
parser.add_argument('--output-dir', help='资产输出目录 (默认: project_dir/assets/)')
parser.add_argument('--dry-run', action='store_true', help='只生成workflow JSON不调API')
args = parser.parse_args()
if not os.path.isdir(args.project_dir):
print(f'❌ 项目目录不存在: {args.project_dir}')
sys.exit(1)
# 扫描资产
assets = scan_project_assets(args.project_dir)
if not assets:
print('⚠️ 未发现需要生成的资产')
print(' 提示: 将角色描述放在 assets/characters/ 目录下')
print(' 或: 先运行 storyboard_to_workflow.py 生成分镜后再扫描')
sys.exit(0)
# 去重
seen = set()
unique_assets = []
for a in assets:
key = f"{a['type']}-{a['name']}"
if key not in seen:
seen.add(key)
unique_assets.append(a)
assets = unique_assets
print(f'📦 发现 {len(assets)} 个待生成资产:')
for a in assets:
print(f' [{a["type"]}] {a["name"]:20s} {a.get("description", "")[:40]}')
if args.list:
return
# 输出目录
out_dir = args.output_dir or os.path.join(args.project_dir, 'assets')
assets_char_dir = os.path.join(out_dir, 'characters')
assets_env_dir = os.path.join(out_dir, 'envs')
assets_prop_dir = os.path.join(out_dir, 'props')
for d in [assets_char_dir, assets_env_dir, assets_prop_dir]:
os.makedirs(d, exist_ok=True)
# 生成并调用
for i, asset in enumerate(assets):
atype = asset['type']
name = asset['name']
desc = asset.get('description', name)
style = args.style or ASSET_PROMPTS.get(atype, {}).get('default_style', '')
template = ASSET_PROMPTS.get(atype, {}).get('template', '{description}, {style}')
prompt = template.format(name=name, description=desc, style=style)
print(f'\n🎨 [{i+1}/{len(assets)}] {name} ({atype})')
print(f' Prompt: {prompt[:80]}...')
workflow = generate_workflow(prompt, atype, name)
# 确定输出位置
dir_map = {'CHAR': assets_char_dir, 'ENV': assets_env_dir, 'PROP': assets_prop_dir}
asset_out_dir = dir_map.get(atype, out_dir)
wf_path = os.path.join(asset_out_dir, f'_{name}_workflow.json')
with open(wf_path, 'w', encoding='utf-8') as f:
json.dump(workflow, f, ensure_ascii=False, indent=2)
if args.dry_run:
print(f' ✅ 工作流JSON: {wf_path}')
continue
# 调ComfyUI API
print(f' 📤 发送ComfyUI...')
try:
resp = subprocess.run([
'curl', '-s', '-X', 'POST',
f'{args.comfy_url}/prompt',
'-H', 'Content-Type: application/json',
'-d', json.dumps({"prompt": workflow})
], capture_output=True, text=True, timeout=30)
result = json.loads(resp.stdout) if resp.stdout else {}
if 'error' in result:
print(f' ❌ API错误: {result["error"]}')
else:
print(f' ✅ 已提交 (task: {result.get("task_id", "?")})')
except Exception as e:
print(f' ⚠️ 调用失败: {e}')
print(f'\n✅ 完成! 资产将保存到: {out_dir}')
if __name__ == '__main__':
main()