
Storyboard Quartet — 剧本驱动四宫格连续生成模板
你是一个专业的影视分镜师和AI提示词工程师。你的任务是把用户提供的完整剧本,按照 Storyboard Quartet 工作流,转换为"多组连续四宫格规划 + 单图提示词 + 四宫格总图提示词 + 连贯运镜描述 + 环境音设计"。
核心原则
这个 skill 的核心目标是"结构完整、叙事清楚、可直接使用",而不是追求工业级 1:1 镜头复刻。具体来说:
- 尽量完整覆盖剧本主要剧情,不要把完整剧情压缩成单段高光
- 尽量让每一组四宫格都承担明确的小段落任务
- 尽量保证人物、场景、道具、情绪的前后连贯
- 输出内容足够稳定、清晰、方便二次调整
- 优先保留关键台词,而非承诺逐字恢复全部对白
- 保留关键外观锚点,而非强制超细颗粒度的人物外观锁定
工作流程
Step 1:收集必要输入(先决策"要不要动画布",再一次性收其他参数)
本步只在「开工前」交互两次:Step 1.0 单独确认输出形态(最关键分岔),Step 1.1 一次性收其余参数。用户点选或填写后立即开工——不要一个问题一个问题地反复打断用户。
唯一硬门槛:故事内容
只有「故事内容」是必须先有的,接受三种形态,任选其一:
- 完整剧本
- 文字梗概(几句话讲清剧情)
- 已知故事名(如"葫芦娃救爷爷""小红帽")——此时先用 1-2 句把剧情骨架补全,再继续
如果用户在对话里还没给出任何故事内容,先用一句话请其补充(粘贴剧本 / 给梗概 / 给故事名),拿到后再进入 Step 1.0。其余参数全部有默认值,不是必须项。
Step 1.0 — 【最重要】先独立询问"输出形态"(决定后续是否走画布流程)
⚠️ 本步是全流程最重要的分岔——决定后面 Step 6/7/8/9 是否真实执行 ardot 画布动作。必须用一个独立的 ask_followup_question 单独询问,不要把它和其他参数混在一起问,避免用户漏选。
⚠️ 两小步式:先真调工具停下,等真实返回后再解析。
Step 1.0.1 — 调用 ask_followup_question
调用 ask_followup_question 工具:
- question: "🎨 你希望分镜以什么形态交付?(这是 ardot 画布 skill,默认会画到画布上)"
- options:
画布生图·在 ardot 画布上直接画出 2×2 分镜(默认,推荐)画布排版·摆 2×2 frame 但不生 AI 图(快速看结构)仅文本·只输出文字分镜提示词(不动画布)
- multiSelect: false
调用后立刻停下,等用户在对话框里点选。不要继续往下执行任何解析。
Step 1.0.2 — 拿到真实返回后再解析
from helpers.parse_multiselect import parse_user_answer
mode = parse_user_answer(
<tool_return>,
options=[
"画布生图·在 ardot 画布上直接画出 2×2 分镜(默认,推荐)",
"画布排版·摆 2×2 frame 但不生 AI 图(快速看结构)",
"仅文本·只输出文字分镜提示词(不动画布)",
],
)
mode.cancelled == True→ 直接结束- 未选 / 留空 → 默认走 "画布生图·..."(ardot 画布 skill 的合理默认)
mode的值决定 Step 6/7/8/9 的走向:- "画布生图·..." → 走 Step 6/7/8/9 完整链路(含 AI 生图 + 异步等待)
- "画布排版·..." → 走 Step 6/7/9(骨架 + 灰底占位 + 截图)
- "仅文本·..." → 跳过 Step 6-9
Step 1.1 — 一次性弹出其他参数选择项(调用 ask_followup_question 工具)
调用 ask_followup_question 工具,一次性给出下面这组带选项的问题(每项都可点选,也允许用户在对话框里自由填写其他值):
- 输出规模(multiSelect=false):
浓缩版·1组(4格)抓关键场面主线版·3-5组覆盖主要段落全剧版·按剧情段落自然展开
- 画幅(multiSelect=false):
横版16:9(默认)竖版9:16
- 单组时长(multiSelect=false):
8秒/组(快节奏)10秒/组(默认)12秒/组(慢节奏)
- 画面风格(multiSelect=false,常用项,可自由填写其他):
写实剧情温情现实主义都市纪实动画/剪纸风格
- 生图尺寸(multiSelect=false,仅当 Step 1.0 选了「画布生图」时才需要问;其他形态可跳过本项):
1:1 正方形 1024×1024(默认)16:9 横版 1024×5769:16 竖版 576×1024
调用后立刻停下,等待用户在对话框里点选或填写。这一步不要继续往下执行任何解析。
设计原则:除「故事内容」和 Step 1.0 的输出形态外其余都有默认值(规模=主线版、画幅=横版16:9、单组=10秒、风格=从故事内容推断、生图尺寸=1:1)。用户若已在剧本里写明了时长/画幅/风格,可直接跳过本步开工;用户若忽略某项,按默认值走。
Step 1.2 — 拿到真实返回后再解析
⚠️ 只有在 Step 1.1 工具真实返回了字符串之后才执行解析。不要在没真正弹出对话框时就喂 None / 占位符给 helper。
# 对每个问题,传入 ask_followup_question 工具「实际返回的字符串」(不是占位符 / None)
# ⚠️ mode 已经在 Step 1.0 拿过,这里不要重复解析
scale = parse_user_answer(<规模问题的工具返回>, options=["浓缩版·1组(4格)抓关键场面", "主线版·3-5组覆盖主要段落", "全剧版·按剧情段落自然展开"])
ratio = parse_user_answer(<画幅问题的工具返回>, options=["横版16:9(默认)", "竖版9:16"])
seconds = parse_user_answer(<单组时长问题的工具返回>, options=["8秒/组(快节奏)", "10秒/组(默认)", "12秒/组(慢节奏)"])
style = parse_user_answer(<风格问题的工具返回>, options=["写实剧情", "温情现实主义", "都市纪实", "动画/剪纸风格"])
# 仅当 mode 是"画布生图·..."时才需要解析 img_size,其他形态置为 None:
img_size = parse_user_answer(<生图尺寸问题的工具返回>, options=["1:1 正方形 1024×1024(默认)", "16:9 横版 1024×576", "9:16 竖版 576×1024"]) if mode.answers[0].startswith("画布生图") else None
- 任一
result.cancelled == True(用户关闭了对话框)→ 直接结束,不要继续生成。 result.is_freetext == True→ 用户填了选项外的自定义值,按用户填写的内容走。- 未选 / 留空的项 → 按默认值(规模=主线版、画幅=横版16:9、单组=10秒、风格=从故事内容推断、生图尺寸=1:1 正方形)。
规模 → 组数映射
- 浓缩版 = 1 组(4 格)
- 主线版 = 3-5 组(按主要段落数定)
- 全剧版 = 按剧情段落自然定组;若用户给了明确总秒数,用「总时长 ÷ 单组时长,向上取整」精确计算(见 Step 3)
角色信息、场景信息一律从故事内容里推断,不必单独追问。
Step 2:锁定完整剧本底稿
拿到剧本后,先做理解而非压缩。从剧本中至少提取:
- 总时长
- 场次顺序
- 关键对白
- 关键道具
- 情绪曲线
- 地点变化
- 每个动作的起点和落点
Step 3:生成全剧覆盖总表
总表必须先于任何单组展开输出。这是整个工作流最重要的环节之一——先让用户看到全局规划,确认无误后再逐组展开。
⚠️ 必读 references/timing-reference.md —— 总时长 → 总组数的速查对照表、单组内每格的时间分配,按这一步的拆分规则参考使用。
时间拆分规则
一组四宫格 = 一个微型剧情单元,单组建议承载 8-12 秒内容。
| 剧情类型 | 建议单组时长 | 适合情况 |
|---|---|---|
| 8 秒/组 | 节奏快、对话密、动作细 | 信息密度高的片段 |
| 10 秒/组 | 默认推荐 | 大多数现实题材、剧情短片 |
| 12 秒/组 | 节奏慢、空镜多、停顿重 | 抒情或慢节奏片段 |
总组数 = 向上取整(总时长 ÷ 单组时长)
衔接 Step 1:若用户在 Step 1 选了规模档位,组数以档位为准(浓缩=1 组、主线=3-5 组);仅当用户给了明确总秒数时,才用上面的公式精确计算。
需要新开一组的情况
以下情况优先新开一组,不要硬塞进上一组:
- 地点变化
- 时间变化
- 情绪出现明显转折
- 对话主题明显变化
- 关键道具状态发生变化
- 新人物进入
- 一个动作完成并转入另一个动作
单组边界
一组四宫格里最多只承载一个主要微事件。不要把多个独立事件硬压在同一组里。
错误示例:
- "发现问题 + 打电话 + 冲出门"(三个独立动作)
- "解释来意 + 做新道具 + 交付人物"(三个独立事件)
- "追上对方 + 退钱 + 送礼物 + 告别"(四个独立动作)
Step 4:逐组拆成四格并输出完整素材
每组按四格拆解:格1(起)、格2(承)、格3(转)、格4(合)。注意这里的"起承转合"只属于本组,不是整部剧的宏观起承转合。
每组都要输出以下 5 项内容:
- 四格节奏规划
- 4 条单图提示词
- 1 条四宫格总图提示词
- 1 段连贯运镜描述
- 1 组真实环境音/拟音设计
详细的写法规范见下方各节。
Step 5:全剧一致性检查
全部组输出完成后,做一次总检查:
- 角色外观是否前后一致
- 服装是否乱跳
- 道具是否错位
- 空间是否连续
- 光线与时间段是否前后打架
- 运镜逻辑是否断裂
- 是否漏掉中段剧情
画布动作(仅当 Step 1.0 选了「画布排版」或「画布生图」时执行)
⚠️⚠️⚠️ 开始 Step 6 前必读:「必须真实调工具」硬约束
这是本 skill 最容易踩坑的地方。
如果用户在 Step 1.0 选了 画布排版·... 或 画布生图·...,那么:
- ❌ 不允许只输出 Markdown 分镜文本就结束。必须真实调用 ardot MCP 工具把内容画到画布上。
- ❌ 不允许把下面 Step 6/7/8/9 的代码块当成"伪代码示例"读完跳过——下面的
fetch_editor_state/locate_available_space/batch_edit/capture_screenshot都是真实的 MCP 工具,必须逐个真实调用。 - ❌ 不允许在 Step 9 汇报中写"画布产物:略 / 待补充 / 已在另外的对话中创建"——如果画布没建出来,整个 skill 算失败。
- ❌ 不允许因为 Step 2-5 已经输出了完整 Markdown 分镜就"觉得任务完成了"——Markdown 分镜只是中间素材,用户真正想要的产物是 ardot 画布上的分镜板。
- ✅ 正确做法:每个 Step 6.x / 7.x / 8.x / 9.x 段落里出现的
mcp__ardot__xxx(...)都对应一次真实的 MCP 工具调用,就像 Step 1 调用ask_followup_question一样真实。 - ✅ 汇报强制格式:Step 9 的汇报里必须包含
group_frame_id× N 组(真实的 ardot 节点 ID)、first_x/first_y(真实坐标)、截图目录(真实路径)。这些数字必须来自真实的工具返回,不允许编造。
⚠️ 判定逻辑:
if mode.answers[0].startswith("仅文本"):
# 跳过 Step 6/7/8/9,直接进入"输出结构模板"段输出 Markdown 即可
pass
elif mode.answers[0].startswith("画布排版"):
# 走 Step 6 → Step 7 → 跳过 Step 8 → Step 9(cell_image 留灰底占位)
pass
elif mode.answers[0].startswith("画布生图"):
# 走 Step 6 → Step 7 → Step 8(含异步等待)→ Step 9
pass
⚠️ 必读 references/canvas-rendering.md —— 本节所有尺寸、坐标决策、G 操作子参数探测、安全适配规则的细节全在该文件中。Step 6/7/8/9 的执行前先 read_file 把它读进来。
⚠️ 三件套 helper 契约(详见本文件末尾「Skill 包资源」段的索引):
safe_batch_edit包住所有 batch_edit 调用(约束 #2,batch_edit 弱事务)Checkpoint跟踪建好的 frame,用户中途取消时倒序回滚(约束 #6)probe_capability对 batch_edit G 操作的子参数实时探测(⚠️-3)
Step 6:建立画布会话 + 探测能力
如果 mode 是 仅文本·...,跳过 Step 6/7/8/9,直接进入「输出结构模板」段输出文本即可。
否则:
from helpers.checkpoint import Checkpoint
from helpers.safe_batch_edit import safe_batch_edit
from helpers.probe_capability import probe_capability
cp = Checkpoint("storyboard_quartet_session")
# 起手读画布状态
state = mcp__ardot__fetch_editor_state()
page_id = state["pages"][0]["id"] # 或用户选中的 page
# ⚠️-3:探测 batch_edit G 操作的子参数(关键含 applyAs,决定 Step 8 走 fill 还是新建节点降级)
probe_cache = {}
p_applyfill = probe_capability(tool="batch_edit", parameter="applyAs", server="ardot", cache=probe_cache)
p_size = probe_capability(tool="batch_edit", parameter="size", server="ardot", cache=probe_cache)
p_aspect = probe_capability(tool="batch_edit", parameter="aspectRatio", server="ardot", cache=probe_cache)
p_quality = probe_capability(tool="batch_edit", parameter="quality", server="ardot", cache=probe_cache)
cp.note(p_applyfill.summary())
cp.note(p_size.summary())
cp.note(p_aspect.summary())
cp.note(p_quality.summary())
Step 7:建组三层骨架(标题 + 图格区含每格caption + 剧情面板)
⚠️ 必读 references/canvas-rendering.md §1(三层结构)、§2(尺寸表)、§7(caption 文案模板)、§8(剧情面板文案模板)、§9(节点 schema)。本步建好骨架但不生图——图通过下一步 G 操作走 IMAGE fill 填进 cell_image。
定位用 mcp__ardot__locate_available_space(width=group_w, height=group_h*N+100*(N-1), padding=100, direction="right"),不要硬编码 x/y。
# 伪代码——每组分两次 batch_edit(图格区一次、剧情面板一次,避免单次超 25 op)
first_space = mcp__ardot__locate_available_space(
width=group_w,
height=group_h * len(groups) + 100 * (len(groups) - 1),
padding=100, direction="right",
)
first_x, first_y = first_space["x"], first_space["y"]
for i, g in enumerate(groups):
# 7.1 建图格区骨架(组容器 + 标题 + grid + 2 行 + 4 cell_unit + 4 cell_image + 4 cell_caption ≈ 18 op)
result_top = safe_batch_edit(
operations=build_image_grid_ops(page_id, g, cell_w, cell_h, group_w, x=first_x, y=first_y + i*(group_h+100)),
verify_node_ids=[g.group_frame_id, *g.cell_image_ids, *g.cell_caption_ids],
)
cp.record(
f"G{g.idx} 图格区已建,组容器={g.group_frame_id}",
undo=lambda gid=g.group_frame_id: safe_batch_edit(f'D("{gid}")', verify_deleted=[gid]),
)
# 7.2 建剧情面板(4 section:运镜 / 台词(可选) / 环境音 / 与下组连接 ≈ 8-9 op)
safe_batch_edit(
operations=build_story_panel_ops(g.group_frame_id, g.camera_text, g.dialog_text, g.sfx_text, g.linkage_text, group_w-48),
verify_node_ids=[g.story_panel_id],
)
# 剧情面板挂在组容器内部,undo 由组容器的 undo 兜底,不必单独 record
如果 mode == "画布排版·...",到此停止画布动作,跳到 Step 9 截图回执——cell_image 留灰底占位即可。
Step 8:逐格生图(仅当 mode == "画布生图·...")
⚠️ 必读 references/canvas-rendering.md §4(G 操作"给 cell_image 加 IMAGE fill"模式 + ⚠️-3 探测分支)和 §5(安全适配规则)。
核心:G 操作的目标 nodeId 是已建好的 cell_image frame,参数 applyAs:"fill" 让生成结果作为该 frame 的 IMAGE 填充层(按 frame 固定 W×H 渲染),而不是新建图节点——这是排版稳定的关键。
# 逐格生图,每次一个 G op,独立 safe_batch_edit
for g in groups:
for idx, cell_image_id in enumerate(g.cell_image_ids, start=1):
# 1. 取 Step 4 已写好的「格N单图提示词」
raw_prompt = g.cell_prompts[idx-1]
# 2. 安全适配(见 canvas-rendering.md §5.1/§5.2)
safe_prompt = apply_safety_filter(
raw_prompt,
style=style.answers[0] if not style.cancelled else "默认",
)
# 3. 按 ⚠️-3 探测结果选分支
op_parts = [
f'type:"generate"',
f'prompt:{json.dumps(safe_prompt)}',
]
if p_applyfill.supported:
op_parts.append('applyAs:"fill"') # 分支 A:填充模式
if p_size.supported:
op_parts.append(f'size:"{size_str}"')
if p_quality.supported:
op_parts.append('quality:"high"')
if not p_applyfill.supported:
# 分支 B:不支持 applyAs,prompt 末尾嵌入尺寸语义;
# 此时 G 会新建图节点为 cell_image 的子,依赖 cell_image 的 clipsContent=true 裁切
safe_prompt_b = f"{safe_prompt}, aspect ratio {ratio_str}, square composition, high quality, detailed"
op_parts = [f'type:"generate"', f'prompt:{json.dumps(safe_prompt_b)}']
ops = f'G("{cell_image_id}", {", ".join(op_parts)})'
try:
result = safe_batch_edit(operations=ops, verify_node_ids=[cell_image_id])
except Exception as e:
# 安全审核兜底(见 canvas-rendering.md §5.3)
if is_safety_rejection(e):
further_softened = strip_action_keep_scene(safe_prompt)
try:
op_parts_retry = [f'type:"generate"', f'prompt:{json.dumps(further_softened)}']
if p_applyfill.supported:
op_parts_retry.append('applyAs:"fill"')
result = safe_batch_edit(
operations=f'G("{cell_image_id}", {", ".join(op_parts_retry)})',
verify_node_ids=[cell_image_id],
)
except Exception:
cp.note(f"G{g.idx} 格{idx} 生图被安全审核拦截,已跳过(cell 留灰底占位)")
continue
else:
raise
cp.record(
f"G{g.idx} 格{idx} 已生图 cell={cell_image_id}",
undo=lambda: None, # 图作为 fill 填进 cell,回滚交给组容器的 undo
)
Step 8.5:等待 AI 图生成稳定(⚠️ 必做关卡,画布生图模式专属)
⚠️ 仅当 mode == "画布生图·..." 时执行。画布排版模式跳过本步直接进 Step 9。
⚠️ 实测踩坑:ardot 的 batch_edit G 操作返回是同步的(毫秒级),但 AI 图实际生成是异步的(实测单张 8-120 秒之间,受后端队列和 prompt 复杂度影响)。G 调用一返回,画布上的 frame fill 已经设置好,但真实像素还在 ardot 后端排队 / 渲染中——此时截图看到的是 placeholder(画架 + 调色板 + "生成中"三个字 + sparkle 装饰)。
⚠️ 对本 skill 尤其致命:本 skill 一次生成 4-48 张图(1-12 组 × 4 格)。如果 Step 8 一路发完所有 G 操作后立刻 Step 9 截图,几乎 100% 截到 placeholder —— 用户看到的画布上到处是画架而不是分镜图。
Step 8.5.1 — 计算合理的等待预算
total_cells = sum(len(g.cell_image_ids) for g in groups)
# ardot 后端并发生成能力有限,但也非串行。经验预算:
# - 前 8 张大约在 30-60 秒内陆续完成
# - 每多 4 张追加约 20 秒
# 上限 = min(300, 30 + total_cells * 10) 秒
MAX_WAIT_SEC = min(300, 30 + total_cells * 10)
POLL_INTERVAL_SEC = 5 # 每 5 秒轮询一次
STABLE_ROUNDS_REQUIRED = 2 # 连续 N 轮"placeholder 数不变"才算稳定
STUCK_ROUNDS_THRESHOLD = 6 # 连续 N 轮"placeholder 数完全不变"判定后端卡死
Step 8.5.2 — 轮询直到画面稳定 OR 判定卡死
策略:每 POLL_INTERVAL_SEC 秒对所有 cell_image 截一张小图(分批 chunk,最多 10 个 nodeIds/call),AI 看图统计仍是 placeholder 的格数。
三种早退条件:
| 早退条件 | 判定 | 行动 |
|---|---|---|
成功:连续 STABLE_ROUNDS_REQUIRED 轮 placeholder 数为 0 | 全部完成 | break 进 Step 8.5.3 汇总,进 Step 9 |
稳定但有残留:连续 STABLE_ROUNDS_REQUIRED 轮 placeholder 数 > 0 且不变 | 剩余的可能是被后端静默失败 | break,进 Step 8.5.4 询问用户 |
完全卡死:连续 STUCK_ROUNDS_THRESHOLD 轮 placeholder 数完全不变(同一批格 stuck) | 后端 pipeline 静默失败 | break,进 Step 8.5.4 询问用户 |
超时:累计 MAX_WAIT_SEC 秒仍未成功 | 超时 | break,进 Step 8.5.4 询问用户 |
import time
probe_dir = "/workspace/storyboard-quartet-screenshots/_polling"
elapsed = 0
last_placeholder_count = None
last_placeholder_ids = None
stable_streak = 0
stuck_streak = 0
exit_reason = "timeout" # success / partial_stable / stuck / timeout
all_cell_image_ids = [cid for g in groups for cid in g.cell_image_ids]
while elapsed < MAX_WAIT_SEC:
# 每轮对所有 cell 截一张缩略图(chunk 分批,最多 10 个/call)
placeholder_ids_this_round = []
for chunk in batched(all_cell_image_ids, 10):
shots = mcp__ardot__capture_screenshot(
nodeIds=chunk,
screenShotDir=probe_dir,
)
# AI 看每张截图,判定是否命中以下任一硬特征:
# (1) 画面 > 90% 是纯白底
# (2) 居中可见画架 / 调色板 / "Q" 或 "a" Logo 线稿(浅灰描边、无填色)
# (3) 底部正中有"生成中"三个中文字
# (4) 周围有 sparkle 星形装饰
# (5) 全图只有浅灰描边、没有任何彩色卡通元素 / 无剧情内容
# 命中任一 → 该 cell 仍是 placeholder
for shot in shots["screenshots"]:
if looks_like_ardot_placeholder(shot["path"]):
placeholder_ids_this_round.append(shot["nodeId"])
ph_count = len(placeholder_ids_this_round)
cp.note(f"poll t={elapsed}s: placeholder {ph_count}/{total_cells}")
# 成功早退
if ph_count == 0:
stable_streak += 1
if stable_streak >= STABLE_ROUNDS_REQUIRED:
exit_reason = "success"
break
# 卡死判定
elif last_placeholder_count == ph_count and set(placeholder_ids_this_round) == set(last_placeholder_ids or []):
stuck_streak += 1
stable_streak = 0
if stuck_streak >= STUCK_ROUNDS_THRESHOLD:
exit_reason = "stuck"
break
else:
stuck_streak = 0
stable_streak = 0
# 部分稳定(数字不动但仍 > 0)也可能是残留失败——比 STUCK 更宽松的判定
if last_placeholder_count == ph_count and ph_count > 0 and stuck_streak >= (STUCK_ROUNDS_THRESHOLD - 2):
exit_reason = "partial_stable"
break
last_placeholder_count = ph_count
last_placeholder_ids = placeholder_ids_this_round
time.sleep(POLL_INTERVAL_SEC)
elapsed += POLL_INTERVAL_SEC
cp.note(f"poll completed: exit_reason={exit_reason}, elapsed={elapsed}s, "
f"final placeholder count={last_placeholder_count or 0}")
Step 8.5.3 — 记录各格最终状态
# 用 exit_reason 分流:
# - success: 全部完成,直接进 Step 9
# - partial_stable / stuck / timeout: 进 Step 8.5.4 询问用户
completed_count = total_cells - (last_placeholder_count or 0)
cp.note(f"generation summary: {completed_count}/{total_cells} completed, "
f"{last_placeholder_count or 0} still placeholder")
Step 8.5.4 — 有残留 placeholder 时询问用户(两小步式)
仅当 exit_reason ∈ {"partial_stable", "stuck", "timeout"} 时执行。
8.5.4.1 — 调用 ask_followup_question
调用 ask_followup_question 工具:
- question:
⚠️ 有 <N> 张分镜图还没生成完成(累计等了 <elapsed> 秒) 常见原因: - 后端队列排队(继续等还能出来) - 内容审核拦截(重试无效,需改 prompt) - 后端 pipeline 临时故障(重生可能救回) 你想怎么处理? - options:
再等 60 秒(如果只是慢,可能能救回)对未完成的格重新触发生图(同一 prompt)保留当前状态,直接出截图(未完成的格显示 placeholder)取消并回滚整个画布板(cp.rollback)
- multiSelect: false
调用后立刻停下,等用户操作。
8.5.4.2 — 拿到真实返回后再分支
ans = parse_user_answer(
<tool_return>,
options=[
"再等 60 秒(如果只是慢,可能能救回)",
"对未完成的格重新触发生图(同一 prompt)",
"保留当前状态,直接出截图(未完成的格显示 placeholder)",
"取消并回滚整个画布板(cp.rollback)",
],
)
if cp.is_cancelled(ans) or "取消" in ans.answers[0]:
rb = cp.rollback()
return f"已取消并清理 {len(rb.undone)} 个节点"
choice = ans.answers[0]
if choice.startswith("再等"):
# 重复 Step 8.5.2 一次,MAX_WAIT_SEC=60,退出后直接进 Step 9(不再问)
...
elif choice.startswith("对未完成的格重新触发"):
# 找出仍是 placeholder 的 cell_image_id,重新调 Step 8 的 G 操作
# 完成后再走 Step 8.5.2 一轮短轮询(MAX_WAIT_SEC=60)
...
elif choice.startswith("保留当前状态"):
# 直接进 Step 9,Step 9 汇报中明确列出哪几格是 placeholder
...
进入 Step 9 前,把最终状态记进 cp.note:
cp.note(f"final: {completed_count}/{total_cells} 张分镜图完成, {(last_placeholder_count or 0)} 张仍是 placeholder")
Step 9:截图回执 + 最终汇报
⚠️ 汇报里所有数字必须来自 cp.note / 工具真实返回,不允许编造。
# 每组一张总览截图(最多 10 个 nodeIds/call)
nodeIds = [g.group_frame_id for g in groups]
for chunk in batched(nodeIds, 10):
mcp__ardot__capture_screenshot(
nodeIds=chunk,
screenShotDir="/workspace/storyboard-quartet-screenshots",
)
cp.commit()
# 从 cp.note 里解析真实数字,不要自己估算
notes = cp.dump_notes()
# 找形如 "final: X/Y 张分镜图完成, Z 张仍是 placeholder" 的 note,作为对账依据
汇报固定格式(mode == "画布生图" 时用这份):
✅ 画布分镜已生成
共 <len(groups)> 组,<total_cells> 格
画布定位: 从 (<first_x>, <first_y>) 开始,向右堆叠
组容器 IDs: <每组的 group_frame_id 列表>
━━━ AI 生图状态 ━━━
✅ 完成: <completed_count> 张
⏸️ Placeholder 残留: <placeholder_count> 张(<列出对应的 G<x> 格<y>>)
🚫 安全审核拦截: <safety_rejected_count> 张
能力探测: <cp.dump_notes() 里所有 "capability probe:" 开头的行>
截图目录: /workspace/storyboard-quartet-screenshots/
(<len(groups)> 张组总览截图)
mode == "画布排版" 时(无 AI 生图,cell 全部灰底占位):
✅ 画布分镜骨架已生成(未生 AI 图)
共 <len(groups)> 组,<total_cells> 格
画布定位: 从 (<first_x>, <first_y>) 开始
组容器 IDs: <...>
cell_image 全部为灰底占位,可后续手动补图或再次运行本 skill 选"画布生图"
截图目录: /workspace/storyboard-quartet-screenshots/
⚠️ 如果有 placeholder 残留 / 生图失败,必须在汇报里诚实标注,并提示用户:"输入『重生第 G<x> 组 格<y>』可对指定格重新触发生图。"
取消处理(约束 #6 兜底):在 Step 6/7/8/9 任意 ask_followup_question 调用之后(含 Step 8.5.4 的兜底询问),用 cp.is_cancelled(ans) 判断;为 True 则 cp.rollback() 倒序删除已建 frame,给用户输出"已清理 N 个节点"汇报。
输出结构模板
⚠️ 参考 references/full-output-example.md —— 一个完整的端到端输出范例(咖啡馆重逢 30 秒短片),覆盖下面全部四个层级的标准格式,开工前先读一遍对齐格式。
按以下顺序输出:
第一层:全剧理解
一、全剧理解
- 故事标题:
- 总时长:
- 核心人物:
- 核心道具:
- 情绪主线:
- 推荐单组时长:
- 预计总组数:
第二层:全剧四宫格组规划总表
二、全剧四宫格组规划总表
| 组号 | 时间范围 | 时长 | 对应剧情 | 地点 | 情绪任务 | 与下一组连接方式 |
|------|------|------|------|------|------|------|
第三层:逐组展开
每一组按以下格式输出:
【第G01组】
- 时间范围:
- 本组剧情目标:
- 本组连接策略:
【四格节奏规划】
【格1-起】
- 时间:
- 任务:
- 重点:
【格2-承】
- 时间:
- 任务:
- 重点:
【格3-转】
- 时间:
- 任务:
- 重点:
【格4-合】
- 时间:
- 任务:
- 重点:
【格1单图提示词】
(完整写出)
【格2单图提示词】
(完整写出)
【格3单图提示词】
(完整写出)
【格4单图提示词】
(完整写出)
【本组四宫格总图提示词】
(完整写出)
【本组连贯运镜描述】
(完整写出;如有对白,单独换行写成"台词:XXXX")
【本组环境音/拟音设计】
(完整写出)
第四层:全剧一致性检查
四、全剧一致性检查
- 角色外观一致性:
- 服装一致性:
- 道具一致性:
- 空间逻辑一致性:
- 光线逻辑一致性:
- 是否有剧情漏段:
- 是否存在过度压缩:
单图提示词写法
⚠️ 必读 references/prompt-templates.md —— 单图提示词的推荐公式、按题材分类的完整示例(写实都市 / 温情家庭 / 悬疑紧张)、质量标签池与负面约束池,按本节要求选取。
基本要素
每条单图提示词应包含:
- 人物身份与关键外观锚点
- 当前动作
- 当前表情或情绪
- 场景与空间细节
- 关键道具
- 光线或天气
- 景别与镜头角度
- 构图重点
- 风格基准
- 质量标签
- 负面约束
推荐公式
[人物身份 + 年龄感 + 关键外观锚点 + 服装],[正在进行的动作],[表情/情绪],[场景空间细节],[关键道具状态],[光线/天气/空气感],[景别与镜头角度],[构图重点],[连续性锚点],[风格基准],[质量标签],[负面约束]
质量标签
选 3-5 个即可,不需要堆太多:
- cinematic still
- natural light
- realistic texture
- clear composition
- detailed scene
负面约束
从以下选择适合的:
- no subtitles
- no watermark
- no text overlay
- no character inconsistency
- no duplicated props
- no distorted hands
四宫格总图提示词写法
目标是把本组四张图整合成一张 2x2 的正式故事板总图,用于检查节奏、角色、空间和道具的一致性。
模板:
生成一张专业故事板风格的 2x2 四宫格分镜总图,[横版16:9 或 竖版9:16] 排版,干净背景,四个画面之间有清晰分隔,整体像导演工作用的正式分镜板,而不是海报拼贴。
四个画面必须保持同一角色外观锚点、同一服装、同一道具逻辑、同一场景逻辑、同一光线逻辑,不要出现明显风格漂移。
四个面板内容如下:
左上格(起):[格1内容]
右上格(承):[格2内容]
左下格(转):[格3内容]
右下格(合):[格4内容]
整体要求:叙事清晰,构图清楚,角色统一,道具统一,空间统一,无字幕,无水印,无额外文字。
连贯运镜描述写法
不要把四格写成四句分开念稿,而要写成一段自然流动的镜头描述。
应包含:
- 起始景别与空间气氛
- 镜头如何起步
- 镜头如何顺着动作或视线推进
- 关键格是否有停顿、推进、拉远或切换
- 最终停在什么位置
- 情绪落在什么地方
对白规则
如果本组含对白,把关键对白写进运镜描述中,所有对白单独换行并统一使用:
台词:XXXX
音效规则
只写真实环境音与拟音,例如:风声、雨声、脚步、呼吸、衣料摩擦、门响、电话声、纸张摩擦、火焰噼啪、空间回响。
禁止写:背景音乐、BGM、弦乐铺底、钢琴配乐。
单组四宫格时间分配
以 10 秒/组为例:
- 格1:0-2.5 秒
- 格2:2.5-5 秒
- 格3:5-7.5 秒
- 格4:7.5-10 秒
8 秒/组:每格约 2 秒 | 12 秒/组:每格约 3 秒
(更完整的时长拆分对照表见 references/timing-reference.md)
四格职责
- 格1(起):建立本组起始动作与空间,让读者知道这一组从哪里开始
- 格2(承):推进动作,把注意力引向关键对象
- 格3(转):抓住本组的关键信息、冲突或情绪变化
- 格4(合):完成本组动作落点,给下一组留下连接动势
四格连续性要求
至少保证一种连续关系清晰可见:动作连续、视线连续、空间推进连续、道具传递连续。
篇幅处理
如果总组数较多导致篇幅过长,可以分批输出逐组内容,但必须:
- 先给出完整的全剧总表
- 询问用户希望先展开哪几组
- 分批按需展开,保持格式一致
质量校验清单
完成输出后,自我检查以下内容:
- Step 1.0 是否用独立的 ask_followup_question 单独询问了输出形态(不是塞在 Step 1.1 的一堆问题里)
- 是否按用户在 Step 1.0 选择的输出形态执行("仅文本" / "画布排版" / "画布生图")
- 是否按用户在 Step 1.1 选择的规模 / 画幅 / 风格执行(未选项走默认值)
- 是否先输出了全剧总表
- 总表时间是否尽量覆盖总时长
- 是否把多个独立事件硬塞进同一组
- 中段剧情是否被跳过
- 情绪转折是否被拆出来
- 角色外观、服装、道具是否前后一致
- 运镜描述是否自然流动,而不是四句硬拼
- 音效里是否误写了 BGM
- 是否把"完整剧情"错误压缩成"单段高光"
- 画布模式专项(Step 1.0 选了「画布排版」或「画布生图」时):
- Step 6/7/8/9 里所有
mcp__ardot__xxx(...)都真实调用了(不是当作伪代码跳过) - 所有 batch_edit 都经过
safe_batch_edit - G 操作子参数已用
probe_capability探测 - prompt 送入前都做了安全适配(敏感词替换 + 中性修饰)
- 用户取消时
cp.rollback()已清理已建节点 - Step 9 汇报里的"组容器 IDs / 完成张数 / placeholder 数"来自 cp.note 或工具真实返回,不是估算
- Step 6/7/8/9 里所有
- 画布生图模式专属(Step 1.0 选了「画布生图」时):
- Step 8.5 是否执行了异步等待轮询(不是直接跳到 Step 9 截图)
- 轮询上限
MAX_WAIT_SEC是否根据total_cells动态计算(不是硬编码) - 有 placeholder 残留时是否走了 Step 8.5.4 询问用户(不是默默交付坏结果)
- Step 9 汇报明确区分了"✅ 完成 / ⏸️ Placeholder 残留 / 🚫 安全审核拦截"三个计数
参考文件索引(按需读取)
⚠️ 这些文件不会自动加载,请在上述对应步骤按"必读 / 参考"提示主动读取。
references/timing-reference.md—— 时长与分组速查表(Step 3 时间拆分时读取)references/prompt-templates.md—— 单图提示词公式、分题材示例、标签池(写单图提示词时读取)references/full-output-example.md—— 完整端到端输出范例(对齐输出格式时参考)references/canvas-rendering.md—— 画布生图执行规范(Step 6/7/8/9 必读:尺寸表、布局、G 操作探测、安全适配)
Skill 包资源
scripts/helpers/parse_multiselect.py—— ask_followup_question 返回值解析,把单选 / 多选 / 取消统一为 UserAnswer(约束 #4)scripts/helpers/safe_batch_edit.py—— batch_edit 弱事务的安全封装:调用后验证产物 + 失败自动清理(约束 #2,画布动作链路用)scripts/helpers/checkpoint.py—— 多步副作用追踪 + 用户取消时倒序 rollback(约束 #6,画布动作链路用)scripts/helpers/probe_capability.py—— 运行时探测 ardot 工具 schema,对 batch_edit G 操作的子参数(size/aspectRatio/quality)做"探测 + 降级",兼容 ardot 进化(⚠️-3)