How I made a 70-second customer story video with Claude Opus 5.5 and Remotion
Fernando Graumann was remodeling my kitchen. I used Claude Opus 5.5 in Cursor and Remotion to turn that job into a 70-second customer story, with the narration driving the edit.

Fernando Graumann was remodeling my kitchen when I started building GridBoss for general contractors. His jobs already ran out of a Google Drive folder he trusted: schedules, weekly updates, photos, selections. We built the product around that folder, on a kitchen that was actually being torn apart. When it came time to tell that story on the Graumann Builders customer page, I wanted a video that could sit next to the writing and not embarrass it.
This is that video. It is about seventy seconds, it is all motion graphics, and I made it in a day with Claude Opus 5.5 inside Cursor and Remotion.
The edit I threw away
The first plan was not Remotion. It was a conventional cut: generated b-roll from Veo, real screen recordings of the product, narration from Google's Chirp text-to-speech, and an assembly in CapCut. The board was about a minute and eleven seconds, with a 9:16 recrop for Instagram. I wrote the likeness rules down before I generated anything, because Fernando is a real person and a partner, and a generated face of him would be the one lie in a video about how we work.
The rules were strict. His name could be spoken. His logo could be on the end card. His face could not be in the frame. Scenes of a builder were written from behind, and the instruction on those shots was to reject the take if you could see eyes, including a profile, a glance, or a reflection in a window. Do not feed a photo of him into the model. A face at the edge of the frame is the thing a viewer remembers.
That plan was careful, and it was still the wrong picture. The story is that GridBoss adapted to a system Fernando already had. The proof is the product: a Drive folder becoming a project, a schedule sheet becoming a timeline, a slipped date becoming a text. Generated job sites, even faceless ones, were a costume on top of that. Screen recordings of the real app were the honest part, and they were also the part that would date the fastest and take the longest to recut every time a label moved.
On the afternoon of September 29 I went looking for whatever animation setup I had already wired up. Remotion was not in the repo. By the end of that day it was, the video was rendered, and the YouTube id was on the customer story page.
Why Remotion
Remotion renders video from React. A composition is a component, a frame is a render, and the output is deterministic. Same props, same pixels. That is the property I wanted. I could put the GridBoss UI on screen as components instead of as a recording, keep the type and the colors in the same tokens as the product, and change a sentence without reopening an editor.
It is also a codebase, which is the part that made Opus 5.5 useful. I was not prompting a video model and hoping the hands looked right. I was asking a coding model to write scenes, then looking at frames and telling it what was wrong. The loop was the same one I use for product work: a plan, a diff, a render, a note.
The script is the timeline
I wrote the narration before the pictures. Twelve lines, in a JSON file the render reads directly. Each line has an id, a minimum length in seconds (hold), and the text. The opening line is the whole argument in six words: "Fernando Graumann builds with precision."
The voice is Google Cloud Text-to-Speech, Chirp 3 HD, the voice named Charon. Not a clone of Fernando. A generated voice claiming to be him would have broken the same rule as a generated face. His name is spoken by a narrator. He is not performed.
Chirp did not say his name correctly on its own. The written word stays "Graumann" on screen and in the transcript. The speech request sends an IPA pronunciation, ˈɡɹɔː.mən, so the audio says it the way he does. Change the pronunciation, re-render the two lines that use the name, and the picture retimes itself. That last part is the piece I care about.
Every beat asks the mp3 how long it is. A slot is the line's hold, or the spoken audio plus four tenths of a second, whichever is longer. The full composition measures every file at render time and sets its own duration.
export const slotFrames = async (line: { id: string; hold: number }) =>
Math.ceil(Math.max(line.hold, (await spoken(line.id)) + 0.4) * FPS);
export const measureSlots = async (): Promise<Slot[]> => {
const frames = await Promise.all(LINES.map(slotFrames));
let from = 0;
return LINES.map((line, i) => {
const slot = { id: line.id, from, frames: frames[i] };
from += frames[i];
return slot;
});
};
spoken opens the mp3 with mediabunny and reads its duration. Remotion's calculateMetadata calls measureSlots before the first frame, then passes the measured slots in as props. Re-record a line and the cut moves. I do not keep a spreadsheet of in-points.
Beats overlap by eight frames so the next one can fade in over the previous. Each sequence plays its own audio. There is no music bed. I liked the quiet, and I did not want a track papering over a beat that was not working.
What I asked the model to do
Opus 5.5 wrote most of the scene code. I wrote the constraints, and I rejected frames.
The constraints that mattered:
- No person on screen. The Veo plan had already spent a lot of words on how to show a builder without showing Fernando. Dropping people entirely made the rule trivial to enforce. An illustration cannot grow a face in the last ten frames.
- On-screen copy comes from the product, not from the model's idea of a contractor app. Tasks, dates, the weekly update, the selection the homeowner has to answer, and the text message are copied from the demo seed and from the real SMS template. They live in one file,
content.ts, with a comment naming the source of each block. Elena Vasquez and Grove Street Kitchen are the sandbox project, pinned to a fixed "today" so the dates do not drift between renders. - The words and the pictures have to agree with the customer story page. If they disagreed, the page won.
The model was good at the tedious correct things. Drawing a kitchen plan as stroked paths. Building a fake Drive folder that lights up the schedule, the weekly update, and the photos as the line mentions them. Laying out the app chrome so it reads as GridBoss and not as a generic dashboard. It was also good at doing those things quickly enough that I could throw a beat away.
It was not good at knowing when it had invented a product. Left alone, a coding model will happily typeset a feature you do not ship, because a plausible screen is easier than reading the seed data. The fix was mechanical: one module of approved copy, and scenes that import it instead of embedding strings. When a label was wrong I changed it in that module, not in twelve components.
The other place I had to steer was taste across time. A still can look finished while the two seconds on either side of it fall apart. Files fly into a drop zone and, for a few frames, the filenames sit on top of each other. A headline lands before the cards it is about. I rendered stills, looked at them, and sent the bad frames back. That is most of what "directing" meant on this project. The per-beat compositions made it cheap. I could re-render six seconds instead of the whole story.
Four things worth stealing
The cold open is timed to the words, not to a typewriter. The line has no pauses, so each word is given a slice of the speech window from 0.43 seconds to 2.3 seconds, weighted by its length. "Precision" is the last word and the only one in the accent color. It fades up, unblurs, and the caret blinks until the sentence is done.

