cang-ying/video-ai-system/tools/storyboard_to_workflow.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

221 lines
8.5 KiB
Python
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
"""
storyboard_to_workflow.py — 分镜JSON → ComfyUI批量出图桥接
读取分镜JSON为每个镜头生成ComfyUI API调用脚本
用法:
python tools/storyboard_to_workflow.py <分镜JSON> [-o 输出目录] [--workflow 基础工作流]
python tools/storyboard_to_workflow.py <分镜JSON> --generate-sh # 生成批量调用shell脚本
"""
import sys, os, json, argparse, shutil
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
# 默认的工作流模板路径ComfyUI蓝图
DEFAULT_WORKFLOW = os.path.expanduser("~/comfy/ComfyUI/blueprints/Text to Image.json")
def load_storyboard(json_path):
"""读取分镜JSON"""
with open(json_path, 'r', encoding='utf-8') as f:
return json.load(f)
def shot_to_prompt(shot, project_style=""):
"""将单镜转换为图像生成prompt"""
desc = shot.get('description', '')
camera = shot.get('camera', '中景')
characters = shot.get('characters', [])
scenes = shot.get('scenes', [])
props = shot.get('props', [])
# 构建英文prompt
parts = []
if camera:
parts.append(camera)
if scenes:
parts.append(f"in {scenes[0]}")
parts.append(desc[:80])
if project_style:
parts.append(project_style)
prompt = ", ".join(parts)
return prompt
def generate_comfyui_api_json(shot, base_workflow_path, prompt, output_dir, shot_num):
"""为单镜生成ComfyUI API调用JSON"""
# 读取基础工作流
try:
with open(base_workflow_path, 'r') as f:
workflow = json.load(f)
except:
# 如果文件不存在,创建一个最小工作流
workflow = {
"3": {
"class_type": "KSampler",
"inputs": {
"seed": shot_num * 100 + 42,
"steps": 20,
"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": 768, "batch_size": 1}},
"6": {"class_type": "CLIPTextEncode", "inputs": {"text": prompt, "clip": ["4", 1]}},
"7": {"class_type": "CLIPTextEncode", "inputs": {"text": "worst quality, low quality, blurry", "clip": ["4", 1]}},
"8": {"class_type": "VAEDecode", "inputs": {"samples": ["3", 0], "vae": ["4", 2]}},
"9": {"class_type": "SaveImage", "inputs": {"filename_prefix": f"shot_{shot_num:04d}", "images": ["8", 0]}}
}
return workflow
# 如果用了现有工作流替换prompt
for node_id, node in workflow.items():
if isinstance(node, dict):
ct = node.get('class_type', '')
if 'CLIPTextEncode' in ct or 'Prompt' in ct:
if 'text' in node.get('inputs', {}):
workflow[node_id]['inputs']['text'] = prompt
if 'KSampler' in ct or 'Sampler' in ct:
if 'seed' in node.get('inputs', {}):
workflow[node_id]['inputs']['seed'] = shot_num * 100 + 42
return workflow
def generate_shell_script(shots, output_dir, comfy_api_url="http://127.0.0.1:8188"):
"""生成批量调用ComfyUI API的shell脚本"""
script_path = os.path.join(output_dir, 'batch_render.sh')
lines = [
'#!/bin/bash',
f'# 批量渲染分镜 - 自动生成',
f'# ComfyUI API: {comfy_api_url}',
f'# 分镜数: {len(shots)}',
f'# 生成时间: {__import__("datetime").datetime.now().isoformat()}',
'',
'set -e',
'',
'TOTAL=' + str(len(shots)),
'SUCCESS=0',
'FAIL=0',
'',
]
for i, shot in enumerate(shots):
sn = shot.get('shot_number', f'S{i+1:02d}')
desc = shot.get('description', '')[:40]
wf_file = os.path.join(output_dir, f'workflow_{i+1:04d}.json')
out_file = os.path.join(output_dir, f'shot_{i+1:04d}.png')
lines.extend([
f'',
f'echo "🎬 [{i+1}/$TOTAL] {sn}: {desc}"',
f'echo " → 发送ComfyUI API..."',
f'RESP=$(curl -s -X POST "{comfy_api_url}/prompt" \\',
f' -H "Content-Type: application/json" \\',
f' -d @{wf_file})',
f'if echo "$RESP" | grep -q "error"; then',
f' echo " ❌ 失败: $RESP"',
f' FAIL=$((FAIL + 1))',
f'else',
f' echo " ✅ 已提交"',
f' SUCCESS=$((SUCCESS + 1))',
f'fi',
])
lines.extend([
'',
'echo "=========================="',
'echo "📊 渲染完成: $SUCCESS 成功, $FAIL 失败 / $TOTAL 总镜"',
])
with open(script_path, 'w') as f:
f.write('\n'.join(lines))
os.chmod(script_path, 0o755)
return script_path
def generate_prompt_file(shots, output_dir, project_style=""):
"""生成每个镜头的prompt文本文件"""
prompt_path = os.path.join(output_dir, 'prompts.txt')
with open(prompt_path, 'w', encoding='utf-8') as f:
for i, shot in enumerate(shots):
sn = shot.get('shot_number', f'S{i+1:02d}')
prompt = shot_to_prompt(shot, project_style)
f.write(f"=== {sn} ===\n")
f.write(f"景别: {shot.get('camera', 'N/A')}\n")
f.write(f"时长: {shot.get('duration', 'N/A')}s\n")
f.write(f"角色: {', '.join(shot.get('characters', []))}\n")
f.write(f"场景: {', '.join(shot.get('scenes', []))}\n")
f.write(f"Prompt: {prompt}\n\n")
return prompt_path
def main():
parser = argparse.ArgumentParser(description='分镜JSON → ComfyUI批量出图桥接')
parser.add_argument('storyboard', help='分镜JSON文件路径')
parser.add_argument('-o', '--output-dir', help='输出目录 (默认: 分镜JSON同目录下的renders/)')
parser.add_argument('--workflow', default=DEFAULT_WORKFLOW, help=f'基础工作流JSON (默认: {DEFAULT_WORKFLOW})')
parser.add_argument('--style', default='', help='全局风格描述 (如: cinematic, anime)')
parser.add_argument('--generate-sh', action='store_true', help='生成批量调用shell脚本')
parser.add_argument('--comfy-url', default='http://127.0.0.1:8188', help='ComfyUI API地址')
args = parser.parse_args()
# 读取分镜
storyboard = load_storyboard(args.storyboard)
shots = storyboard.get('shots', [])
if not shots:
print(f'❌ 分镜JSON中没有shots')
sys.exit(1)
# 输出目录
if args.output_dir:
out_dir = args.output_dir
else:
sb_dir = os.path.dirname(os.path.abspath(args.storyboard))
out_dir = os.path.join(sb_dir, 'renders')
os.makedirs(out_dir, exist_ok=True)
print(f'📖 分镜: {args.storyboard}')
print(f'🎬 共 {len(shots)}')
print(f'📁 输出: {out_dir}')
print()
# 1. 生成prompt文件
prompt_path = generate_prompt_file(shots, out_dir, args.style)
print(f'✅ Prompt文件: {prompt_path}')
# 2. 为每镜生成工作流JSON
wf_dir = os.path.join(out_dir, 'workflows')
os.makedirs(wf_dir, exist_ok=True)
for i, shot in enumerate(shots):
sn = shot.get('shot_number', f'S{i+1:02d}')
prompt = shot_to_prompt(shot, args.style)
workflow = generate_comfyui_api_json(shot, args.workflow, prompt, wf_dir, i+1)
wf_path = os.path.join(wf_dir, f'workflow_{i+1:04d}.json')
with open(wf_path, 'w', encoding='utf-8') as f:
json.dump(workflow, f, ensure_ascii=False, indent=2)
print(f'✅ 工作流JSON: {len(shots)}个 → {wf_dir}/')
# 3. 可选生成批量shell脚本
if args.generate_sh:
sh_path = generate_shell_script(shots, out_dir, args.comfy_url)
print(f'✅ 批量脚本: {sh_path}')
print(f' 运行: bash {sh_path}')
# 4. 总结
print()
print('📊 各镜一览:')
for i, shot in enumerate(shots):
sn = shot.get('shot_number', f'S{i+1:02d}')
prompt = shot_to_prompt(shot, args.style)
print(f' {sn:>4} | {shot.get("camera","?"): <4} | {shot.get("duration","?"):>2}s | {prompt[:50]}...')
if __name__ == '__main__':
main()