This workflow corresponds to n8n.io template #13676 — we link there as the canonical source.
This workflow follows the Agent → Execute Workflow Trigger recipe pattern — see all workflows that pair these two integrations.
The workflow JSON
Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →
{
"id": "zyBv0G4EeHomIfXd",
"meta": {
"templateCredsSetupCompleted": true
},
"name": "Shorts Creation v10 - Telegram Filming",
"tags": [],
"nodes": [
{
"id": "25d380bf-7e85-4b68-b5a1-ed31013a02af",
"name": "Stage 3",
"type": "n8n-nodes-base.stickyNote",
"position": [
5888,
1696
],
"parameters": {
"color": 6,
"width": 320,
"height": 200,
"content": "## Stage 3: AI Analysis\n\nGPT-4o:\n1. Analyzes transcript\n2. Identifies best moments\n3. Generates 3 concepts\n4. Creates full avatar scripts\n\nEach concept uses different\nsource material."
},
"typeVersion": 1
},
{
"id": "03bd4748-2e0f-49be-8061-49491f0249d2",
"name": "Stage 4",
"type": "n8n-nodes-base.stickyNote",
"position": [
6784,
1696
],
"parameters": {
"color": 7,
"width": 320,
"height": 200,
"content": "## Stage 4: HeyGen Avatar\n\n**Single API Call**\n\nGenerates ONE video with:\n- Hook (5-8 sec)\n- Body narration (25-40 sec)\n- CTA (3-5 sec)\n\nDimensions: 1080x1920 (vertical)"
},
"typeVersion": 1
},
{
"id": "8639a448-1fb7-4223-8559-9dcfff630a48",
"name": "Stage 5",
"type": "n8n-nodes-base.stickyNote",
"position": [
8672,
1696
],
"parameters": {
"color": 3,
"width": 340,
"height": 220,
"content": "## Stage 5: AI Video Director\n\n**Dynamic Layouts**\n\nAI generates RenderScript with:\n- avatar_full (hook/cta)\n- split_screen (demo)\n- pip_overlay (focus)\n\nTransitions every 4-8 seconds!\n\nNo template required."
},
"typeVersion": 1
},
{
"id": "cd82f2ca-ef7e-4216-9d3d-ead1076e97c2",
"name": "Loop Through Concepts",
"type": "n8n-nodes-base.splitInBatches",
"position": [
2592,
1920
],
"parameters": {
"options": {
"reset": false
}
},
"typeVersion": 3
},
{
"id": "11a9919e-ab3a-42e6-9fc4-dd44c73b721f",
"name": "HeyGen - Generate Full Avatar",
"type": "n8n-nodes-base.httpRequest",
"position": [
3936,
1744
],
"parameters": {
"url": "https://api.heygen.com/v2/video/generate",
"method": "POST",
"options": {
"timeout": 60000
},
"jsonBody": "={\n \"video_inputs\": [\n {\n \"character\": {\n \"type\": \"avatar\",\n \"avatar_id\": \"{{ [\"e707a00d8b5549108f795f635d2de923\", \"eaa27b740b054bc18076968b9c5e3646\", \"e707a00d8b5549108f795f635d2de923\"][$('Loop Through Concepts').item.json.concept_number - 1] }}\",\n \"avatar_style\": \"normal\"\n },\n \"voice\": {\n \"type\": \"text\",\n \"input_text\": {{ $('Loop Through Concepts').item.json.full_script.toJsonString() }},\n \"voice_id\": \"8fac12c49e844f60aa672bff8273d154\",\n \"speed\": 1.0,\n \"pitch\": 5, \n \"emotion\": \"Friendly\", \n \"stability\": 0.4, \n \"similarity_boost\": 0.85, \n \"style_exaggeration\": 0.5, \n \"speaker_boost\": true\n }\n }\n ],\n \"dimension\": {\n \"width\": 1920,\n \"height\": 1080\n }\n}",
"sendBody": true,
"sendHeaders": true,
"specifyBody": "json",
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth",
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
}
]
}
},
"credentials": {
"httpHeaderAuth": {
"name": "<your credential>"
}
},
"typeVersion": 4.2
},
{
"id": "dbb8ab81-5f14-498d-b182-8e498e0e7e2e",
"name": "Set Avatar Video ID",
"type": "n8n-nodes-base.set",
"position": [
4160,
1744
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "avatar_video_id",
"name": "avatar_video_id",
"type": "string",
"value": "={{ $json.data.video_id }}"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "aa91af19-85a7-44b0-912e-334e4ef5b0bb",
"name": "Poll Avatar Status",
"type": "n8n-nodes-base.httpRequest",
"position": [
4608,
1664
],
"parameters": {
"url": "=https://api.heygen.com/v1/video_status.get?video_id={{ $('Set Avatar Video ID').item.json.avatar_video_id }}",
"options": {},
"sendHeaders": true,
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth",
"headerParameters": {
"parameters": [
{
"name": "Accept",
"value": "application/json"
}
]
}
},
"credentials": {
"httpHeaderAuth": {
"name": "<your credential>"
}
},
"typeVersion": 4.2
},
{
"id": "4f91bcc2-54a1-4f20-a92c-ea460f596d4d",
"name": "Avatar Done?",
"type": "n8n-nodes-base.if",
"position": [
4832,
1744
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 1,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "avatar-done",
"operator": {
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.data.status }}",
"rightValue": "completed"
}
]
}
},
"typeVersion": 2
},
{
"id": "4c36afcc-eeba-4dd1-b312-5b2dd8c35c19",
"name": "Set Avatar URL",
"type": "n8n-nodes-base.set",
"position": [
5056,
1744
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "avatar_video_url",
"name": "avatar_video_url",
"type": "string",
"value": "={{ $json.data.video_url }}"
},
{
"id": "4a528e4d-7dfc-45c5-9dd7-762ccd5903bd",
"name": "avatar_duration",
"type": "number",
"value": "={{ $json.data.duration }}"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "6f445fdb-84d9-418c-b082-4bcf702cef22",
"name": "Creatomate - Render",
"type": "n8n-nodes-base.httpRequest",
"position": [
6000,
1744
],
"parameters": {
"url": "https://api.creatomate.com/v2/renders",
"method": "POST",
"options": {
"timeout": 120000
},
"jsonBody": "={{ $json}}",
"sendBody": true,
"sendHeaders": true,
"specifyBody": "json",
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth",
"headerParameters": {
"parameters": [
{}
]
}
},
"credentials": {
"httpHeaderAuth": {
"name": "<your credential>"
}
},
"typeVersion": 4.2
},
{
"id": "43290299-e7b3-472d-afdf-d281eab6bcae",
"name": "Set Render ID",
"type": "n8n-nodes-base.set",
"position": [
6224,
1744
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "render_id",
"name": "render_id",
"type": "string",
"value": "={{ Array.isArray($json) ? $json[0].id : $json.id }}"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "9410f289-7181-4c5a-8194-e67fa6d94675",
"name": "Wait for Render",
"type": "n8n-nodes-base.wait",
"position": [
6448,
1744
],
"parameters": {
"amount": 30
},
"typeVersion": 1.1
},
{
"id": "15b534ae-3780-4ddc-a957-7e5b2f35b113",
"name": "Poll Render Status",
"type": "n8n-nodes-base.httpRequest",
"position": [
6672,
1664
],
"parameters": {
"url": "=https://api.creatomate.com/v2/renders/{{ $('Set Render ID').item.json.render_id }}",
"options": {},
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth"
},
"credentials": {
"httpHeaderAuth": {
"name": "<your credential>"
}
},
"retryOnFail": true,
"typeVersion": 4.2,
"waitBetweenTries": 5000
},
{
"id": "cfdd0819-a613-487a-ae58-47f6be30d7d1",
"name": "Render Done?",
"type": "n8n-nodes-base.if",
"position": [
6896,
1744
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 1,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "render-done",
"operator": {
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.status }}",
"rightValue": "succeeded"
}
]
}
},
"typeVersion": 2
},
{
"id": "9360db52-8aad-49f0-9bdd-ed4ea1f6e9e9",
"name": "Download Rendered Video",
"type": "n8n-nodes-base.httpRequest",
"position": [
7120,
1744
],
"parameters": {
"url": "={{ $json.url }}",
"options": {
"response": {
"response": {
"responseFormat": "file"
}
}
}
},
"typeVersion": 4.2
},
{
"id": "e0c53c3c-fdb2-42e0-8d9e-2f97e639d1e3",
"name": "Upload to Google Drive",
"type": "n8n-nodes-base.googleDrive",
"position": [
7344,
1744
],
"parameters": {
"name": "={{ $('AI Video Director').item.json.output[0].content[0].text.title }}_{{ $('Loop Through Concepts').item.json.concept_index }}.mp4",
"driveId": {
"__rl": true,
"mode": "list",
"value": "My Drive"
},
"options": {},
"folderId": {
"__rl": true,
"mode": "list",
"value": "1KnH7S9ltO1VMhahtNQcKLkUnFwekbOqL",
"cachedResultUrl": "https://drive.google.com/drive/folders/1KnH7S9ltO1VMhahtNQcKLkUnFwekbOqL",
"cachedResultName": "Adam Goodyer Short-Form Content"
}
},
"credentials": {
"googleDriveOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 3
},
{
"id": "c569730f-a65d-4d44-9ebf-36e9a0662fdf",
"name": "Creatomate Template Builder Code1",
"type": "n8n-nodes-base.code",
"position": [
5776,
1744
],
"parameters": {
"jsCode": "// ============================================================\n// CREATOMATE TEMPLATE BUILDER v23.0 (STEPPED FLASH FIXED)\n// ============================================================\n// \n// FIX v23.0:\n// - FIXED: Flash now uses SEQUENTIAL COMPOSITIONS on same track\n// - Each composition has rgba opacity baked into fill_color\n// - No animations needed - stepped opacity creates smooth fade-out\n// - Compositions with empty elements[] act as solid color overlays\n// - Zoom animation uses back-out easing for punch feel\n//\n// ============================================================\n\n// ============================================================\n// \ud83c\udf9b\ufe0f INPUT NODES - All data sources in one place\n// ============================================================\n\nconst INPUT_NODES = {\n aiDirector: $('AI Video Director').first().json,\n conceptLoop: $('Loop Through Concepts').first().json,\n avatarUrl: $('Set Avatar URL').first().json,\n flashBroll: $('Aggregate Flash B-Roll').first().json,\n effectsLibrary: $('Creatomate Effects Library').first().json,\n shortsSource: $('Shorts Trigger').first().json?.source || {}\n};\n\n// ============================================================\n// \ud83c\udf9b\ufe0f CONFIGURABLE SETTINGS\n// ============================================================\n\n// Video Output Settings\nconst VIDEO_SETTINGS = {\n outputFormat: 'mp4',\n width: 1080,\n height: 1920,\n frameRate: 30,\n previewWidth: 1080,\n previewHeight: 1920,\n usePreviewSize: true\n};\n\n// ============================================================\n// \ud83d\ude80 SCROLL-STOPPER INTRO SETTINGS (STEPPED COMPOSITION APPROACH)\n// ============================================================\n// \n// Psychology of short-form scroll-stopping:\n// 1. INSTANT IMPACT: White flash at frame 0 triggers attention\n// 2. MOTION PARALLAX: Zoom creates depth, signals \"something happening\"\n// 3. TIMING: Must complete < 0.5s to not annoy, but long enough to register\n// 4. CONTRAST: White on dark background = maximum visual punch\n//\n// TECHNICAL: Uses sequential compositions on same track with\n// decreasing rgba opacity baked into fill_color. This creates\n// a smooth stepped fade-out effect without needing animations.\n//\n// ============================================================\n\nconst INTRO_EFFECT = {\n enabled: true,\n \n // Stepped flash using sequential compositions\n // Each step is a composition with opacity in rgba fill_color\n flash: {\n enabled: true,\n baseTrack: 30, // High track = renders on top\n steps: [\n // Step 1: FULL PUNCH - 100% white (quick hit)\n { time: 0, duration: 0.017, opacity: 1.0 },\n // Step 2: 80% white\n { time: 0.017, duration: 0.017, opacity: 0.8 },\n // Step 3: 55% white\n { time: 0.034, duration: 0.017, opacity: 0.55 },\n // Step 4: 30% white\n { time: 0.051, duration: 0.025, opacity: 0.3 },\n // Step 5: 15% white\n { time: 0.076, duration: 0.035, opacity: 0.15 },\n // Step 6: 5% white (quick fade out)\n { time: 0.111, duration: 0.04, opacity: 0.05 }\n ]\n },\n \n // Zoom settings - creates depth and \"impact\" feeling\n zoom: {\n enabled: true,\n startScale: \"150%\", // Start zoomed in more for punch\n endScale: \"100%\", // Settle to normal\n duration: 0.4, // Quick but not jarring\n easing: \"back-out\" // Overshoots slightly for punch feel\n }\n};\n\n// Screen Recording Glow Effect\nconst SCREEN_GLOW = {\n enabled: true,\n color: '#355A15',\n primaryOpacity: '60%',\n secondaryOpacity: '40%',\n primaryBlur: '25 vmin',\n secondaryBlur: '15 vmin'\n};\n\n// Caption Settings\nconst CAPTION_SETTINGS = {\n enabled: true,\n fontFamily: 'Montserrat',\n fontWeight: 900,\n fontSize: '9.5 vmin',\n fillColor: '#FFFFFF',\n strokeColor: '#000000',\n strokeWidth: '1.1 vmin',\n shadowColor: 'rgba(0,0,0,0.95)',\n shadowBlur: '1.3 vmin',\n maxLength: 14,\n effect: 'highlight',\n positionY: '68%'\n};\n\n// Background Settings\nconst BACKGROUND = {\n color: '#0a0a0a'\n};\n\n// Audio Settings\nconst AUDIO_SETTINGS = {\n baseUrl: 'https://pub-1710591b286a4bf3877267e2ce0828e1.r2.dev',\n defaultVolume: '7%',\n fadeIn: 0.5,\n fadeOut: 1.5\n};\n\n// ============================================================\n// \ud83d\udce5 PARSE AI DIRECTOR OUTPUT\n// ============================================================\n\nlet directorData;\nconst aiOutput = INPUT_NODES.aiDirector.output;\n\nif (aiOutput && typeof aiOutput === 'object' && !Array.isArray(aiOutput)) {\n directorData = aiOutput;\n} else if (Array.isArray(aiOutput)) {\n try {\n const messageBlock = aiOutput.find(o => o.type === 'message');\n if (messageBlock) {\n const textContent = messageBlock.content[0].text;\n directorData = typeof textContent === 'string' \n ? JSON.parse(textContent) \n : textContent;\n } else {\n const textContent = aiOutput[1]?.content?.[0]?.text;\n directorData = typeof textContent === 'string' \n ? JSON.parse(textContent) \n : textContent;\n }\n } catch (e) {\n console.error('Parse error:', e.message);\n directorData = {};\n }\n} else {\n directorData = {};\n}\n\n// ============================================================\n// \ud83d\udcca DERIVED CONSTANTS\n// ============================================================\n\nconst conceptData = INPUT_NODES.conceptLoop;\nconst avatarUrl = INPUT_NODES.avatarUrl.avatar_video_url;\nconst avatarDuration = INPUT_NODES.avatarUrl.avatar_duration || INPUT_NODES.avatarUrl.duration || null;\nconst flashBrollGenerated = INPUT_NODES.flashBroll.flash_broll_generated || [];\nconst lib = INPUT_NODES.effectsLibrary;\nconst sourceData = INPUT_NODES.shortsSource;\n\n// Build AI B-roll URL lookup\nconst aiBrollUrls = {};\nflashBrollGenerated.forEach(clip => {\n const index = (clip.flash_number || 1) - 1;\n aiBrollUrls[index] = clip.video_url;\n});\n\n// Storyboard and styling\nconst storyboard = directorData.enhanced_storyboard || directorData.storyboard || [];\nconst conceptType = directorData.concept_type || conceptData.concept_type || 'bold';\nconst accentColor = lib.accent_colors?.[conceptType] || lib.accent_colors?.[Object.keys(lib.accent_colors || {})[0]] || '#00E676';\n\n// Final constants\nconst AVATAR_URL = avatarUrl;\nconst SCREEN_URL = sourceData.video_url || directorData.video_url;\nconst AI_BROLL_URLS = aiBrollUrls;\nconst STORYBOARD = storyboard;\nconst ACTUAL_AVATAR_DURATION = avatarDuration || directorData.total_duration_seconds || directorData.total_duration;\nconst ACCENT_COLOR = accentColor;\n\n// Audio track selection\nconst audioTrackId = directorData.audio_track || Object.keys(lib.audio_tracks || {})[0] || 7;\nconst selectedTrack = lib.audio_tracks?.[audioTrackId];\n\n// ============================================================\n// \ud83d\udd27 HELPER FUNCTIONS\n// ============================================================\n\nfunction getDefaultLayout() {\n const layoutKeys = Object.keys(lib.layouts || {});\n const withAvatar = layoutKeys.find(k => lib.layouts[k].avatar);\n const anyLayout = layoutKeys[0];\n \n if (withAvatar) return lib.layouts[withAvatar];\n if (anyLayout) return lib.layouts[anyLayout];\n \n return {\n id: \"FALLBACK\",\n avatar: { x: \"50%\", y: \"50%\", width: \"100%\", height: \"100%\" },\n screen: null,\n divider_y: null\n };\n}\n\nfunction getLayout(layoutId) {\n if (layoutId && lib.layouts?.[layoutId]) {\n return lib.layouts[layoutId];\n }\n return getDefaultLayout();\n}\n\n// ============================================================\n// \ud83c\udfac TRANSITION SYSTEM (DYNAMIC FROM EFFECTS LIBRARY)\n// ============================================================\n\nconst OVERLAY_TRANSITION_TYPES = [\n 'flash', 'double_flash', 'rgb_glitch', 'accent_flash', \n 'glitch_bars', 'zoom_shake', 'whip_pan'\n];\n\nfunction isOverlayTransition(transitionId) {\n if (!transitionId) return false;\n const trans = lib.transitions?.[transitionId];\n if (!trans) return false;\n return OVERLAY_TRANSITION_TYPES.includes(trans.type);\n}\n\n// ============================================================\n// \ud83d\ude80 SCROLL-STOPPER INTRO BUILDERS (STEPPED COMPOSITION APPROACH)\n// ============================================================\n\n/**\n * Build intro flash overlay elements using STEPPED COMPOSITIONS.\n * \n * KEY INSIGHT: Creatomate's fade animations don't support fade-OUT.\n * Instead, we use sequential compositions on the SAME TRACK with\n * decreasing rgba opacity baked into fill_color. This creates a\n * smooth stepped fade-out effect without needing any animations.\n * \n * - All compositions on same track = they play SEQUENTIALLY\n * - Each has explicit time + duration for precise control\n * - Opacity is in rgba format: \"rgba(255,255,255,0.75)\"\n * - Empty elements[] array = solid color overlay\n * \n * @returns {Array} Array of composition elements for the flash effect\n */\nfunction buildIntroFlashElements() {\n if (!INTRO_EFFECT.enabled || !INTRO_EFFECT.flash.enabled) return [];\n \n const flashElements = [];\n const track = INTRO_EFFECT.flash.baseTrack;\n \n INTRO_EFFECT.flash.steps.forEach((step) => {\n flashElements.push({\n type: \"composition\",\n track: track, // ALL on same track = sequential playback\n time: step.time,\n duration: step.duration,\n fill_color: `rgba(255,255,255,${step.opacity})`, // Opacity baked in!\n width: \"100%\",\n height: \"100%\",\n elements: [] // Empty = solid color overlay\n });\n });\n \n return flashElements;\n}\n\n/**\n * Build intro zoom animation using PROPER Creatomate syntax.\n * \n * Uses back-out easing for that \"punch\" feel where it slightly\n * overshoots then settles back. Combined with flash, creates\n * the scroll-stopping impact moment.\n * \n * @returns {Object|null} Animation object for the animations array\n */\nfunction buildIntroZoomAnimation() {\n if (!INTRO_EFFECT.enabled || !INTRO_EFFECT.zoom.enabled) return null;\n \n return {\n type: \"scale\",\n time: 0, // Start immediately\n duration: INTRO_EFFECT.zoom.duration,\n easing: INTRO_EFFECT.zoom.easing,\n start_scale: INTRO_EFFECT.zoom.startScale,\n end_scale: INTRO_EFFECT.zoom.endScale,\n scope: \"element\",\n fade: false\n };\n}\n\n// ============================================================\n// \ud83c\udfac OTHER TRANSITION/ANIMATION HELPERS\n// ============================================================\n\nfunction buildTransitionOverlays(time, transitionId) {\n const trans = lib.transitions?.[transitionId];\n if (!trans) return [];\n \n const elements = [];\n let trackOffset = 20;\n \n switch (trans.type) {\n case 'flash':\n case 'double_flash':\n // Use stepped composition approach for mid-video flashes too\n const flashSteps = [\n { timeOffset: 0, duration: 0.03, opacity: 1.0 },\n { timeOffset: 0.03, duration: 0.03, opacity: 0.7 },\n { timeOffset: 0.06, duration: 0.04, opacity: 0.4 },\n { timeOffset: 0.10, duration: 0.05, opacity: 0.15 }\n ];\n flashSteps.forEach((step, i) => {\n elements.push({\n type: \"composition\",\n track: trackOffset + i,\n time: time + step.timeOffset,\n duration: step.duration,\n fill_color: `rgba(255,255,255,${step.opacity})`,\n width: \"100%\",\n height: \"100%\",\n elements: []\n });\n });\n break;\n \n case 'rgb_glitch':\n // Build RGB color layers\n const rgbSteps = [\n { color: \"255,0,0\", opacity: 0.5, xOffset: \"-2%\" },\n { color: \"0,255,0\", opacity: 0.5, xOffset: \"0%\" },\n { color: \"0,0,255\", opacity: 0.5, xOffset: \"2%\" }\n ];\n rgbSteps.forEach((layer, i) => {\n elements.push({\n type: \"composition\",\n track: trackOffset + i,\n time: time,\n duration: trans.duration || 0.12,\n fill_color: `rgba(${layer.color},${layer.opacity})`,\n width: \"100%\",\n height: \"100%\",\n x: layer.xOffset,\n blend_mode: \"screen\",\n elements: []\n });\n });\n // Follow-up white flash\n const whiteFlashSteps = [\n { timeOffset: 0, duration: 0.03, opacity: 1.0 },\n { timeOffset: 0.03, duration: 0.05, opacity: 0.5 },\n { timeOffset: 0.08, duration: 0.07, opacity: 0.15 }\n ];\n whiteFlashSteps.forEach((step, i) => {\n elements.push({\n type: \"composition\",\n track: trackOffset + rgbSteps.length + i,\n time: time + (trans.duration || 0.12) * 0.5 + step.timeOffset,\n duration: step.duration,\n fill_color: `rgba(255,255,255,${step.opacity})`,\n width: \"100%\",\n height: \"100%\",\n elements: []\n });\n });\n break;\n \n case 'accent_flash':\n // Accent color punch followed by white\n const accentSteps = [\n { timeOffset: 0, duration: 0.04, opacity: 0.7, color: ACCENT_COLOR.replace('#', '') },\n { timeOffset: 0.04, duration: 0.04, opacity: 0.3, color: ACCENT_COLOR.replace('#', '') }\n ];\n // Convert hex to rgb for accent\n const hexToRgb = (hex) => {\n const r = parseInt(hex.substring(0, 2), 16);\n const g = parseInt(hex.substring(2, 4), 16);\n const b = parseInt(hex.substring(4, 6), 16);\n return `${r},${g},${b}`;\n };\n const accentRgb = hexToRgb(ACCENT_COLOR.replace('#', ''));\n accentSteps.forEach((step, i) => {\n elements.push({\n type: \"composition\",\n track: trackOffset + i,\n time: time + step.timeOffset,\n duration: step.duration,\n fill_color: `rgba(${accentRgb},${step.opacity})`,\n width: \"100%\",\n height: \"100%\",\n elements: []\n });\n });\n // White flash after accent\n const whiteAfterAccent = [\n { timeOffset: 0.05, duration: 0.03, opacity: 1.0 },\n { timeOffset: 0.08, duration: 0.04, opacity: 0.6 },\n { timeOffset: 0.12, duration: 0.08, opacity: 0.2 }\n ];\n whiteAfterAccent.forEach((step, i) => {\n elements.push({\n type: \"composition\",\n track: trackOffset + accentSteps.length + i,\n time: time + step.timeOffset,\n duration: step.duration,\n fill_color: `rgba(255,255,255,${step.opacity})`,\n width: \"100%\",\n height: \"100%\",\n elements: []\n });\n });\n break;\n \n case 'zoom_shake':\n // White flash with zoom feel\n const zoomFlashSteps = [\n { timeOffset: 0, duration: 0.04, opacity: 1.0 },\n { timeOffset: 0.04, duration: 0.06, opacity: 0.6 },\n { timeOffset: 0.10, duration: 0.10, opacity: 0.25 },\n { timeOffset: 0.20, duration: 0.10, opacity: 0.08 }\n ];\n zoomFlashSteps.forEach((step, i) => {\n elements.push({\n type: \"composition\",\n track: trackOffset + i,\n time: time + step.timeOffset,\n duration: step.duration,\n fill_color: `rgba(255,255,255,${step.opacity})`,\n width: \"100%\",\n height: \"100%\",\n elements: []\n });\n });\n break;\n \n case 'whip_pan':\n // Horizontal wipe-style flash\n elements.push({\n type: \"composition\",\n track: trackOffset,\n time: time,\n duration: trans.duration || 0.15,\n fill_color: \"rgba(255,255,255,1)\",\n width: trans.width || \"120%\",\n height: \"100%\",\n y: \"50%\",\n x_anchor: \"50%\",\n y_anchor: \"50%\",\n elements: [],\n animations: [\n {\n type: \"slide\",\n time: 0,\n duration: trans.duration || 0.15,\n direction: \"0\u00b0\",\n distance: \"120%\",\n easing: \"quadratic-out\"\n }\n ]\n });\n break;\n \n default:\n // Generic flash fallback using stepped approach\n if (trans.color) {\n const genericSteps = [\n { timeOffset: 0, duration: 0.03, opacity: 1.0 },\n { timeOffset: 0.03, duration: 0.05, opacity: 0.5 },\n { timeOffset: 0.08, duration: 0.07, opacity: 0.15 }\n ];\n genericSteps.forEach((step, i) => {\n elements.push({\n type: \"composition\",\n track: trackOffset + i,\n time: time + step.timeOffset,\n duration: step.duration,\n fill_color: `rgba(255,255,255,${step.opacity})`,\n width: \"100%\",\n height: \"100%\",\n elements: []\n });\n });\n }\n }\n \n return elements;\n}\n\nfunction buildTransitionAnimation(transitionId) {\n if (!transitionId) return null;\n \n const trans = lib.transitions?.[transitionId];\n if (!trans) return null;\n \n if (OVERLAY_TRANSITION_TYPES.includes(trans.type)) return null;\n if (trans.type === 'cut') return null;\n \n const animation = {\n time: 0,\n duration: trans.duration || 0.15,\n transition: true,\n easing: trans.easing || \"quadratic-out\"\n };\n \n switch (trans.type) {\n case 'scale':\n animation.type = \"scale\";\n animation.start_scale = trans.start_scale || \"120%\";\n break;\n \n case 'slide':\n animation.type = \"slide\";\n animation.direction = trans.direction || \"0\u00b0\";\n if (trans.distance) animation.distance = trans.distance;\n animation.fade = trans.fade !== undefined ? trans.fade : false;\n break;\n \n case 'wipe':\n animation.type = \"wipe\";\n animation.direction = trans.direction || \"left\";\n break;\n \n case 'fade':\n animation.type = \"fade\";\n break;\n \n default:\n return null;\n }\n \n return animation;\n}\n\nfunction getAvatarAnimation(animationId) {\n if (!animationId) return [];\n \n const anim = lib.avatar_animations?.[animationId];\n if (!anim || anim.type === 'none') return [];\n \n switch (anim.type) {\n case 'scale':\n return [{ \n type: \"scale\", \n time: 0, \n duration: anim.duration || 0.2, \n start_scale: anim.start_scale || \"108%\", \n easing: anim.easing || \"quadratic-out\"\n }];\n \n case 'slide':\n return [{ \n type: \"slide\", \n time: 0, \n duration: anim.duration || 0.25, \n direction: anim.direction === 'up' ? \"90\u00b0\" : \"270\u00b0\",\n distance: anim.distance || \"10%\",\n easing: anim.easing || \"back-out\"\n }];\n \n case 'fade':\n return [{\n type: \"fade\",\n time: 0,\n duration: anim.duration || 0.3,\n start_opacity: anim.start_opacity || \"0%\"\n }];\n \n default:\n return [];\n }\n}\n\nfunction getScreenMotion(motionId, duration) {\n const defaultMotion = { \n scale: [{ time: \"0 s\", value: \"100%\" }], \n x: [{ time: \"0 s\", value: \"50%\" }], \n y: [{ time: \"0 s\", value: \"50%\" }] \n };\n \n if (!motionId) return defaultMotion;\n \n const motion = lib.screen_motions?.[motionId];\n if (!motion) return defaultMotion;\n \n if (motion.type === 'static') return defaultMotion;\n \n if (motion.type === 'zoom') {\n return {\n scale: [\n { time: \"0 s\", value: motion.start_scale || \"100%\", easing: motion.easing || \"linear\" },\n { time: `${duration} s`, value: motion.end_scale || \"108%\" }\n ],\n x: [{ time: \"0 s\", value: \"50%\" }],\n y: [{ time: \"0 s\", value: \"50%\" }]\n };\n }\n \n if (motion.type === 'pan') {\n return {\n scale: [{ time: \"0 s\", value: \"100%\" }],\n x: [\n { time: \"0 s\", value: motion.start_x || \"50%\", easing: motion.easing || \"linear\" },\n { time: `${duration} s`, value: motion.end_x || \"50%\" }\n ],\n y: [\n { time: \"0 s\", value: motion.start_y || \"50%\", easing: motion.easing || \"linear\" },\n { time: `${duration} s`, value: motion.end_y || \"50%\" }\n ]\n };\n }\n \n return defaultMotion;\n}\n\nfunction buildTextOverlay(styleId, content, time, duration) {\n if (!styleId || !content) return null;\n \n const style = lib.text_styles?.[styleId];\n if (!style) return null;\n \n const fillColor = (style.fill_color || \"#FFFFFF\").replace(\"{{ACCENT_COLOR}}\", ACCENT_COLOR);\n const bgColor = style.background_color?.replace(\"{{ACCENT_COLOR}}\", ACCENT_COLOR);\n \n const textElement = {\n type: \"text\",\n track: 10,\n time: time + 0.2,\n duration: duration - 0.3,\n text: content,\n x: style.position?.x || \"50%\",\n y: style.position?.y || \"78%\",\n width: \"92%\",\n x_anchor: \"50%\",\n y_anchor: \"50%\",\n x_alignment: \"50%\",\n font_family: style.font_family,\n font_weight: style.font_weight,\n font_size: style.font_size,\n fill_color: fillColor,\n animations: [\n { type: \"scale\", time: 0, duration: 0.25, easing: \"back-out\", start_scale: \"90%\" },\n { type: \"fade\", time: \"end\", duration: 0.15 }\n ]\n };\n \n if (style.stroke_color) textElement.stroke_color = style.stroke_color;\n if (style.stroke_width) textElement.stroke_width = style.stroke_width;\n if (style.shadow_color) textElement.shadow_color = style.shadow_color;\n if (style.shadow_blur) textElement.shadow_blur = style.shadow_blur;\n if (bgColor) textElement.background_color = bgColor;\n if (style.background_x_padding) textElement.background_x_padding = style.background_x_padding;\n if (style.background_y_padding) textElement.background_y_padding = style.background_y_padding;\n if (style.background_border_radius) textElement.background_border_radius = style.background_border_radius;\n \n return textElement;\n}\n\nfunction getBrollVideoUrl(beat) {\n if (beat.broll_type === 'screen') return SCREEN_URL;\n if (beat.broll_type === 'ai_generated' && beat.ai_broll_index != null && beat.ai_broll_index >= 0) {\n return AI_BROLL_URLS[beat.ai_broll_index] || null;\n }\n return null;\n}\n\nfunction getBrollTrimSettings(beat, duration) {\n if (beat.broll_type === 'screen') {\n const startTime = beat.broll_start_seconds || 0;\n const aiSpecifiedDuration = beat.broll_end_seconds && beat.broll_start_seconds \n ? beat.broll_end_seconds - beat.broll_start_seconds \n : null;\n \n return {\n trim_start: startTime,\n trim_duration: duration + 0.5,\n _ai_specified_duration: aiSpecifiedDuration,\n _was_corrected: aiSpecifiedDuration !== null && aiSpecifiedDuration < duration\n };\n }\n \n if (beat.broll_type === 'ai_generated') {\n return { \n trim_start: 0, \n trim_duration: duration + 0.5\n };\n }\n \n return null;\n}\n\n// ============================================================\n// \ud83c\udfd7\ufe0f BUILD ELEMENTS ARRAY\n// ============================================================\n\nconst elements = [];\nlet avatarTrimPosition = 0;\nlet overlayTransitionCount = 0;\nlet brollDurationCorrections = [];\nlet introFlashCount = 0;\nlet introZoomApplied = false;\n\n// Track 1: Background Music\nif (selectedTrack) {\n elements.push({\n type: \"audio\",\n track: 1,\n source: `${AUDIO_SETTINGS.baseUrl}/${selectedTrack.file || selectedTrack.name.toLowerCase().replace(/ /g, '_') + '.mp3'}`,\n volume: selectedTrack.volume || AUDIO_SETTINGS.defaultVolume,\n loop: true,\n audio_fade_in: AUDIO_SETTINGS.fadeIn,\n audio_fade_out: AUDIO_SETTINGS.fadeOut\n });\n}\n\n// Track 2: Master Avatar (hidden, for audio & caption sync)\nelements.push({\n id: \"avatar-master\",\n type: \"video\",\n track: 2,\n time: 0,\n source: AVATAR_URL,\n volume: \"100%\",\n y: \"200%\",\n fit: \"cover\"\n});\n\n// Track 3: Black Background\nelements.push({\n type: \"shape\",\n track: 3,\n time: 0,\n duration: ACTUAL_AVATAR_DURATION + 5,\n shape: \"rectangle\",\n width: \"100%\",\n height: \"100%\",\n fill_color: BACKGROUND.color\n});\n\n// ============================================================\n// \ud83d\ude80 ADD INTRO FLASH ELEMENTS (Track 30 - stepped compositions)\n// ============================================================\nconst introFlashElements = buildIntroFlashElements();\nintroFlashElements.forEach(el => {\n elements.push(el);\n introFlashCount++;\n});\n\n// ============================================================\n// \ud83c\udfac PROCESS EACH STORYBOARD BEAT\n// ============================================================\n\nlet currentTime = 0;\n\nSTORYBOARD.forEach((beat, idx) => {\n const duration = beat.duration_seconds || 4;\n const layout = getLayout(beat.layout);\n const screenMotion = getScreenMotion(beat.screen_motion, duration);\n const avatarAnimations = getAvatarAnimation(beat.avatar_animation);\n \n const isIntroBeat = idx === 0;\n \n // Handle transitions (not for first beat)\n let transitionAnimation = null;\n if (!isIntroBeat && beat.transition_in) {\n if (isOverlayTransition(beat.transition_in)) {\n const overlayElements = buildTransitionOverlays(currentTime, beat.transition_in);\n overlayElements.forEach(el => elements.push(el));\n overlayTransitionCount++;\n } else {\n transitionAnimation = buildTransitionAnimation(beat.transition_in);\n }\n }\n \n // ============================================================\n // AVATAR ELEMENT\n // ============================================================\n if (layout.avatar) {\n const avatarElement = {\n type: \"video\",\n track: 4,\n time: currentTime,\n duration: duration,\n source: AVATAR_URL,\n trim_start: avatarTrimPosition,\n trim_duration: duration,\n volume: 0,\n x: layout.avatar.x,\n y: layout.avatar.y,\n width: layout.avatar.width,\n height: layout.avatar.height,\n x_anchor: \"50%\",\n y_anchor: \"50%\",\n fit: \"cover\",\n clip: true\n };\n \n if (layout.avatar.border_radius) {\n avatarElement.border_radius = layout.avatar.border_radius;\n }\n \n // Build animations array\n const allAnimations = [];\n \n // INTRO: First beat gets scroll-stopper zoom\n if (isIntroBeat && INTRO_EFFECT.enabled && INTRO_EFFECT.zoom.enabled) {\n const introZoom = buildIntroZoomAnimation();\n if (introZoom) {\n allAnimations.push(introZoom);\n introZoomApplied = true;\n }\n } else {\n if (transitionAnimation) allAnimations.push(transitionAnimation);\n avatarAnimations.forEach(a => allAnimations.push(a));\n }\n \n if (allAnimations.length > 0) {\n avatarElement.animations = allAnimations;\n }\n \n elements.push(avatarElement);\n }\n \n // ============================================================\n // B-ROLL ELEMENT\n // ============================================================\n const brollUrl = getBrollVideoUrl(beat);\n const brollTrim = getBrollTrimSettings(beat, duration);\n \n if (brollTrim && brollTrim._was_corrected) {\n brollDurationCorrections.push({\n beat_index: idx,\n beat_type: beat.beat_type,\n beat_duration: duration,\n ai_specified_duration: brollTrim._ai_specified_duration,\n correction_applied: true\n });\n }\n \n if (layout.screen && brollUrl && brollTrim) {\n \n const isScreenRecording = beat.broll_type === 'screen' || layout.is_padded_screen === true;\n const fitMode = isScreenRecording ? 'contain' : 'cover';\n \n const videoWidth = isScreenRecording ? '92%' : '120%';\n const videoHeight = isScreenRecording ? '88%' : '130%';\n \n const innerElements = [];\n \n if (isScreenRecording && SCREEN_GLOW.enabled) {\n innerElements.push({\n type: \"shape\",\n track: 1,\n shape: \"rectangle\",\n width: \"100%\",\n height: \"100%\",\n fill_color: \"#0d0d0d\"\n });\n \n innerElements.push({\n type: \"shape\",\n track: 2,\n shape: \"rectangle\",\n width: \"70%\",\n height: \"60%\",\n x: \"50%\",\n y: \"50%\",\n x_anchor: \"50%\",\n y_anchor: \"50%\",\n fill_color: SCREEN_GLOW.color,\n opacity: SCREEN_GLOW.primaryOpacity,\n shadow_color: SCREEN_GLOW.color,\n shadow_blur: SCREEN_GLOW.primaryBlur,\n border_radius: \"5 vmin\"\n });\n \n innerElements.push({\n type: \"shape\",\n track: 3,\n shape: \"rectangle\",\n width: \"50%\",\n height: \"40%\",\n x: \"50%\",\n y: \"50%\",\n x_anchor: \"50%\",\n y_anchor: \"50%\",\n fill_color: SCREEN_GLOW.color,\n opacity: SCREEN_GLOW.secondaryOpacity,\n shadow_color: SCREEN_GLOW.color,\n shadow_blur: SCREEN_GLOW.secondaryBlur,\n border_radius: \"3 vmin\"\n });\n \n innerElements.push({\n type: \"shape\",\n track: 4,\n shape: \"rectangle\",\n width: \"93%\",\n height: \"89%\",\n x: \"50%\",\n y: \"50%\",\n x_anchor: \"50%\",\n y_anchor: \"50%\",\n fill_color: \"#0a0a0a\",\n border_radius: \"1.2 vmin\",\n shadow_color: \"rgba(0,0,0,0.8)\",\n shadow_blur: \"2 vmin\"\n });\n }\n \n innerElements.push({\n type: \"video\",\n track: 5,\n source: brollUrl,\n volume: 0,\n trim_start: brollTrim.trim_start,\n trim_duration: brollTrim.trim_duration,\n x: \"50%\",\n y: \"50%\",\n x_anchor: \"50%\",\n y_anchor: \"50%\",\n width: videoWidth,\n height: videoHeight,\n fit: fitMode,\n border_radius: isScreenRecording ? \"1 vmin\" : \"0\",\n ...screenMotion\n });\n \n if (isScreenRecording && SCREEN_GLOW.enabled) {\n innerElements.push({\n type: \"shape\",\n track: 6,\n shape: \"rectangle\",\n width: \"92%\",\n height: \"88%\",\n x: \"50%\",\n y: \"50%\",\n x_anchor: \"50%\",\n y_anchor: \"50%\",\n fill_color: \"rgba(0,0,0,0)\",\n border_color: \"rgba(255,255,255,0.1)\",\n border_width: \"0.3 vmin\",\n border_radius: \"1 vmin\"\n });\n }\n \n const compositionElement = {\n type: \"composition\",\n track: 5,\n time: currentTime,\n duration: duration,\n x: layout.screen.x,\n y: layout.screen.y,\n width: layout.screen.width,\n height: layout.screen.height,\n x_anchor: \"50%\",\n y_anchor: \"50%\",\n clip: true,\n elements: innerElements\n };\n \n // INTRO: First beat gets scroll-stopper zoom on screen too\n if (isIntroBeat && INTRO_EFFECT.enabled && INTRO_EFFECT.zoom.enabled) {\n const introZoom = buildIntroZoomAnimation();\n if (introZoom) compositionElement.animations = [introZoom];\n } else if (!layout.avatar && transitionAnimation) {\n compositionElement.animations = [transitionAnimation];\n }\n \n elements.push(compositionElement);\n }\n \n // ============================================================\n // DIVIDER LINE\n // ============================================================\n if (layout.divider_y) {\n elements.push({\n type: \"shape\",\n track: 6,\n time: currentTime,\n duration: duration,\n shape: \"rectangle\",\n x: \"50%\",\n y: layout.divider_y,\n width: \"100%\",\n height: \"0.5%\",\n x_anchor: \"50%\",\n y_anchor: \"50%\",\n fill_color: ACCENT_COLOR,\n shadow_color: \"rgba(0,0,0,0.5)\",\n shadow_blur: \"0.5 vmin\"\n });\n }\n \n // ============================================================\n // TEXT OVERLAY\n // ============================================================\n if (beat.text_overlay && beat.text_content) {\n const textElement = buildTextOverlay(beat.text_overlay, beat.text_content, currentTime, duration);\n if (textElement) elements.push(textElement);\n }\n \n currentTime += duration;\n avatarTrimPosition += duration;\n});\n\n// ============================================================\n// \ud83d\udcdd AUTO-CAPTIONS\n// ============================================================\nif (CAPTION_SETTINGS.enabled) {\n elements.push({\n type: \"text\",\n track: 11,\n transcript_source: \"avatar-master\",\n transcript_effect: CAPTION_SETTINGS.effect,\n transcript_maximum_length: CAPTION_SETTINGS.maxLength,\n x: \"50%\",\n y: CAPTION_SETTINGS.positionY,\n width: \"90%\",\n height: \"20%\",\n x_anchor: \"50%\",\n y_anchor: \"50%\",\n x_alignment: \"50%\",\n font_family: CAPTION_SETTINGS.fontFamily,\n font_weight: CAPTION_SETTINGS.fontWeight,\n font_size: CAPTION_SETTINGS.fontSize,\n fill_color: CAPTION_SETTINGS.fillColor,\n stroke_color: CAPTION_SETTINGS.strokeColor,\n stroke_width: CAPTION_SETTINGS.strokeWidth,\n shadow_color: CAPTION_SETTINGS.shadowColor,\n shadow_blur: CAPTION_SETTINGS.shadowBlur\n });\n}\n\n// ============================================================\n// \ud83d\udce4 OUTPUT\n// ============================================================\n\nconst outputWidth = VIDEO_SETTINGS.usePreviewSize ? VIDEO_SETTINGS.previewWidth : VIDEO_SETTINGS.width;\nconst outputHeight = VIDEO_SETTINGS.usePreviewSize ? VIDEO_SETTINGS.previewHeight : VIDEO_SETTINGS.height;\n\nconst outputScript = {\n output_format: VIDEO_SETTINGS.outputFormat,\n width: outputWidth,\n height: outputHeight,\n frame_rate: VIDEO_SETTINGS.frameRate,\n elements: elements,\n \n _debug: {\n version: \"v23.0-stepped-flash\",\n fix_notes: [\n \"FIXED: Flash now uses SEQUENTIAL COMPOSITIONS on same track\",\n \"Each composition has rgba opacity baked into fill_color\",\n \"No animations needed - stepped opacity creates smooth fade-out\",\n \"Compositions with empty elements[] act as solid color overlays\",\n \"Zoom animation uses back-out easing for punch feel\"\n ],\n total_beats: STORYBOARD.length,\n overlay_transitions: overlayTransitionCount,\n ai_broll_clips: Object.keys(AI_BROLL_URLS).length,\n avatar_duration: ACTUAL_AVATAR_DURATION,\n concept_type: conceptType,\n \n // INTRO DEBUG\n intro_effect: {\n enabled: INTRO_EFFECT.enabled,\n flash_steps_added: introFlashCount,\n zoom_applied: introZoomApplied,\n flash_config: INTRO_EFFECT.flash.enabled ? {\n step_count: INTRO_EFFECT.flash.steps.length,\n total_duration: INTRO_EFFECT.flash.steps.reduce((sum, s) => Math.max(sum, s.time + s.duration), 0),\n approach: \"sequential compositions with rgba opacity\"\n } : null,\n zoom_config: INTRO_EFFECT.zoom.enabled ? {\n start_scale: INTRO_EFFECT.zoom.startScale,\n end_scale: INTRO_EFFECT.zoom.endScale,\n duration: INTRO_EFFECT.zoom.duration,\n easing: INTRO_EFFECT.zoom.easing\n } : null\n },\n \n layouts_available: Object.keys(lib.layouts || {}),\n transitions_available: Object.keys(lib.transitions || {}),\n broll_corrections_applied: brollDurationCorrections.length\n }\n};\n\nif (ACTUAL_AVATAR_DURATION && typeof ACTUAL_AVATAR_DURATION === 'number') {\n outputScript.duration = ACTUAL_AVATAR_DURATION;\n}\n\nreturn [{ json: outputScript }];"
},
"typeVersion": 2
},
{
"id": "0c129bf6-6116-4c4f-b692-6c3091a6512a",
"name": "Wait for Avatar",
"type": "n8n-nodes-base.wait",
"position": [
4384,
1744
],
"parameters": {
"amount": 45
},
"typeVersion": 1.1
},
{
"id": "9085960a-9cac-4432-a200-76e915320c5e",
"name": "Shorts Trigger",
"type": "n8n-nodes-base.executeWorkflowTrigger",
"position": [
1072,
1664
],
"parameters": {
"inputSource": "passthrough"
},
"typeVersion": 1.1
},
{
"id": "345e3552-e9c6-4cdf-a8df-a23f865898bd",
"name": "Extract Snippets",
"type": "n8n-nodes-base.code",
"position": [
1296,
1664
],
"parameters": {
"jsCode": "// ============================================================================\n// EXTRACT SHORT FORM SNIPPETS v4.0 - WITH B-ROLL CLIPS\n// ============================================================================\n// CHANGE: Now includes key_moments (4-8 second B-roll clips) from Gemini\n// WHY: AI scriptwriter needs to know what's visually happening on screen\n// to pick timestamps with maximum movement and write accurate descriptions\n// ============================================================================\n\nconst input = $('Shorts Trigger').first().json;\n\n// Get the pre-analyzed data from parent workflow\nconst source = input.source || {};\nconst overview = input.overview || {};\nconst transcript = input.transcript || {};\nconst links = input.links || {};\n\n// NEW: B-roll clips from Gemini visual analysis (4-8 second moments)\nconst brollClips = input.key_moments || [];\nconst visualAnalysis = input.visual_analysis || {};\n\n// Sections now include nested segments[] with sentence-level timestamps\nconst sections = transcript.sections || [];\nconst segments = transcript.segments || [];\nconst bestSectionsForShorts = transcript.best_sections_for_shorts || [];\n\n// Convert sections to key_moments format (for downstream compatibility)\nconst keyMoments = sections\n .filter(section => section.hook_potential && section.hook_potential !== 'low')\n .map(section => ({\n section_index: section.index,\n timestamp_start: section.start,\n timestamp_end: section.end,\n start_formatted: section.start_formatted,\n end_formatted: section.end_formatted,\n duration_seconds: section.duration_seconds,\n description: section.summary,\n hook_potential: mapHookPotential(section.hook_potential),\n quote: section.quotable_moment || null,\n tools_shown: section.key_topics || [],\n section_title: section.title,\n section_type: section.section_type,\n segment_count: section.segment_count || (section.segments ? section.segments.length : 0)\n }));\n\n// Map hook_potential to our concept types\nfunction mapHookPotential(potential) {\n if (potential === 'high') return 'bold';\n if (potential === 'medium') return 'curiosity';\n return 'pain';\n}\n\n// Use best_sections_for_shorts as best_clips (already identified by AI!)\nconst bestClips = bestSectionsForShorts.map(sectionIndex => {\n const section = sections[sectionIndex];\n if (!section) return null;\n return {\n section_index: sectionIndex,\n start: section.start,\n end: section.end,\n start_formatted: section.start_formatted,\n end_formatted: section.end_formatted,\n duration_seconds: section.duration_seconds,\n description: section.summary,\n hook_potential: section.hook_potential,\n segment_count: section.segment_count || (section.segments ? section.segments.length : 0)\n };\n}).filter(Boolean);\n\n// If no best_sections identified, pick top 3 high-potential sections\nif (bestClips.length === 0) {\n const highPotential = sections\n .filter(s => s.hook_potential === 'high' || s.hook_potential === 'medium')\n .slice(0, 3);\n \n highPotential.forEach(section => {\n bestClips.push({\n section_index: section.index,\n start: section.start,\n end: section.end,\n start_formatted: section.start_formatted,\n end_formatted: section.end_formatted,\n duration_seconds: section.duration_seconds,\n description: section.summary,\n hook_potential: section.hook_potential,\n segment_count: section.segment_count || (section.segments ? section.segments.length : 0)\n });\n });\n}\n\n// ============================================================================\n// ENRICH B-ROLL CLIPS WITH SECTION CONTEXT\n// ============================================================================\n// Match each B-roll clip to its corresponding section for context\n// ============================================================================\n\nconst enrichedBrollClips = brollClips.map((clip, index) => {\n // Find which section this clip falls into\n const matchingSection = sections.find(s => \n clip.start_seconds >= s.start && clip.start_seconds < s.end\n );\n \n return {\n index: index,\n start: clip.start,\n end: clip.end,\n start_seconds: clip.start_seconds,\n end_seconds: clip.end_seconds,\n duration_seconds: (clip.end_seconds || 0) - (clip.start_seconds || 0),\n app: clip.app,\n action: clip.action,\n screen_description: clip.screen_description || null,\n // Section context\n section_index: matchingSection ? matchingSection.index : null,\n section_title: matchingSection ? matchingSection.title : null\n };\n});\n\nreturn [{\n json: {\n // Video metadata\n video_id: source.video_id,\n video_name: source.video_name,\n video_url: source.video_url,\n duration: source.duration_seconds,\n duration_formatted: source.duration_formatted,\n \n // Pre-analyzed overview (no more re-summarizing!)\n video_summary: overview.summary,\n one_liner: overview.one_liner,\n main_argument: overview.main_argument,\n target_audience: overview.target_audience,\n content_style: overview.content_style,\n tone: overview.tone,\n key_takeaways: overview.key_takeaways || [],\n problems_addressed: overview.problems_addressed || [],\n tools_mentioned: overview.tools_mentioned || [],\n frameworks_explained: overview.frameworks_explained || [],\n \n // Section-based moments (exact timestamps!)\n key_moments: keyMoments,\n best_clips: bestClips,\n \n // =========================================================================\n // NEW: B-ROLL CLIPS FROM GEMINI VISUAL ANALYSIS (v4 UPDATE)\n // 4-8 second clips with high visual movement - use these for body_segments!\n // =========================================================================\n broll_clips: enrichedBrollClips,\n broll_count: enrichedBrollClips.length,\n visual_analysis: {\n enabled: visualAnalysis.enabled || brollClips.length > 0,\n total_clips: brollClips.length,\n unique_apps: visualAnalysis.unique_apps || [...new Set(brollClips.map(c => c.app).filter(Boolean))],\n total_broll_time: enrichedBrollClips.reduce((sum, c) => sum + (c.duration_seconds || 0), 0),\n coverage_percentage: visualAnalysis.coverage_percentage || null\n },\n \n // Main topics\n main_topics: transcript.main_topics || [],\n best_sections_for_shorts: bestSectionsForShorts,\n \n // =========================================================================\n // FULL SECTION DATA WITH NESTED SEGMENTS\n // Each section now contains segments[] array with sentence-level timestamps\n // =========================================================================\n sections: sections,\n \n // Stats\n segment_count: transcript.segment_count,\n section_count: transcript.section_count,\n \n // Pass through links for CTAs\n links: links\n }\n}];"
},
"typeVersion": 2
},
{
"id": "dddc1e5b-4bfa-476a-a8de-4d565c6727a4",
"name": "Creatomate Effects Library",
"type": "n8n-nodes-base.code",
"position": [
3488,
1648
],
"parameters": {
"jsCode": "// ============================================================\n// CREATOMATE EFFECTS LIBRARY v2.0 - ENHANCED TRANSITIONS\n// ============================================================\n// Place this node BEFORE the AI Video Director node\n// \n// CHANGELOG v2.0:\n// - Added 6 NEW transition effects for high-retention content:\n// * RGB_GLITCH - Red/Cyan color channel split with position jitter\n// * ACCENT_FLASH - Brand color flash before white (depth effect)\n// * GLITCH_BARS - Moving horizontal scan lines (cyan/magenta)\n// * ZOOM_SHAKE - Flash with scale jitter and position shake\n// * DOUBLE_FLASH - Quick double-tap flash for punchy moments\n// * WHIP_PAN - Flash sweeps horizontally across screen\n// - Removed unused transitions: WIPE_LEFT_FAST, WIPE_RIGHT_FAST, WIPE_UP, FADE_QUICK, CUT\n// - Added transition variety guidelines\n//\n// CHANGELOG v1.9:\n// - Added SPLIT_70_30 layout (screen 70% top, avatar 30% bottom)\n// - Added SCREEN_PADDED layout (fullscreen screen recording with centered padding and glow)\n// ============================================================\n\nreturn [{\n json: {\n // ============================================================\n // LAYOUT LIBRARY\n // ============================================================\n layouts: {\n \n AVATAR_FULLSCREEN: {\n id: \"AVATAR_FULLSCREEN\",\n description: \"Avatar fills entire frame - hooks and CTAs ONLY\",\n avatar: { x: \"50%\", y: \"50%\", width: \"100%\", height: \"100%\" },\n screen: null,\n divider_y: null,\n best_for: [\"hook_snap\", \"cta_close\"],\n energy: \"high\",\n restriction: \"ONLY use for first beat (hook_snap) and last beat (cta_close)\"\n },\n \n SCREEN_FULLSCREEN: {\n id: \"SCREEN_FULLSCREEN\",\n description: \"AI B-roll fills entire frame edge-to-edge - cinematic moments\",\n avatar: null,\n screen: { x: \"50%\", y: \"50%\", width: \"100%\", height: \"100%\" },\n divider_y: null,\n is_padded_screen: false,\n best_for: [\"ai_broll\", \"cinematic_moments\", \"dramatic_reveals\"],\n energy: \"high\",\n restriction: \"Use ONLY with broll_type: ai_generated\"\n },\n \n SCREEN_PADDED: {\n id: \"SCREEN_PADDED\",\n description: \"Fullscreen screen recording with centered padding and glow - for widescreen B-roll without avatar\",\n avatar: null,\n screen: { x: \"50%\", y: \"50%\", width: \"100%\", height: \"100%\" },\n divider_y: null,\n is_padded_screen: true,\n best_for: [\"screen_broll_fullscreen\", \"desktop_recordings\", \"widescreen_demos\", \"detailed_walkthroughs\"],\n energy: \"medium\",\n restriction: \"Use ONLY with broll_type: screen\"\n },\n \n SPLIT_50_50: {\n id: \"SPLIT_50_50\",\n description: \"Screen 50% top, Avatar 50% bottom - equal split\",\n avatar: { x: \"50%\", y: \"75%\", width: \"100%\", height: \"50%\" },\n screen: { x: \"50%\", y: \"25%\", width: \"100%\", height: \"50%\" },\n divider_y: \"50%\",\n best_for: [\"balanced_demos\", \"conversational_teaching\", \"screen_with_presence\"],\n energy: \"medium\"\n },\n \n SPLIT_60_40: {\n id: \"SPLIT_60_40\",\n description: \"Screen 60% top, Avatar 40% bottom - screen-dominant\",\n avatar: { x: \"50%\", y: \"80%\", width: \"100%\", height: \"40%\" },\n screen: { x: \"50%\", y: \"30%\", width: \"100%\", height: \"60%\" },\n divider_y: \"60%\",\n best_for: [\"standard_demos\", \"code_walkthroughs\", \"body_flash\", \"transitions\"],\n energy: \"medium\"\n },\n \n SPLIT_70_30: {\n id: \"SPLIT_70_30\",\n description: \"Screen 70% top, Avatar 30% bottom - maximum screen focus\",\n avatar: { x: \"50%\", y: \"85%\", width: \"100%\", height: \"30%\" },\n screen: { x: \"50%\", y: \"35%\", width: \"100%\", height: \"70%\" },\n divider_y: \"70%\",\n best_for: [\"detailed_demos\", \"code_focus\", \"body_demo\", \"screen_heavy_content\", \"complex_ui\"],\n energy: \"medium\"\n }\n },\n\n // ============================================================\n // TRANSITION LIBRARY\n // ============================================================\n transitions: {\n \n // ===========
Credentials you'll need
Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.
googleDriveOAuth2ApigooglePalmApigoogleSheetsOAuth2ApihttpHeaderAuthopenAiApi
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
This workflow is a fully automated YouTube Shorts production pipeline. It takes the structured output from a video digestion workflow (transcript, key moments, metadata) and produces finished, rendered vertical shorts complete with AI-generated avatar narration, AI-generated…
Source: https://n8n.io/workflows/13676/ — original creator credit. Request a take-down →
Related workflows
Workflows that share integrations, category, or trigger type with this one. All free to copy and import.
🎯 Create viral TikToks, Shorts, Reels, podcasts, and ASMR videos in minutes — all on autopilot.
Generate AI viral videos with NanoBanana & VEO3, shared on socials via Blotato 2. Uses @blotato/n8n-nodes-blotato, googleSheets, lmChatOpenAi, toolThink. Event-driven trigger; 94 nodes.
How it Works
The best content automation template in the market is now even better—with “deep research” on time-sensitive topics\! Unlike most n8n content automation templates that are mainly for “demo purposes,”
This template is designed for marketers, content creators, and e-commerce brands who want to automate the creation of professional ad videos at scale. It’s ideal for teams looking to generate consiste