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
- 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>"}.
- 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.
- 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).