How my Peter in Tech channel makes its explainer reels. Two voices talk through a tech topic over Minecraft parkour: S asks the questions, P explains. Claude writes the dialogue, Coqui clones the voices, Remotion builds the video and n8n runs every step on my laptop. No paid APIs.
11reels rendered
226dialogue lines voiced
2cloned voices
11background clips
18n8n nodes
0paid APIs
The big picture
Three lanes of work run side by side, and n8n is the conductor. The writing lane decides what gets said, the voice lane turns every line into speech in a cloned voice, and the visuals lane supplies the background and the two speakers. Remotion is where they meet.
content moving between toolsn8n calling a serviceresult written back to the sheet
Inside the n8n workflow
The real workflow, step by step. One run makes one reel: it picks the next row marked “ready”, voices every line, asks Remotion to render, then keeps checking until the video is done and writes the link back to the sheet.
the main pathpolling looprender failed
What the data looks like
1 · A row in the sheet
Claude writes the dialogue as JSON; I paste it in and set the status to ready.
id video_002
topic What Are RAGs
statusreadyscript_json [
{"actor":"S", "text":"Everyone keeps saying RAG. What even is it?"},
{"actor":"P", "text":"RAG stands for Retrieval Augmented Generation. Fancy words for a simple idea."},
… 16 more lines
]
2 · One scene after the voice step
Each line now has its own WAV file and an exact length in frames (30 fps, plus 5 frames of breathing room).
{
"actor": "S",
"text": "Everyone keeps saying RAG. What even is it?",
"audioUrl": "http://127.0.0.1:8080/audio/000_S_….wav",
"durationInFrames": 104
}
// example: 3.3 s of audio → 99 + 5 = 104 frames
3 · What Remotion receives
One request starts the render; n8n then polls the job until it reports completed or failed.
POST :3000/renders
{
"scenes": [ … 18 scenes … ],
"backgroundVideoUrl": 1 of 11 clips,
"postCaption": "Everyone is throwing…"
}
→ { "jobId": "db945c0d-…" }
GET :3000/renders/db945c0d-…
→ { "status": "completed" }