Claude Video Export Fork: Zip Upload & Self-Hosted 4K Export
A technical breakdown of JaydenL33's claude-video-export fork — adding zip upload, HTML file selection, and aspect-ratio controls to the open-source Claude Design video exporter.
Claude Video Export Fork: Zip Upload, HTML Picker & Self-Hosted 4K Export
Claude Design and Claude Artifacts can produce impressive animated videos using the animations.jsx <Stage> starter, but getting those animations out as actual video files has historically meant either paying for a hosted service like claude2video.com or wrangling manual ffmpeg pipelines. The original claude-video-export by Dawood Trumboo solved the core rendering problem — headless Chromium frame capture with a bundled ffmpeg — but left some workflow gaps if you wanted a experience closer to a hosted service without the subscription.
This fork (JaydenL33/claude-video-export) addresses those gaps. It adds zip upload support, an HTML file picker for multi-file projects, aspect-ratio cropping (16:9, 9:16, 1:1), and keeps everything self-hosted and free. This article walks through every change, what's documented, what isn't, and what you need to know before using it in production.
Last updated: 13 July 2026. This fork is under active development. The details below reflect the state of the main branch at this date.
Two Versions, Easy to Confuse
If you've landed on the GitHub repo, it's worth understanding the relationship between the two projects before diving in:
- Upstream (
dawoodtrumboo/claude-video-export) — the original open-source release. Renders Stage-based Claude Design projects to 4K/60 H.264 MP4 via drag-and-drop folder upload or CLI. Supports bulk import, voiceover muxing, and basic resolution/fps controls. No aspect-ratio cropping (renders at native canvas size). No zip upload. No HTML file selection. - This fork (
JaydenL33/claude-video-export) — adds zip upload with automatic extraction, an HTML file picker modal for projects with multiple HTML files, aspect-ratio selection (16:9, 9:16, 1:1) with centered cover cropping, and a refined UI inspired by claude2video.com. Everything remains self-hosted and free.
The upstream README documents the original feature set well. What it doesn't cover — because it doesn't have these features — is any of the fork's additions. The fork's README documents the new features in the fork notes section, but some implementation details are only discoverable by reading the source.
Key Differences at a Glance
| Feature | Upstream | This Fork |
|---|---|---|
| Folder drag-and-drop | ✅ | ✅ |
| Zip upload | ❌ | ✅ — drop a .zip, auto-extracts |
| HTML file picker | ❌ | ✅ — modal to pick which HTML to render |
| Aspect ratio cropping | ❌ (native only) | ✅ — 16:9, 9:16, 1:1 with centered crop |
CLI --aspect flag | ❌ | ✅ |
| 4K / 1080p resolution | ✅ | ✅ |
| 60 / 30 fps | ✅ | ✅ |
| Bulk import | ✅ | ✅ |
| Voiceover muxing | ✅ | ✅ |
| Self-hosted / free | ✅ | ✅ |
Port config (PORT env) | ✅ | ✅ |
Zip Upload — How It Works
The zip upload feature is the most visible addition. Instead of requiring a native folder drag-and-drop (which some browsers restrict, especially on mobile or via remote desktop), you can now drop a .zip file directly onto the page.
Client Side
The UI detects a single .zip file drop by checking e.dataTransfer.files[0].name.toLowerCase().endsWith('.zip'). When it matches, it calls handleZip(file) instead of the folder-walk path:
// public/index.html — zip detection on drop
const dtFiles = e.dataTransfer.files;
if (dtFiles.length === 1 && dtFiles[0].name.toLowerCase().endsWith('.zip')) {
return handleZip(dtFiles[0]);
}
The zip file is uploaded via POST /upload-zip/:job with the raw binary body. The server extracts it using a pure-JS zip extractor built into server.mjs — no system unzip required. It handles both stored (uncompressed) and deflate-compressed entries.
Server Side
The extraction logic in server.mjs parses the local file header structure directly:
// server.mjs — pure-JS ZIP extraction (simplified)
function extractZip(buf, destDir) {
// Parses ZIP local file headers: signature (0x04034b50),
// compression method, CRC-32, compressed/uncompressed sizes,
// filename length, extra field length, then filename + data.
// Handles method 0 (stored) and method 8 (deflate).
}
After extraction, the server walks the output directory collecting all .html files and returns the list to the client:
const htmlFiles = [];
(function walk(d) {
for (const name of fs.readdirSync(d)) {
const fp = path.join(d, name);
if (fs.statSync(fp).isDirectory()) {
if (!/node_modules|__export|\.git/.test(name)) walk(fp);
} else if (/\.html?$/i.test(name)) {
htmlFiles.push(path.relative(projectDir, fp));
}
}
})(projectDir);
return send(res, 200, { job, files: htmlFiles });
What's Documented vs What's Not
The fork's README mentions zip upload in the feature list, but several details are only in the source:
| Detail | Documented? |
|---|---|
| Zip upload is supported | ✅ README fork notes |
| Pure-JS extractor (no system dependency) | ❌ Not mentioned |
| Supports stored + deflate compression | ❌ Not mentioned |
| Returns list of HTML files found | ❌ Not mentioned |
| Error handling for invalid zips | ❌ Not mentioned |
| Maximum zip size / memory limit | ❌ Not mentioned |
HTML File Picker — Multi-File Project Support
When a zip (or folder) contains multiple HTML files, the fork shows a modal picker letting you choose which file to export. This is essential for real-world Claude Design projects that may include index.html, test.html, standalone.html, or other variants.
The Modal UI
The modal appears after upload, listing every HTML file found in the project. Each file gets a radio button, a filename label, and a clickable row. The user selects one and clicks "Start Export":
<!-- public/index.html — modal structure -->
<div class="modal-overlay" id="html-modal">
<div class="modal">
<h2 id="modal-title">Pick a file to export</h2>
<p class="mdesc" id="modal-desc"></p>
<div id="modal-files"></div>
<div class="mactions">
<button class="drop-btn" id="modal-cancel">Cancel</button>
<button class="drop-btn" id="modal-start" style="background:var(--accent);color:#1a0d07">Start Export</button>
</div>
</div>
</div>
The selected file path is sent to the server as cfg.html in the /start/:job POST body, which passes it through to exportVideo() as the htmlFile option:
// server.mjs — passing htmlFile to the engine
exportVideo({
projectDir, htmlFile: cfg.html || undefined, outPath,
res: cfg.res || '4k', aspect: cfg.aspect || '16:9',
// ...
});
The engine then uses htmlFile to resolve the entry point instead of auto-detecting via findMainHtml():
// engine.mjs — explicit htmlFile overrides auto-detection
const mainHtml = htmlFile ? path.join(projectDir, htmlFile) : findMainHtml(projectDir);
What's Documented vs What's Not
| Detail | Documented? |
|---|---|
| HTML file picker exists | ✅ README fork notes |
| Auto-shown when multiple HTML files detected | ❌ Not mentioned |
Selected file passed as html in POST body | ❌ Not mentioned |
| Falls back to auto-detection when not specified | ❌ Not mentioned |
Aspect Ratio Cropping
The upstream renders at the canvas's native resolution. The fork adds aspect-ratio cropping with three options: 16:9 (landscape), 9:16 (portrait/reels), and 1:1 (square). The crop is a centered cover crop — the animation is never stretched.
How It Works
The aspect ratio is applied during the ffmpeg encode pass using a filter chain. The engine calculates the target dimensions from the source canvas size and the selected ratio, then applies a centered crop:
// engine.mjs — aspect ratio handling (conceptual)
// 4K outputs: 3840×2160 (16:9), 2160×3840 (9:16), 2160×2160 (1:1)
// 1080p outputs: 1920×1080 (16:9), 1080×1920 (9:16), 1080×1080 (1:1)
The UI exposes this as a segmented control:
<div class="seg" id="seg-aspect">
<button data-v="16:9" class="on">16:9</button>
<button data-v="9:16">9:16</button>
<button data-v="1:1">1:1</button>
</div>
The CLI also supports --aspect 16:9|9:16|1:1. When omitted, it falls back to native (legacy behaviour).
What's Documented vs What's Not
| Detail | Documented? |
|---|---|
| Aspect ratio options exist | ✅ README |
| Centered cover crop (not stretch) | ✅ README |
CLI --aspect flag | ✅ README |
| Exact output dimensions per ratio/resolution | ✅ README |
| What happens to content outside the crop area | ❌ Not explicitly called out |
Architecture Overview
The fork keeps the same modular architecture as the upstream, with the additions layered on top:
claude-video-export/
├── server.mjs web app — added zip upload, HTML picker, aspect ratio in POST body
├── engine.mjs capture + encode — added aspect-ratio crop in ffmpeg pipeline
├── cli.mjs CLI — added --aspect flag
├── public/index.html UI — added zip drop detection, modal, aspect ratio selector
└── starter/animations.jsx unchanged from upstream
Data Flow for Zip Upload
- User drops
.zip→ client detects.zipextension → callshandleZip(file) POST /upload-zip/:jobwith raw zip body → server extracts with pure-JS extractor- Server walks extracted files → returns list of HTML files found
- Client shows modal with HTML file list → user selects one
POST /start/:jobwith{ html: "selected/file.html", res, aspect, fps, audio }- Engine runs
exportVideo()with explicithtmlFile→ renders → encodes → serves MP4
What We Observed During Testing
A few things worth noting that aren't called out in the docs:
The pure-JS zip extractor handles common cases but isn't a full unzip replacement. It supports stored (method 0) and deflate (method 8) entries, which covers virtually all zips created by modern archivers. But it doesn't handle encryption, split archives, or exotic compression methods. If your zip uses AES encryption (e.g. from 7-Zip with default settings), extraction will fail.
The HTML picker shows all .html files, including non-entrypoints. If your project has node_modules or build output with HTML files, they'll appear in the list. The server filters out node_modules, __export, and .git directories during the walk, but any HTML file outside those paths is fair game. Pick carefully.
Aspect ratio cropping is applied during encode, not during capture. Frames are captured at the full canvas resolution, then the crop is applied in the ffmpeg filter graph. This means the capture step is slightly slower (more pixels to render) but the crop is无损 and deterministic.
The --aspect flag in the CLI defaults to native when omitted. This preserves backward compatibility with the upstream CLI behaviour. If you're scripting exports, be explicit about the aspect ratio rather than relying on the default.
What's Actually Different From the Upstream
To be precise about the delta between the two repos:
| Change | Type |
|---|---|
| Zip upload with pure-JS extractor | New feature |
| HTML file picker modal | New feature |
| Aspect ratio cropping (16:9, 9:16, 1:1) | New feature |
CLI --aspect flag | New feature |
| Centered cover crop in ffmpeg pipeline | Engine change |
htmlFile option in exportVideo() | Engine change |
| Refined UI styling | Cosmetic |
| Updated README with fork notes | Documentation |
Everything else — the rendering engine, the animations.jsx starter, the batch export system, the voiceover detection, the SSE progress streaming, the CLI bulk mode — is inherited from the upstream and works identically.
Suggested Workflow for Using This Fork
If you're adopting this fork for your own use:
- Clone and install —
git clone,npm install,npx playwright install chromium - Start the server —
npm startopenshttp://localhost:4747 - Export a single project — drag a project folder or
.ziponto the page, pick the HTML file, choose aspect ratio and resolution, download the MP4 - Batch export — flip to Bulk mode and drop a parent folder containing multiple Stage-based projects
- CLI for automation — use
node cli.mjsin CI or scripts with--aspectand--batchflags - Keep the upstream in view — this fork tracks the upstream
mainbranch. Check for upstream changes periodically and merge if they're relevant
Conclusion
This fork fills a specific gap: it gives you the convenience features of a hosted service like claude2video.com — zip upload, file selection, aspect ratio controls — while keeping everything self-hosted and free. The core rendering engine is unchanged from the upstream, so you're not sacrificing reliability or quality for the extra workflow flexibility.
If you're already using the upstream and wishing it had zip support or aspect ratio cropping, this fork is worth a look. If you need the absolute latest upstream changes and don't need these features, stick with the original. Either way, both projects are MIT-licensed and open for contribution.
Need a Hand Building on This?
We're Proanalytica Technologies, a Sydney-based consultancy that builds web, cloud, mobile, and AI automation solutions. If you're working with Claude Design exports, video rendering pipelines, or custom open-source tooling and want to talk through architecture, performance optimisation, or integrating this into your workflow, get in touch.
Jayden Lee
Founder of Proanalytica Technologies. Machine learning engineer and software developer based in Sydney, NSW. Helping Greater Sydney small businesses build better digital infrastructure.
Need help with your Sydney business?
From web design and WordPress maintenance to ServiceM8 setup and AI automation — we work with Greater Sydney SMBs.
Get in Touch