{
  "id": "fo01fX5smicEJD3L",
  "name": "Generate AI Camera Moves with Seedance and Build Previs Review Board",
  "tags": [],
  "nodes": [
    {
      "id": "7a347a23-2955-4b5f-beea-28feb37fbf04",
      "name": "Overview: AI Previs Pipeline",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -464,
        16
      ],
      "parameters": {
        "width": 660,
        "height": 612,
        "content": "## \ud83c\udfac AI Previs \u2014 Virtual Cinematography Pipeline\n\n### How it works\nA supervisor fills out a web form describing a complex shot \u2014 script snippet, lens specs, and move complexity. GPT-4o translates that intent into three distinct camera choreography briefs, then Seedance generates a short video for each. All three options land in Slack, Jira, ClickUp, and Telegram so the supervisor can simply pick A, B, or C.\n\nThe plate image you supply is attached to every Seedance generation as a visual reference, keeping all options grounded in the real location. Rendered videos are also archived to Google Drive for later use as lighting references.\n\n### Setup steps\n1. **Form Trigger** \u2014 the webhook URL is auto-generated. Share it with your production team.\n2. **Azure OpenAI** \u2014 connect your Azure OpenAI credential and confirm the deployment name matches `gpt-4o-mini` (or update it to your deployment).\n3. **Seedance API** \u2014 replace the `Authorization` bearer token with your own key, stored as an HTTP Header Auth credential.\n4. **Slack** \u2014 connect Slack via OAuth2 and update the `channelId` to your target channel.\n5. **Jira** \u2014 connect your Jira Cloud credential; update `project` and `issueType` IDs to match your board.\n6. **ClickUp** \u2014 connect your ClickUp credential and update `team`, `space`, `folder`, and `list` IDs.\n7. **Google Drive** \u2014 connect via OAuth2 and update the `folderId` to your previs archive folder.\n8. **Telegram** \u2014 connect your bot credential and confirm the `chatId` is correct.\n9. Do a test run using a simple shot description before going live."
      },
      "typeVersion": 1
    },
    {
      "id": "14d23d60-e548-4e18-845f-470e1406cab2",
      "name": "Section: Brief Intake & AI",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        528,
        544
      ],
      "parameters": {
        "color": 7,
        "width": 1104,
        "height": 824,
        "content": "## \ud83d\udccb Brief Intake & AI Choreography\nCollects the shot brief via a structured form, maps it to clean fields, then sends it to GPT-4o to generate three distinct camera choreography options \u2014 each with a Seedance-ready prompt, style description, and a note for the supervisor on when to choose it."
      },
      "typeVersion": 1
    },
    {
      "id": "46757a89-c42e-429d-9cba-5dddf1c3c429",
      "name": "Section: Seedance Generation & Polling",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1648,
        592
      ],
      "parameters": {
        "color": 7,
        "width": 1256,
        "height": 756,
        "content": "## \ud83c\udfa5 Seedance Video Generation\nBuilds a Seedance API request for each camera option \u2014 plate image attached as visual reference \u2014 then submits and polls every 20 seconds until the render completes. Runs in parallel for all three options."
      },
      "typeVersion": 1
    },
    {
      "id": "85e0a360-2deb-46d0-9004-3c9ed5fdfd5b",
      "name": "Section: Key Frame & Board Compile",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2928,
        576
      ],
      "parameters": {
        "color": 7,
        "width": 852,
        "height": 756,
        "content": "## \ud83d\uddc2\ufe0f Key Frame Extraction & Board Assembly\nOnce all moves are rendered, key frames are tagged at three timecodes (open, peak, landing). All options are compiled into a single previs board package \u2014 formatted for Slack, Jira, ClickUp, and Confluence \u2014 ready to send in one pass."
      },
      "typeVersion": 1
    },
    {
      "id": "6d2e6783-3225-4520-b8f3-7c6239ed336c",
      "name": "Section: Supervisor Delivery",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        3776,
        192
      ],
      "parameters": {
        "color": 7,
        "width": 500,
        "height": 1140,
        "content": "## \ud83d\udce4 Supervisor Delivery\nPublishes the previs board simultaneously to Slack (with A/B/C pick prompt), Jira (review task), ClickUp (production record), and Telegram. The Google Drive step archives each rendered video as a lighting reference for the comp team."
      },
      "typeVersion": 1
    },
    {
      "id": "34700215-d243-43be-b911-9ac8dade75f7",
      "name": "Security: Credentials Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        4336,
        1008
      ],
      "parameters": {
        "color": 3,
        "width": 296,
        "height": 320,
        "content": "## \ud83d\udd10 Credentials & Security\nUse OAuth2 for Slack, Google Drive, and Jira. Store the Seedance and ClickUp API keys as named n8n credentials \u2014 never paste raw tokens into node parameters. Replace all personal IDs, folder paths, and chat IDs with your own values before sharing."
      },
      "typeVersion": 1
    },
    {
      "id": "b889bca2-6204-4889-bd3f-94878b3c94a7",
      "name": "Extract & Map Form Fields",
      "type": "n8n-nodes-base.code",
      "position": [
        864,
        816
      ],
      "parameters": {
        "jsCode": "const form = $input.first().json;\n\nconst complexityMap = {\n  'Simple \u2014 single axis move':             'simple',\n  'Medium \u2014 multi-axis choreography':      'medium',\n  'Complex \u2014 impossible / virtual camera': 'complex',\n  'Hero \u2014 signature shot / oner':          'hero'\n};\n\nreturn [{ json: {\n  shotCode:         (form['Shot Code'] || '').trim(),\n  scriptSnippet:    (form['Script Snippet'] || '').trim(),\n  lensSpecs:        (form['Lens / Camera Specs'] || 'unspecified').trim(),\n  plateImageUrl:    (form['Plate Image URL'] || '').trim(),\n  complexity:       complexityMap[form['Move Complexity']] || 'medium',\n  supervisorEmail:  (form['Supervisor Email'] || '').trim(),\n  sequenceCode:     (form['Shot Code'] || '').split('_')[0],\n  requestTimestamp: new Date().toISOString()\n}}];"
      },
      "typeVersion": 2
    },
    {
      "id": "8aff767d-f57d-481a-ba0f-099b7244a206",
      "name": "AI Agent: Generate Camera Options",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "onError": "continueErrorOutput",
      "position": [
        1104,
        816
      ],
      "parameters": {
        "text": "==Shot Code: {{ $json.shotCode }}\nScript / Scene Description: {{ $json.scriptSnippet }}\nLens & Camera Specs: {{ $json.lensSpecs }}\nMove Complexity: {{ $json.complexity }}\nPlate Image URL: {{ $json.plateImageUrl }}\nSupervisor Email: {{ $json.supervisorEmail }}\n\nBased on the above, generate 3 distinct camera choreography options as a JSON object with this exact structure:\n{\n  \"shotIntent\": \"one sentence summarising what this shot is trying to achieve dramatically\",\n  \"moves\": [\n    {\n      \"moveId\": \"PREVIS-V1\",\n      \"moveName\": \"short punchy name\",\n      \"moveIcon\": \"\ud83c\udfac\",\n      \"moveStyle\": \"one-line camera style description\",\n      \"supervisorNote\": \"why a supervisor would choose this option\",\n      \"seedancePrompt\": \"full detailed Seedance-ready prompt describing the camera move, subject, environment, speed, mood, photorealistic\"\n    },\n    { \"moveId\": \"PREVIS-V2\", \"moveName\": \"\", \"moveIcon\": \"\", \"moveStyle\": \"\", \"supervisorNote\": \"\", \"seedancePrompt\": \"\" },\n    { \"moveId\": \"PREVIS-V3\", \"moveName\": \"\", \"moveIcon\": \"\", \"moveStyle\": \"\", \"supervisorNote\": \"\", \"seedancePrompt\": \"\" }\n  ]\n}\n\nReturn ONLY the raw JSON object. No markdown, no backticks, no explanation.",
        "options": {
          "systemMessage": "=You are a virtual cinematography expert for a VFX production pipeline. You translate director intent and technical shot briefs into precise camera choreography options. Return ONLY valid raw JSON \u2014 no markdown, no backticks, no preamble."
        },
        "promptType": "define"
      },
      "typeVersion": 2.1
    },
    {
      "id": "cabb59d4-a356-429c-9059-5570f7adb202",
      "name": "Azure OpenAI: GPT-4o Mini",
      "type": "@n8n/n8n-nodes-langchain.lmChatAzureOpenAi",
      "position": [
        1008,
        1024
      ],
      "parameters": {
        "model": "gpt-4o-mini",
        "options": {}
      },
      "typeVersion": 1
    },
    {
      "id": "9525d5d8-0c97-4904-aa8f-7281afa794e6",
      "name": "Parse AI Response \u2192 Seedance Items",
      "type": "n8n-nodes-base.code",
      "position": [
        1408,
        816
      ],
      "parameters": {
        "jsCode": "const raw = $input.first().json.text ||\n            $input.first().json.output ||\n            $input.first().json.content || '';\n\nconst formData = $('Extract & Map Form Fields').first().json;\n\nlet aiParsed;\ntry {\n  const cleaned = raw.replace(/```json|```/g, '').replace(/\\n/g, ' ').trim();\n  aiParsed = JSON.parse(cleaned);\n} catch(e) {\n  const match = raw.match(/\\{[\\s\\S]*\\}/);\n  if (match) {\n    try { aiParsed = JSON.parse(match[0]); }\n    catch(e2) { throw new Error(`AI parse failed: ${raw.substring(0,200)}`); }\n  } else {\n    throw new Error(`No JSON in AI output: ${raw.substring(0,200)}`);\n  }\n}\n\nreturn (aiParsed.moves || []).map(move => ({\n  json: {\n    ...formData,\n    moveId:         move.moveId,\n    moveName:       move.moveName,\n    moveIcon:       move.moveIcon || '\ud83c\udfac',\n    moveStyle:      move.moveStyle,\n    supervisorNote: move.supervisorNote,\n    shotIntent:     aiParsed.shotIntent,\n    safePrompt:     JSON.stringify(\n      `${move.seedancePrompt}. Shot: ${formData.shotCode}. ${formData.lensSpecs}. --duration 5 --camerafixed false`\n    ).slice(1,-1),\n    totalMoves:     (aiParsed.moves || []).length\n  }\n}));"
      },
      "typeVersion": 2
    },
    {
      "id": "54904b08-41d8-4bac-9b36-a87eefde4df7",
      "name": "Build Seedance API Request",
      "type": "n8n-nodes-base.code",
      "position": [
        1648,
        816
      ],
      "parameters": {
        "jsCode": "const input = $input.first().json;\n\nconst body = {\n  model: 'seedance-1-5-pro-251215',\n  content: [\n    { type: 'text', text: input.safePrompt },\n    { type: 'image_url', image_url: { url: input.plateImageUrl } }\n  ],\n  generate_audio: false,\n  ratio: '16:9',\n  duration: 5,\n  watermark: false\n};\n\nreturn [{ json: { ...input, requestBody: JSON.stringify(body) } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "245d7f53-5280-47d7-9525-e425db8d8574",
      "name": "Seedance: Submit Camera Move Job",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1888,
        816
      ],
      "parameters": {
        "url": "https://ark.ap-southeast.bytepluses.com/api/v3/contents/generations/tasks",
        "method": "POST",
        "options": {},
        "jsonBody": "={{ JSON.parse($json.requestBody) }}",
        "sendBody": true,
        "sendHeaders": true,
        "specifyBody": "json",
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "Bearer YOUR_TOKEN_HERE"
            },
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        }
      },
      "typeVersion": 4.3
    },
    {
      "id": "0c6ad07f-9cbe-4ead-a782-f674860d591f",
      "name": "Store Job ID + Move Metadata",
      "type": "n8n-nodes-base.code",
      "position": [
        2128,
        816
      ],
      "parameters": {
        "jsCode": "const httpResult = $input.first().json;\nconst moveData = $('Build Seedance API Request').first().json;\nreturn [{ json: { ...moveData, id: httpResult.id } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "7ef36442-395c-4c11-98ca-ccc3966426c1",
      "name": "Poll: Check Move Render Status",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        2368,
        816
      ],
      "parameters": {
        "url": "=https://ark.ap-southeast.bytepluses.com/api/v3/contents/generations/tasks/{{ $json.id }}",
        "options": {},
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "Bearer YOUR_TOKEN_HERE"
            }
          ]
        }
      },
      "typeVersion": 4.3
    },
    {
      "id": "cb03e00d-d69e-4868-8e48-ba4b03a6a21d",
      "name": "Move Render Complete?",
      "type": "n8n-nodes-base.if",
      "position": [
        2608,
        816
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "caseSensitive": false,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "previs-done",
              "operator": {
                "type": "string",
                "operation": "equals"
              },
              "leftValue": "={{ $json.status }}",
              "rightValue": "succeeded"
            }
          ]
        }
      },
      "typeVersion": 2
    },
    {
      "id": "fb29ed79-0a2a-45e5-ab32-c090fac7c84d",
      "name": "Wait 20s Before Retry",
      "type": "n8n-nodes-base.wait",
      "position": [
        2736,
        1088
      ],
      "parameters": {
        "amount": 20
      },
      "typeVersion": 1.1
    },
    {
      "id": "d4ec5e79-3892-4428-947a-ea81b8e89b96",
      "name": "Collect Move + Tag Key Frames",
      "type": "n8n-nodes-base.code",
      "position": [
        2944,
        800
      ],
      "parameters": {
        "jsCode": "const pollResult = $input.first().json;\nconst moveData   = $('Store Job ID + Move Metadata').first().json;\n\nlet videoUrl = null;\nif (pollResult.content && pollResult.content.video_url) {\n  videoUrl = pollResult.content.video_url;\n}\nif (!videoUrl) videoUrl = `Not found. Job: ${pollResult.id}`;\n\nconst keyFrames = [\n  { frame: 1,   timecode: '00:00:00:01', label: 'Opening frame \u2014 camera start position' },\n  { frame: 48,  timecode: '00:00:02:00', label: 'Peak move \u2014 maximum camera energy' },\n  { frame: 120, timecode: '00:00:05:00', label: 'Landing frame \u2014 final composition' }\n];\n\nreturn [{ json: {\n  moveId:          moveData.moveId,\n  moveName:        moveData.moveName,\n  moveIcon:        moveData.moveIcon,\n  moveStyle:       moveData.moveStyle,\n  supervisorNote:  moveData.supervisorNote,\n  shotIntent:      moveData.shotIntent,\n  shotCode:        moveData.shotCode,\n  sequenceCode:    moveData.sequenceCode,\n  complexity:      moveData.complexity,\n  lensSpecs:       moveData.lensSpecs,\n  scriptSnippet:   moveData.scriptSnippet,\n  supervisorEmail: moveData.supervisorEmail,\n  plateImageUrl:   moveData.plateImageUrl,\n  videoUrl,\n  jobId:           pollResult.id,\n  resolution:      pollResult.resolution,\n  duration:        pollResult.duration,\n  keyFrames,\n  totalMoves:      moveData.totalMoves,\n  generatedAt:     new Date().toISOString()\n}}];"
      },
      "typeVersion": 2
    },
    {
      "id": "80193cdb-af35-410c-b200-9eb25bf987cc",
      "name": "Download Lighting Reference Video",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        3216,
        1152
      ],
      "parameters": {
        "url": "={{ $json.videoUrl }}",
        "options": {
          "response": {
            "response": {
              "responseFormat": "file"
            }
          }
        }
      },
      "typeVersion": 4.3
    },
    {
      "id": "45fc8958-c402-4236-932a-c22a9c8740e5",
      "name": "Google Drive: Archive Lighting Ref",
      "type": "n8n-nodes-base.googleDrive",
      "position": [
        3488,
        1152
      ],
      "parameters": {
        "name": "=={{ $json.shotCode }}_lighting_{{ $now.toFormat('yyyyMMdd_HHmmss') }}.mp4",
        "driveId": {
          "__rl": true,
          "mode": "list",
          "value": "My Drive"
        },
        "options": {},
        "folderId": {
          "__rl": true,
          "mode": "list",
          "value": "YOUR_GOOGLE_DRIVE_FOLDER_ID",
          "cachedResultUrl": "https://drive.google.com/drive/folders/YOUR_GOOGLE_DRIVE_FOLDER_ID",
          "cachedResultName": "Previs Archive Folder"
        }
      },
      "typeVersion": 3
    },
    {
      "id": "0eb98dd8-b354-4491-8fc0-f314c1b00809",
      "name": "Jira: Create Previs Review Task",
      "type": "n8n-nodes-base.jira",
      "position": [
        3888,
        896
      ],
      "parameters": {
        "project": {
          "__rl": true,
          "mode": "list",
          "value": "YOUR_JIRA_PROJECT_ID",
          "cachedResultName": "Your Project Name"
        },
        "summary": "=[AI Previs] {{ $json.shotCode }} \u2013 {{ $json.totalMoves }} Camera Options | Supervisor Pick Required",
        "issueType": {
          "__rl": true,
          "mode": "list",
          "value": "YOUR_JIRA_ISSUE_TYPE_ID",
          "cachedResultName": "Task"
        },
        "additionalFields": {
          "description": "=AI Previs Board Generated \u2014 {{ $json.shotCode }}\n\nShot Intent: {{ $json.shotIntent }}\nScript: {{ $json.scriptSnippet }}\nLens: {{ $json.lensSpecs }}\nComplexity: {{ $json.complexity }}\n\nCamera Options:\n{{ $json.jiraDesc }}\n\nStatus: Awaiting supervisor selection.\nGenerated: {{ $json.generatedAt }}"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "ca884283-0c15-40a0-acab-dd306aad90f1",
      "name": "ClickUp: Create Previs Production Record",
      "type": "n8n-nodes-base.clickUp",
      "position": [
        3888,
        368
      ],
      "parameters": {
        "list": "YOUR_CLICKUP_LIST_ID",
        "name": "=[AI Previs] {{ $json.shotCode }}",
        "team": "YOUR_CLICKUP_TEAM_ID",
        "space": "YOUR_CLICKUP_SPACE_ID",
        "folder": "YOUR_CLICKUP_FOLDER_ID",
        "additionalFields": {
          "content": "==Shot Code: {{ $json.shotCode }}\nSequence: {{ $json.sequenceCode }}\nShot Intent: {{ $json.shotIntent }}\nScript: {{ $json.scriptSnippet }}\nLens: {{ $json.lensSpecs }}\nComplexity: {{ $json.complexity }}\nSupervisor: {{ $json.supervisorEmail }}\nTotal Options: {{ $json.totalMoves }}\nGenerated: {{ $json.generatedAt }}\n\n--- CAMERA OPTIONS ---\n{{ $json.jiraDesc }}\n\n--- VIDEO LINKS ---\n{{ $json.allMoves.map(m => m.moveId + ': ' + m.videoUrl).join('\\n') }}"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "ef4a7cd1-cfa4-47f2-a13d-9e99f8d0face",
      "name": "Telegram: Deliver Previs to Supervisor",
      "type": "n8n-nodes-base.telegram",
      "position": [
        3904,
        1136
      ],
      "parameters": {
        "text": "==\ud83c\udfac *AI Previs Board Ready \u2014 {{ $json.shotCode }}*\n\n\ud83d\udccb *Sequence:* {{ $json.sequenceCode }} | *Complexity:* {{ $json.complexity }}\n\ud83c\udfaf *Shot Intent:* {{ $json.shotIntent }}\n\ud83d\udcdd *Script:* {{ $json.scriptSnippet }}\n\ud83c\udfa5 *Lens:* {{ $json.lensSpecs }}\n\ud83d\udce7 *Supervisor:* {{ $json.supervisorEmail }}\n\n*\u2500\u2500 {{ $json.totalMoves }} Camera Option(s) Generated \u2500\u2500*\n\n{{ $json.allMoves.map(function(m, i) { return (i+1) + '. ' + m.moveIcon + ' *' + m.moveName + '* (' + m.moveId + ')\\n   Style: ' + m.moveStyle + '\\n   Note: ' + m.supervisorNote + '\\n   Resolution: ' + (m.resolution || 'N/A') + '\\n   Video: ' + m.videoUrl; }).join('\\n\\n') }}\n\n\ud83d\uddbc\ufe0f *Plate Ref:* {{ $json.plateImageUrl }}\n\n_Supervisor \u2014 watch the options above and reply with your pick (PREVIS-V1, V2, or V3) to lock it to the Jira task._\n_Generated: {{ $json.generatedAt }}_",
        "chatId": "=YOUR_TELEGRAM_CHAT_ID",
        "additionalFields": {
          "parse_mode": "Markdown"
        }
      },
      "typeVersion": 1.1
    },
    {
      "id": "8d594e49-ac8c-4a28-9cbd-bb7d173b1feb",
      "name": "Telegram: Alert on AI Agent Failure",
      "type": "n8n-nodes-base.telegram",
      "position": [
        1440,
        1104
      ],
      "parameters": {
        "text": "\u26a0\ufe0f *AI Previs: AI Agent Error*\n\nThe camera choreography agent failed to return valid JSON. The workflow has retried from form extraction.\n\nTime: {{ new Date().toISOString() }}",
        "chatId": "=YOUR_TELEGRAM_CHAT_ID",
        "additionalFields": {
          "parse_mode": "Markdown"
        }
      },
      "typeVersion": 1.1
    },
    {
      "id": "6acad659-e831-4f3f-8c32-486bc806f8e3",
      "name": "Form: Previs Brief Input1",
      "type": "n8n-nodes-base.formTrigger",
      "position": [
        624,
        816
      ],
      "parameters": {
        "options": {},
        "formTitle": "AI Previs \u2014 Virtual Cinematography Request",
        "formFields": {
          "values": [
            {
              "fieldLabel": "Shot Code",
              "placeholder": "SQ080_SH010",
              "requiredField": true
            },
            {
              "fieldType": "textarea",
              "fieldLabel": "Script Snippet",
              "placeholder": "Camera starts behind hero, swoops around in slow motion, then races through the crowd toward the villain...",
              "requiredField": true
            },
            {
              "fieldLabel": "Lens / Camera Specs",
              "placeholder": "14mm anamorphic, drone rig, 240fps slowmo"
            },
            {
              "fieldLabel": "Plate Image URL",
              "placeholder": "https://your-server.com/location_plate.jpg",
              "requiredField": true
            },
            {
              "fieldType": "dropdown",
              "fieldLabel": "Move Complexity",
              "fieldOptions": {
                "values": [
                  {
                    "option": "Simple \u2014 single axis move"
                  },
                  {
                    "option": "Medium \u2014 multi-axis choreography"
                  },
                  {
                    "option": "Complex \u2014 impossible / virtual camera"
                  },
                  {
                    "option": "Hero \u2014 signature shot / oner"
                  }
                ]
              },
              "requiredField": true
            },
            {
              "fieldType": "email",
              "fieldLabel": "Supervisor Email",
              "placeholder": "user@example.com"
            }
          ]
        },
        "formDescription": "Describe your complex shot. AI will translate your intent into technical camera choreography briefs, then generate multiple video options for supervisor selection."
      },
      "typeVersion": 2.2
    },
    {
      "id": "2b397314-283e-4c19-8b80-808704e21e06",
      "name": "Compile Previs Board Package1",
      "type": "n8n-nodes-base.code",
      "position": [
        3184,
        800
      ],
      "parameters": {
        "jsCode": "const allMoves = $input.all().map(i => i.json);\nconst first = allMoves[0];\n\nconst moveCards = allMoves.map((m, idx) => {\n  const letter = String.fromCharCode(65 + idx);\n  return `${m.moveIcon} *Option ${letter} \u2014 ${m.moveName}* (${m.moveId})\\n` +\n    `> \ud83c\udfa5 Style: ${m.moveStyle}\\n` +\n    `> \ud83d\udca1 ${m.supervisorNote}\\n` +\n    `> \ud83c\udfac <${m.videoUrl}|Watch Option ${letter}>\\n` +\n    `> \ud83d\udccd Key Frames: ${m.keyFrames.map(k => k.label).join(' \u2192 ')}`;\n}).join('\\n\\n');\n\nconst jiraDesc = allMoves.map((m, idx) => {\n  const letter = String.fromCharCode(65 + idx);\n  return `*Option ${letter}: ${m.moveName}*\\n${m.supervisorNote}\\nVideo: ${m.videoUrl}`;\n}).join('\\n\\n');\n\nconst tableRows = allMoves.map((m, idx) => {\n  const letter = String.fromCharCode(65 + idx);\n  return `<tr>\n    <td><strong>Option ${letter}</strong></td>\n    <td>${m.moveIcon} ${m.moveName}</td>\n    <td>${m.moveStyle}</td>\n    <td>${m.supervisorNote}</td>\n    <td><a href=\"${m.videoUrl}\">Watch</a></td>\n    <td>${m.resolution || '720p'}</td>\n  </tr>`;\n}).join('');\n\nconst lookbookHtml = `<h1>\ud83c\udfac AI Previs Board \u2014 ${first.shotCode}</h1>\n<p><strong>Sequence:</strong> ${first.sequenceCode} | <strong>Shot Intent:</strong> ${first.shotIntent}</p>\n<p><strong>Script:</strong> ${first.scriptSnippet}</p>\n<p><strong>Lens/Camera:</strong> ${first.lensSpecs} | <strong>Complexity:</strong> ${first.complexity}</p>\n<h2>Camera Options \u2014 Supervisor: Please select one</h2>\n<table><thead><tr><th>Option</th><th>Name</th><th>Style</th><th>Director Note</th><th>Video</th><th>Res</th></tr></thead><tbody>${tableRows}</tbody></table>\n<h2>Key Frame Guide</h2>\n<ul>${first.keyFrames.map(k => `<li><strong>${k.timecode}</strong> \u2014 ${k.label}</li>`).join('')}</ul>\n<p><em>Generated by AI Previs Pipeline \u2014 ${new Date().toISOString()}</em></p>`;\n\nreturn [{ json: {\n  shotCode:        first.shotCode,\n  sequenceCode:    first.sequenceCode,\n  shotIntent:      first.shotIntent,\n  scriptSnippet:   first.scriptSnippet,\n  lensSpecs:       first.lensSpecs,\n  complexity:      first.complexity,\n  supervisorEmail: first.supervisorEmail,\n  plateImageUrl:   first.plateImageUrl,\n  allMoves,\n  moveCards,\n  jiraDesc,\n  lookbookHtml,\n  totalMoves:      allMoves.length,\n  generatedAt:     new Date().toISOString()\n}}];"
      },
      "typeVersion": 2
    },
    {
      "id": "d222ad6e-e474-4331-92b3-4f5f40ac36b2",
      "name": "Slack: Publish Previs Board1",
      "type": "n8n-nodes-base.slack",
      "position": [
        3888,
        624
      ],
      "parameters": {
        "text": "=\ud83c\udfac *AI Previs Board Ready \u2014 {{ $json.shotCode }}*\n\n\ud83d\udccb *Sequence:* {{ $json.sequenceCode }} | *Complexity:* {{ $json.complexity }}\n\ud83c\udfaf *Shot Intent:* {{ $json.shotIntent }}\n\ud83d\udcdd *Script:* {{ $json.scriptSnippet }}\n\ud83c\udfa5 *Lens:* {{ $json.lensSpecs }}\n\n*\u2500\u2500 3 Camera Options for Supervisor Review \u2500\u2500*\n{{ $json.moveCards }}\n\n\ud83d\udc46 *Supervisor \u2014 reply with Option A, B, or C. Your choice will be auto-attached to the shot task in Jira.*\n\n_AI previs generated in {{ $json.totalMoves }} camera variations. Generated: {{ $json.generatedAt }}_",
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "list",
          "value": "YOUR_SLACK_CHANNEL_ID",
          "cachedResultName": "your-channel-name"
        },
        "otherOptions": {},
        "authentication": "oAuth2"
      },
      "typeVersion": 2.3
    },
    {
      "id": "1edb0914-e112-43e4-948f-66113b3d9553",
      "name": "On Workflow Error",
      "type": "n8n-nodes-base.errorTrigger",
      "position": [
        624,
        1680
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "30233fa0-16ff-4328-9b00-0e7a978b1680",
      "name": "Slack: Error Alert",
      "type": "n8n-nodes-base.slack",
      "position": [
        880,
        1680
      ],
      "parameters": {
        "text": "=\u274c *AI-Assisted Clean Plate & Object Removal\n\nError: {{ $json.message }}\nTime: {{ new Date().toISOString() }}",
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "list",
          "value": "C0ANFAL4WJ2",
          "cachedResultName": "social"
        },
        "otherOptions": {},
        "authentication": "oAuth2"
      },
      "typeVersion": 2.3
    },
    {
      "id": "41a8c016-ee06-4dd8-9bb1-61c46b6decce",
      "name": "Section: Error Handler",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        544,
        1520
      ],
      "parameters": {
        "color": 7,
        "width": 492,
        "height": 328,
        "content": "## \u26a0\ufe0f Error Handler\nCatches any failure across the entire workflow and immediately sends a Slack alert to the ops channel. Wire this to every sub-workflow or critical node to ensure no silent failures."
      },
      "typeVersion": 1
    }
  ],
  "active": false,
  "settings": {
    "binaryMode": "separate",
    "executionOrder": "v1"
  },
  "versionId": "9eea25f1-e862-4f3c-85a0-0bc6c172a9b6",
  "connections": {
    "On Workflow Error": {
      "main": [
        [
          {
            "node": "Slack: Error Alert",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Move Render Complete?": {
      "main": [
        [
          {
            "node": "Collect Move + Tag Key Frames",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Wait 20s Before Retry",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait 20s Before Retry": {
      "main": [
        [
          {
            "node": "Poll: Check Move Render Status",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Azure OpenAI: GPT-4o Mini": {
      "ai_languageModel": [
        [
          {
            "node": "AI Agent: Generate Camera Options",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Extract & Map Form Fields": {
      "main": [
        [
          {
            "node": "AI Agent: Generate Camera Options",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Form: Previs Brief Input1": {
      "main": [
        [
          {
            "node": "Extract & Map Form Fields",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Seedance API Request": {
      "main": [
        [
          {
            "node": "Seedance: Submit Camera Move Job",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Store Job ID + Move Metadata": {
      "main": [
        [
          {
            "node": "Poll: Check Move Render Status",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Collect Move + Tag Key Frames": {
      "main": [
        [
          {
            "node": "Compile Previs Board Package1",
            "type": "main",
            "index": 0
          },
          {
            "node": "Download Lighting Reference Video",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Compile Previs Board Package1": {
      "main": [
        [
          {
            "node": "Slack: Publish Previs Board1",
            "type": "main",
            "index": 0
          },
          {
            "node": "Jira: Create Previs Review Task",
            "type": "main",
            "index": 0
          },
          {
            "node": "ClickUp: Create Previs Production Record",
            "type": "main",
            "index": 0
          },
          {
            "node": "Telegram: Deliver Previs to Supervisor",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Poll: Check Move Render Status": {
      "main": [
        [
          {
            "node": "Move Render Complete?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Seedance: Submit Camera Move Job": {
      "main": [
        [
          {
            "node": "Store Job ID + Move Metadata",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "AI Agent: Generate Camera Options": {
      "main": [
        [
          {
            "node": "Parse AI Response \u2192 Seedance Items",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Telegram: Alert on AI Agent Failure",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Download Lighting Reference Video": {
      "main": [
        [
          {
            "node": "Google Drive: Archive Lighting Ref",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Telegram: Alert on AI Agent Failure": {
      "main": [
        [
          {
            "node": "Extract & Map Form Fields",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse AI Response \u2192 Seedance Items": {
      "main": [
        [
          {
            "node": "Build Seedance API Request",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}