The camera is a logarithm. The second beat starts on the kitchen plan, pulls back to the house, then to an outline of Massachusetts with pins for a kitchen, a bath, and an addition. The scale change is about a thousand to one. A linear zoom across that range spends almost all of its time visually stuck at one end. Interpolating the log of the scale spends the time evenly. A linear pan of the focal point has the opposite problem: across that zoom it throws the house off the screen. The code moves the house's position on screen in a straight line, then solves for the focal point.
// Across a 1000x zoom a linear focus pan throws the house off-screen, so
// move the house's screen position linearly and solve for focus.
const s = Math.exp(lerp(Math.log(S_HOUSE), 0, p));
return {
s,
focus: {
x: HOUSE_FOCUS.x - ((HOUSE_FOCUS.x - MAP_CENTER.x) * p) / s,
y: HOUSE_FOCUS.y - ((HOUSE_FOCUS.y - MAP_CENTER.y) * p) / s,
},
};

The product is drawn, not recorded. Walls use @remotion/paths and evolvePath, so the plan draws itself. The app that the plan turns into is a React recreation of the Grove Street project: the same sidebar, the same tabs, the same upcoming tasks. The folder view names the real artifacts, schedule sheet and weekly update and photos, and a dashed "client view" shows up beside them to say what the homeowner will actually see.


The sheet and the portal are the same data. A cell in the schedule flips to Done, and the matching row in the portal ticks. The chip under them says the thing I most wanted a contractor to believe: same sheets, same crew, nothing new to learn. Later, a date slips two days and a phone receives the text GridBoss actually sends, including the stop-to-opt-out line. I would rather show a boring true message than a punchy invented one.

The last beats widen out. The kitchen plan comes back with the line "Built with Graumann Builders," because the features in the video were shaped on his job, not in a vacuum. Then four trades, builders and electrical and HVAC and septic, each shown with a feature that product really has for that work. Then the end card.


What I left unfinished
The original board asked for burned-in captions, a 9:16 cut, and a music bed. None of those are in the file I uploaded. Captions are the one I miss. The narration is short and clear, and a lot of people will watch this muted. A vertical cut is a crop plus a reflow, not a new film, and Remotion is a reasonable place to do it. I have not done it yet.
I also did not throw away the Veo takes. They are still on disk, next to the plan that describes them. They are not in the timeline. If I ever need a job site, I would rather shoot Fernando's, with his permission, than generate one.
Render, then the page
The render is one script.
cd marketing/motion
npm run render:story
That writes out/graumann-story.mp4. The master I uploaded is about twelve megabytes and a minute and ten seconds. I put it on YouTube and set youtubeId on the customer story. The page already had a lite embed: a poster image, and the player only loads from youtube-nocookie.com after a click. The VideoObject structured data turns on with the same id. Until the id was set, the page showed a coming-soon card. I liked having that slot waiting. It kept the video from becoming a side project with no address.
The whole path, from "Remotion is not installed" to a public embed, was one afternoon. That is not because the model made a film. It is because the film was a small React app with a strict script, a measured soundtrack, and a rule that the pixels had to match a product I already had. Opus 5.5 is fast at that kind of app. The directing was deciding which frames were true.
If you are building something where the product is the picture, start from the script and the real UI, and let the duration fall out of the audio. Generate people only when you are prepared to say exactly whose face you are allowed to invent. For this story, the answer was nobody's.
I build software the same way I built this video: with the people who will use it, on the job they already have. If that is the engagement you want, book a call. If you want the contractor side of this story, the page is Graumann Builders on GridBoss.