AutomationFlowsAI & RAG › Generate Faceless Video Scripts From a Topic with Gemini or Claude Haiku

Generate Faceless Video Scripts From a Topic with Gemini or Claude Haiku

ByFaceless Channel OS @trungcln on n8n.io

This workflow collects a topic and settings via an n8n form, generates a short scene-by-scene faceless video script using either Google Gemini or Anthropic Claude Haiku, then normalizes the model output into a consistent JSON structure and outputs one item per scene. Receives a…

Event trigger★★★★☆ complexity13 nodesForm TriggerHTTP Request
AI & RAG Trigger: Event Nodes: 13 Complexity: ★★★★☆ Added:

This workflow corresponds to n8n.io template #17408 — we link there as the canonical source.

This workflow follows the Form Trigger → HTTP Request 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 →

Download .json
{
  "name": "Turn one topic into a scene-by-scene faceless video script (JSON)",
  "nodes": [
    {
      "id": "overview",
      "name": "Overview",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -720,
        -420
      ],
      "parameters": {
        "color": 1,
        "width": 660,
        "height": 1180,
        "content": "## Turn one topic into a scene-by-scene faceless video script (JSON)\n\nType a topic, pick a language and how many scenes, and get back a clean JSON script: one `voice_over` line plus one English `image_prompt` per scene. This is the writing half of a faceless-video pipeline \u2014 feed the output straight into a voiceover step and an image step to build the full video.\n\nTwo model options: **Gemini** (has a free tier) or **Claude Haiku** (cheap, paid). A router sends your choice to the right API, and a parser normalises either reply into the same `{ \"scenes\": [...] }` shape, then splits it so every downstream node gets one scene at a time.\n\n### How it works\n\n1. A form takes your topic, language, scene count, and model choice.\n2. A Code node validates the language and sets a per-scene word budget so the finished video stays under 60 seconds (a Shorts limit).\n3. A switch routes to Gemini or Claude, each asked to return strict JSON.\n4. A parser reads either provider's response shape, strips code fences, and validates the scenes array.\n5. Split Out emits one item per scene, ready to pipe into TTS + image generation.\n\n### Setup\n\n- Add your API key as a credential on the model node you use: Gemini via a **Query Auth** credential (`key`), Claude via a **Header Auth** credential (`x-api-key`). No keys are hardcoded.\n- Works on n8n Cloud and self-hosted \u2014 this template is HTTP only, no Execute Command.\n\n### Customization tips\n\nChange `TOTAL_WORDS` in the first Code node to make scripts longer or shorter. Swap in any other chat model by editing one HTTP node and adding its response shape to the parser.\n"
      },
      "typeVersion": 1
    },
    {
      "id": "section---input",
      "name": "Section - input",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -40,
        -180
      ],
      "parameters": {
        "color": 7,
        "width": 240,
        "height": 420,
        "content": "## 1. Input\nTopic, language, number of scenes, and which model to use."
      },
      "typeVersion": 1
    },
    {
      "id": "section---budget",
      "name": "Section - budget",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        200,
        -180
      ],
      "parameters": {
        "color": 7,
        "width": 240,
        "height": 420,
        "content": "## 2. Budget\nValidates the language and sets a per-scene word cap for a <60s Short."
      },
      "typeVersion": 1
    },
    {
      "id": "section---model",
      "name": "Section - model",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        640,
        -320
      ],
      "parameters": {
        "color": 7,
        "width": 300,
        "height": 640,
        "content": "## 3. Write the script\nRouter sends your choice to Gemini (free tier) or Claude Haiku. Both return strict JSON."
      },
      "typeVersion": 1
    },
    {
      "id": "section---parse",
      "name": "Section - parse",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        960,
        -180
      ],
      "parameters": {
        "color": 7,
        "width": 460,
        "height": 420,
        "content": "## 4. Parse + split\nNormalises either reply into one scenes array, then emits one scene per item."
      },
      "typeVersion": 1
    },
    {
      "id": "note---add-api-key",
      "name": "Note - add api key",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        640,
        320
      ],
      "parameters": {
        "color": 3,
        "width": 300,
        "height": 140,
        "content": "## Add your API key\nAttach a credential to the model node you pick. Nothing is hardcoded."
      },
      "typeVersion": 1
    },
    {
      "id": "form-trigger",
      "name": "Video idea form",
      "type": "n8n-nodes-base.formTrigger",
      "notes": "Entry point. Topic, language, scene count and model choice flow downstream from here.",
      "position": [
        0,
        0
      ],
      "parameters": {
        "options": {},
        "formTitle": "Topic to faceless video script (JSON)",
        "formFields": {
          "values": [
            {
              "fieldType": "text",
              "fieldLabel": "Topic",
              "requiredField": true
            },
            {
              "fieldType": "dropdown",
              "fieldLabel": "Language",
              "fieldOptions": {
                "values": [
                  {
                    "option": "English (US)"
                  },
                  {
                    "option": "Vietnamese"
                  },
                  {
                    "option": "Spanish"
                  },
                  {
                    "option": "French"
                  },
                  {
                    "option": "German"
                  },
                  {
                    "option": "Portuguese (Brazil)"
                  },
                  {
                    "option": "Indonesian"
                  }
                ]
              },
              "requiredField": true
            },
            {
              "fieldType": "dropdown",
              "fieldLabel": "Number of Scenes",
              "fieldOptions": {
                "values": [
                  {
                    "option": "2"
                  },
                  {
                    "option": "3"
                  },
                  {
                    "option": "4"
                  },
                  {
                    "option": "5"
                  },
                  {
                    "option": "6"
                  },
                  {
                    "option": "7"
                  },
                  {
                    "option": "8"
                  },
                  {
                    "option": "9"
                  },
                  {
                    "option": "10"
                  }
                ]
              },
              "requiredField": true
            },
            {
              "fieldType": "dropdown",
              "fieldLabel": "LLM Provider",
              "fieldOptions": {
                "values": [
                  {
                    "option": "Gemini (free)"
                  },
                  {
                    "option": "Claude (Haiku)"
                  }
                ]
              },
              "requiredField": true
            }
          ]
        },
        "formDescription": "Enter a topic and pick your options. You get back a scene-by-scene script as JSON: one voice_over line and one English image_prompt per scene, sized to fit a video under 60 seconds."
      },
      "typeVersion": 2.2
    },
    {
      "id": "prepare",
      "name": "Set language and word budget",
      "type": "n8n-nodes-base.code",
      "notes": "Validates the language and computes a per-scene word cap so the finished video stays under 60s. Edit TOTAL_WORDS to change script length.",
      "position": [
        220,
        0
      ],
      "parameters": {
        "jsCode": "// Validate the language and set a per-scene word budget that keeps the finished\n// video under 60s (so it still counts as a YouTube Short).\n//\n// Only languages that put SPACES between words belong here: a downstream karaoke /\n// caption step splits text on whitespace, so Chinese/Japanese/Thai would break.\nconst LANGUAGES = {\n  'English (US)':        'English',\n  'Vietnamese':          'Vietnamese',\n  'Spanish':             'Spanish',\n  'French':              'French',\n  'German':              'German',\n  'Portuguese (Brazil)': 'Brazilian Portuguese',\n  'Indonesian':          'Indonesian',\n};\n\nconst picked = $json['Language'];\nconst languageName = LANGUAGES[picked];\nif (!languageName) {\n  throw new Error(`Unsupported language: \"${picked}\". Add it to the LANGUAGES map in this node.`);\n}\n\n// Word budget = what keeps the script short enough for a <60s Short.\n// A typical TTS voice speaks ~2.32 words/sec including pauses; aim at ~55s => ~128 words\n// for the WHOLE video, split across the scenes as a hard per-scene cap handed to the LLM.\n// Raise TOTAL_WORDS if you don't care about the 60s Shorts limit (~2.32 words = 1 second).\nconst TOTAL_WORDS = 125;\nconst scenes = Number($json['Number of Scenes']) || 5;\nconst wordsPerScene = Math.max(8, Math.floor(TOTAL_WORDS / scenes));\n\nreturn { json: { ...$json, languageName, wordsPerScene } };\n"
      },
      "typeVersion": 2
    },
    {
      "id": "router",
      "name": "Route to the chosen model",
      "type": "n8n-nodes-base.switch",
      "notes": "Sends the item to Gemini or Claude based on the LLM Provider field.",
      "position": [
        460,
        0
      ],
      "parameters": {
        "mode": "rules",
        "rules": {
          "values": [
            {
              "outputKey": "Gemini",
              "conditions": {
                "conditions": [
                  {
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "leftValue": "={{ $json['LLM Provider'] }}",
                    "rightValue": "Gemini (free)"
                  }
                ]
              }
            },
            {
              "outputKey": "Claude",
              "conditions": {
                "conditions": [
                  {
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "leftValue": "={{ $json['LLM Provider'] }}",
                    "rightValue": "Claude (Haiku)"
                  }
                ]
              }
            }
          ]
        },
        "options": {
          "fallbackOutput": "none"
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "gen-gemini",
      "name": "Generate script (Gemini, free tier)",
      "type": "n8n-nodes-base.httpRequest",
      "notes": "Add a Query Auth credential with name 'key' = your Gemini API key. responseMimeType json + thinkingBudget 0 make it return strict JSON fast.",
      "position": [
        740,
        -120
      ],
      "parameters": {
        "url": "https://generativelanguage.googleapis.com/v1beta/models/gemini-flash-latest:generateContent",
        "method": "POST",
        "options": {
          "response": {
            "response": {
              "responseFormat": "json"
            }
          }
        },
        "jsonBody": "={{ JSON.stringify({ contents: [ { parts: [ { text: `You are a short-form video scriptwriter. Write a video script in ${$json.languageName} about the topic: \\\"${$json['Topic']}\\\". Split it into exactly ${$json['Number of Scenes'] || 5} scenes. Each voice_over MUST be at most ${$json.wordsPerScene} words - this is a hard limit, the finished video has to stay under 60 seconds. Return ONLY valid JSON, no explanation, matching this schema: {\\\"scenes\\\": [{\\\"voice_over\\\": \\\"at most ${$json.wordsPerScene} words in ${$json.languageName}\\\", \\\"image_prompt\\\": \\\"describe the illustration in ENGLISH\\\"}]}` } ] } ], generationConfig: { responseMimeType: 'application/json', maxOutputTokens: 4096, thinkingConfig: { thinkingBudget: 0 } } }) }}",
        "sendBody": true,
        "contentType": "json",
        "specifyBody": "json",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpQueryAuth"
      },
      "typeVersion": 4.4
    },
    {
      "id": "gen-claude",
      "name": "Generate script (Claude Haiku)",
      "type": "n8n-nodes-base.httpRequest",
      "notes": "Add a Header Auth credential with name 'x-api-key' = your Anthropic API key. A json_schema output forces valid JSON scenes.",
      "position": [
        740,
        120
      ],
      "parameters": {
        "url": "https://api.anthropic.com/v1/messages",
        "method": "POST",
        "options": {
          "response": {
            "response": {
              "responseFormat": "json"
            }
          }
        },
        "jsonBody": "={{ JSON.stringify({ model: 'claude-haiku-4-5', max_tokens: 4096, system: `You are a short-form video scriptwriter writing in ${$json.languageName}.`, messages: [ { role: 'user', content: `Write a short video script in ${$json.languageName} about the topic: \\\"${$json['Topic']}\\\". Split it into exactly ${$json['Number of Scenes'] || 5} scenes. Each voice_over MUST be at most ${$json.wordsPerScene} words - this is a hard limit, the finished video has to stay under 60 seconds. Each scene also needs an image_prompt describing the illustration in ENGLISH for an AI image generator.` } ], output_config: { format: { type: 'json_schema', schema: { type: 'object', properties: { scenes: { type: 'array', items: { type: 'object', properties: { voice_over: { type: 'string' }, image_prompt: { type: 'string' } }, required: ['voice_over','image_prompt'], additionalProperties: false } } }, required: ['scenes'], additionalProperties: false } } } }) }}",
        "sendBody": true,
        "contentType": "json",
        "sendHeaders": true,
        "specifyBody": "json",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "headerParameters": {
          "parameters": [
            {
              "name": "anthropic-version",
              "value": "2023-06-01"
            }
          ]
        }
      },
      "typeVersion": 4.4
    },
    {
      "id": "parse",
      "name": "Parse the script JSON",
      "type": "n8n-nodes-base.code",
      "notes": "Reads either provider's response shape, strips ``` fences, and validates a non-empty scenes array before continuing.",
      "position": [
        1020,
        0
      ],
      "parameters": {
        "jsCode": "const raw = $input.first().json;\nlet content;\nif (raw.candidates) {                                // Gemini\n  content = raw.candidates[0] && raw.candidates[0].content && raw.candidates[0].content.parts && raw.candidates[0].content.parts[0] && raw.candidates[0].content.parts[0].text;\n} else if (Array.isArray(raw.content)) {             // Claude\n  const textBlock = raw.content.find((b) => b.type === 'text');\n  content = textBlock && textBlock.text;\n} else if (raw.choices) {                            // OpenAI-style, if you swap the model\n  content = raw.choices[0] && raw.choices[0].message && raw.choices[0].message.content;\n}\nif (!content) {\n  throw new Error('The LLM returned no content. Check that the API key on the model node is valid and still has quota.');\n}\ncontent = content.trim();\nif (content.startsWith('```')) {\n  content = content.replace(/^```[a-zA-Z]*\\n/, '').replace(/```$/, '').trim();\n}\nlet parsed;\ntry {\n  parsed = JSON.parse(content);\n} catch (e) {\n  throw new Error('Could not parse the LLM reply as JSON: ' + e.message + ' -- raw: ' + content.slice(0, 300));\n}\nconst scenes = parsed.scenes || parsed.Scenes || [];\nif (!Array.isArray(scenes) || scenes.length === 0) {\n  throw new Error('The LLM reply contained no valid scenes array. Check the prompt and the raw model output.');\n}\nreturn [{ json: { scenes } }];\n",
        "language": "javaScript"
      },
      "typeVersion": 2
    },
    {
      "id": "split",
      "name": "Split into individual scenes",
      "type": "n8n-nodes-base.splitOut",
      "notes": "Emits one item per scene: { voice_over, image_prompt }. Pipe into a TTS step and an image step to build the full video.",
      "position": [
        1240,
        0
      ],
      "parameters": {
        "include": "noOtherFields",
        "options": {},
        "fieldToSplitOut": "scenes"
      },
      "typeVersion": 1
    }
  ],
  "settings": {
    "executionOrder": "v1"
  },
  "connections": {
    "Video idea form": {
      "main": [
        [
          {
            "node": "Set language and word budget",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse the script JSON": {
      "main": [
        [
          {
            "node": "Split into individual scenes",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Route to the chosen model": {
      "main": [
        [
          {
            "node": "Generate script (Gemini, free tier)",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Generate script (Claude Haiku)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set language and word budget": {
      "main": [
        [
          {
            "node": "Route to the chosen model",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate script (Claude Haiku)": {
      "main": [
        [
          {
            "node": "Parse the script JSON",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate script (Gemini, free tier)": {
      "main": [
        [
          {
            "node": "Parse the script JSON",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
Pro

For the full experience including quality scoring and batch install features for each workflow upgrade to Pro

About this workflow

This workflow collects a topic and settings via an n8n form, generates a short scene-by-scene faceless video script using either Google Gemini or Anthropic Claude Haiku, then normalizes the model output into a consistent JSON structure and outputs one item per scene. Receives a…

Source: https://n8n.io/workflows/17408/ — original creator credit. Request a take-down →

More AI & RAG workflows → · Browse all categories →

Related workflows

Workflows that share integrations, category, or trigger type with this one. All free to copy and import.

AI & RAG

This self-hosted workflow takes a topic from an n8n form, generates a short multi-scene script with Google Gemini or Anthropic Claude, creates voiceover with Edge TTS, downloads scene images from Poll

Form Trigger, HTTP Request, Execute Command +1
AI & RAG

This workflow is ideal for content creators, video marketers, and research professionals who need to extract actionable insights, detailed transcripts, or metadata from YouTube videos efficiently. It

HTTP Request, Google Drive, Gmail +2
AI & RAG

Legal, Procurement, and Compliance teams at mid-size companies. ESN and agencies selling AI-powered contract review as a service.

Form Trigger, HTTP Request, Form +4
AI & RAG

This workflow takes a Loom link, extracts the video ID, uses the Loom API to download the video, then sends it to Gemini along with your question. Finally, it sends the output to Slack.

HTTP Request, Form Trigger, Slack
AI & RAG

This template is designed for filmmakers, content creators, social media managers, and AI developers who want to harness OpenAI's Sora 2 for creating physically accurate, cinematic videos with synchro

HTTP Request, Form Trigger, Form +1