{
  "name": "Storyboard I2V Pipeline (17 shots) with Retry/Wait/State/Notify",
  "nodes": [
    {
      "id": "Trigger_In",
      "name": "HTTP Trigger (POST /i2v)",
      "type": "n8n-nodes-base.httpTrigger",
      "typeVersion": 1,
      "position": [
        200,
        200
      ],
      "parameters": {
        "path": "i2v",
        "options": {
          "response": {
            "response": "onReceived",
            "responseCode": 200,
            "responseData": "Received. Processing started."
          }
        }
      }
    },
    {
      "id": "Parse_Input",
      "name": "Parse Input JSON",
      "type": "n8n-nodes-base.function",
      "typeVersion": 2,
      "position": [
        420,
        200
      ],
      "parameters": {
        "functionCode": "const body = items[0].json;\n// \u671f\u671b\u8f93\u5165: { shots:[{id,image_prompt,video_prompt,durationSec}], options:{maxPolls,pollWaitSec,maxRetries} }\nif (!body.shots || !Array.isArray(body.shots) || body.shots.length === 0) {\n  throw new Error('shots \u6570\u7ec4\u7f3a\u5931\u6216\u4e3a\u7a7a');\n}\nconst defaults = { maxPolls: 10, pollWaitSec: 20, maxRetries: 2 };\nconst options = Object.assign({}, defaults, body.options || {});\nreturn [{ json: { shots: body.shots, options, projectId: body.project_id || body.projectId || 'project-unknown', callbackUrl: body.callback_url || body.callbackUrl || '' } }];"
      }
    },
    {
      "id": "Split_Shots",
      "name": "Split In Batches (Shots)",
      "type": "n8n-nodes-base.splitInBatches",
      "typeVersion": 2,
      "position": [
        640,
        200
      ],
      "parameters": {
        "options": {
          "batchSize": 1
        }
      }
    },
    {
      "id": "Set_Shot",
      "name": "Set Current Shot",
      "type": "n8n-nodes-base.function",
      "typeVersion": 2,
      "position": [
        860,
        200
      ],
      "parameters": {
        "functionCode": "const { shots, options, projectId, callbackUrl } = $items('Parse Input JSON')[0].json;\nconst current = $items('Split In Batches (Shots)').json;\nreturn [{ json: { shot: current, options, projectId, callbackUrl, __ctx: { imagePolls: 0, videoPolls: 0 } } }];"
      }
    },
    {
      "id": "Image_Create",
      "name": "HTTP Image Create (replace with your model)",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 3,
      "position": [
        1080,
        140
      ],
      "parameters": {
        "url": "https://api.replicate.com/v1/predictions",
        "method": "POST",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendBody": true,
        "jsonParameters": true,
        "headerParametersJson": "{ \"Authorization\": \"Token {{$credentials.ReplicateApi.credentials.apiKey}}\", \"Content-Type\": \"application/json\" }",
        "bodyParametersJson": "{\n  \"version\": \"YOUR_IMAGE_MODEL_VERSION\",\n  \"input\": {\n    \"prompt\": \"{{$json.shot.image_prompt}}\",\n    \"seed\": 12345,\n    \"output_format\": \"png\"\n  }\n}"
      },
      "notesInFlow": true,
      "notes": "\u5c06 URL/Headers/Body \u66ff\u6362\u4e3a\u4f60\u7684\u56fe\u7247\u751f\u6210\u670d\u52a1\uff08\u5982\u8c46\u53053.0\u7b49\uff09\u3002"
    },
    {
      "id": "Image_Status",
      "name": "Check Image Status",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 3,
      "position": [
        1300,
        140
      ],
      "parameters": {
        "url": "={{$json['urls'] && $json['urls']['get'] ? $json['urls']['get'] : $json['id'] ? 'https://api.replicate.com/v1/predictions/' + $json['id'] : ''}}",
        "method": "GET",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "jsonParameters": true,
        "headerParametersJson": "{ \"Authorization\": \"Token {{$credentials.ReplicateApi.credentials.apiKey}}\" }"
      }
    },
    {
      "id": "Image_Switch",
      "name": "Switch Image State",
      "type": "n8n-nodes-base.switch",
      "typeVersion": 3,
      "position": [
        1520,
        140
      ],
      "parameters": {
        "dataType": "string",
        "value1": "={{$json.status}}",
        "rules": {
          "rules": [
            {
              "operation": "equal",
              "value": "succeeded"
            },
            {
              "operation": "equal",
              "value": "failed"
            },
            {
              "operation": "contains",
              "value": "processing"
            }
          ]
        }
      }
    },
    {
      "id": "Image_Wait",
      "name": "Wait (Image Poll)",
      "type": "n8n-nodes-base.wait",
      "typeVersion": 1,
      "position": [
        1740,
        80
      ],
      "parameters": {
        "waitTill": "timeInterval",
        "timeInterval": {
          "unit": "seconds",
          "value": "={{$items('Parse Input JSON')[0].json.options.pollWaitSec}}"
        }
      }
    },
    {
      "id": "Image_Poll_Count",
      "name": "Inc Image Poll Count",
      "type": "n8n-nodes-base.function",
      "typeVersion": 2,
      "position": [
        1940,
        80
      ],
      "parameters": {
        "functionCode": "const ctx = $json.__ctx || { imagePolls: 0, videoPolls: 0 };\nctx.imagePolls += 1;\nreturn [{ json: Object.assign({}, $json, { __ctx: ctx }) }];"
      }
    },
    {
      "id": "Image_Poll_Limit",
      "name": "Switch Image Poll Limit",
      "type": "n8n-nodes-base.switch",
      "typeVersion": 3,
      "position": [
        2140,
        80
      ],
      "parameters": {
        "dataType": "number",
        "value1": "={{$json.__ctx.imagePolls}}",
        "rules": {
          "rules": [
            {
              "operation": "smaller",
              "value": "={{$items('Parse Input JSON')[0].json.options.maxPolls}}"
            },
            {
              "operation": "largerEqual",
              "value": "={{$items('Parse Input JSON').json.options.maxPolls}}"
            }
          ]
        }
      }
    },
    {
      "id": "Image_Requery",
      "name": "Re-Check Image Status",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 3,
      "position": [
        2340,
        40
      ],
      "parameters": {
        "url": "={{$items('Check Image Status').json.id ? 'https://api.replicate.com/v1/predictions/' + $items('Check Image Status').json.id : ''}}",
        "method": "GET",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "jsonParameters": true,
        "headerParametersJson": "{ \"Authorization\": \"Token {{$credentials.ReplicateApi.credentials.apiKey}}\" }"
      }
    },
    {
      "id": "Image_Poll_Exceeded",
      "name": "Notify Image Timeout",
      "type": "n8n-nodes-base.emailSend",
      "typeVersion": 2,
      "position": [
        2340,
        120
      ],
      "parameters": {
        "fromEmail": "pipeline@studio.local",
        "toEmail": "producer@studio.local",
        "subject": "Image generation timeout",
        "text": "Shot {{$items('Set Current Shot')[0].json.shot.id}} image generation timeout after {{$items('Parse Input JSON').json.options.maxPolls}} polls."
      }
    },
    {
      "id": "Image_Failed",
      "name": "Notify Image Failed",
      "type": "n8n-nodes-base.emailSend",
      "typeVersion": 2,
      "position": [
        1740,
        200
      ],
      "parameters": {
        "fromEmail": "pipeline@studio.local",
        "toEmail": "producer@studio.local",
        "subject": "Image generation failed",
        "text": "Shot {{$items('Set Current Shot')[0].json.shot.id}} image failed. Please review logs and trigger manual retry if needed."
      }
    },
    {
      "id": "Image_Succeeded",
      "name": "Set Image URL",
      "type": "n8n-nodes-base.function",
      "typeVersion": 2,
      "position": [
        1740,
        260
      ],
      "parameters": {
        "functionCode": "const out = items.json;\nconst imageUrl = Array.isArray(out.output) ? out.output : out.output || out.urls?.get || '';\nreturn [{ json: Object.assign({}, out, { imageUrl }) }];"
      }
    },
    {
      "id": "Video_Create",
      "name": "HTTP Video Create (replace with your model)",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 3,
      "position": [
        1960,
        260
      ],
      "parameters": {
        "url": "https://api.replicate.com/v1/predictions",
        "method": "POST",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendBody": true,
        "jsonParameters": true,
        "headerParametersJson": "{ \"Authorization\": \"Token {{$credentials.ReplicateApi.credentials.apiKey}}\", \"Content-Type\": \"application/json\" }",
        "bodyParametersJson": "{\n  \"version\": \"YOUR_I2V_MODEL_VERSION\",\n  \"input\": {\n    \"prompt\": \"{{$items(\\\"Set Current Shot\\\")[0].json.shot.video_prompt}}\",\n    \"image\": \"{{$items(\\\"Set Image URL\\\").json.imageUrl}}\",\n    \"duration\": {{$items(\\\"Set Current Shot\\\").json.shot.durationSec || 3}}\n  }\n}"
      },
      "notesInFlow": true,
      "notes": "\u66ff\u6362\u4e3a\u5373\u68a63.0/Stable Video Diffusion/Wan 2.2 I2V \u7684\u521b\u5efa\u63a5\u53e3\u3002"
    },
    {
      "id": "Video_Status",
      "name": "Check Video Status",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 3,
      "position": [
        2180,
        260
      ],
      "parameters": {
        "url": "={{$json['urls'] && $json['urls']['get'] ? $json['urls']['get'] : $json['id'] ? 'https://api.replicate.com/v1/predictions/' + $json['id'] : ''}}",
        "method": "GET",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "jsonParameters": true,
        "headerParametersJson": "{ \"Authorization\": \"Token {{$credentials.ReplicateApi.credentials.apiKey}}\" }"
      }
    },
    {
      "id": "Video_Switch",
      "name": "Switch Video State",
      "type": "n8n-nodes-base.switch",
      "typeVersion": 3,
      "position": [
        2400,
        260
      ],
      "parameters": {
        "dataType": "string",
        "value1": "={{$json.status}}",
        "rules": {
          "rules": [
            {
              "operation": "equal",
              "value": "succeeded"
            },
            {
              "operation": "equal",
              "value": "failed"
            },
            {
              "operation": "contains",
              "value": "processing"
            }
          ]
        }
      }
    },
    {
      "id": "Video_Wait",
      "name": "Wait (Video Poll)",
      "type": "n8n-nodes-base.wait",
      "typeVersion": 1,
      "position": [
        2620,
        200
      ],
      "parameters": {
        "waitTill": "timeInterval",
        "timeInterval": {
          "unit": "seconds",
          "value": "={{$items('Parse Input JSON')[0].json.options.pollWaitSec}}"
        }
      }
    },
    {
      "id": "Video_Poll_Count",
      "name": "Inc Video Poll Count",
      "type": "n8n-nodes-base.function",
      "typeVersion": 2,
      "position": [
        2820,
        200
      ],
      "parameters": {
        "functionCode": "const ctx = $json.__ctx || { imagePolls: 0, videoPolls: 0 };\nctx.videoPolls += 1;\nreturn [{ json: Object.assign({}, $json, { __ctx: ctx }) }];"
      }
    },
    {
      "id": "Video_Poll_Limit",
      "name": "Switch Video Poll Limit",
      "type": "n8n-nodes-base.switch",
      "typeVersion": 3,
      "position": [
        3020,
        200
      ],
      "parameters": {
        "dataType": "number",
        "value1": "={{$json.__ctx.videoPolls}}",
        "rules": {
          "rules": [
            {
              "operation": "smaller",
              "value": "={{$items('Parse Input JSON')[0].json.options.maxPolls}}"
            },
            {
              "operation": "largerEqual",
              "value": "={{$items('Parse Input JSON').json.options.maxPolls}}"
            }
          ]
        }
      }
    },
    {
      "id": "Video_Requery",
      "name": "Re-Check Video Status",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 3,
      "position": [
        3220,
        160
      ],
      "parameters": {
        "url": "={{$items('Check Video Status').json.id ? 'https://api.replicate.com/v1/predictions/' + $items('Check Video Status').json.id : ''}}",
        "method": "GET",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "jsonParameters": true,
        "headerParametersJson": "{ \"Authorization\": \"Token {{$credentials.ReplicateApi.credentials.apiKey}}\" }"
      }
    },
    {
      "id": "Video_Poll_Exceeded",
      "name": "Notify Video Timeout",
      "type": "n8n-nodes-base.emailSend",
      "typeVersion": 2,
      "position": [
        3220,
        240
      ],
      "parameters": {
        "fromEmail": "pipeline@studio.local",
        "toEmail": "producer@studio.local",
        "subject": "Video generation timeout",
        "text": "Shot {{$items('Set Current Shot')[0].json.shot.id}} video generation timeout after {{$items('Parse Input JSON').json.options.maxPolls}} polls."
      }
    },
    {
      "id": "Video_Failed",
      "name": "Notify Video Failed",
      "type": "n8n-nodes-base.emailSend",
      "typeVersion": 2,
      "position": [
        2620,
        320
      ],
      "parameters": {
        "fromEmail": "pipeline@studio.local",
        "toEmail": "producer@studio.local",
        "subject": "Video generation failed",
        "text": "Shot {{$items('Set Current Shot')[0].json.shot.id}} video failed. Please review logs."
      }
    },
    {
      "id": "Video_Succeeded",
      "name": "Set Video URL",
      "type": "n8n-nodes-base.function",
      "typeVersion": 2,
      "position": [
        2620,
        380
      ],
      "parameters": {
        "functionCode": "const out = items.json;\nconst videoUrl = Array.isArray(out.output) ? out.output : out.output || out.urls?.get || '';\nreturn [{ json: Object.assign({}, out, { videoUrl }) }];"
      }
    },
    {
      "id": "Aggregate_Result",
      "name": "Aggregate Shot Result",
      "type": "n8n-nodes-base.function",
      "typeVersion": 2,
      "position": [
        2840,
        380
      ],
      "parameters": {
        "functionCode": "const shot = $items('Set Current Shot')[0].json.shot;\nconst imageUrl = $items('Set Image URL').json.imageUrl || '';\nconst videoUrl = $items('Set Video URL').json.videoUrl || '';\nreturn [{ json: { id: shot.id, imageUrl, videoUrl } }];"
      }
    },
    {
      "id": "Continue_Batch",
      "name": "Continue Batch",
      "type": "n8n-nodes-base.splitInBatches",
      "typeVersion": 2,
      "position": [
        3060,
        380
      ],
      "parameters": {
        "options": {
          "continue": true
        }
      }
    },
    {
      "id": "Collect_All",
      "name": "Collect All Results",
      "type": "n8n-nodes-base.merge",
      "typeVersion": 2,
      "position": [
        3280,
        380
      ],
      "parameters": {
        "mode": "passThrough",
        "combine": "mergeByIndex"
      }
    },
    {
      "id": "Return_Final",
      "name": "Respond JSON",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1,
      "position": [
        3500,
        380
      ],
      "parameters": {
        "responseBody": "={{ { projectId: $items('Parse Input JSON')[0].json.projectId, shots: $items('Collect All Results').map(i => i.json) } }}",
        "responseCode": 200
      }
    }
  ],
  "connections": {
    "Parse Input JSON": {
      "main": [
        [
          {
            "node": "Split In Batches (Shots)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "HTTP Trigger (POST /i2v)": {
      "main": [
        [
          {
            "node": "Parse Input JSON",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Split In Batches (Shots)": {
      "main": [
        [
          {
            "node": "Set Current Shot",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Collect All Results",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set Current Shot": {
      "main": [
        [
          {
            "node": "HTTP Image Create (replace with your model)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "HTTP Image Create (replace with your model)": {
      "main": [
        [
          {
            "node": "Check Image Status",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check Image Status": {
      "main": [
        [
          {
            "node": "Switch Image State",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Switch Image State": {
      "main": [
        [
          {
            "node": "Set Image URL",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Notify Image Failed",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Wait (Image Poll)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait (Image Poll)": {
      "main": [
        [
          {
            "node": "Inc Image Poll Count",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Inc Image Poll Count": {
      "main": [
        [
          {
            "node": "Switch Image Poll Limit",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Switch Image Poll Limit": {
      "main": [
        [
          {
            "node": "Re-Check Image Status",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Notify Image Timeout",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set Image URL": {
      "main": [
        [
          {
            "node": "HTTP Video Create (replace with your model)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "HTTP Video Create (replace with your model)": {
      "main": [
        [
          {
            "node": "Check Video Status",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check Video Status": {
      "main": [
        [
          {
            "node": "Switch Video State",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Switch Video State": {
      "main": [
        [
          {
            "node": "Set Video URL",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Notify Video Failed",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Wait (Video Poll)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait (Video Poll)": {
      "main": [
        [
          {
            "node": "Inc Video Poll Count",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Inc Video Poll Count": {
      "main": [
        [
          {
            "node": "Switch Video Poll Limit",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Switch Video Poll Limit": {
      "main": [
        [
          {
            "node": "Re-Check Video Status",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Notify Video Timeout",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set Video URL": {
      "main": [
        [
          {
            "node": "Aggregate Shot Result",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Aggregate Shot Result": {
      "main": [
        [
          {
            "node": "Continue Batch",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Continue Batch": {
      "main": [
        [
          {
            "node": "Split In Batches (Shots)",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Collect All Results": {
      "main": [
        [
          {
            "node": "Respond JSON",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "meta": {
    "templateCredsSetupCompleted": false
  },
  "settings": {
    "saveExecutionProgress": "DEFAULT",
    "executionOrder": "v1"
  },
  "staticData": null,
  "tags": [
    {
      "name": "i2v"
    },
    {
      "name": "retry"
    },
    {
      "name": "polling"
    }
  ]
}