cang-ying/video-ai-system/tools/generate_assets_local.py
Zhuyuan Operations ce32073d07 D205 面板功能大升级+本地漫剧管线+经验自动机制+飞书资料归档
- 面板: 会话持久化(oc_sess.json)/余额实时/文件栏(搜索+MD+拖拽)/朗读(edge-tts)/工具过程实时显示/漫剧画布
- 面板: 经验自动检索注入+存经验按钮(experience.py)
- 管线: local_motion本地运镜/IMAGE-FIRST-GUIDE/LOCAL-PIPELINE-V1
- 经验: EED-EXPER-014 面板工程+管线+飞书扒取全记录
- 资料: 飞书《清欢AIGC伪真人短剧全流程》归档
2026-08-03 22:55:02 +08:00

244 lines
9.5 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
"""
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()