HTML Video
@html-docs/html-video is the deterministic rendering core behind HTML Docs' generated-video feature. A language model authors ordinary HTML, CSS, and a small seek-driven JavaScript function. This package validates that source, opens it in an isolated Chromium page, seeks to every frame, and pipes PNG frames into FFmpeg.
The model is the author, not the renderer. The renderer is fully local and open source: Puppeteer/Chromium produces pixels and FFmpeg encodes MP4, WebM, or GIF. A hosted service is optional operational infrastructure, not part of the composition format.
Modular explainer projects
For narrated or multi-scene work, author a project directory instead of one large JSON string:
video-project/
video.project.json
global.css
audio/voiceover.wav
audio/words.json
assets/
scenes/01-hook.html
scenes/01-hook.css
scenes/01-hook.js
The compiler concatenates scene modules into the stable version-1 composition format, embeds declared local assets as data URLs, measures the real voiceover, and derives scene boundaries from narration cues. Every cue owns exact text, one scene, and one or more unique target IDs. Builds fail when cue text does not cover the narration exactly, timed words are unassigned, cues overlap or cross scene boundaries, or targets are missing.
html-docs-video build ./video-project
html-docs-video check ./video-project
html-docs-video audit ./video-project
html-docs-video render ./video-project --output ./video-project/final.mp4
audit samples the opening, development, and resolve of every scene plus cue
midpoints. It writes a quality report, individual PNGs, and a contact sheet; it
scores narration ownership, framing diversity, explanatory structure, visible
development, and deterministic seeking. publish enforces the same gate.
Composition contract
Every composition is a versioned JSON object with dimensions, frame rate, duration, HTML, CSS, script, scenes, assets, and user-overridable variables. Its script registers one API:
window.__HTML_VIDEO__ = {
renderFrame({ root, timeMs, progress, variables }) {
root.querySelector('.title').style.opacity = String(Math.min(1, timeMs / 500))
},
}
Calling renderFrame at the same timestamp must always produce the same pixels. Wall clocks, unseeded randomness, timers, network calls, self-playing CSS animations, storage access, and runtime imports are rejected. The app also captures the same timestamp twice and compares the PNG bytes before it starts a full render.
Local-agent pipeline
- The owner copies a video brief from the document editor into a local Codex or Claude Code session with the HTML Docs skill.
- That existing agent session reads the document and authors the composition JSON. HTML Docs does not call a separate hosted model.
- Static and browser validation run on the user's computer.
- Local Chromium captures each frame and local FFmpeg encodes the MP4.
- The CLI registers the composition and receives one-use Supabase upload tokens from HTML Docs.
- The MP4 and poster upload directly from the user's computer to Supabase Storage; they never pass through Vercel.
- A lightweight completion call verifies the objects, inserts an atomic
<video>block, and links the composition/render records.
Local requirements
- Set
HTML_VIDEO_CHROMIUM_PATHorHTML_VIDEO_FFMPEG_PATHonly when automatic binary discovery is unsuitable. - Set
HTMLDOCS_API_KEYto an account API key created in HTML Docs settings (or pass--api-key). - The document must be owned by the API-key account.
Run the database migration in scripts/sql/html_video.sql. The app lazily creates the public doc-videos storage bucket for rendered media; source compositions remain behind server-side access.
CLI development loop
pnpm --filter @html-docs/html-video cli check fixtures/hello.json
pnpm --filter @html-docs/html-video cli snapshot fixtures/hello.json --at 0,1500,2999
pnpm --filter @html-docs/html-video cli render fixtures/hello.json --output /tmp/hello.mp4
pnpm --filter @html-docs/html-video cli publish fixtures/hello.json \
--document <document-id> \
--prompt "Animate the key ideas" \
--provider codex \
--voiceover /path/to/narration.aiff \
--output /tmp/hello.mp4
After the package is published, agents can use the equivalent portable command:
npx @html-docs/html-video publish composition.json \
--document <document-id> \
--prompt "Animate the key ideas" \
--provider claude
--voiceover accepts a local audio file and muxes it into MP4 output. Use
--replace-region <region-key> to update an existing generated-video block
instead of inserting another one. Compositions must run for at least 3 seconds.
There is no framework-imposed upper duration limit; narration and media
generation remain local, and long renders are bounded only by the user's
machine, storage, and upload capacity.
The package API exposes the same loadVideoInput, compileVideoProject,
validateCompositionStatic, auditComposition, captureSnapshots, and
renderComposition functions used by the CLI, so rendering can move to a queue
or dedicated worker later without changing stored compositions or document
embeds.