SHOT_1 extracts (run even if shot doesn't exist; returns sentinel) ----
id: shot1_img_prompt label: "镜头1图提示" label_en: "Shot 1 image prompt" kind: llm_chat depends_on: [final_script] with: system: "Return one line of text. No quotes, no prefix, no commentary." task: | If the script contains a "=== SHOT_1 ===" block: output exactly the value after "IMAGE_PROMPT:" inside that block. Single line, no quotes, no label. If it does NOT (because N_SHOTS < 1): output exactly the literal sentinel: SHOT_ABSENT
Script:
{{ outputs.final_script | truncate(8000) }}
id: shot1_vid_prompt label: "镜头1视频提示" label_en: "Shot 1 video prompt" kind: llm_chat depends_on: [final_script] with: system: "Return one line of text. No quotes, no prefix, no commentary." task: | If the script contains a "=== SHOT_1 ===" block: output exactly the value after "VIDEO_PROMPT:" inside that block. Single line. If it does NOT: output exactly: SHOT_ABSENT
Script:
{{ outputs.final_script | truncate(8000) }}
id: shot1_duration label: "镜头1时长" label_en: "Shot 1 duration" kind: llm_chat depends_on: [final_script] with: system: "Return exactly one integer or the literal SHOT_ABSENT. No commentary." task: | If the script contains a "=== SHOT_1 ===" block: output exactly the integer after "DURATION_S:" inside that block, clamped to [3, 15]. Digits only, no units. If it does NOT: output exactly: SHOT_ABSENT
Script:
{{ outputs.final_script | truncate(8000) }}
id: shot2_img_prompt label: "镜头2图提示" label_en: "Shot 2 image prompt" kind: llm_chat depends_on: [final_script] with: system: "Return one line of text. No quotes, no prefix, no commentary." task: | If the script contains a "=== SHOT_2 ===" block: output exactly the value after "IMAGE_PROMPT:" inside that block. Single line, no quotes, no label. If it does NOT (because N_SHOTS < 2): output exactly the literal sentinel: SHOT_ABSENT
Script:
{{ outputs.final_script | truncate(8000) }}
id: shot2_vid_prompt label: "镜头2视频提示" label_en: "Shot 2 video prompt" kind: llm_chat depends_on: [final_script] with: system: "Return one line of text. No quotes, no prefix, no commentary." task: | If the script contains a "=== SHOT_2 ===" block: output exactly the value after "VIDEO_PROMPT:" inside that block. Single line. If it does NOT: output exactly: SHOT_ABSENT
Script:
{{ outputs.final_script | truncate(8000) }}
id: shot2_duration label: "镜头2时长" label_en: "Shot 2 duration" kind: llm_chat depends_on: [final_script] with: system: "Return exactly one integer or the literal SHOT_ABSENT. No commentary." task: | If the script contains a "=== SHOT_2 ===" block: output exactly the integer after "DURATION_S:" inside that block, clamped to [3, 15]. Digits only, no units. If it does NOT: output exactly: SHOT_ABSENT
Script:
{{ outputs.final_script | truncate(8000) }}
id: shot3_img_prompt label: "镜头3图提示" label_en: "Shot 3 image prompt" kind: llm_chat depends_on: [final_script] with: system: "Return one line of text. No quotes, no prefix, no commentary." task: | If the script contains a "=== SHOT_3 ===" block: output exactly the value after "IMAGE_PROMPT:" inside that block. Single line, no quotes, no label. If it does NOT (because N_SHOTS < 3): output exactly the literal sentinel: SHOT_ABSENT
Script:
{{ outputs.final_script | truncate(8000) }}
id: shot3_vid_prompt label: "镜头3视频提示" label_en: "Shot 3 video prompt" kind: llm_chat depends_on: [final_script] with: system: "Return one line of text. No quotes, no prefix, no commentary." task: | If the script contains a "=== SHOT_3 ===" block: output exactly the value after "VIDEO_PROMPT:" inside that block. Single line. If it does NOT: output exactly: SHOT_ABSENT
Script:
{{ outputs.final_script | truncate(8000) }}
id: shot3_duration label: "镜头3时长" label_en: "Shot 3 duration" kind: llm_chat depends_on: [final_script] with: system: "Return exactly one integer or the literal SHOT_ABSENT. No commentary." task: | If the script contains a "=== SHOT_3 ===" block: output exactly the integer after "DURATION_S:" inside that block, clamped to [3, 15]. Digits only, no units. If it does NOT: output exactly: SHOT_ABSENT
Script:
{{ outputs.final_script | truncate(8000) }}
id: shot4_img_prompt label: "镜头4图提示" label_en: "Shot 4 image prompt" kind: llm_chat depends_on: [final_script] with: system: "Return one line of text. No quotes, no prefix, no commentary." task: | If the script contains a "=== SHOT_4 ===" block: output exactly the value after "IMAGE_PROMPT:" inside that block. Single line, no quotes, no label. If it does NOT (because N_SHOTS < 4): output exactly the literal sentinel: SHOT_ABSENT
Script:
{{ outputs.final_script | truncate(8000) }}
id: shot4_vid_prompt label: "镜头4视频提示" label_en: "Shot 4 video prompt" kind: llm_chat depends_on: [final_script] with: system: "Return one line of text. No quotes, no prefix, no commentary." task: | If the script contains a "=== SHOT_4 ===" block: output exactly the value after "VIDEO_PROMPT:" inside that block. Single line. If it does NOT: output exactly: SHOT_ABSENT
Script:
{{ outputs.final_script | truncate(8000) }}
id: shot4_duration label: "镜头4时长" label_en: "Shot 4 duration" kind: llm_chat depends_on: [final_script] with: system: "Return exactly one integer or the literal SHOT_ABSENT. No commentary." task: | If the script contains a "=== SHOT_4 ===" block: output exactly the integer after "DURATION_S:" inside that block, clamped to [3, 15]. Digits only, no units. If it does NOT: output exactly: SHOT_ABSENT
Script:
{{ outputs.final_script | truncate(8000) }}
id: shot5_img_prompt label: "镜头5图提示" label_en: "Shot 5 image prompt" kind: llm_chat depends_on: [final_script] with: system: "Return one line of text. No quotes, no prefix, no commentary." task: | If the script contains a "=== SHOT_5 ===" block: output exactly the value after "IMAGE_PROMPT:" inside that block. Single line, no quotes, no label. If it does NOT (because N_SHOTS < 5): output exactly the literal sentinel: SHOT_ABSENT
Script:
{{ outputs.final_script | truncate(8000) }}
id: shot5_vid_prompt label: "镜头5视频提示" label_en: "Shot 5 video prompt" kind: llm_chat depends_on: [final_script] with: system: "Return one line of text. No quotes, no prefix, no commentary." task: | If the script contains a "=== SHOT_5 ===" block: output exactly the value after "VIDEO_PROMPT:" inside that block. Single line. If it does NOT: output exactly: SHOT_ABSENT
Script:
{{ outputs.final_script | truncate(8000) }}
id: shot5_duration label: "镜头5时长" label_en: "Shot 5 duration" kind: llm_chat depends_on: [final_script] with: system: "Return exactly one integer or the literal SHOT_ABSENT. No commentary." task: | If the script contains a "=== SHOT_5 ===" block: output exactly the integer after "DURATION_S:" inside that block, clamped to [3, 15]. Digits only, no units. If it does NOT: output exactly: SHOT_ABSENT
Script:
{{ outputs.final_script | truncate(8000) }}
id: shot6_img_prompt label: "镜头6图提示" label_en: "Shot 6 image prompt" kind: llm_chat depends_on: [final_script] with: system: "Return one line of text. No quotes, no prefix, no commentary." task: | If the script contains a "=== SHOT_6 ===" block: output exactly the value after "IMAGE_PROMPT:" inside that block. Single line, no quotes, no label. If it does NOT (because N_SHOTS < 6): output exactly the literal sentinel: SHOT_ABSENT
Script:
{{ outputs.final_script | truncate(8000) }}
id: shot6_vid_prompt label: "镜头6视频提示" label_en: "Shot 6 video prompt" kind: llm_chat depends_on: [final_script] with: system: "Return one line of text. No quotes, no prefix, no commentary." task: | If the script contains a "=== SHOT_6 ===" block: output exactly the value after "VIDEO_PROMPT:" inside that block. Single line. If it does NOT: output exactly: SHOT_ABSENT
Script:
{{ outputs.final_script | truncate(8000) }}
id: shot6_duration label: "镜头6时长" label_en: "Shot 6 duration" kind: llm_chat depends_on: [final_script] with: system: "Return exactly one integer or the literal SHOT_ABSENT. No commentary." task: | If the script contains a "=== SHOT_6 ===" block: output exactly the integer after "DURATION_S:" inside that block, clamped to [3, 15]. Digits only, no units. If it does NOT: output exactly: SHOT_ABSENT
Script:
{{ outputs.final_script | truncate(8000) }}
id: shot7_img_prompt label: "镜头7图提示" label_en: "Shot 7 image prompt" kind: llm_chat depends_on: [final_script] with: system: "Return one line of text. No quotes, no prefix, no commentary." task: | If the script contains a "=== SHOT_7 ===" block: output exactly the value after "IMAGE_PROMPT:" inside that block. Single line, no quotes, no label. If it does NOT (because N_SHOTS < 7): output exactly the literal sentinel: SHOT_ABSENT
Script:
{{ outputs.final_script | truncate(8000) }}
id: shot7_vid_prompt label: "镜头7视频提示" label_en: "Shot 7 video prompt" kind: llm_chat depends_on: [final_script] with: system: "Return one line of text. No quotes, no prefix, no commentary." task: | If the script contains a "=== SHOT_7 ===" block: output exactly the value after "VIDEO_PROMPT:" inside that block. Single line. If it does NOT: output exactly: SHOT_ABSENT
Script:
{{ outputs.final_script | truncate(8000) }}
id: shot7_duration label: "镜头7时长" label_en: "Shot 7 duration" kind: llm_chat depends_on: [final_script] with: system: "Return exactly one integer or the literal SHOT_ABSENT. No commentary." task: | If the script contains a "=== SHOT_7 ===" block: output exactly the integer after "DURATION_S:" inside that block, clamped to [3, 15]. Digits only, no units. If it does NOT: output exactly: SHOT_ABSENT
Script:
{{ outputs.final_script | truncate(8000) }}
id: shot8_img_prompt label: "镜头8图提示" label_en: "Shot 8 image prompt" kind: llm_chat depends_on: [final_script] with: system: "Return one line of text. No quotes, no prefix, no commentary." task: | If the script contains a "=== SHOT_8 ===" block: output exactly the value after "IMAGE_PROMPT:" inside that block. Single line, no quotes, no label. If it does NOT (because N_SHOTS < 8): output exactly the literal sentinel: SHOT_ABSENT
Script:
{{ outputs.final_script | truncate(8000) }}
id: shot8_vid_prompt label: "镜头8视频提示" label_en: "Shot 8 video prompt" kind: llm_chat depends_on: [final_script] with: system: "Return one line of text. No quotes, no prefix, no commentary." task: | If the script contains a "=== SHOT_8 ===" block: output exactly the value after "VIDEO_PROMPT:" inside that block. Single line. If it does NOT: output exactly: SHOT_ABSENT
Script:
{{ outputs.final_script | truncate(8000) }}
id: shot8_duration label: "镜头8时长" label_en: "Shot 8 duration" kind: llm_chat depends_on: [final_script] with: system: "Return exactly one integer or the literal SHOT_ABSENT. No commentary." task: | If the script contains a "=== SHOT_8 ===" block: output exactly the integer after "DURATION_S:" inside that block, clamped to [3, 15]. Digits only, no units. If it does NOT: output exactly: SHOT_ABSENT
Script:
{{ outputs.final_script | truncate(8000) }}
id: shot9_img_prompt label: "镜头9图提示" label_en: "Shot 9 image prompt" kind: llm_chat depends_on: [final_script] with: system: "Return one line of text. No quotes, no prefix, no commentary." task: | If the script contains a "=== SHOT_9 ===" block: output exactly the value after "IMAGE_PROMPT:" inside that block. Single line, no quotes, no label. If it does NOT (because N_SHOTS < 9): output exactly the literal sentinel: SHOT_ABSENT
Script:
{{ outputs.final_script | truncate(8000) }}
id: shot9_vid_prompt label: "镜头9视频提示" label_en: "Shot 9 video prompt" kind: llm_chat depends_on: [final_script] with: system: "Return one line of text. No quotes, no prefix, no commentary." task: | If the script contains a "=== SHOT_9 ===" block: output exactly the value after "VIDEO_PROMPT:" inside that block. Single line. If it does NOT: output exactly: SHOT_ABSENT
Script:
{{ outputs.final_script | truncate(8000) }}
id: shot9_duration label: "镜头9时长" label_en: "Shot 9 duration" kind: llm_chat depends_on: [final_script] with: system: "Return exactly one integer or the literal SHOT_ABSENT. No commentary." task: | If the script contains a "=== SHOT_9 ===" block: output exactly the integer after "DURATION_S:" inside that block, clamped to [3, 15]. Digits only, no units. If it does NOT: output exactly: SHOT_ABSENT
Script:
{{ outputs.final_script | truncate(8000) }}
id: shot10_img_prompt label: "镜头10图提示" label_en: "Shot 10 image prompt" kind: llm_chat depends_on: [final_script] with: system: "Return one line of text. No quotes, no prefix, no commentary." task: | If the script contains a "=== SHOT_10 ===" block: output exactly the value after "IMAGE_PROMPT:" inside that block. Single line, no quotes, no label. If it does NOT (because N_SHOTS < 10): output exactly the literal sentinel: SHOT_ABSENT
Script:
{{ outputs.final_script | truncate(8000) }}
id: shot10_vid_prompt label: "镜头10视频提示" label_en: "Shot 10 video prompt" kind: llm_chat depends_on: [final_script] with: system: "Return one line of text. No quotes, no prefix, no commentary." task: | If the script contains a "=== SHOT_10 ===" block: output exactly the value after "VIDEO_PROMPT:" inside that block. Single line. If it does NOT: output exactly: SHOT_ABSENT
Script:
{{ outputs.final_script | truncate(8000) }}
id: shot10_duration label: "镜头10时长" label_en: "Shot 10 duration" kind: llm_chat depends_on: [final_script] with: system: "Return exactly one integer or the literal SHOT_ABSENT. No commentary." task: | If the script contains a "=== SHOT_10 ===" block: output exactly the integer after "DURATION_S:" inside that block, clamped to [3, 15]. Digits only, no units. If it does NOT: output exactly: SHOT_ABSENT
Script:
{{ outputs.final_script | truncate(8000) }}
id: shot1_image label: "镜头1图像" label_en: "Shot 1 image" kind: skill_exec skill: nano-banana-pro depends_on: [shot1_img_prompt, review_normalize] when: "'DECISION: proceed' in outputs.review_normalize and 'SHOT_ABSENT' not in outputs.shot1_img_prompt" with: prompt: "{{ outputs.shot1_img_prompt | truncate(800) }}" filename: "{{ inputs.workspace_dir }}/meta_short_drama/{{ inputs.user_message | slugify | truncate(40) }}/1_shot.png" aspect_ratio: "9:16" image_size: "1K" max_retries: 1 fallback_model: "google/gemini-3-pro-image-preview" placeholder_on_fail: "yes"
id: shot1_video label: "镜头1视频" label_en: "Shot 1 video" kind: skill_exec skill: seedance-2-prompt depends_on: [shot1_vid_prompt, shot1_duration, reference_image, shot1_image, review_normalize] when: "'DECISION: proceed' in outputs.review_normalize and 'SHOT_ABSENT' not in outputs.shot1_vid_prompt" on_failure: shot1_video_fallback with:
prompt: "Mode: All-Reference. Assets Mapping: reference[1] is the full-cast identity anchor (USE strictly for character likeness, faces, hair, skin tone, outfits, and accessories — keep these byte-identical to the reference across cuts). reference[2] is THIS shot's scene composition reference (USE for camera angle, framing, character blocking, prop placement, and background layout). Shot directive: {{ outputs.shot1_vid_prompt | truncate(700) }}" filename: "{{ inputs.workspace_dir }}/meta_short_drama/{{ inputs.user_message | slugify | truncate(40) }}/1_shot.mp4" input_image: "" input_reference: "{{ inputs.workspace_dir }}/meta_short_drama/{{ inputs.user_message | slugify | truncate(40) }}/reference.png" input_reference_2: "{{ inputs.workspace_dir }}/meta_short_drama/{{ inputs.user_message | slugify | truncate(40) }}/1_shot.png" aspect_ratio: "9:16"
| int(5) parses the duration extract as an integer, fallingduration: "{{ outputs.shot1_duration | int(5) }}" model: "bytedance/seedance-2.0" max_retries: 2
id: shot1_video_fallback label: "镜头1视频兜底" label_en: "Shot 1 video fallback" kind: skill_exec skill: video-still-animator with: input_image: "{{ inputs.workspace_dir }}/meta_short_drama/{{ inputs.user_message | slugify | truncate(40) }}/1_shot.png" output_path: "{{ inputs.workspace_dir }}/meta_short_drama/{{ inputs.user_message | slugify | truncate(40) }}/1_shot.mp4" duration: "{{ outputs.shot1_duration | int(5) }}" width: 720 height: 1280 fps: 24
id: shot2_image label: "镜头2图像" label_en: "Shot 2 image" kind: skill_exec skill: nano-banana-pro depends_on: [shot2_img_prompt, review_normalize] when: "'DECISION: proceed' in outputs.review_normalize and 'SHOT_ABSENT' not in outputs.shot2_img_prompt" with: prompt: "{{ outputs.shot2_img_prompt | truncate(800) }}" filename: "{{ inputs.workspace_dir }}/meta_short_drama/{{ inputs.user_message | slugify | truncate(40) }}/2_shot.png" aspect_ratio: "9:16" image_size: "1K" max_retries: 1 fallback_model: "google/gemini-3-pro-image-preview" placeholder_on_fail: "yes"
id: shot2_video label: "镜头2视频" label_en: "Shot 2 video" kind: skill_exec skill: seedance-2-prompt depends_on: [shot2_vid_prompt, shot2_duration, reference_image, shot2_image, review_normalize] when: "'DECISION: proceed' in outputs.review_normalize and 'SHOT_ABSENT' not in outputs.shot2_vid_prompt" on_failure: shot2_video_fallback with:
prompt: "Mode: All-Reference. Assets Mapping: reference[1] is the full-cast identity anchor (USE strictly for character likeness, faces, hair, skin tone, outfits, and accessories — keep these byte-identical to the reference across cuts). reference[2] is THIS shot's scene composition reference (USE for camera angle, framing, character blocking, prop placement, and background layout). Shot directive: {{ outputs.shot2_vid_prompt | truncate(700) }}" filename: "{{ inputs.workspace_dir }}/meta_short_drama/{{ inputs.user_message | slugify | truncate(40) }}/2_shot.mp4" input_image: "" input_reference: "{{ inputs.workspace_dir }}/meta_short_drama/{{ inputs.user_message | slugify | truncate(40) }}/reference.png" input_reference_2: "{{ inputs.workspace_dir }}/meta_short_drama/{{ inputs.user_message | slugify | truncate(40) }}/2_shot.png" aspect_ratio: "9:16"
| int(5) parses the duration extract as an integer, fallingduration: "{{ outputs.shot2_duration | int(5) }}" model: "bytedance/seedance-2.0" max_retries: 2
id: shot2_video_fallback label: "镜头2视频兜底" label_en: "Shot 2 video fallback" kind: skill_exec skill: video-still-animator with: input_image: "{{ inputs.workspace_dir }}/meta_short_drama/{{ inputs.user_message | slugify | truncate(40) }}/2_shot.png" output_path: "{{ inputs.workspace_dir }}/meta_short_drama/{{ inputs.user_message | slugify | truncate(40) }}/2_shot.mp4" duration: "{{ outputs.shot2_duration | int(5) }}" width: 720 height: 1280 fps: 24
id: shot3_image label: "镜头3图像" label_en: "Shot 3 image" kind: skill_exec skill: nano-banana-pro depends_on: [shot3_img_prompt, review_normalize] when: "'DECISION: proceed' in outputs.review_normalize and 'SHOT_ABSENT' not in outputs.shot3_img_prompt" with: prompt: "{{ outputs.shot3_img_prompt | truncate(800) }}" filename: "{{ inputs.workspace_dir }}/meta_short_drama/{{ inputs.user_message | slugify | truncate(40) }}/3_shot.png" aspect_ratio: "9:16" image_size: "1K" max_retries: 1 fallback_model: "google/gemini-3-pro-image-preview" placeholder_on_fail: "yes"
id: shot3_video label: "镜头3视频" label_en: "Shot 3 video" kind: skill_exec skill: seedance-2-prompt depends_on: [shot3_vid_prompt, shot3_duration, reference_image, shot3_image, review_normalize] when: "'DECISION: proceed' in outputs.review_normalize and 'SHOT_ABSENT' not in outputs.shot3_vid_prompt" on_failure: shot3_video_fallback with:
prompt: "Mode: All-Reference. Assets Mapping: reference[1] is the full-cast identity anchor (USE strictly for character likeness, faces, hair, skin tone, outfits, and accessories — keep these byte-identical to the reference across cuts). reference[2] is THIS shot's scene composition reference (USE for camera angle, framing, character blocking, prop placement, and background layout). Shot directive: {{ outputs.shot3_vid_prompt | truncate(700) }}" filename: "{{ inputs.workspace_dir }}/meta_short_drama/{{ inputs.user_message | slugify | truncate(40) }}/3_shot.mp4" input_image: "" input_reference: "{{ inputs.workspace_dir }}/meta_short_drama/{{ inputs.user_message | slugify | truncate(40) }}/reference.png" input_reference_2: "{{ inputs.workspace_dir }}/meta_short_drama/{{ inputs.user_message | slugify | truncate(40) }}/3_shot.png" aspect_ratio: "9:16"
| int(5) parses the duration extract as an integer, fallingduration: "{{ outputs.shot3_duration | int(5) }}" model: "bytedance/seedance-2.0" max_retries: 2
id: shot3_video_fallback label: "镜头3视频兜底" label_en: "Shot 3 video fallback" kind: skill_exec skill: video-still-animator with: input_image: "{{ inputs.workspace_dir }}/meta_short_drama/{{ inputs.user_message | slugify | truncate(40) }}/3_shot.png" output_path: "{{ inputs.workspace_dir }}/meta_short_drama/{{ inputs.user_message | slugify | truncate(40) }}/3_shot.mp4" duration: "{{ outputs.shot3_duration | int(5) }}" width: 720 height: 1280 fps: 24
id: shot4_image label: "镜头4图像" label_en: "Shot 4 image" kind: skill_exec skill: nano-banana-pro depends_on: [shot4_img_prompt, review_normalize] when: "'DECISION: proceed' in outputs.review_normalize and 'SHOT_ABSENT' not in outputs.shot4_img_prompt" with: prompt: "{{ outputs.shot4_img_prompt | truncate(800) }}" filename: "{{ inputs.workspace_dir }}/meta_short_drama/{{ inputs.user_message | slugify | truncate(40) }}/4_shot.png" aspect_ratio: "9:16" image_size: "1K" max_retries: 1 fallback_model: "google/gemini-3-pro-image-preview" placeholder_on_fail: "yes"
id: shot4_video label: "镜头4视频" label_en: "Shot 4 video" kind: skill_exec skill: seedance-2-prompt depends_on: [shot4_vid_prompt, shot4_duration, reference_image, shot4_image, review_normalize] when: "'DECISION: proceed' in outputs.review_normalize and 'SHOT_ABSENT' not in outputs.shot4_vid_prompt" on_failure: shot4_video_fallback with:
prompt: "Mode: All-Reference. Assets Mapping: reference[1] is the full-cast identity anchor (USE strictly for character likeness, faces, hair, skin tone, outfits, and accessories — keep these byte-identical to the reference across cuts). reference[2] is THIS shot's scene composition reference (USE for camera angle, framing, character blocking, prop placement, and background layout). Shot directive: {{ outputs.shot4_vid_prompt | truncate(700) }}" filename: "{{ inputs.workspace_dir }}/meta_short_drama/{{ inputs.user_message | slugify | truncate(40) }}/4_shot.mp4" input_image: "" input_reference: "{{ inputs.workspace_dir }}/meta_short_drama/{{ inputs.user_message | slugify | truncate(40) }}/reference.png" input_reference_2: "{{ inputs.workspace_dir }}/meta_short_drama/{{ inputs.user_message | slugify | truncate(40) }}/4_shot.png" aspect_ratio: "9:16"
| int(5) parses the duration extract as an integer, fallingduration: "{{ outputs.shot4_duration | int(5) }}" model: "bytedance/seedance-2.0" max_retries: 2
id: shot4_video_fallback label: "镜头4视频兜底" label_en: "Shot 4 video fallback" kind: skill_exec skill: video-still-animator with: input_image: "{{ inputs.workspace_dir }}/meta_short_drama/{{ inputs.user_message | slugify | truncate(40) }}/4_shot.png" output_path: "{{ inputs.workspace_dir }}/meta_short_drama/{{ inputs.user_message | slugify | truncate(40) }}/4_shot.mp4" duration: "{{ outputs.shot4_duration | int(5) }}" width: 720 height: 1280 fps: 24
id: shot5_image label: "镜头5图像" label_en: "Shot 5 image" kind: skill_exec skill: nano-banana-pro depends_on: [shot5_img_prompt, review_normalize] when: "'DECISION: proceed' in outputs.review_normalize and 'SHOT_ABSENT' not in outputs.shot5_img_prompt" with: prompt: "{{ outputs.shot5_img_prompt | truncate(800) }}" filename: "{{ inputs.workspace_dir }}/meta_short_drama/{{ inputs.user_message | slugify | truncate(40) }}/5_shot.png" aspect_ratio: "9:16" image_size: "1K" max_retries: 1 fallback_model: "google/gemini-3-pro-image-preview" placeholder_on_fail: "yes"
id: shot5_video label: "镜头5视频" label_en: "Shot 5 video" kind: skill_exec skill: seedance-2-prompt depends_on: [shot5_vid_prompt, shot5_duration, reference_image, shot5_image, review_normalize] when: "'DECISION: proceed' in outputs.review_normalize and 'SHOT_ABSENT' not in outputs.shot5_vid_prompt" on_failure: shot5_video_fallback with:
prompt: "Mode: All-Reference. Assets Mapping: reference[1] is the full-cast identity anchor (USE strictly for character likeness, faces, hair, skin tone, outfits, and accessories — keep these byte-identical to the reference across cuts). reference[2] is THIS shot's scene composition reference (USE for camera angle, framing, character blocking, prop placement, and background layout). Shot directive: {{ outputs.shot5_vid_prompt | truncate(700) }}" filename: "{{ inputs.workspace_dir }}/meta_short_drama/{{ inputs.user_message | slugify | truncate(40) }}/5_shot.mp4" input_image: "" input_reference: "{{ inputs.workspace_dir }}/meta_short_drama/{{ inputs.user_message | slugify | truncate(40) }}/reference.png" input_reference_2: "{{ inputs.workspace_dir }}/meta_short_drama/{{ inputs.user_message | slugify | truncate(40) }}/5_shot.png" aspect_ratio: "9:16"
| int(5) parses the duration extract as an integer, fallingduration: "{{ outputs.shot5_duration | int(5) }}" model: "bytedance/seedance-2.0" max_retries: 2
id: shot5_video_fallback label: "镜头5视频兜底" label_en: "Shot 5 video fallback" kind: skill_exec skill: video-still-animator with: input_image: "{{ inputs.workspace_dir }}/meta_short_drama/{{ inputs.user_message | slugify | truncate(40) }}/5_shot.png" output_path: "{{ inputs.workspace_dir }}/meta_short_drama/{{ inputs.user_message | slugify | truncate(40) }}/5_shot.mp4" duration: "{{ outputs.shot5_duration | int(5) }}" width: 720 height: 1280 fps: 24
id: shot6_image label: "镜头6图像" label_en: "Shot 6 image" kind: skill_exec skill: nano-banana-pro depends_on: [shot6_img_prompt, review_normalize] when: "'DECISION: proceed' in outputs.review_normalize and 'SHOT_ABSENT' not in outputs.shot6_img_prompt" with: prompt: "{{ outputs.shot6_img_prompt | truncate(800) }}" filename: "{{ inputs.workspace_dir }}/meta_short_drama/{{ inputs.user_message | slugify | truncate(40) }}/6_shot.png" aspect_ratio: "9:16" image_size: "1K" max_retries: 1 fallback_model: "google/gemini-3-pro-image-preview" placeholder_on_fail: "yes"
id: shot6_video label: "镜头6视频" label_en: "Shot 6 video" kind: skill_exec skill: seedance-2-prompt depends_on: [shot6_vid_prompt, shot6_duration, reference_image, shot6_image, review_normalize] when: "'DECISION: proceed' in outputs.review_normalize and 'SHOT_ABSENT' not in outputs.shot6_vid_prompt" on_failure: shot6_video_fallback with:
prompt: "Mode: All-Reference. Assets Mapping: reference[1] is the full-cast identity anchor (USE strictly for character likeness, faces, hair, skin tone, outfits, and accessories — keep these byte-identical to the reference across cuts). reference[2] is THIS shot's scene composition reference (USE for camera angle, framing, character blocking, prop placement, and background layout). Shot directive: {{ outputs.shot6_vid_prompt | truncate(700) }}" filename: "{{ inputs.workspace_dir }}/meta_short_drama/{{ inputs.user_message | slugify | truncate(40) }}/6_shot.mp4" input_image: "" input_reference: "{{ inputs.workspace_dir }}/meta_short_drama/{{ inputs.user_message | slugify | truncate(40) }}/reference.png" input_reference_2: "{{ inputs.workspace_dir }}/meta_short_drama/{{ inputs.user_message | slugify | truncate(40) }}/6_shot.png" aspect_ratio: "9:16"
| int(5) parses the duration extract as an integer, fallingduration: "{{ outputs.shot6_duration | int(5) }}" model: "bytedance/seedance-2.0" max_retries: 2
id: shot6_video_fallback label: "镜头6视频兜底" label_en: "Shot 6 video fallback" kind: skill_exec skill: video-still-animator with: input_image: "{{ inputs.workspace_dir }}/meta_short_drama/{{ inputs.user_message | slugify | truncate(40) }}/6_shot.png" output_path: "{{ inputs.workspace_dir }}/meta_short_drama/{{ inputs.user_message | slugify | truncate(40) }}/6_shot.mp4" duration: "{{ outputs.shot6_duration | int(5) }}" width: 720 height: 1280 fps: 24
id: shot7_image label: "镜头7图像" label_en: "Shot 7 image" kind: skill_exec skill: nano-banana-pro depends_on: [shot7_img_prompt, review_normalize] when: "'DECISION: proceed' in outputs.review_normalize and 'SHOT_ABSENT' not in outputs.shot7_img_prompt" with: prompt: "{{ outputs.shot7_img_prompt | truncate(800) }}" filename: "{{ inputs.workspace_dir }}/meta_short_drama/{{ inputs.user_message | slugify | truncate(40) }}/7_shot.png" aspect_ratio: "9:16" image_size: "1K" max_retries: 1 fallback_model: "google/gemini-3-pro-image-preview" placeholder_on_fail: "yes"
id: shot7_video label: "镜头7视频" label_en: "Shot 7 video" kind: skill_exec skill: seedance-2-prompt depends_on: [shot7_vid_prompt, shot7_duration, reference_image, shot7_image, review_normalize] when: "'DECISION: proceed' in outputs.review_normalize and 'SHOT_ABSENT' not in outputs.shot7_vid_prompt" on_failure: shot7_video_fallback with:
prompt: "Mode: All-Reference. Assets Mapping: reference[1] is the full-cast identity anchor (USE strictly for character likeness, faces, hair, skin tone, outfits, and accessories — keep these byte-identical to the reference across cuts). reference[2] is THIS shot's scene composition reference (USE for camera angle, framing, character blocking, prop placement, and background layout). Shot directive: {{ outputs.shot7_vid_prompt | truncate(700) }}" filename: "{{ inputs.workspace_dir }}/meta_short_drama/{{ inputs.user_message | slugify | truncate(40) }}/7_shot.mp4" input_image: "" input_reference: "{{ inputs.workspace_dir }}/meta_short_drama/{{ inputs.user_message | slugify | truncate(40) }}/reference.png" input_reference_2: "{{ inputs.workspace_dir }}/meta_short_drama/{{ inputs.user_message | slugify | truncate(40) }}/7_shot.png" aspect_ratio: "9:16"
| int(5) parses the duration extract as an integer, fallingduration: "{{ outputs.shot7_duration | int(5) }}" model: "bytedance/seedance-2.0" max_retries: 2
id: shot7_video_fallback label: "镜头7视频兜底" label_en: "Shot 7 video fallback" kind: skill_exec skill: video-still-animator with: input_image: "{{ inputs.workspace_dir }}/meta_short_drama/{{ inputs.user_message | slugify | truncate(40) }}/7_shot.png" output_path: "{{ inputs.workspace_dir }}/meta_short_drama/{{ inputs.user_message | slugify | truncate(40) }}/7_shot.mp4" duration: "{{ outputs.shot7_duration | int(5) }}" width: 720 height: 1280 fps: 24
id: shot8_image label: "镜头8图像" label_en: "Shot 8 image" kind: skill_exec skill: nano-banana-pro depends_on: [shot8_img_prompt, review_normalize] when: "'DECISION: proceed' in outputs.review_normalize and 'SHOT_ABSENT' not in outputs.shot8_img_prompt" with: prompt: "{{ outputs.shot8_img_prompt | truncate(800) }}" filename: "{{ inputs.workspace_dir }}/meta_short_drama/{{ inputs.user_message | slugify | truncate(40) }}/8_shot.png" aspect_ratio: "9:16" image_size: "1K" max_retries: 1 fallback_model: "google/gemini-3-pro-image-preview" placeholder_on_fail: "yes"
id: shot8_video label: "镜头8视频" label_en: "Shot 8 video" kind: skill_exec skill: seedance-2-prompt depends_on: [shot8_vid_prompt, shot8_duration, reference_image, shot8_image, review_normalize] when: "'DECISION: proceed' in outputs.review_normalize and 'SHOT_ABSENT' not in outputs.shot8_vid_prompt" on_failure: shot8_video_fallback with:
prompt: "Mode: All-Reference. Assets Mapping: reference[1] is the full-cast identity anchor (USE strictly for character likeness, faces, hair, skin tone, outfits, and accessories — keep these byte-identical to the reference across cuts). reference[2] is THIS shot's scene composition reference (USE for camera angle, framing, character blocking, prop placement, and background layout). Shot directive: {{ outputs.shot8_vid_prompt | truncate(700) }}" filename: "{{ inputs.workspace_dir }}/meta_short_drama/{{ inputs.user_message | slugify | truncate(40) }}/8_shot.mp4" input_image: "" input_reference: "{{ inputs.workspace_dir }}/meta_short_drama/{{ inputs.user_message | slugify | truncate(40) }}/reference.png" input_reference_2: "{{ inputs.workspace_dir }}/meta_short_drama/{{ inputs.user_message | slugify | truncate(40) }}/8_shot.png" aspect_ratio: "9:16"
| int(5) parses the duration extract as an integer, fallingduration: "{{ outputs.shot8_duration | int(5) }}" model: "bytedance/seedance-2.0" max_retries: 2
id: shot8_video_fallback label: "镜头8视频兜底" label_en: "Shot 8 video fallback" kind: skill_exec skill: video-still-animator with: input_image: "{{ inputs.workspace_dir }}/meta_short_drama/{{ inputs.user_message | slugify | truncate(40) }}/8_shot.png" output_path: "{{ inputs.workspace_dir }}/meta_short_drama/{{ inputs.user_message | slugify | truncate(40) }}/8_shot.mp4" duration: "{{ outputs.shot8_duration | int(5) }}" width: 720 height: 1280 fps: 24
id: shot9_image label: "镜头9图像" label_en: "Shot 9 image" kind: skill_exec skill: nano-banana-pro depends_on: [shot9_img_prompt, review_normalize] when: "'DECISION: proceed' in outputs.review_normalize and 'SHOT_ABSENT' not in outputs.shot9_img_prompt" with: prompt: "{{ outputs.shot9_img_prompt | truncate(800) }}" filename: "{{ inputs.workspace_dir }}/meta_short_drama/{{ inputs.user_message | slugify | truncate(40) }}/9_shot.png" aspect_ratio: "9:16" image_size: "1K" max_retries: 1 fallback_model: "google/gemini-3-pro-image-preview" placeholder_on_fail: "yes"
id: shot9_video label: "镜头9视频" label_en: "Shot 9 video" kind: skill_exec skill: seedance-2-prompt depends_on: [shot9_vid_prompt, shot9_duration, reference_image, shot9_image, review_normalize] when: "'DECISION: proceed' in outputs.review_normalize and 'SHOT_ABSENT' not in outputs.shot9_vid_prompt" on_failure: shot9_video_fallback with:
prompt: "Mode: All-Reference. Assets Mapping: reference[1] is the full-cast identity anchor (USE strictly for character likeness, faces, hair, skin tone, outfits, and accessories — keep these byte-identical to the reference across cuts). reference[2] is THIS shot's scene composition reference (USE for camera angle, framing, character blocking, prop placement, and background layout). Shot directive: {{ outputs.shot9_vid_prompt | truncate(700) }}" filename: "{{ inputs.workspace_dir }}/meta_short_drama/{{ inputs.user_message | slugify | truncate(40) }}/9_shot.mp4" input_image: "" input_reference: "{{ inputs.workspace_dir }}/meta_short_drama/{{ inputs.user_message | slugify | truncate(40) }}/reference.png" input_reference_2: "{{ inputs.workspace_dir }}/meta_short_drama/{{ inputs.user_message | slugify | truncate(40) }}/9_shot.png" aspect_ratio: "9:16"
| int(5) parses the duration extract as an integer, fallingduration: "{{ outputs.shot9_duration | int(5) }}" model: "bytedance/seedance-2.0" max_retries: 2
id: shot9_video_fallback label: "镜头9视频兜底" label_en: "Shot 9 video fallback" kind: skill_exec skill: video-still-animator with: input_image: "{{ inputs.workspace_dir }}/meta_short_drama/{{ inputs.user_message | slugify | truncate(40) }}/9_shot.png" output_path: "{{ inputs.workspace_dir }}/meta_short_drama/{{ inputs.user_message | slugify | truncate(40) }}/9_shot.mp4" duration: "{{ outputs.shot9_duration | int(5) }}" width: 720 height: 1280 fps: 24
id: shot10_image label: "镜头10图像" label_en: "Shot 10 image" kind: skill_exec skill: nano-banana-pro depends_on: [shot10_img_prompt, review_normalize] when: "'DECISION: proceed' in outputs.review_normalize and 'SHOT_ABSENT' not in outputs.shot10_img_prompt" with: prompt: "{{ outputs.shot10_img_prompt | truncate(800) }}" filename: "{{ inputs.workspace_dir }}/meta_short_drama/{{ inputs.user_message | slugify | truncate(40) }}/10_shot.png" aspect_ratio: "9:16" image_size: "1K" max_retries: 1 fallback_model: "google/gemini-3-pro-image-preview" placeholder_on_fail: "yes"
id: shot10_video label: "镜头10视频" label_en: "Shot 10 video" kind: skill_exec skill: seedance-2-prompt depends_on: [shot10_vid_prompt, shot10_duration, reference_image, shot10_image, review_normalize] when: "'DECISION: proceed' in outputs.review_normalize and 'SHOT_ABSENT' not in outputs.shot10_vid_prompt" on_failure: shot10_video_fallback with:
prompt: "Mode: All-Reference. Assets Mapping: reference[1] is the full-cast identity anchor (USE strictly for character likeness, faces, hair, skin tone, outfits, and accessories — keep these byte-identical to the reference across cuts). reference[2] is THIS shot's scene composition reference (USE for camera angle, framing, character blocking, prop placement, and background layout). Shot directive: {{ outputs.shot10_vid_prompt | truncate(700) }}" filename: "{{ inputs.workspace_dir }}/meta_short_drama/{{ inputs.user_message | slugify | truncate(40) }}/10_shot.mp4" input_image: "" input_reference: "{{ inputs.workspace_dir }}/meta_short_drama/{{ inputs.user_message | slugify | truncate(40) }}/reference.png" input_reference_2: "{{ inputs.workspace_dir }}/meta_short_drama/{{ inputs.user_message | slugify | truncate(40) }}/10_shot.png" aspect_ratio: "9:16"
| int(5) parses the duration extract as an integer, fallingduration: "{{ outputs.shot10_duration | int(5) }}" model: "bytedance/seedance-2.0" max_retries: 2
id: shot10_video_fallback label: "镜头10视频兜底" label_en: "Shot 10 video fallback" kind: skill_exec skill: video-still-animator with: input_image: "{{ inputs.workspace_dir }}/meta_short_drama/{{ inputs.user_message | slugify | truncate(40) }}/10_shot.png" output_path: "{{ inputs.workspace_dir }}/meta_short_drama/{{ inputs.user_message | slugify | truncate(40) }}/10_shot.mp4" duration: "{{ outputs.shot10_duration | int(5) }}" width: 720 height: 1280 fps: 24
id: ending_image label: "结尾图" label_en: "Closing image" kind: skill_exec skill: title-card-image depends_on: [ending_text_extract, review_normalize] when: "'DECISION: proceed' in outputs.review_normalize" with: text: "{{ outputs.ending_text_extract | truncate(20) }}" subtitle: "" output: "{{ inputs.workspace_dir }}/meta_short_drama/{{ inputs.user_message | slugify | truncate(40) }}/99_ending.png" background: "#0a0a10" text_color: "#e0e0e8" font_size: 96 width: 720 height: 1280
id: ending_video label: "结尾视频" label_en: "Closing video" kind: skill_exec skill: video-still-animator depends_on: [ending_image, review_normalize] when: "'DECISION: proceed' in outputs.review_normalize" with: input_image: "{{ inputs.workspace_dir }}/meta_short_drama/{{ inputs.user_message | slugify | truncate(40) }}/99_ending.png" output_path: "{{ inputs.workspace_dir }}/meta_short_drama/{{ inputs.user_message | slugify | truncate(40) }}/99_ending.mp4" duration: 2 width: 720 height: 1280 fps: 24 zoom_rate: 0.0005
id: merge label: "视频合并" label_en: "Video merge" kind: skill_exec skill: video-merger depends_on:
id: subtitles_srt label: "字幕 SRT" label_en: "Subtitle SRT" kind: skill_exec skill: srt-from-script depends_on: [final_script, review_normalize] when: "'DECISION: proceed' in outputs.review_normalize" with: script: "{{ outputs.final_script }}" output_path: "{{ inputs.workspace_dir }}/meta_short_drama/{{ inputs.user_message | slugify | truncate(40) }}/subs.srt" gap_ms: 200 leading_offset_ms: 2000
id: subtitled_final label: "字幕成片" label_en: "Subtitled video" kind: skill_exec skill: subtitle-burner depends_on: [merge, subtitles_srt, review_normalize] when: "'DECISION: proceed' in outputs.review_normalize" with: input: "{{ inputs.workspace_dir }}/meta_short_drama/{{ inputs.user_message | slugify | truncate(40) }}/final.mp4" subtitles: "{{ inputs.workspace_dir }}/meta_short_drama/{{ inputs.user_message | slugify | truncate(40) }}/subs.srt" output: "{{ inputs.workspace_dir }}/meta_short_drama/{{ inputs.user_message | slugify | truncate(40) }}/final_subtitled.mp4" font_size: 42 margin_v: 80
id: deliver label: "交付" label_en: "Delivery" kind: llm_chat depends_on: [final_script, review_normalize, script_save] with: system: "Write a concise delivery message in the user's language. No emoji. Branch on DECISION." task: | Compose a 4-10 line summary tailored to the user's decision.
User original request:
{{ inputs.user_message | xml_escape | truncate(400) }}
Decision marker:
{{ outputs.review_normalize | truncate(400) }}
Final script:
{{ outputs.final_script | truncate(2500) }}
Script saved at:
{{ outputs.script_save | truncate(200) }}
Merge output:
{{ outputs.get('merge', '') | truncate(800) }}
Subtitled-final output:
{{ outputs.get('subtitled_final', '') | truncate(800) }}
Branching rules:
- If "DECISION: proceed":
* Title (from final_script OVERVIEW.TITLE), shot count, total duration.
* Headline path = subtitled_final (the burned-in subtitle MP4).
* Also list: un-subtitled merge path, SRT path, script.txt path,
folder containing intermediates.
* Mention HAS_OVERRIDES if yes.
- If "DECISION: cancel":
* Acknowledge, note the script was still saved at script_save's
path so it's not lost.
* Offer to re-trigger.
Respond in the same language as the user's original request.
End-to-end short-drama generator with one free-form user-review gate before any paid step. 1-10 shots (default 5), title card + ending card, in-language burned subtitles, and the generated script is saved to disk regardless of outcome.
intake_extract scans the user message for RENDER_STYLE,
IDENTITY_ANCHOR, and N_SHOTS (1-10). Fills in defaults when missing.script_draft calls ai-video-script with the inferred values
pasted verbatim into every shot prompt.review_gate — single free-form pause. The user can approve,
rewrite render style / character / shot count / shot details, or
cancel in plain language.review_normalize parses the free-form reply.script_revised (conditional) redrafts when overrides present.final_script echoes the canonical script.script_save writes script.txt to the run folder
(always — even on cancel, so the user keeps the draft).title_extract / subtitle_extract / ending_text_extract
pull cover/ending text in the script's language.cover_image + cover_video — Pillow title card + 2s Ken-Burns
clip (0_cover.mp4 — sorts first in merge).__SHOT_ABSENT__.
Image/video steps gate on the sentinel so unused slots stay dormant.nano-banana-pro, retry +
fallback model + placeholder PNG (image step never aborts DAG).reference_prompt_extract + reference_image — one extra
nano-banana-pro call produces reference.png, a full-cast neutral
lineup of every named character on a neutral backdrop. Used as the
universal IDENTITY anchor for every shot's seedance call so the
character does not drift across cuts (nano-banana would otherwise
re-roll subtly different faces per shot).seedance-2.0, retry twice;
on persistent refusal the Ken-Burns substitute fires using the
shot's PNG. Each shot passes TWO reference images to seedance,
AND the per-shot prompt is wrapped with an explicit "Assets
Mapping" preamble in the upstream JiMeng convention so seedance
knows the role of each reference:
reference[1] = reference.png (full-cast identity anchor — used
strictly for character likeness / faces / hair /
outfits / accessories across all shots)
reference[2] = N_shot.png (this shot's scene composition
reference — used for camera angle, framing,
blocking, prop placement, background layout)
The Assets Mapping preamble is in English even when the per-shot
directive is Chinese — seedance parses English instruction prefixes
reliably regardless of the user-content language. Empty / missing
references are still filtered before the API call (so direct CLI
callers using a single anchor remain backwards-compatible).ending_image + ending_video — Pillow "完" / "THE END" card
99_ending.mp4 — sorts last).merge — video-merger stitches 0_cover + active shots
99_ending via numeric-prefix sort. ffmpeg cross-fade transitions.subtitles_srt — SRT cues from VOICEOVER per shot, shifted by
the 2-second cover duration so cue timing matches the merged
timeline.subtitled_final — subtitle-burner burns the SRT into
final_subtitled.mp4.deliver — always runs, branches on DECISION. Lists the saved
script path so the user keeps a copy regardless.<workspace>/meta_short_drama/<slug>/
script.txt # full final script (always)
reference.png # full-cast identity reference (used by every shot_video)
0_cover.png 0_cover.mp4
1_shot.png 1_shot.mp4 ┐
2_shot.png 2_shot.mp4 ├ only for active shots (1..N_SHOTS)
... ┘
99_ending.png 99_ending.mp4
subs.srt
final.mp4 # merged, no subtitles
final_subtitled.mp4 # subtitled — the deliverable
| Skill | Purpose | Models / Tools |
|---|---|---|
ai-video-script |
Structured shot list (1-10 shots) | LLM |
nano-banana-pro |
Per-shot first-frame PNG | OpenRouter Gemini 3.1 / 3 pro |
seedance-2-prompt |
Per-shot MP4 | OpenRouter Seedance 2.0 (or Volcengine ARK) |
video-still-animator |
Ken-Burns fallback / cover & ending clips | ffmpeg ≥ 5.0 |
video-merger |
Stitch cover + shots + ending | ffmpeg ≥ 5.0 |
srt-from-script |
VOICEOVER → SRT with cover offset | Python stdlib |
subtitle-burner |
Burn SRT into MP4 | ffmpeg + libass |
title-card-image |
Pillow cover + ending PNG cards | Pillow |
(builtin) write_file |
Save script.txt (no skill needed) | OpenSquilla builtin |
text-file-read |
Re-read script.txt after review pause | Python stdlib |
Environment:
OPENROUTER_API_KEY must be set.ffmpeg and ffprobe on PATH.high — writes files, spends real OpenRouter credits, runs ffmpeg
subprocesses. The review_gate ensures user consent before any paid step.
__SHOT_ABSENT__ gating keeps unused slots dormant.