🎬

Make Video

Verified

by OpenClaw Launch

Your bot becomes a video director: it plans the scenes (titles, callouts, stat reveals, charts, comparisons, terminal demos), submits the plan to the managed render service, and delivers a finished 1080p MP4 back into your chat. Powered by the open-source OpenMontage pipeline with Remotion and FFmpeg. Text and data videos are included with your plan (Lite 2/day, Pro 10/day); bots with a fal API key configured can also weave in AI-generated images and clips.

videomediacreativeexplainermp4render
View on GitHub

Make Video

You are the director. You design the video as a JSON scene plan ("props"), then a remote

render service turns it into a real MP4 (1080p H.264). Rendering takes about 10 seconds of

wall time per 1 second of video, and jobs may queue, so tell the user the video is being

rendered and that it takes a few minutes.

Credentials

Read the file render-token.json in this skill's own directory (next to this SKILL.md).

It contains {"url": ..., "token": ...}. Use them below. If the file is missing or the API

returns 401, tell the user to reinstall the make-video skill from the dashboard Skills page

(that regenerates the token). Never print the token into chat.

Daily limits by plan (the API enforces them): Lite 2 videos/day, Pro 10 videos/day. Free

trials do not include video rendering. If the API returns 429 with a limit message,

explain it to the user rather than retrying.

Render API

  1. Submit:

curl -s -X POST <url>/jobs -H "Authorization: Bearer <token>" \

-H 'Content-Type: application/json' --data @props-body.json

where props-body.json is {"composition":"Explainer","props":{...}} — schema below.

Returns {"jobId":"<id>"}.

  1. Poll every 30-60s (never faster):

curl -s <url>/jobs/<jobId> -H "Authorization: Bearer <token>"

-> {"status":"queued|running|done|failed","progress":0-100,"error":...}

If failed: read the error, fix the props, resubmit once. Two failures: show the user

the error and stop.

  1. Download when done:

curl -s -o video.mp4 <url>/jobs/<jobId>/video -H "Authorization: Bearer <token>"

Save into the workspace, then send the file to the user in the chat.

Props schema (composition "Explainer")

Top level: {"theme":"flat-motion-graphics","cuts":[...],"overlays":[],"captions":[],"audio":{}}

Each cut plays sequentially. Cut fields: id (slug), type, in_seconds, out_seconds,

plus type-specific fields. Keep total duration 20-90 seconds, 3-8 seconds per cut.

Cut types and their EXACT fields. The renderer checks for these specific field names and

silently falls back to printing the cut's id as a single word if they are missing, so a cut

with the wrong field names renders as a near-blank frame. Verified against the composition.

  • hero_title: {"text","subtitle","backgroundColor"} — opening/closing card
  • text_card: {"text","subtitle"} — body copy
  • callout: {"text","subtitle","callout_type"} — callout_type is info | warning | tip | quote.

Use callout_type "quote" for quotations; there is NO separate "quote" cut type.

  • stat_card: {"stat","subtitle"} — "stat" is a STRING, e.g. "10x" or "1080p"
  • kpi_grid: {"title","columns",

"chartData":[{"label":"Videos per day","value":10,"suffix":"","prefix":"","change":12}]}

columns is 2, 3 or 4. "value" MUST be a number, not a string.

Values of 1000 or more are ABBREVIATED on screen: 1080 with suffix "p" renders as

"1.1Kp", not "1080p". For a fact like 1080p or 4K, use a stat_card instead — its

"stat" is a plain string and is printed verbatim.

  • bar_chart / pie_chart: {"title","showValues",

"chartData":[{"label":"Mon","value":3}]} — "value" is a number

  • line_chart: {"title","chartSeries":[...]} — note chartSeries, not chartData
  • comparison: {"leftLabel","leftValue","rightLabel","rightValue"} — four flat STRINGS.

There are no nested left/right objects and no item lists.

  • terminal_scene: {"terminalTitle","prompt","steps":[...]} where each step is one of

{"kind":"cmd","text":"make-video render"} typed out character by character

{"kind":"out","text":"1080p H.264, 30fps"} printed instantly

{"kind":"pause","seconds":1}

{"kind":"pill","text":"captions on","color":"#ec4899"}

  • progress_bar: {"progress":0.75,"progressLabel":"Rendering"}

Per-cut styling (optional, any type): "accentColor", "color", "backgroundColor",

"backgroundImage", "backgroundVideo", "backgroundOverlay" (0-1), "fontSize".

NOT cut types: section_title, stat_reveal, provider_chip. Those are OVERLAY types and go in

the top-level "overlays" array with {"type","in_seconds","out_seconds","text","subtitle"}.

Set "source":"" on all of the above. Fields you are unsure about: leave out.

AI-generated footage (optional)

A cut with a non-empty "source" URL (public .jpg/.png or .mp4) renders that media

full-screen instead of its text layout. If your instance has image or video generation

available (for example a fal API key in your config), you can generate assets first and

reference their URLs as cut sources. Default to cheap models (e.g. FLUX schnell for

stills); use expensive video models only when the user explicitly asks and accepts the

cost. 1-3 assets per video is plenty. Confirm each URL returns 200 before submitting.

If you have no media generation available, make a clean text/stat/chart video instead —

do not fail the request.

Captions (strongly recommended whenever there is a voiceover)

Most people watch social video with the sound off, so put the narration on screen. Captions

are WORD-LEVEL and use this exact shape in the top-level "captions" array:

"captions": [{"word":"Captions","startMs":250,"endMs":540},

{"word":"make","startMs":560,"endMs":850}]

Field names are "word", "startMs" and "endMs" — not text/start_seconds/end_seconds. The

renderer shows a few words at a time on a dark pill at the bottom and highlights the current

word. Build them by splitting your narration on spaces and allotting ~310ms per word, which

matches the TTS pace, starting around 250ms.

Music (optional)

"audio": {"music": {"src":"<public url to an mp3/wav>", "volume":0.25,

"fadeInSeconds":1, "fadeOutSeconds":2, "loop":true,

"offsetSeconds":0}}

Only use a track the user has the rights to and give it a URL that returns 200. Keep volume

low (0.15-0.3) under narration. There is no built-in music library on the render node.

Rules

  • Design 5-10 cuts that tell one clear story: hook -> 3-6 points -> closing card.
  • Prefer stats/charts when the user's content has numbers; text cards otherwise.
  • Whenever you add a voiceover, add captions too. Silent-autoplay is the norm on social.
  • Always deliver the actual MP4 file into the chat, not a link.
  • Clean up temp files; keep only the final mp4 in the workspace.

Voiceover (optional)

Add a narration track by including this in the job body next to "composition" and "props":

"voiceover": {"text": "<narration>", "voice": "en"}

Voices: "en" (English) or "zh" (Chinese). Generated on the render node by an offline TTS

voice — no external services, no extra cost, included in the same render.

Rules:

  • Write narration that matches the cuts' story and reads naturally aloud.
  • Pace it to the video length: the voices speak about 3.2 words per second, so a 40-second

video needs roughly 125-130 words of narration.

  • Narration shorter than the video is fine: the video always plays to its full length and

the tail is simply silent. Narration longer than the video is cut off at the end.

  • Max 1800 characters. Use the zh voice for Chinese narration.
  • Music: supported, but the user must supply the track URL (see the Music section above).