Freebie for the RENDER keyword (reel-79A, "This video was a text file"). Deliver as a public GitHub Gist. By the end you have a folder on your machine where an HTML file renders to a finished vertical MP4 — and an AI agent that can edit that file for you.
Claude has no video model. Ask it for a video and you get a description of one. But Claude is very good at HTML, CSS and JavaScript — and HyperFrames turns exactly that into frames. So the agent writes the thing it is already good at, and a renderer does the rest.
The output is deterministic: same file in, same video out, every time. No timeline app, no export dialog, no "please wait, rendering". The video is a file you can diff and version.
- Node.js 22 or newer —
node -v - FFmpeg —
ffmpeg -version(macOS:brew install ffmpeg)
That is the whole list. Chrome is downloaded automatically on the first render. Check it yourself:
npx hyperframes doctorEverything with a ✓ is fine. The TTS and music entries may say "Not installed" — those are optional and you do not need them for this.
npx hyperframes init my-video --resolution portrait
cd my-video--resolution portrait gives you 1080×1920, which is what Reels, Shorts and TikTok want.
Use landscape for 1920×1080 or square for 1080×1080.
Heads up:
initalso installs HyperFrames skills into~/.claude/skills/and~/.agents/skills/so your coding agent knows the framework. That is what makes the agent part work. If you would rather not have that, setHYPERFRAMES_SKIP_SKILLS=1before running it.
Open index.html. The whole contract is three things:
- A root element with
data-composition-id,data-duration,data-width,data-height. - Every animated element carries
class="clip"plus its owndata-start/data-duration. - One paused GSAP timeline, registered on
window.__timelines["<composition-id>"].
That last point is the one that matters. The renderer does not play your animation — it
seeks the timeline to each frame and screenshots it. A paused timeline that can be seeked
to any point renders correctly. Anything driven by setInterval, requestAnimationFrame or
a CSS animation will not.
Here is a complete 6-second starter you can paste over index.html:
<!doctype html>
<html lang="en" data-resolution="portrait">
<head>
<meta charset="UTF-8" />
<script src="https://cdn.jsdelivr.net/npm/gsap@3.14.2/dist/gsap.min.js"></script>
<style>
* { margin:0; padding:0; box-sizing:border-box; }
html, body { width:1080px; height:1920px; overflow:hidden; background:#0A101C; }
#root { width:100%; height:100%; position:relative;
font-family: ui-sans-serif, system-ui, sans-serif; }
#ring { position:absolute; left:290px; top:700px; width:500px; height:500px;
border:10px solid #F0455F; border-radius:50%; opacity:0; }
#line1 { position:absolute; left:90px; top:1320px; color:#fff;
font-size:104px; font-weight:800; letter-spacing:-.03em; opacity:0; }
#line2 { position:absolute; left:90px; top:1450px; color:#F0455F;
font-size:104px; font-weight:800; letter-spacing:-.03em; opacity:0; }
</style>
</head>
<body>
<div id="root" data-composition-id="main" data-start="0" data-duration="6"
data-width="1080" data-height="1920">
<div id="ring" class="clip" data-start="0" data-duration="6" data-track-index="0"></div>
<div id="line1" class="clip" data-start="0" data-duration="6" data-track-index="1">This file</div>
<div id="line2" class="clip" data-start="0" data-duration="6" data-track-index="2">is the video.</div>
</div>
<script>
const tl = gsap.timeline({ paused: true });
tl.fromTo("#ring", { opacity:0, scale:.4, rotate:-90 },
{ opacity:1, scale:1, rotate:0, duration:1.2, ease:"power3.out" }, 0);
tl.to("#ring", { rotate:360, duration:4.8, ease:"none" }, 1.2);
tl.fromTo("#line1", { opacity:0, y:40 }, { opacity:1, y:0, duration:.5, ease:"power2.out" }, .8);
tl.fromTo("#line2", { opacity:0, y:40 }, { opacity:1, y:0, duration:.5, ease:"power2.out" }, 1.2);
window.__timelines["main"] = tl;
tl.seek(0);
</script>
</body>
</html>npm run checkThis runs the linter, loads the page in headless Chrome, samples the timeline at several points and reports JS errors, missing assets, layout overflow and text contrast. It takes a few seconds and it will save you a five-minute render of a black screen.
npm run renderor, to pick the filename:
npx hyperframes render -o out.mp4Measured on an M4 Max, the starter above: 6.0 s of 1080×1920 H.264 at 30 fps, rendered in 5.7 s. Roughly real time. A longer or heavier composition takes proportionally longer.
This is the actual point. Open the project folder in Claude Code (or Cursor, or any agent that reads skills) and describe the change in words:
Make the headline enter one word at a time, and hold the last frame for half a second.
The agent edits index.html, you run npm run check and npm run render. Because the video
is a text file, an agent can change it — and because the render is deterministic, you can
review the diff before you ever look at the MP4.
I hit all three building the reel you just watched. They share one symptom: the render exits successfully and hands you a finished MP4 with nothing moving in it.
- Font paths get rewritten everywhere. The compiler replaces every occurrence of a local
font path with an inlined data URI — including inside a JavaScript string. If your script
embeds data that happens to contain
fonts/, that string is destroyed and the script dies withUnexpected identifier 'data'. Fix: put a zero-width space inside the word (fonts​/) in embedded data. </script>inside embedded data ends your script tag. Escape it as<\/script>.- GSAP scales around the element centre, whatever CSS
transform-originsays. For a camera wrapper doing a push-in on a screenshot, set it explicitly in the tween:transformOrigin: '0 0'.
How to catch all three in one glance: watch the render output for PAGEERROR, and check
that the reported duration matches your composition's data-duration. If the renderer waits
45 seconds and then reports a different duration, your timeline never registered.
Nothing. HyperFrames is open source under Apache-2.0
(github.com/heygen-com/hyperframes, 52,287 stars as of 22 Sep 2026). Everything above runs
locally. There is a hosted cloud render and a HeyGen account flow, and you never have to touch
either one.
Verified end-to-end on 22 Sep 2026: hyperframes 0.8.60, Node v26.8.1, FFmpeg 9.0.1, macOS. init → check → render, no errors, output probed at 1080×1920, h264, 30 fps, 6.000 s.