{
  "id": "FszwgV9nOsYJ0wiC",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "Generate Youtube video shorts and upload",
  "tags": [],
  "nodes": [
    {
      "id": "6de813c9-6a82-46db-877e-df391286b3fb",
      "name": "Pexels",
      "type": "n8n-nodes-base.httpRequest",
      "notes": "Free API key from pixabay.com/api/docs\nReplace YOUR_PIXABAY_KEY with your actual key.\nReturns vertical video URLs.",
      "position": [
        272,
        176
      ],
      "parameters": {
        "url": "=https://api.pexels.com/videos/search?query={{ encodeURIComponent($json.keyword) }}&min_duration=55&max_duration=65&orientation=portrait&per_page=1",
        "options": {},
        "sendHeaders": true,
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "headerParameters": {
          "parameters": []
        }
      },
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4
    },
    {
      "id": "56d28b28-a1f4-4243-99e5-0d86446893fe",
      "name": "Merge",
      "type": "n8n-nodes-base.merge",
      "position": [
        544,
        -16
      ],
      "parameters": {
        "mode": "combine",
        "options": {},
        "combineBy": "combineAll"
      },
      "typeVersion": 3.2
    },
    {
      "id": "2fb60b2f-8ecc-4074-9478-417b97d57916",
      "name": "Check Merge Status",
      "type": "n8n-nodes-base.if",
      "position": [
        1408,
        -16
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "383d112e-2cc6-4dd4-8985-f09ce0bd1781",
              "operator": {
                "name": "filter.operator.equals",
                "type": "string",
                "operation": "equals"
              },
              "leftValue": "={{ $json.status }}",
              "rightValue": "COMPLETED"
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "48e55db7-24da-4dd5-bec2-a6c8ab133cfc",
      "name": "Wait 30 Seconds for Merge",
      "type": "n8n-nodes-base.wait",
      "position": [
        960,
        -16
      ],
      "parameters": {
        "amount": 30
      },
      "typeVersion": 1.1
    },
    {
      "id": "7ac2b8d9-79a2-4fa6-88d4-c33114ce8e6e",
      "name": "Merge Audio with Video",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        736,
        -16
      ],
      "parameters": {
        "url": "https://queue.fal.run/fal-ai/ffmpeg-api/merge-audio-video",
        "method": "POST",
        "options": {},
        "jsonBody": "={\n  \"video_url\":\"{{ $json.videos[0].video_files[0].link }}\",\n  \"audio_url\":\"{{ $json.audio_url }}\"\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.3
    },
    {
      "id": "ccf48742-d99e-4420-a60d-e5971a7015d8",
      "name": "Check Video Merge Status",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1184,
        -80
      ],
      "parameters": {
        "url": "=https://queue.fal.run/fal-ai/ffmpeg-api/requests/{{ $('Merge Audio with Video').item.json.request_id }}/status ",
        "options": {},
        "sendQuery": true,
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "queryParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        }
      },
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "8e90cde3-8e44-41bc-b99f-8c20be9afbfd",
      "name": "Fetch Final Video",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1632,
        -16
      ],
      "parameters": {
        "url": "=https://queue.fal.run/fal-ai/ffmpeg-api/requests/{{ $json.request_id }}",
        "options": {},
        "sendHeaders": true,
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "headerParameters": {
          "parameters": [
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        }
      },
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "6a878e67-fa63-457c-acac-4a28640fc088",
      "name": "Fetch Video File from URL",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1856,
        -16
      ],
      "parameters": {
        "url": "={{ $json.video.url }}",
        "options": {}
      },
      "typeVersion": 4.4
    },
    {
      "id": "5e9a4fad-843f-4110-aee6-a7a224be26aa",
      "name": "Set Audio Url",
      "type": "n8n-nodes-base.set",
      "position": [
        272,
        -96
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "29fbf21e-301d-4a1c-b6c9-723c9c03fb61",
              "name": "audio_url",
              "type": "string",
              "value": "=https://n3wstorage.b-cdn.net/test/{{ $binary.data.fileName }}.{{ $binary.data.fileExtension }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "61b7cb79-045c-4bb7-9fd3-5e7ae3070374",
      "name": "Upload a video",
      "type": "n8n-nodes-base.youTube",
      "position": [
        2080,
        -16
      ],
      "parameters": {
        "title": "={{ $('Generate voiceover').item.json.output.title }}",
        "options": {
          "description": "={{ $('Set vars').item.json.summary }}"
        },
        "resource": "video",
        "operation": "upload",
        "categoryId": "17",
        "regionCode": "IT"
      },
      "credentials": {
        "youTubeOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "c82cbcbe-5457-4355-87dd-bb4bb572bf84",
      "name": "OpenAI Chat Model",
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
      "position": [
        -720,
        208
      ],
      "parameters": {
        "model": {
          "__rl": true,
          "mode": "list",
          "value": "gpt-5.4",
          "cachedResultName": "gpt-5.4"
        },
        "options": {},
        "builtInTools": {}
      },
      "credentials": {
        "openAiApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.3
    },
    {
      "id": "479f6f2f-1ddc-473b-8363-0d98d317399f",
      "name": "On form submission",
      "type": "n8n-nodes-base.formTrigger",
      "position": [
        -1184,
        -16
      ],
      "parameters": {
        "options": {},
        "formTitle": "Short-form vertical video content",
        "formFields": {
          "values": [
            {
              "fieldType": "textarea",
              "fieldLabel": "Prompt"
            }
          ]
        },
        "formDescription": "Short-form vertical video content (YouTube Shorts / Reels / TikTok)"
      },
      "typeVersion": 2.6
    },
    {
      "id": "0bf4dc35-8123-4ca6-99ea-f668492a8e72",
      "name": "Set vars",
      "type": "n8n-nodes-base.set",
      "position": [
        -960,
        -16
      ],
      "parameters": {
        "values": {
          "string": [
            {
              "name": "summary",
              "value": "={{ $json.Prompt }}"
            },
            {
              "name": "channelName",
              "value": "XXX"
            }
          ]
        },
        "options": {},
        "keepOnlySet": true
      },
      "typeVersion": 1
    },
    {
      "id": "3e9df2f0-598e-43d4-a046-19e0ea804de1",
      "name": "Structured Output Parser",
      "type": "@n8n/n8n-nodes-langchain.outputParserStructured",
      "position": [
        -512,
        192
      ],
      "parameters": {
        "schemaType": "manual",
        "inputSchema": "{\n  \"type\": \"object\",\n  \"properties\": {\n    \"title\": {\n      \"type\": \"string\",\n      \"description\": \"Short scroll-stopping title for the video, max 60 characters\"\n    },\n    \"script\": {\n      \"type\": \"string\",\n      \"description\": \"Spoken voiceover script, max 150 words, plain text only\"\n    },\n    \"keyword\": {\n      \"type\": \"string\",\n      \"description\": \"1-4 word English keyword for stock footage search\"\n    }\n  }\n}"
      },
      "typeVersion": 1.3
    },
    {
      "id": "0af8d644-460e-4132-b6d3-d510f59a7a45",
      "name": "Generate voiceover",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "position": [
        -688,
        -16
      ],
      "parameters": {
        "text": "=Summary: {{ $json.summary }}\nChannel: {{ $json.channelName }}",
        "options": {
          "systemMessage": "You are a scriptwriter for short-form vertical video content (YouTube Shorts / Reels / TikTok).\nWrite a punchy 60-second voiceover script based on the title and summary below, plus a title and a footage search keyword for the finished video.\n\nTASK\nProduce three things from the input above:\n1. A short, scroll-stopping title for the video.\n2. A single spoken-word script meant to be read aloud in under 60 seconds.\n3. A keyword to search stock footage that visually matches the script.\n\nHARD RULES \u2014 SCRIPT\n- The first three words must hook the listener: a surprising fact, a bold claim, a question, or a direct command. Never open with \"Imagine\", \"In a world\", \"Did you know\", or any generic intro.\n- Maximum 150 words total, including the closing line.\n- Plain spoken text only. No stage directions, no parentheses, no brackets, no camera or visual cues, no emojis, no hashtags, no labels like \"Voiceover:\" or \"Scene:\".\n- No filler words (basically, actually, just, really, very, kind of, sort of, etc.).\n- Short, punchy sentences. Vary rhythm: mix very short fragments with slightly longer lines.\n- Tone: confident and direct, matched to the subject in the summary \u2014 serious for serious topics, energetic for lighter ones.\n- End with exactly this line, replacing the placeholder with the channel name above: Follow {{ $json.channelName }} for more\n\nHARD RULES \u2014 TITLE\n- Maximum 60 characters.\n- No clickbait that misrepresents the content. No emojis, no hashtags, no quotation marks.\n- Must reflect the actual content of the script, not just the input title.\n\nHARD RULES \u2014 KEYWORD\n- 1 to 4 words describing a concrete, filmable visual (an object, a place, an action, a scene) that matches the mood and topic of the script.\n- No abstract concepts (e.g. not \"innovation\" or \"freedom\") \u2014 only things a stock footage search can return real clips for (e.g. \"city traffic at night\", \"hands typing keyboard\").\n- English only, regardless of the input language, since stock libraries are indexed in English.\n\nBEFORE YOU ANSWER\nCount the words in the script. If over 150, cut whole sentences \u2014 never compress ideas into broken grammar."
        },
        "promptType": "define",
        "hasOutputParser": true
      },
      "typeVersion": 3.1
    },
    {
      "id": "d8ec9c12-db6c-444d-b3af-8cc1325a3d32",
      "name": "Voiceover vars",
      "type": "n8n-nodes-base.set",
      "position": [
        -384,
        -16
      ],
      "parameters": {
        "values": {
          "string": [
            {
              "name": "script",
              "value": "={{ $json.output.script }}"
            },
            {
              "name": "keyword",
              "value": "={{ $json.output.keyword }}"
            },
            {
              "name": "title",
              "value": "={{ $json.output.title }}"
            }
          ]
        },
        "options": {},
        "keepOnlySet": true
      },
      "typeVersion": 1
    },
    {
      "id": "aab242e7-45f4-4625-aecd-1eeee3e3b99d",
      "name": "Text-to-Speech",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -160,
        -112
      ],
      "parameters": {
        "url": "https://api.elevenlabs.io/v1/text-to-speech/Kq9pDHHIMmJsG9PEqOtv",
        "method": "POST",
        "options": {},
        "sendBody": true,
        "sendHeaders": true,
        "authentication": "genericCredentialType",
        "bodyParameters": {
          "parameters": [
            {
              "name": "text",
              "value": "={{ $json.script }}"
            },
            {
              "name": "model_id",
              "value": "eleven_v3"
            }
          ]
        },
        "genericAuthType": "httpHeaderAuth",
        "headerParameters": {
          "parameters": [
            {
              "name": "Accept",
              "value": "audio/mpeg"
            }
          ]
        }
      },
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.4
    },
    {
      "id": "cfd86998-468b-4fa7-87dc-ab816620a267",
      "name": "Upload audio",
      "type": "n8n-nodes-base.ftp",
      "position": [
        64,
        -112
      ],
      "parameters": {
        "path": "=/PATH/{{ $binary.data.fileName}}.{{$binary.data.fileExtension }}",
        "options": {},
        "operation": "upload"
      },
      "credentials": {
        "ftp": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "c2f74345-a15a-41a2-a47d-e475d4461608",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1232,
        -1024
      ],
      "parameters": {
        "width": 992,
        "height": 672,
        "content": "## Generate Youtube video shorts\n\n### How it works\n\n1. The workflow starts from a form submission and sets the input fields for the short-video generation process.\n2. It uses an AI agent to generate voiceover content, then sends that text to text-to-speech and uploads the resulting audio file.\n3. It searches Pexels for matching video clips and merges the chosen footage with the uploaded audio.\n4. It repeatedly checks the merge job status, waits when needed, and fetches the finished video when rendering is complete.\n5. It downloads the final video file and uploads it to YouTube as a short.\n\n### Setup steps\n\n* [ ] Connect and configure the form trigger so it collects the fields needed to generate each short.\n* [ ] Set up the OpenAI credentials for the AI Agent and verify the structured output parser matches the expected voiceover fields.\n* [ ] Configure the ElevenLabs text-to-speech request, including API key, voice settings, and output format.\n* [ ] Configure the FTP upload target used for storing the generated audio file and ensure the returned audio URL is mapped correctly.\n* [ ] Add a valid Pexels API key and confirm the video search query uses the generated prompt or keywords.\n* [ ] Configure the Fal.ai ffmpeg queue endpoints for merging audio and video, including polling and final file retrieval.\n* [ ] Connect the YouTube credential/account for the upload step and verify the video privacy, title, and short-form settings.\n\n### Customization\n\nYou can customize the voice style, Pexels search query, merge timing, and YouTube upload metadata to fit different short-video formats.\n"
      },
      "typeVersion": 1
    },
    {
      "id": "61b3fcb3-1137-4279-aec0-97c38f5e2460",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1248,
        -304
      ],
      "parameters": {
        "color": 7,
        "width": 464,
        "height": 688,
        "content": "## Form input and setup\n\nCollects the initial form submission and maps the incoming fields into workflow variables"
      },
      "typeVersion": 1
    },
    {
      "id": "89bc080a-6cdb-4cbb-b03d-8794383aeaa9",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -752,
        -304
      ],
      "parameters": {
        "color": 7,
        "width": 512,
        "height": 688,
        "content": "## Generate voiceover scrip\n\nUses an AI agent with OpenAI and structured parsing to produce the voiceover content for the short."
      },
      "typeVersion": 1
    },
    {
      "id": "00a04568-f3fb-474a-b52f-83ee96eba485",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -208,
        -304
      ],
      "parameters": {
        "color": 7,
        "width": 672,
        "height": 688,
        "content": "## Prepare voiceover assets\n\nReshapes the generated voiceover output into variables, sends text to speech, uploads the audio, and stores the resulting audio URL for later merging.\nSearches Pexels for suitable video clips and combines the selected result with the prepared audio input before the render step."
      },
      "typeVersion": 1
    },
    {
      "id": "41957cad-e76d-4c67-b9fa-b6fcb79e2c38",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        496,
        -304
      ],
      "parameters": {
        "color": 7,
        "width": 1280,
        "height": 688,
        "content": "## Render merged video\n\nStarts the audio-video merge job and polls the render service until the output is ready."
      },
      "typeVersion": 1
    },
    {
      "id": "e0e5a378-cb69-4d46-87e4-f16525a95b10",
      "name": "Sticky Note5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1808,
        -304
      ],
      "parameters": {
        "color": 7,
        "width": 496,
        "height": 688,
        "content": "## Download and upload video\n\nFetches the finished video file from the render URL and uploads it to YouTube."
      },
      "typeVersion": 1
    },
    {
      "id": "126831f4-3f30-4e4c-bf0b-872258d6153d",
      "name": "Sticky Note8",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -192,
        -1136
      ],
      "parameters": {
        "color": 7,
        "width": 736,
        "height": 784,
        "content": "## MY NEW YOUTUBE CHANNEL\n\ud83d\udc49 [Subscribe to my new **YouTube channel**](https://youtube.com/@n3witalia). Here I\u2019ll share videos and Shorts with practical tutorials and **FREE templates for n8n**.\n\n[![image](https://n3wstorage.b-cdn.net/n3witalia/youtube-n8n-cover.jpg)](https://youtube.com/@n3witalia)"
      },
      "typeVersion": 1
    }
  ],
  "active": false,
  "settings": {
    "binaryMode": "separate",
    "availableInMCP": false,
    "executionOrder": "v1"
  },
  "versionId": "59ec09dd-7194-46f1-82dd-dc1c97d10905",
  "nodeGroups": [],
  "connections": {
    "Merge": {
      "main": [
        [
          {
            "node": "Merge Audio with Video",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Pexels": {
      "main": [
        [
          {
            "node": "Merge",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Set vars": {
      "main": [
        [
          {
            "node": "Generate voiceover",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Upload audio": {
      "main": [
        [
          {
            "node": "Set Audio Url",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set Audio Url": {
      "main": [
        [
          {
            "node": "Merge",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Text-to-Speech": {
      "main": [
        [
          {
            "node": "Upload audio",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Voiceover vars": {
      "main": [
        [
          {
            "node": "Text-to-Speech",
            "type": "main",
            "index": 0
          },
          {
            "node": "Pexels",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Final Video": {
      "main": [
        [
          {
            "node": "Fetch Video File from URL",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "OpenAI Chat Model": {
      "ai_languageModel": [
        [
          {
            "node": "Generate voiceover",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Check Merge Status": {
      "main": [
        [
          {
            "node": "Fetch Final Video",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Wait 30 Seconds for Merge",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate voiceover": {
      "main": [
        [
          {
            "node": "Voiceover vars",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "On form submission": {
      "main": [
        [
          {
            "node": "Set vars",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge Audio with Video": {
      "main": [
        [
          {
            "node": "Wait 30 Seconds for Merge",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check Video Merge Status": {
      "main": [
        [
          {
            "node": "Check Merge Status",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Structured Output Parser": {
      "ai_outputParser": [
        [
          {
            "node": "Generate voiceover",
            "type": "ai_outputParser",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Video File from URL": {
      "main": [
        [
          {
            "node": "Upload a video",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait 30 Seconds for Merge": {
      "main": [
        [
          {
            "node": "Check Video Merge Status",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}