cang-ying/video-ai-system/tools/run_storyboard.py
Zhuyuan Operations 80805449ca EED: 经验记忆推送——技能库(11技能+开源角色设定板)+遇事不决先搜索铁律+人物三/四视图+场景四视图工业级方法论+声画装配+多视图工具
- skill/ 技能库:index.json 11 技能 + community_techniques.md 社区技巧 + character_sheet_learn.md 角色版学习 + scene_4view_learn.md 场景四视图工业级方法 + character-sheet-generator 开源技能(7风格模板)
- tools/character_turnaround.py:人物三/四视图(主视觉+三视图/展示台)+场景四视角,Z-Image 本地
- tools/audio_pipeline.py:Edge-TTS配音+字幕+BGM+混音,Agent stage⑥有声成片
- agent_short_drama.py:一键短剧 Agent --until audio 全链路
- eed_web.py:技能库端点(/api/skills,/api/skill)+E2BIG根治(字节截断+巨兽降级)+一键短剧按钮
2026-08-01 03:16:48 +08:00

118 lines
4.3 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 引擎: 豆包 AI 拆解剧本 → 结构化分镜 JSON
# 通用版 — 支持任意剧本文件
# DEPENDS: lib/doubao_chat.py
import sys, os, json, argparse
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from lib.doubao_chat import breakdown_script
def extract_json(text):
"""从 AI 回复中提取 JSON 内容(兼容带 ```json 包裹的情况)"""
if '```json' in text:
return text.split('```json')[1].split('```')[0].strip()
if '```' in text:
return text.split('```')[1].split('```')[0].strip()
# 尝试直接解析
maybe = text.strip()
if maybe.startswith('{'):
return maybe
return maybe
def main():
parser = argparse.ArgumentParser(description='豆包AI → 拆解剧本为结构化分镜JSON')
parser.add_argument('script', help='剧本文件路径 (.hdlp 或 .txt)')
parser.add_argument('-e', '--episode', type=int, default=1, help='集号 (默认: 1)')
parser.add_argument('-o', '--output', help='输出路径 (默认: 自动推导)')
parser.add_argument('--pro', action='store_true', help='使用 Pro 模型 (doubao-pro, 更强更贵)')
parser.add_argument('--dry-run', action='store_true', help='只打印将发送的提示词不调API')
args = parser.parse_args()
# 读剧本
if not os.path.isfile(args.script):
print(f'❌ 找不到剧本文件: {args.script}')
sys.exit(1)
with open(args.script, 'r', encoding='utf-8') as f:
script_text = f.read()
print(f'📖 剧本: {args.script}')
print(f'📐 长度: {len(script_text)} 字符')
print(f'🎬 集号: EP{args.episode:02d}')
print(f'🧠 模型: {"doubao-pro" if args.pro else "doubao-lite"}')
print()
if args.dry_run:
print('🧪 DRY RUN — 将发送的提示词:')
print('=' * 60)
from lib.doubao_chat import breakdown_script as bd
print(bd.__doc__)
print('=' * 60)
return
# 调用豆包 API
print('🚀 发送豆包 AI 拆解剧本...')
result = breakdown_script(script_text, args.episode, model='pro' if args.pro else 'lite')
if 'error' in result:
print('❌ API ERROR:', json.dumps(result, ensure_ascii=False, indent=2))
sys.exit(1)
if 'content' not in result or not result['content']:
print('❌ API 返回空内容:', json.dumps(result, ensure_ascii=False, indent=2))
sys.exit(1)
content = result['content']
json_str = extract_json(content)
# 解析 JSON 验证有效性
try:
parsed = json.loads(json_str)
shot_count = len(parsed.get('shots', []))
except json.JSONDecodeError as e:
print(f'⚠️ JSON 解析失败: {e}')
print('原始内容:')
print(content[:500])
shot_count = 0
parsed = None
# 确定输出路径
if args.output:
out_path = args.output
else:
# 从剧本路径自动推导
script_dir = os.path.dirname(os.path.abspath(args.script))
script_base = os.path.splitext(os.path.basename(args.script))[0]
# 如果剧本文件名包含 SCRIPT 或 BREAKDOWN替换为 STORYBOARD
if 'SCRIPT' in script_base.upper() or 'BREAKDOWN' in script_base.upper():
storyboard_name = script_base.replace('SCRIPT', 'STORYBOARD').replace('BREAKDOWN', 'STORYBOARD')
else:
model_tag = 'PRO' if args.pro else 'AI'
storyboard_name = f'STORYBOARD-{model_tag}-EP{args.episode:02d}'
out_path = os.path.join(script_dir, f'{storyboard_name}.json')
# 保存
with open(out_path, 'w', encoding='utf-8') as f:
f.write(json_str if parsed else content.strip())
print(f'\n✅ 分镜已保存: {out_path}')
if parsed:
print(f'📊 共 {shot_count}')
print(f'💰 tokens: {result.get("tokens", "N/A")}')
print(f'⏱️ 完成原因: {result.get("finish", "N/A")}')
# 打印前3镜预览
if parsed and shot_count > 0:
print('\n📋 前3镜预览:')
for s in parsed['shots'][:3]:
print(f' {s.get("shot_number","?"):>4} | {s.get("camera","?"): <4} | {s.get("duration","?"):>2}s | {s.get("description","")[:40]}')
if __name__ == '__main__':
main()