Habitmon is a habit tracker I've been building: check in with one tap, then see your streaks, your year and your patterns in charts. It needed a launch video. I don't know After Effects. I do know React.
So I made the video the way I make the app: in code, with Claude Code doing most of the typing. It took about 14 hours from an empty project to the final files, 19 commits, and 7 rounds of review from an AI critic that never fully signed off. Here's how the pipeline works, what Claude was good at, and where I had to step in. The whole setup is open source: claude-video-studio.
TL;DR
- The video is a Remotion project: React components rendered to MP4. 30 seconds, 60 fps, in 9:16 and 16:9.
- Claude Code ran a 9-step pipeline and stopped at 7 gates for my approval.
- The music came first. Every cut, animation and sound effect is placed in beats, not frames.
- The UI in the video is the app's real UI, ported into the video project and fed by a demo account.
- A separate critic subagent judged every draft from rendered frames only, never from the code.
The setup
| Job | Tool |
|---|---|
| Picture | Remotion 4 (React 19), Recharts for the charts (same library as the app) |
| Director and editor | Claude Code, with a CLAUDE.md, one skill and one subagent |
| Music | ACE-Step 1.5, an open-source music model, run on my laptop |
| Voice | ElevenLabs (free plan) |
| Sound effects, mixing | Python: synthesised effects, librosa for beats, pyloudnorm for loudness, ffmpeg |
| App screens | Playwright against the app running locally with a seeded demo account |
The pipeline: Claude builds, I approve
The whole pipeline is one Claude Code skill: /habitmon-video promo-30s. It works through the steps in order, writes each step's output to a file, and stops at every gate until I type "approved". If I come back the next day, it reads the review log and picks up after the last approved gate.
The gates exist for two reasons:
- Claude can't hear, and can't watch video in real time. It can measure tempo, loudness and clipping. It can't tell whether a voice sounds warm or a cut feels late. Those calls are mine.
- Cheap decisions first. Changing a script line costs nothing. Re-rendering 60 fps finals costs 40 minutes.
The house rules live in CLAUDE.md: a render contract ("every frame is a pure function of the frame number": no CSS transitions, no Math.random, no Date), a list of banned looks (a lone centred title on a gradient, crossfades, glow, particle bursts, bounce), and the 9:16 safe zones where TikTok/Reels/Shorts put their own buttons.
Step 1: a brief that checks the claims
Before a single frame, Claude read the app's code and listed what Habitmon can actually do, with the file that proves each claim. That caught something: the marketing site promised "forgiving streaks", milestone notifications and shareable progress cards. None of them exist in the code (the streak query counts strictly consecutive days). So none of them are in the video, and the critic caps the brand score at 4 if a claim isn't on the list.
Step 2: a script that fits the beat
The script is a table: one voice line per shot, each pinned to a beat. Eight lines, 48 words in 30 seconds. That's slower than the voice's natural pace of about 2.3 words a second, which leaves room for the music and the logo. Claude wrote two hook options, "Did you show up today?" and "Showed up today?", and I picked the first: it's personal, and the tap on screen answers it within two seconds. On-screen text is always the spoken words or a shorter caption, never a paraphrase, because a lot of people watch with the sound off.
Step 3: audio first
In most edits the music goes on last. Here it goes first, because its beat grid is the film's clock.
- Music. ACE-Step generated 3 candidate tracks locally. A script measured each one's tempo and how tightly its beats sit on a grid; the pick runs at 120 BPM with under 15 ms of drift.
-
Timeline in beats.
timeline.jsonholds named marks ("tap": 6,"chart": 36), shots between marks, and voice lines and sound effects attached to marks. A hook re-resolves it into seconds every time the file changes. -
Scenes ask "which beat?"
useTimeline()turns a mark into a frame for the current frame rate, so 30 fps drafts and 60 fps finals share one timeline, and moving one mark moves everything attached to it. - Voice. Three ElevenLabs voices auditioned on the hardest line; I picked one, then 8 lines were generated with timestamps. Two lines overran their slots, so the marks moved to where the words are actually spoken. The logo lands on the same beat as the word "Habitmon."
- Sound effects. 32 pops, ticks, whooshes, a riser and a thump, all synthesised in Python. No samples, no licences to track.
- Mix. Music ducked about 9 dB under every word, mastered to −14 LUFS (the level YouTube normalises to).
Here's the whole film on that clock:
Steps 4–6: real UI, not mockups
One rule: never invent UI. Claude ported the app's own components (the calendar, the habit list, the stat cards and all four charts) into the video project, with static data from a seeded demo account. The numbers on screen are that demo account's.
The storyboard wasn't sketches either. Each shot's key pose was built in real scene code and rendered as stills, so I approved what the film would actually look like. My first round of feedback was blunt: show more of the charts, change the title font, the end card looks bad, and the violet backdrop is everywhere. Revision 2 fixed all four: Inter Tight titles, all four charts, the backdrop on the hook only, and an end card where the app's own header bar grows to fill the frame.
Both formats come from the same timeline, but each gets its own layout. The 9:16 version uses the app's real phone layout instead of a squeezed desktop dashboard:
Steps 7–9: the critic, the phone check, the finals
This is the part I'd reuse on any project. After every draft render, a script builds a review kit:
- contact sheets at 2 frames per second
- the whole film at 360 px wide, about phone size
- strips of 12 consecutive frames around every cut, where jumps and pops hide
- a 9:16 overlay with the platforms' button zones in red
- metrics: blank frames, the longest static hold, whether sounds land on their visuals, loudness
A separate subagent, frame-critic, reviews that kit. Its prompt starts with "You are reviewing a video you did not make." It never sees the code, so it judges what a viewer sees, not what the code meant to do. It scores 8 criteria, needs a timecode as evidence for every score ("no 8 without evidence"), and has hard caps so it can't be generous: a fade used as a transition can't score above 6, whatever else is good.
Each round, Claude fixes only the three worst problems, commits, and renders again.
The critic wanted every score at 8. After 7 rounds, motion and composition were still at 6. The remaining notes were things like "title exits finish on an opacity drop instead of a full mask lift" and "a bare panel band for 0.23 s". I watched both drafts on my phone, once with sound and once muted, decided they were good, and shipped. The critic's job is finding problems. Deciding which ones matter is still mine.
Only then came the finals: 60 fps with motion blur, about 19 minutes of rendering per format, the mastered mix added in, and captions written from the voice's timestamps.
Three bugs worth knowing about
Recharts axis labels flickered in the render. With animation off, axis labels still jumped between two sizes, in pairs of frames. Remotion renders frames in several browser tabs at once, and Recharts keeps measured layout state per mount, so different tabs measured differently. The fix is to remount every chart on every frame:
const frame = useCurrentFrame();
<AreaChart key={frame} data={data} width={w} height={h}>
<Area dataKey="rate" isAnimationActive={false} />
</AreaChart>
Rounds 1–3 overwrote their drafts. Only the review sheets survived. Now each round's videos are kept and every round is a git commit, so any reviewed version can be rebuilt.
A metric that lied at 60 fps. The "static hold" check fired on the finals for a slow camera push that passed at 30 fps: at twice the frame rate, each frame changes half as much. Normalise per-frame metrics to time before comparing frame rates.
By the numbers
| Length | 30 s: 60 beats at 120 BPM, 6 shots |
| Formats | 1080×1920 and 1920×1080, 60 fps with motion blur |
| Voiceover | 8 lines, 48 words, 309 characters |
| Sound | 32 synthesised effects; −14.1 LUFS, −1.2 dBTP |
| Code | ~3,000 lines of TSX (scenes, motion helpers, ported UI), ~1,900 lines of tooling |
| Review | 7 critic rounds, 19 commits |
| Final render | ~19 minutes per format on an M1 Pro |
| Total | ~14 hours from an empty project to finals |
What I'd do differently
- Set the stop rule before round 1. I made that call at round 7, after the scores had stopped moving for several rounds. Deciding it up front ("three rounds, or every score at 8") is cheaper.
- Design the 9:16 version from the phone layout from the start. Squeezing the desktop dashboard into a tall frame cost two critique rounds before it moved to the real phone layout.
- Commit every round from day one. Losing three rounds of drafts made it impossible to compare before and after.
Credits
Voice: ElevenLabs (elevenlabs.io), free plan · Music: ACE-Step 1.5, generated locally · Sound effects synthesised in code · Built with Claude Code and Remotion · Fonts: Inter Tight and Roboto.
The skill, the critic and the tools are on GitHub as claude-video-studio (MIT): run /promo-video setup and point it at your own app. Questions are welcome in the comments.
This article was drafted with Claude and checked and edited by me. The video itself was built with Claude Code, as described above.








Top comments (0)