This workflow corresponds to n8n.io template #17566 — we link there as the canonical source.
This workflow follows the Executecommand → 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 →
{
"meta": {
"templateCredsSetupNotes": "One credential, for the script step only. Gemini (free): Query Auth named exactly 'key' (a literal query param name, not a label), value = free key from aistudio.google.com/apikey, assigned to 'Generate Script (Gemini, free)'. OR Claude: Header Auth named 'x-api-key', value = your Anthropic key, assigned to 'Generate Script (Claude Haiku)'. Edge TTS, Pollinations and FFmpeg need no credential at all. Requires self-hosted n8n started with NODES_EXCLUDE=\"[]\" and N8N_RESTRICT_FILE_ACCESS_TO=\"/tmp\" (macOS: \"/tmp;/private/tmp\"), plus ffmpeg built with libass and `pipx install edge-tts`. See the SETUP sticky note inside the workflow."
},
"name": "Faceless Video Factory (self-hosted) \u2014 Idea to Rendered Video",
"nodes": [
{
"id": "note-overview",
"name": "Overview",
"type": "n8n-nodes-base.stickyNote",
"position": [
0,
-1160
],
"parameters": {
"content": ""
},
"typeVersion": 1
},
{
"id": "node-form",
"name": "Video Idea Form",
"type": "n8n-nodes-base.formTrigger",
"notes": "Entry point. Buyer just fills a web form \u2014 no JSON editing needed to run a video.",
"position": [
0,
300
],
"parameters": {},
"notesInFlow": true,
"typeVersion": 2.6
},
{
"id": "note-form",
"name": "Note: Form",
"type": "n8n-nodes-base.stickyNote",
"position": [
0,
700
],
"parameters": {
"content": ""
},
"typeVersion": 1
},
{
"id": "node-router",
"name": "LLM Provider Router",
"type": "n8n-nodes-base.switch",
"notes": "Routes to Gemini (free, 20 requests/day) or Claude Haiku ($0.0017/video), based on the LLM Provider picked in the form.",
"position": [
400,
300
],
"parameters": {},
"notesInFlow": true,
"typeVersion": 3.4
},
{
"id": "node-script-gemini",
"name": "Generate Script (Gemini, free)",
"type": "n8n-nodes-base.httpRequest",
"notes": "Calls Gemini to write the script in the language picked in the form. FREE TIER = 20 requests/day (also 5/min). Past that you get 429 Quota exceeded until the next day \u2014 retryOnFail below only rescues the transient 503 \"high demand\", not the quota. Switch to Claude Haiku when you hit it.",
"maxTries": 5,
"position": [
620,
200
],
"parameters": {},
"notesInFlow": true,
"retryOnFail": true,
"typeVersion": 4.4,
"waitBetweenTries": 8000
},
{
"id": "node-script",
"name": "Generate Script (Claude Haiku)",
"type": "n8n-nodes-base.httpRequest",
"notes": "Calls Claude Haiku 4.5 to write the script in the language picked in the form. Measured cost: 332 in + 266 out tokens = $0.0017 per video (~600 videos per $1). Uses structured outputs, so the API guarantees the JSON matches the schema. Needs a Header Auth credential named x-api-key holding your Anthropic key.",
"position": [
620,
400
],
"parameters": {},
"notesInFlow": true,
"typeVersion": 4.4
},
{
"id": "node-parse",
"name": "Parse Script JSON",
"type": "n8n-nodes-base.code",
"notes": "Turns the LLM's raw reply into a clean scenes array. Handles the Gemini, Claude and OpenAI response shapes, and strips a markdown code fence if the model adds one.",
"onError": "continueRegularOutput",
"position": [
840,
300
],
"parameters": {},
"notesInFlow": true,
"typeVersion": 2
},
{
"id": "node-splitout",
"name": "Split Into Individual Scenes",
"type": "n8n-nodes-base.splitOut",
"notes": "From here on, the workflow runs once per scene (voiceover, caption, image all happen per-scene).",
"position": [
1040,
300
],
"parameters": {},
"notesInFlow": true,
"typeVersion": 1
},
{
"id": "note-edgetts",
"name": "Note: Edge TTS (free voice)",
"type": "n8n-nodes-base.stickyNote",
"position": [
1260,
700
],
"parameters": {
"content": ""
},
"typeVersion": 1
},
{
"id": "node-edgetts-prep",
"name": "Prepare Edge TTS Paths",
"type": "n8n-nodes-base.code",
"notes": "Computes a shared temp folder for this run + per-scene text/audio file paths.",
"position": [
1260,
160
],
"parameters": {},
"notesInFlow": true,
"typeVersion": 2
},
{
"id": "node-edgetts-mkdir",
"name": "Create Edge TTS Folder",
"type": "n8n-nodes-base.executeCommand",
"notes": "mkdir -p is safe to run once per scene (idempotent) since all scenes in a run share the same folder.",
"position": [
1460,
160
],
"parameters": {},
"executeOnce": false,
"notesInFlow": true,
"typeVersion": 1
},
{
"id": "node-edgetts-writetext",
"name": "Write Scene Text (Edge TTS)",
"type": "n8n-nodes-base.code",
"notes": "Writes the scene voice_over into a binary property so it can be saved to disk \u2014 edge-tts reads text from a file to avoid shell-escaping problems with accents and other non-ASCII characters. Re-pulls the paths from Prepare Edge TTS Paths because Create Edge TTS Folder (Execute Command) wipes json+binary.",
"position": [
1660,
160
],
"parameters": {},
"notesInFlow": true,
"typeVersion": 2
},
{
"id": "node-edgetts-savetext",
"name": "Save Scene Text File",
"type": "n8n-nodes-base.readWriteFile",
"notes": "Writes the text file edge-tts will read from.",
"position": [
1860,
160
],
"parameters": {},
"notesInFlow": true,
"typeVersion": 1.1
},
{
"id": "node-edgetts-run",
"name": "Run Edge TTS",
"type": "n8n-nodes-base.executeCommand",
"notes": "Calls the free, local edge-tts CLI. The voice comes from the Language Config node \u2014 change the map there, not here.",
"position": [
2060,
160
],
"parameters": {},
"executeOnce": false,
"notesInFlow": true,
"typeVersion": 1
},
{
"id": "node-edgetts-readaudio",
"name": "Read Edge TTS Audio",
"type": "n8n-nodes-base.readWriteFile",
"notes": "Reads the mp3 edge-tts just wrote back into the workflow as binary, so the renderer can use it. Re-reads the path from Prepare Edge TTS Paths because Run Edge TTS (Execute Command) wipes json+binary.",
"position": [
2260,
160
],
"parameters": {},
"notesInFlow": true,
"typeVersion": 1.1
},
{
"id": "node-edgetts-duration",
"name": "Get Audio Duration",
"type": "n8n-nodes-base.executeCommand",
"notes": "Reports the mp3 duration AND where edge-tts left silence (lead-in, sentence pauses, tail). The captions node needs the silences to keep the highlight in sync with the voice.",
"position": [
2460,
160
],
"parameters": {},
"executeOnce": false,
"notesInFlow": true,
"typeVersion": 1
},
{
"id": "node-edgetts-captions",
"name": "Build Estimated Captions (Edge TTS)",
"type": "n8n-nodes-base.code",
"notes": "Edge TTS returns no per-word timestamps, so timings are estimated: word durations are shared out by character length across SPEECH time only, stepping over the silences edge-tts inserts (~0.2s lead-in, ~1s per sentence pause, ~0.9s tail \u2014 over 20% of a short clip). Using the raw file duration instead stretches every caption and the voice drifts ahead of the highlight.",
"onError": "continueRegularOutput",
"position": [
2660,
160
],
"parameters": {},
"notesInFlow": true,
"typeVersion": 2
},
{
"id": "node-merge",
"name": "Merge Audio + Image per Scene",
"type": "n8n-nodes-base.merge",
"notes": "Lines the voiceover branch back up with the image branch for the same scene (matched by position/order).",
"position": [
3080,
300
],
"parameters": {},
"notesInFlow": true,
"typeVersion": 3.2
},
{
"id": "note-ffmpeg-branch",
"name": "Note: Render",
"type": "n8n-nodes-base.stickyNote",
"position": [
3280,
700
],
"parameters": {
"content": ""
},
"typeVersion": 1
},
{
"id": "node-ffmpeg-prep",
"name": "Prepare Render Paths (FFmpeg)",
"type": "n8n-nodes-base.code",
"notes": "Computes a shared render folder for this run + per-scene file paths (audio/image/subtitle/segment).",
"position": [
3280,
300
],
"parameters": {},
"notesInFlow": true,
"typeVersion": 2
},
{
"id": "node-ffmpeg-mkdir",
"name": "Create Render Folder (FFmpeg)",
"type": "n8n-nodes-base.executeCommand",
"notes": "mkdir -p is safe to run once per scene (idempotent) since all scenes in a run share the same folder.",
"position": [
3480,
300
],
"parameters": {},
"executeOnce": false,
"notesInFlow": true,
"typeVersion": 1
},
{
"id": "node-ffmpeg-ass",
"name": "Build Karaoke Subtitle (ASS)",
"type": "n8n-nodes-base.code",
"notes": "Builds the karaoke .ass for one scene. Lines are grouped by character budget (not word count) so long-word languages like German do not run off screen, and WrapStyle 0 wraps anything that still overflows.",
"position": [
3680,
300
],
"parameters": {},
"notesInFlow": true,
"typeVersion": 2
},
{
"id": "node-ffmpeg-write-ass",
"name": "Write Scene Subtitle File",
"type": "n8n-nodes-base.readWriteFile",
"notes": "Writes the .ass karaoke subtitle to local disk so ffmpeg's subtitles filter can burn it in.",
"position": [
3880,
300
],
"parameters": {},
"notesInFlow": true,
"typeVersion": 1.1
},
{
"id": "node-ffmpeg-render-segment",
"name": "Render Scene Segment (FFmpeg)",
"type": "n8n-nodes-base.executeCommand",
"notes": "Ken Burns zoom on the still image + burned-in karaoke captions, trimmed to the audio length (-shortest).",
"position": [
4080,
300
],
"parameters": {},
"executeOnce": false,
"notesInFlow": true,
"typeVersion": 1
},
{
"id": "node-ffmpeg-check-segment",
"name": "Check Segment Result",
"type": "n8n-nodes-base.code",
"notes": "Execute Command's output replaces json with {exitCode, stdout, stderr} \u2014 this re-attaches sceneIndex/segmentPath and fails loudly if ffmpeg errored.",
"position": [
4280,
300
],
"parameters": {},
"notesInFlow": true,
"typeVersion": 2
},
{
"id": "node-ffmpeg-aggregate",
"name": "Combine All Segments",
"type": "n8n-nodes-base.aggregate",
"notes": "Collects every rendered scene segment's path back into one item, ready to be concatenated in order.",
"position": [
4480,
300
],
"parameters": {},
"notesInFlow": true,
"typeVersion": 1
},
{
"id": "node-ffmpeg-concat-list",
"name": "Build Concat List",
"type": "n8n-nodes-base.code",
"notes": "Sorts segments by scene order and writes ffmpeg's concat-demuxer list file format.",
"position": [
4680,
300
],
"parameters": {},
"notesInFlow": true,
"typeVersion": 2
},
{
"id": "node-ffmpeg-write-concat",
"name": "Write Concat List File",
"type": "n8n-nodes-base.readWriteFile",
"notes": "Writes the concat list to disk so ffmpeg's concat demuxer can read it.",
"position": [
4880,
300
],
"parameters": {},
"notesInFlow": true,
"typeVersion": 1.1
},
{
"id": "node-ffmpeg-concat-video",
"name": "Concatenate Final Video (FFmpeg)",
"type": "n8n-nodes-base.executeCommand",
"notes": "Joins every scene into the final vertical MP4. Video is stream-copied (no re-encode); audio is normalised to about -14 LUFS, the loudness social platforms expect. Without this the voice lands near -20 LUFS and sounds noticeably quiet in feed.",
"position": [
5080,
300
],
"parameters": {},
"executeOnce": true,
"notesInFlow": true,
"typeVersion": 1
},
{
"id": "node-ffmpeg-read-final",
"name": "Read Final Video File",
"type": "n8n-nodes-base.readWriteFile",
"notes": "Reads the finished MP4 back into the workflow as binary output \u2014 the free-path deliverable, no external service involved.",
"position": [
5280,
300
],
"parameters": {},
"notesInFlow": true,
"typeVersion": 1.1
},
{
"id": "node-collect-audio-info",
"name": "Collect Audio Info",
"type": "n8n-nodes-base.code",
"notes": "Bundles the scene text, its karaoke word timings and the audio path for the renderer.",
"position": [
2860,
160
],
"parameters": {},
"notesInFlow": true,
"typeVersion": 2
},
{
"id": "node-image-path",
"name": "Prepare Local Image Path",
"type": "n8n-nodes-base.code",
"notes": "Computes the local disk path AND the Pollinations URL for this scene.",
"position": [
1260,
460
],
"parameters": {},
"notesInFlow": true,
"typeVersion": 2
},
{
"id": "node-image-mkdir",
"name": "Create Image Folder",
"type": "n8n-nodes-base.executeCommand",
"notes": "mkdir -p is safe to run once per scene.",
"position": [
1460,
460
],
"parameters": {},
"executeOnce": false,
"notesInFlow": true,
"typeVersion": 1
},
{
"id": "node-collect-image-info",
"name": "Collect Image Info",
"type": "n8n-nodes-base.code",
"notes": "Keeps the prompt and the local imagePath. No more remote upload/URL here.",
"position": [
1860,
460
],
"parameters": {},
"notesInFlow": true,
"typeVersion": 2
},
{
"id": "lang-config-0001",
"name": "Language Config",
"type": "n8n-nodes-base.code",
"notes": "Single source of truth: language -> script language + Edge TTS voice, AND the per-scene word budget that keeps the video under 60s (edge-tts speaks ~2.32 words/sec, so ~125 words = ~55s).",
"position": [
200,
300
],
"parameters": {},
"notesInFlow": true,
"typeVersion": 2
},
{
"id": "sticky-setup-0001",
"name": "SETUP \u2014 do this first",
"type": "n8n-nodes-base.stickyNote",
"position": [
840,
-940
],
"parameters": {
"content": ""
},
"typeVersion": 1
},
{
"id": "node-image-download",
"name": "Download Scene Image",
"type": "n8n-nodes-base.executeCommand",
"notes": "Downloads the Pollinations image with curl. Deliberately NOT the HTTP Request node: that node fires every item CONCURRENTLY (it builds all the promises then Promise.allSettled), so a 10-scene video hits Pollinations with 10 requests at once and gets 429 \u2014 its Batching option only staggers the starts, it does not serialise them. Execute Command awaits inside its item loop, so scenes download one at a time.",
"maxTries": 5,
"position": [
1660,
460
],
"parameters": {},
"notesInFlow": true,
"retryOnFail": true,
"typeVersion": 1,
"waitBetweenTries": 8000
}
],
"connections": {
"Run Edge TTS": {
"main": [
[
{
"node": "Read Edge TTS Audio",
"type": "main",
"index": 0
}
]
]
},
"Language Config": {
"main": [
[
{
"node": "LLM Provider Router",
"type": "main",
"index": 0
}
]
]
},
"Video Idea Form": {
"main": [
[
{
"node": "Language Config",
"type": "main",
"index": 0
}
]
]
},
"Build Concat List": {
"main": [
[
{
"node": "Write Concat List File",
"type": "main",
"index": 0
}
]
]
},
"Parse Script JSON": {
"main": [
[
{
"node": "Split Into Individual Scenes",
"type": "main",
"index": 0
}
]
]
},
"Collect Audio Info": {
"main": [
[
{
"node": "Merge Audio + Image per Scene",
"type": "main",
"index": 0
}
]
]
},
"Collect Image Info": {
"main": [
[
{
"node": "Merge Audio + Image per Scene",
"type": "main",
"index": 1
}
]
]
},
"Get Audio Duration": {
"main": [
[
{
"node": "Build Estimated Captions (Edge TTS)",
"type": "main",
"index": 0
}
]
]
},
"Create Image Folder": {
"main": [
[
{
"node": "Download Scene Image",
"type": "main",
"index": 0
}
]
]
},
"LLM Provider Router": {
"main": [
[
{
"node": "Generate Script (Gemini, free)",
"type": "main",
"index": 0
}
],
[
{
"node": "Generate Script (Claude Haiku)",
"type": "main",
"index": 0
}
]
]
},
"Read Edge TTS Audio": {
"main": [
[
{
"node": "Get Audio Duration",
"type": "main",
"index": 0
}
]
]
},
"Check Segment Result": {
"main": [
[
{
"node": "Combine All Segments",
"type": "main",
"index": 0
}
]
]
},
"Combine All Segments": {
"main": [
[
{
"node": "Build Concat List",
"type": "main",
"index": 0
}
]
]
},
"Download Scene Image": {
"main": [
[
{
"node": "Collect Image Info",
"type": "main",
"index": 0
}
]
]
},
"Save Scene Text File": {
"main": [
[
{
"node": "Run Edge TTS",
"type": "main",
"index": 0
}
]
]
},
"Create Edge TTS Folder": {
"main": [
[
{
"node": "Write Scene Text (Edge TTS)",
"type": "main",
"index": 0
}
]
]
},
"Prepare Edge TTS Paths": {
"main": [
[
{
"node": "Create Edge TTS Folder",
"type": "main",
"index": 0
}
]
]
},
"Write Concat List File": {
"main": [
[
{
"node": "Concatenate Final Video (FFmpeg)",
"type": "main",
"index": 0
}
]
]
},
"Prepare Local Image Path": {
"main": [
[
{
"node": "Create Image Folder",
"type": "main",
"index": 0
}
]
]
},
"Write Scene Subtitle File": {
"main": [
[
{
"node": "Render Scene Segment (FFmpeg)",
"type": "main",
"index": 0
}
]
]
},
"Write Scene Text (Edge TTS)": {
"main": [
[
{
"node": "Save Scene Text File",
"type": "main",
"index": 0
}
]
]
},
"Build Karaoke Subtitle (ASS)": {
"main": [
[
{
"node": "Write Scene Subtitle File",
"type": "main",
"index": 0
}
]
]
},
"Split Into Individual Scenes": {
"main": [
[
{
"node": "Prepare Edge TTS Paths",
"type": "main",
"index": 0
},
{
"node": "Prepare Local Image Path",
"type": "main",
"index": 0
}
]
]
},
"Create Render Folder (FFmpeg)": {
"main": [
[
{
"node": "Build Karaoke Subtitle (ASS)",
"type": "main",
"index": 0
}
]
]
},
"Merge Audio + Image per Scene": {
"main": [
[
{
"node": "Prepare Render Paths (FFmpeg)",
"type": "main",
"index": 0
}
]
]
},
"Prepare Render Paths (FFmpeg)": {
"main": [
[
{
"node": "Create Render Folder (FFmpeg)",
"type": "main",
"index": 0
}
]
]
},
"Render Scene Segment (FFmpeg)": {
"main": [
[
{
"node": "Check Segment Result",
"type": "main",
"index": 0
}
]
]
},
"Generate Script (Claude Haiku)": {
"main": [
[
{
"node": "Parse Script JSON",
"type": "main",
"index": 0
}
]
]
},
"Generate Script (Gemini, free)": {
"main": [
[
{
"node": "Parse Script JSON",
"type": "main",
"index": 0
}
]
]
},
"Concatenate Final Video (FFmpeg)": {
"main": [
[
{
"node": "Read Final Video File",
"type": "main",
"index": 0
}
]
]
},
"Build Estimated Captions (Edge TTS)": {
"main": [
[
{
"node": "Collect Audio Info",
"type": "main",
"index": 0
}
]
]
}
}
}
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
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 Pollinations, burns karaoke captions with FFmpeg, and outputs a final vertical MP4.…
Source: https://n8n.io/workflows/17566/ — 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.
This is the final piece of the AI content factory. This workflow takes your text-based video scripts and automatically generates high-quality audio voiceovers for each one, turning your text into read
Transcript Audio To Text. Uses googleDocs, googleDrive, openAi, gmail. Event-driven trigger; 31 nodes.
Turn email overload into audio insights — automatically.
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
Legal, Procurement, and Compliance teams at mid-size companies. ESN and agencies selling AI-powered contract review as a service.