Skip to content
Salah Adawi Salah Adawi
remotion tutorial programmatic-video

How I generate YouTube Shorts programmatically with Remotion

The full pattern behind my Arabic learning Shorts: videos defined as JSON, audio measured into frames before render time, and the flags that make Remotion renders reliable.

Salah Adawi

Salah Adawi

3 min read

I make short vertical videos for my Palestinian Arabic app. Each one teaches a single phrase from the app’s corpus, and none of them ever passes through a video editor. A video, in this pipeline, is an entry in a JSON file. Rendering it is a shell command.

This post is the whole pattern. It’s specific to my setup in the examples and general in the shape, so if you want to mass-produce videos from data, vocabulary cards, quotes, product clips, whatever, you can steal the structure directly.

The mental model: a video is a React component

Remotion renders React to video. Your component receives the current frame number, you describe what that frame looks like, and Remotion plays it 30 times a second in a real Chrome instance, screenshotting as it goes. A composition is a component plus dimensions, fps, and a duration in frames.

That’s the entire trick. Everything else in this post is about feeding it data.

Put every video in a data file

The core move: don’t write one component per video. Write one component per format, then register a composition for every entry in a JSON file.

My phrase Shorts live in remotion/phrases.json. The root file maps over them:

import phrases from "./phrases.json";
import phraseTimings from "./timings/phrases.json";

{phrases.map((spec) => {
  const timing = phraseTimings[spec.id];
  if (!timing) return null;
  return (
    <Composition
      key={spec.id}
      id={`phrase-${spec.id}`}
      component={PhraseShort}
      durationInFrames={phraseDuration(timing)}
      fps={30}
      width={1080}
      height={1920}
      defaultProps={{ spec, timing }}
    />
  );
})}

Adding a video means adding a JSON entry. Fixing a typo in one means editing JSON and re-rendering. Changing the design means editing one component and re-rendering all of them. The editor never opens, because there is no editor.

Measure the audio before you render

Here’s the problem that shapes the whole pipeline. durationInFrames has to be known when the composition is registered, and my videos are driven by audio clips whose lengths vary. You can load audio at render time and ask Remotion to wait, but the cleaner pattern is to measure everything up front and bake the numbers into data.

So a generator script runs ffprobe on every clip and converts seconds to frames:

const seconds = parseFloat(
  execSync(`ffprobe -v error -show_entries format=duration -of csv=p=0 "${file}"`)
);
const frames = Math.round(seconds * 30);

The results go into a timings file keyed by the same id, something like { "a1-u01-l1-s1": { "fast": 22, "examples": [22] } }. The composition then does pure arithmetic with frame counts. No loading states, no guessing, and the render is deterministic: the same JSON always produces the same video.

Let a script assemble the whole thing

One script turns a corpus sentence id into a ready-to-render entry. It copies the reviewed audio clip into the project, measures it, picks up to two related example sentences, and upserts both JSON files.

node scripts/gen-phrase-short.mjs a1-u01-l1-s1     # one video
node scripts/gen-phrase-short.mjs --lesson a1-u01-l1   # a lesson's worth

The examples are the part I’d underline. I don’t choose them. If the main phrase is a line from a dialogue, the example is the next line of that dialogue, so the viewer hears it in context. If it’s a standalone sentence, the script picks the same-lesson sibling that shares the most words with it. The selection logic is a few lines, and the pairings come out looking hand-picked because the corpus itself encodes what belongs together. If your source data has any structure at all, let it do the curating.

If your language needs text shaping, Chrome is the feature

Arabic letters connect, and the same letter takes a different form at the start, middle, or end of a word. Picking those forms and joining them is called shaping, and lightweight text renderers don’t do it. My site’s social card images are drawn with Satori, which is fast and cannot shape Arabic at all; the workaround there is a folder of 339 pre-rendered SVGs, one per Arabic word the cards might need.

Remotion has no such problem, because the thing drawing every frame is Chrome, and Chrome ships real text shaping. Write Arabic in JSX and it comes out joined and right-to-left on the first try. If your content is in Arabic, Hebrew, Urdu, or any Indic script, this one property is worth more than any feature on the comparison page.

Rendering without tears

Two practical lessons from actually running this.

First, Remotion renders with a pool of parallel browser pages, and on my machine the default pool dies with “Visited http://localhost:PORT/index.html but got no response” and nothing else to go on. The reliable setup: single-threaded, the system Chrome instead of the bundled browser, and a pinned port.

npx remotion render remotion/index.ts phrase-a1-u01-l1-s1 out/phrase.mp4 \
  --browser-executable="/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" \
  --concurrency=1 --port=3340

Slower, and it finishes every time. My render script has these flags baked in.

Second, before any full render, render a single frame with npx remotion still. It takes seconds and catches nearly everything that would have ruined the video: a missing audio file, a caption that overflows, an id that doesn’t resolve.

Two audio rules I learned the hard way

Trim the silence off your clips before measuring them. Dead air at the tail of a clip becomes dead frames in the timing math, and the video drags. ffmpeg’s silenceremove filter handles it.

And don’t stretch audio to make a “slow” repeat for learners. I tried an atempo-slowed second take and it sounded robotic, so the slow take is gone. The same clip twice at natural speed teaches better than a mangled clip at half speed.

What you end up with

A component per format, two JSON files, a generator script, and a render script. A whole lesson becomes a batch of Shorts with one command, and when the design changes, every video regenerates from the same data. The videos are a build artifact now, the same as the site.

Back to Blog
Share:

Related Posts