Skills Artificial Intelligence AI Short Drama Generation Workflow

AI Short Drama Generation Workflow

v20260803
meta-short-drama
This meta-skill manages the end-to-end generation of AI-powered short dramas from a given topic. It first drafts a detailed, shot-by-shot shooting script, pausing for user review and adjustments. After confirmation, it generates consistent visual assets (universal character identity and scene compositions) and renders the final video clips. The output includes the fully rendered MP4, subtitles, title cards, and the complete script file.
Get Skill
466 downloads
Overview
  • 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) }}
      

    ---- SHOT_2 extracts (run even if shot doesn't exist; returns sentinel) ----

    • 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) }}
      

    ---- SHOT_3 extracts (run even if shot doesn't exist; returns sentinel) ----

    • 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) }}
      

    ---- SHOT_4 extracts (run even if shot doesn't exist; returns sentinel) ----

    • 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) }}
      

    ---- SHOT_5 extracts (run even if shot doesn't exist; returns sentinel) ----

    • 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) }}
      

    ---- SHOT_6 extracts (run even if shot doesn't exist; returns sentinel) ----

    • 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) }}
      

    ---- SHOT_7 extracts (run even if shot doesn't exist; returns sentinel) ----

    • 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) }}
      

    ---- SHOT_8 extracts (run even if shot doesn't exist; returns sentinel) ----

    • 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) }}
      

    ---- SHOT_9 extracts (run even if shot doesn't exist; returns sentinel) ----

    • 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) }}
      

    ---- SHOT_10 extracts (run even if shot doesn't exist; returns sentinel) ----

    • 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) }}
      

    ---- SHOT_1 image / video / fallback ----

    • 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:

      Prepend Assets Mapping so seedance knows the role of each

      input_reference image. Mirrors the upstream JiMeng prompt

      convention (see references/recipes.md "Mode: All-Reference"):

      @image1 / reference[1] = identity anchor (full-cast lineup)

      @image2 / reference[2] = scene composition (this shot)

      Keeping the preamble in English even when the shot directive

      is Chinese — seedance parses English instruction prefixes

      reliably regardless of the user-content language.

      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, falling

      back to 5 if the LLM emitted anything non-numeric (sentinel

      SHOT_ABSENT, units like "10s", chain-of-thought text). A

      raw truncate would slice "SHOT_ABSENT" to "__S" and crash

      the downstream CLI's duration validator.

      duration: "{{ 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

    ---- SHOT_2 image / video / fallback ----

    • 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:

      Prepend Assets Mapping so seedance knows the role of each

      input_reference image. Mirrors the upstream JiMeng prompt

      convention (see references/recipes.md "Mode: All-Reference"):

      @image1 / reference[1] = identity anchor (full-cast lineup)

      @image2 / reference[2] = scene composition (this shot)

      Keeping the preamble in English even when the shot directive

      is Chinese — seedance parses English instruction prefixes

      reliably regardless of the user-content language.

      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, falling

      back to 5 if the LLM emitted anything non-numeric (sentinel

      SHOT_ABSENT, units like "10s", chain-of-thought text). A

      raw truncate would slice "SHOT_ABSENT" to "__S" and crash

      the downstream CLI's duration validator.

      duration: "{{ 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

    ---- SHOT_3 image / video / fallback ----

    • 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:

      Prepend Assets Mapping so seedance knows the role of each

      input_reference image. Mirrors the upstream JiMeng prompt

      convention (see references/recipes.md "Mode: All-Reference"):

      @image1 / reference[1] = identity anchor (full-cast lineup)

      @image2 / reference[2] = scene composition (this shot)

      Keeping the preamble in English even when the shot directive

      is Chinese — seedance parses English instruction prefixes

      reliably regardless of the user-content language.

      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, falling

      back to 5 if the LLM emitted anything non-numeric (sentinel

      SHOT_ABSENT, units like "10s", chain-of-thought text). A

      raw truncate would slice "SHOT_ABSENT" to "__S" and crash

      the downstream CLI's duration validator.

      duration: "{{ 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

    ---- SHOT_4 image / video / fallback ----

    • 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:

      Prepend Assets Mapping so seedance knows the role of each

      input_reference image. Mirrors the upstream JiMeng prompt

      convention (see references/recipes.md "Mode: All-Reference"):

      @image1 / reference[1] = identity anchor (full-cast lineup)

      @image2 / reference[2] = scene composition (this shot)

      Keeping the preamble in English even when the shot directive

      is Chinese — seedance parses English instruction prefixes

      reliably regardless of the user-content language.

      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, falling

      back to 5 if the LLM emitted anything non-numeric (sentinel

      SHOT_ABSENT, units like "10s", chain-of-thought text). A

      raw truncate would slice "SHOT_ABSENT" to "__S" and crash

      the downstream CLI's duration validator.

      duration: "{{ 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

    ---- SHOT_5 image / video / fallback ----

    • 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:

      Prepend Assets Mapping so seedance knows the role of each

      input_reference image. Mirrors the upstream JiMeng prompt

      convention (see references/recipes.md "Mode: All-Reference"):

      @image1 / reference[1] = identity anchor (full-cast lineup)

      @image2 / reference[2] = scene composition (this shot)

      Keeping the preamble in English even when the shot directive

      is Chinese — seedance parses English instruction prefixes

      reliably regardless of the user-content language.

      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, falling

      back to 5 if the LLM emitted anything non-numeric (sentinel

      SHOT_ABSENT, units like "10s", chain-of-thought text). A

      raw truncate would slice "SHOT_ABSENT" to "__S" and crash

      the downstream CLI's duration validator.

      duration: "{{ 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

    ---- SHOT_6 image / video / fallback ----

    • 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:

      Prepend Assets Mapping so seedance knows the role of each

      input_reference image. Mirrors the upstream JiMeng prompt

      convention (see references/recipes.md "Mode: All-Reference"):

      @image1 / reference[1] = identity anchor (full-cast lineup)

      @image2 / reference[2] = scene composition (this shot)

      Keeping the preamble in English even when the shot directive

      is Chinese — seedance parses English instruction prefixes

      reliably regardless of the user-content language.

      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, falling

      back to 5 if the LLM emitted anything non-numeric (sentinel

      SHOT_ABSENT, units like "10s", chain-of-thought text). A

      raw truncate would slice "SHOT_ABSENT" to "__S" and crash

      the downstream CLI's duration validator.

      duration: "{{ 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

    ---- SHOT_7 image / video / fallback ----

    • 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:

      Prepend Assets Mapping so seedance knows the role of each

      input_reference image. Mirrors the upstream JiMeng prompt

      convention (see references/recipes.md "Mode: All-Reference"):

      @image1 / reference[1] = identity anchor (full-cast lineup)

      @image2 / reference[2] = scene composition (this shot)

      Keeping the preamble in English even when the shot directive

      is Chinese — seedance parses English instruction prefixes

      reliably regardless of the user-content language.

      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, falling

      back to 5 if the LLM emitted anything non-numeric (sentinel

      SHOT_ABSENT, units like "10s", chain-of-thought text). A

      raw truncate would slice "SHOT_ABSENT" to "__S" and crash

      the downstream CLI's duration validator.

      duration: "{{ 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

    ---- SHOT_8 image / video / fallback ----

    • 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:

      Prepend Assets Mapping so seedance knows the role of each

      input_reference image. Mirrors the upstream JiMeng prompt

      convention (see references/recipes.md "Mode: All-Reference"):

      @image1 / reference[1] = identity anchor (full-cast lineup)

      @image2 / reference[2] = scene composition (this shot)

      Keeping the preamble in English even when the shot directive

      is Chinese — seedance parses English instruction prefixes

      reliably regardless of the user-content language.

      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, falling

      back to 5 if the LLM emitted anything non-numeric (sentinel

      SHOT_ABSENT, units like "10s", chain-of-thought text). A

      raw truncate would slice "SHOT_ABSENT" to "__S" and crash

      the downstream CLI's duration validator.

      duration: "{{ 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

    ---- SHOT_9 image / video / fallback ----

    • 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:

      Prepend Assets Mapping so seedance knows the role of each

      input_reference image. Mirrors the upstream JiMeng prompt

      convention (see references/recipes.md "Mode: All-Reference"):

      @image1 / reference[1] = identity anchor (full-cast lineup)

      @image2 / reference[2] = scene composition (this shot)

      Keeping the preamble in English even when the shot directive

      is Chinese — seedance parses English instruction prefixes

      reliably regardless of the user-content language.

      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, falling

      back to 5 if the LLM emitted anything non-numeric (sentinel

      SHOT_ABSENT, units like "10s", chain-of-thought text). A

      raw truncate would slice "SHOT_ABSENT" to "__S" and crash

      the downstream CLI's duration validator.

      duration: "{{ 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

    ---- SHOT_10 image / video / fallback ----

    • 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:

      Prepend Assets Mapping so seedance knows the role of each

      input_reference image. Mirrors the upstream JiMeng prompt

      convention (see references/recipes.md "Mode: All-Reference"):

      @image1 / reference[1] = identity anchor (full-cast lineup)

      @image2 / reference[2] = scene composition (this shot)

      Keeping the preamble in English even when the shot directive

      is Chinese — seedance parses English instruction prefixes

      reliably regardless of the user-content language.

      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, falling

      back to 5 if the LLM emitted anything non-numeric (sentinel

      SHOT_ABSENT, units like "10s", chain-of-thought text). A

      raw truncate would slice "SHOT_ABSENT" to "__S" and crash

      the downstream CLI's duration validator.

      duration: "{{ 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

    =========================================================================

    Ending card image + 1.5s video.

    =========================================================================

    • 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

    =========================================================================

    Stitch cover + shots(1..10 that exist) + ending. video-merger sorts

    numeric prefix; 0_cover < 1..10_shot < 99_ending.

    =========================================================================

    • id: merge label: "视频合并" label_en: "Video merge" kind: skill_exec skill: video-merger depends_on:

      • cover_video
      • shot1_video
      • shot2_video
      • shot3_video
      • shot4_video
      • shot5_video
      • shot6_video
      • shot7_video
      • shot8_video
      • shot9_video
      • shot10_video
      • ending_video
      • review_normalize when: "'DECISION: proceed' in outputs.review_normalize" with: input_dir: "{{ inputs.workspace_dir }}/meta_short_drama/{{ inputs.user_message | slugify | truncate(40) }}" output_path: "{{ inputs.workspace_dir }}/meta_short_drama/{{ inputs.user_message | slugify | truncate(40) }}/final.mp4" mode: "full" transition: 0.5 fps: 24 crf: 22 preset: "medium"
    • 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.
      

