Programmatic video from data
Turn a CSV, JSON file, or database row into N rendered MP4s using one HTML template.
Ten minutes from a spreadsheet to a folder of personalized MP4s. One HTML template, one variants file, one command.
What you'll learn
- The
{{$VAR}}token model and where tokens are legal - How
--variants variants.jsonmaps rows to renders - Wiring a CSV → JSON → render pipeline that survives CI
The result
The template
A HyperFrames template is plain HTML with {{$VAR}} tokens anywhere text or attribute values appear. The renderer substitutes tokens before headless Chrome ever sees the document, so there's nothing client-side to break.
<!doctype html>
<html>
<head>
<style>
body { margin:0; height:100vh; display:grid; place-items:center;
background: {{$BG}}; font-family: ui-sans-serif, system-ui; color:#fff; }
.card { text-align:center; padding:64px 96px; }
h2 { font-size: 88px; margin:0; letter-spacing:-0.03em; }
p { font-size: 28px; opacity:.85; margin-top:12px;
animation: in 1s ease-out both; }
@keyframes in { from { opacity:0; transform: translateY(12px); } }
</style>
</head>
<body>
<div class="card">
<h2>Hello, {{$NAME}}.</h2>
<p>{{$HEADLINE}}</p>
</div>
</body>
</html>Three rows in, three MP4s out, each written to the out path declared in the variants file. No template engine, no build step.
From CSV to variants
Most teams have a CSV, not a JSON file. A 12-line script bridges them:
# csv-to-variants.mjs
import { readFileSync, writeFileSync } from "node:fs";
const rows = readFileSync("contacts.csv", "utf8").trim().split("\n");
const [head, ...body] = rows.map(r => r.split(","));
const variants = body.map(cols => {
const vars = Object.fromEntries(head.map((k, i) => [k, cols[i]]));
return { out: `out/${vars.NAME.toLowerCase()}.mp4`, vars };
});
writeFileSync("variants.json", JSON.stringify(variants, null, 2));node csv-to-variants.mjs && hyperframes render card.html --variants variants.json --workers 8Where tokens are legal
- Text content:
<h1>{{$NAME}}</h1> - HTML attributes:
<img src="{{$LOGO_URL}}"> - CSS values:
background: {{$BG}}; data-*timing:data-start="{{$START}}"
Determinism, even at N=10,000
Every render gets the same fonts, the same easing curves, the same frame timing. Two CI runs of the same variants.json produce byte-identical MP4s — useful when you content-hash outputs to skip re-rendering rows that haven't changed.
hyperframes render card.html --variants variants.json --json > render-log.jsonThe --json log includes the input hash for each variant. Diff against the previous run and re-render only the rows that moved.
Tweak it
- Add an
idxfield to each variant and use it as a query param so each render gets a unique seed for any randomness. - Split a 10k-row variants file into chunks and run one
hyperframes renderper worker on a queue. - Drop
--variantsand pass--vars name=Adafor one-off renders during development.