Narrated slide videos for people who would rather describe a video than storyboard one. You tell Claude Code what you want. Claude writes the slides and narration. video-claw ships the MP4.
Optional lip-synced AI presenter via fal.ai OmniHuman. Optional burned-in word-aligned captions via ElevenLabs.
Built for macOS. Works on Linux.
# One command: install, then walk you through API key setup.
curl -fsSL https://raw.githubusercontent.com/amirfish1/video-claw/main/install.sh | bash
Or the manual two-step:
pipx install git+https://github.com/amirfish1/video-claw
video-claw setup # interactive key wizard with cost notes
video-claw install-skill # optional: register the Claude Code skill
Then open Claude Code in any directory and say:
"Make me a 30-second video about how SSDs work."
Claude reads the video-claw skill, drafts slides.py + the HTML files,
asks for any missing API keys, runs video-claw render, and hands you the
MP4 in out/. You never touch the CLI directly unless you want to.
The skill triggers on natural-language prompts like "make a video about X", "turn this outline into a narrated walkthrough", "build a short for Shorts/TikTok/Reels". No slash command, no remembered syntax.
demo.mp4
(also at docs/demo.mp4 in the repo)
- HTML slides. Write them like web pages with full CSS. No bespoke DSL.
- Two orientations.
horizontal(1920x1080) for YouTube and embeds,short(1080x1920) for Shorts, TikTok, Reels. - Works with zero keys.
providerdefaults to"auto": it uses ElevenLabs if you have a key, else Deepgram, else free macOS local TTS — no config, no hard failure. Add keys to upgrade quality. - Captions always. Word-aligned from ElevenLabs; estimated from Deepgram / macOS. Never silently dropped.
- Free / $0 mode. Set
CONFIG["mode"] = "free"— local macOS voice, static Becky avatar, free captions, zero paid API calls (macOS only). See Free / $0 mode. - Optional lipsync. Set
lipsync: Trueon any slide; fal.ai overlays a circular AI presenter in the bottom-right. - Word-aligned captions. Burned in via libass when available, otherwise shipped as an SRT sidecar.
- Preview gate. Local web server shows every slide PNG in a grid before
any paid API is hit. Press
yto spend, anything else to abort. - Content-hash cache. Re-runs are seconds, not minutes. Iterate freely.
- Claude skill ships in the box.
video-claw install-skillcopies the bundled skill into~/.claude/skills/video-claw/. Any Claude Code session, in any directory, can drive video-claw on natural-language prompts after that.
Don't want to spend a cent? Set one key in your project's CONFIG:
CONFIG = {
# ...
"mode": "free",
}That's the whole switch. It forces a zero-cost render and overrides anything in the project that would otherwise spend money:
- macOS
saynarration — defaults to theZoe (Premium)voice. Premium voices download once via System Settings → Accessibility → Spoken Content → System Voice → Manage Voices. If Zoe isn't installed, free mode falls back to the always-presentSamantha(and prints a hint). Override withtts: {"macos_voice": "Samantha"}. - No lip-sync — any slide
lipsync: Trueis ignored; fal.ai is never called. - Static presenter — a circular avatar badge on every slide (bundled Becky,
or your own via
avatar: {"image": "assets/you.png"}), bottom-right. - Free captions — estimated word timing, burned in locally with libass.
macOS only — it needs Apple's say. On Linux, use ElevenLabs/Deepgram.
Or just ask Claude. Say "make me a free video about X" (or "$0", "no
budget") and the skill sets mode: "free" for you — no file editing. You'll
know it's active when the render prints
[boot] $0 mode: local macOS TTS, no paid APIs.
Here's a free-mode render — Zoe voice, Becky badge, estimated captions, rendered with no API keys:
free-mode-demo.mp4
(also at docs/free-mode-demo.mp4 in the repo)
pipx install git+https://github.com/amirfish1/video-claw
video-claw install-skill
pipx puts the CLI on PATH in its own venv (works on stock Homebrew Python
where plain pip install would fail via PEP 668). If pipx is missing:
brew install pipx.
When you upgrade with pipx upgrade video-claw, re-run video-claw install-skill to pick up the latest SKILL.md.
python3 -m venv .venv
source .venv/bin/activate
python3 -m pip install git+https://github.com/amirfish1/video-claw
video-claw install-skill
- Python 3.10+.
- A Chromium-class browser on disk. Google Chrome, Chromium, or Brave all work. The renderer shells out to whichever it finds. Playwright is not required.
- ffmpeg + ffprobe on PATH. Homebrew ffmpeg handles everything except
caption burn-in. The package falls back to shipping an SRT sidecar in that
case. To burn captions into the MP4, install
imageio-ffmpeg(which carries a libass-enabled binary):pipx inject video-claw imageio-ffmpeg.
No keys are required — on a Mac, provider:"auto" renders with free local
TTS out of the box. Add a TTS key to upgrade quality; lipsync is opt-in per
slide. Keys only become mandatory when you pin a paid provider explicitly or
run on Linux with no key.
| Key | Required when | Where to get it |
|---|---|---|
ELEVENLABS_API_KEY |
Default TTS, also drives word-level captions | https://elevenlabs.io/app/settings/api-keys |
FAL_API_KEY |
Any slide has lipsync: True |
https://fal.ai/dashboard/keys |
DEEPGRAM_API_KEY |
Only when tts.provider="deepgram" (cheaper, no captions) |
https://console.deepgram.com |
Resolution order: env var, then ~/.config/video-claw/keys.env (mode 0600,
created automatically when you run keys set).
video-claw keys set EL=sk_xxx FAL=xxx
video-claw keys test
keys test shows [opt] for optional keys that aren't set; only required
keys fail.
Typical 5-minute narrated video with ~30 seconds of lipsync bookends:
| Item | Rate | 5-min video |
|---|---|---|
| ElevenLabs Turbo v2.5 (with timestamps) | ~$0.30 / min of audio | ~$1.50 |
| fal.ai OmniHuman 1.5 lipsync | $0.16 / sec of generated video | ~$4.80 (30 sec) |
| Total | ~$6.30 |
Deepgram Aura-2 TTS is ~$0.015 / 1k chars and skips captions; for a 5-min video that's roughly $0.10. Captions need ElevenLabs.
The content-hash cache means you only pay once per unique narration + voice + speed. Re-render to fix a CSS typo: free.
Before the preview gate (including with --yes or --no-preview), render
prints a rough spend estimate for ElevenLabs, Deepgram, and any flagged
fal.ai lipsync slides. It uses the rates above, five characters per word,
and 150 words/minute. Slide speed changes the estimated lipsync duration;
TTS synthesis is priced before the local speed adjustment. The estimate
assumes all audio and lipsync must be generated: cache hits, absent avatars,
or skipped overlays can reduce spend. Actual charges vary by model, plan,
and generated duration. Free local TTS without lipsync prints no paid estimate.
You can drive video-claw directly from the shell if you want. This is the loop Claude runs for you under the hood:
video-claw init my-vid # scaffold a project directory
cd my-vid
video-claw keys set EL=sk_... # or export ELEVENLABS_API_KEY
video-claw preview # eyeball the slide PNGs first (no spend)
video-claw render # spends EL + optional fal, ships out/<name>.mp4
Out of the box, init scaffolds a 4-slide self-referential demo. Running
video-claw render immediately produces a working ~30-second video about
what video-claw does, useful as a sanity check on your install. To make
your own video, overwrite slides.py and the HTML files in slides/
(by hand, or by asking Claude).
A project is a directory. After video-claw init my-vid:
my-vid/
├── slides.py # SLIDES = [...] and CONFIG = {...}
├── slides/
│ ├── _shared.css # design tokens
│ ├── intro.html
│ ├── tell_claude.html
│ ├── claude_writes.html
│ └── outro.html
├── assets/
│ └── avatar.png # used by slides with lipsync: True
├── out/
│ └── video.mp4 # final output
└── video_build/ # cache + intermediates (safe to delete)
slides.py:
CONFIG = {
"title": "My demo",
"orientation": "horizontal", # or "short"
"out_path": "out/demo.mp4",
"tts": {
"provider": "elevenlabs",
"voice_id": "cgSgspJ2msm6clMCkdW9", # Jessica
"model": "eleven_turbo_v2_5",
"speaking_rate": 1.0,
},
}
SLIDES = [
{
"type": "html",
"html": "slides/intro.html",
"narration": "Hi. Here is what we're covering today.",
"lipsync": True,
},
{
"type": "html",
"html": "slides/point_one.html",
"narration": "Point one. The hook.",
},
{
"type": "image",
"image": "assets/screenshot.png",
"narration": "This is the dashboard.",
"title": "Dashboard",
},
{
"type": "video",
"video": "assets/demo.mp4",
"narration": "Here is a five-second clip of the app.",
"title": "Live demo",
},
{
"type": "html",
"html": "slides/outro.html",
"narration": "Thanks for watching.",
"lipsync": True,
},
]Slide types:
html. Chromium renders the file at canvas size. Use_shared.cssfor tokens.image. Wrapped in a Chromium template, gets a title strip iftitleis set.video. Drawn as a backdrop with a viewport rectangle; the clip is composited via ffmpeg.
Optional fields on any slide:
lipsync: Trueor"fal". Overlay a fal.ai OmniHuman circle on this slide (requiresFAL_API_KEYandassets/avatar.png).lipsync: "static". Overlay a still circular avatar locally, with no FAL request or avatar charge. Uses the sameassets/avatar.png, with the bundled portrait as fallback. Works on HTML, image, and embedded video slides.lipsync: False. Disable the lipsync presenter on this slide. A separately enabledavatar.staticbadge remains independent of this setting.speed: 1.2. Overridetts.speaking_ratefor this slide only.title: "...". Drawn as an amber-accented title strip for image/video slides.
Set CONFIG["tts"]["lipsync"] = "static" for a project-wide static presenter;
each slide can override it. Override the portrait with lipsync_avatar on a
slide or CONFIG["lipsync"]["avatar"]. Static avatars work with any TTS provider;
TTS itself may still have a cost. mode: "free" forces local macOS TTS and
keeps the existing static-badge preset.
ElevenLabs narrates the full script in one timestamped request, then slices per-slide audio using the original character alignment. This preserves the speaker's delivery across cuts. The master response is cached by full text, voice, and model; changing only a slide's speed reuses that response. Audio is cut before each slide's speed adjustment, and captions follow the same local timeline. The master and slices retain pauses (no silence removal). Missing or incompatible alignment stops the render rather than silently buying per-slide retries. Other TTS providers keep their existing per-slide flow. Scripts must fit the selected ElevenLabs model's single-request text limit.
video-claw init [path] # scaffold a project
video-claw render # the main command
video-claw preview # render slide PNGs only, no TTS spend
video-claw keys list # show configured keys (masked)
video-claw keys set EL=sk_x # save a key
video-claw keys test # verify each key against its provider
video-claw keys path # print the keys.env path
render flags:
--yes/-y. Skip the interactive preview gate.--no-preview. Render without even opening the preview server.--no-browser. Keep the preview URL and confirmation gate without opening a browser. Also available onpreview;VIDEO_CLAW_NO_BROWSER=1(ortrue/yes) has the same effect for both commands. Server TTL behavior is unchanged. The default still opens a browser.--out path.mp4. Override the output path.
- For each slide: HTML/image/video to PNG (cached by content hash).
- Open
http://127.0.0.1:<random>/_preview_index.htmland wait foryin the terminal. The URL is also printed as a copy-paste banner. - For each slide: TTS to mp3 + alignment.json, then to m4a with silenceremove plus atempo (cached).
- For slides with
lipsync: True: upload narration mp3 + avatar PNG to fal.ai, poll for the lipsync MP4, crop it to a transparent circle (cached). - Per-slide MP4: PNG backdrop + m4a audio + optional lipsync circle overlay (cached).
- Concat all slide MP4s.
- If ElevenLabs alignment data exists: build an .ass subtitle file and burn it in with libass. Falls back to an SRT sidecar if libass is unavailable.
- Final MP4 at
out/<name>.mp4.
"Couldn't find a Chrome/Chromium binary". Install Google Chrome
(https://www.google.com/chrome/), or set CHROME_BIN=/path/to/chrome.
Caption burn fails. Stock Homebrew ffmpeg lacks libass. Install
imageio-ffmpeg (pipx inject video-claw imageio-ffmpeg, or
pip install imageio-ffmpeg inside your venv). The package will auto-use
its bundled binary just for the caption burn step. Until then, the SRT
sidecar at video_build/captions.srt is your captions track.
Preview did not open in browser. The URL is printed as a banner before the launcher fires. Copy it from the terminal. Headless sessions (SSH, containers) will always need this path.
Circle lipsync looks washed out. The avatar source PNG has a transparent
background. OmniHuman preserves transparency; the circle crop then has
artifacts. Fix: flatten the avatar onto solid black before saving it to
assets/avatar.png. The bundled default avatar is already flat.
TTS cache misses. Before synthesis, the render prints the number of cached narrations and misses. After a successful render, it remembers synthesis settings and narration hashes (not narration text) in the build cache, so the next run can explain changed voices/models/providers or changed narration. Voice names remain part of the synthesis key: selecting a different voice must generate different audio. Speed changes reuse raw TTS and re-encode locally. Older build caches without diagnostic metadata still work; unexplained missing audio is reported without guessing its cause.
Stale cache. Set MNV_FORCE_REGEN=1 in the environment to bypass every
cache layer. Or rm -rf video_build/.
video-claw also ships as a Claude Code skill. The package wheel bundles
SKILL.md plus its references, and video-claw install-skill lays them
down at ~/.claude/skills/video-claw/. The install.sh one-liner offers
to do this for you (default Yes) when it detects a ~/.claude/ directory;
otherwise run it yourself once:
video-claw install-skill
After that any Claude Code session, in any directory, can drive video-claw
on natural-language prompts. The trigger phrases are listed inside
SKILL.md ("make me a video about X", "build a short for Reels", etc.).
Niche audience — if you don't use Claude Code, ignore this section and use the manual CLI above.
MIT. See LICENSE.