meta-short-drama

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.

What it does

  1. intake_extract scans the user message for RENDER_STYLE, IDENTITY_ANCHOR, and N_SHOTS (1-10). Fills in defaults when missing.
  2. script_draft calls ai-video-script with the inferred values pasted verbatim into every shot prompt.
  3. review_gate — single free-form pause. The user can approve, rewrite render style / character / shot count / shot details, or cancel in plain language.
  4. review_normalize parses the free-form reply.
  5. script_revised (conditional) redrafts when overrides present.
  6. final_script echoes the canonical script.
  7. script_save writes script.txt to the run folder (always — even on cancel, so the user keeps the draft).
  8. title_extract / subtitle_extract / ending_text_extract pull cover/ending text in the script's language.
  9. cover_image + cover_video — Pillow title card + 2s Ken-Burns clip (0_cover.mp4 — sorts first in merge).
  10. Per-shot extracts × 10 — for shots 1..10 the LLM emits either the real prompts/duration OR the literal sentinel __SHOT_ABSENT__. Image/video steps gate on the sentinel so unused slots stay dormant.
  11. Image generation per active shotnano-banana-pro, retry + fallback model + placeholder PNG (image step never aborts DAG).
  12. 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).
  13. Video generation per active shotseedance-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).
  14. ending_image + ending_video — Pillow "完" / "THE END" card
    • 1.5s Ken-Burns clip (99_ending.mp4 — sorts last).
  15. mergevideo-merger stitches 0_cover + active shots
    • 99_ending via numeric-prefix sort. ffmpeg cross-fade transitions.
  16. subtitles_srt — SRT cues from VOICEOVER per shot, shifted by the 2-second cover duration so cue timing matches the merged timeline.
  17. subtitled_finalsubtitle-burner burns the SRT into final_subtitled.mp4.
  18. deliver — always runs, branches on DECISION. Lists the saved script path so the user keeps a copy regardless.

Outputs

<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

Dependencies

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.
  • Pillow installed (already in opensquilla deps).

Risk

high — writes files, spends real OpenRouter credits, runs ffmpeg subprocesses. The review_gate ensures user consent before any paid step.

Limits (v2)

  • 1-10 shots; default 5. The DAG always declares 10 slots but __SHOT_ABSENT__ gating keeps unused slots dormant.
  • Per-shot duration follows the script's DURATION_S (clamped 3-15s by seedance API). Total drama length scales linearly.
  • 9:16 portrait.
  • Per-shot seedance failures fall back to Ken-Burns. Image step has its own placeholder fallback. Prompt-extract llm_chats still abort the run if they return malformed output.
  • Concurrent runs with identical user_message collide on the same slug-derived subdir.

When NOT to use

  • Single image / single clip / script-only / stitch-only — use the underlying skills directly.
Info
Name meta-short-drama
Version v20260803
Size 95.02KB
Updated At 2026-08-04
Language