How to Build an AI-Video Scroll Animation Page
Scroll the box below.
Assembled. Start scrolling.
849 KB · 192 frames · generated for $0.64
Generate one widescreen product photo. Feed it to the video model as the first frame, because image-to-video is the only thing that stops it inventing a different product.
Roll the cheapest tier three times and keep the best. The difference between two runs of the same model is bigger than the difference between models.
Then re-encode with -g 8 and scrub the video with scroll position. An image
sequence of the same clip is 4.7x heavier and takes 5x
longer to become drawable on a phone.
- A product that exists only as pixels, with no logo and no licensing problem
- An eight second exploded-view clip of it, generated for $0.64
- A scroll-scrubbed hero that ships at 849 KB in 2 requests
- The numbers to defend that choice: bytes, requests, paint time, scroll time
- The four prompt mistakes that cost me $0.44 before the technique worked
Why the footage is the wall
The scroll part is old and well documented. You pin a tall section of the page, work out how far the reader has scrolled through it as a number between 0 and 1, and use that number to pick what to show. It is about fifteen lines of code and a dozen sites explain it well.
Every one of those explanations begins the same way: first, load your image sequence. Or: first, import your 3D model.
Both of those assume you already own something expensive. An image sequence
is a folder of numbered still pictures, one per frame, and video runs at 24 frames a second, so
eight seconds means 192 separate photographs of the same product, in the same
light, from the same angle, each one slightly further through the motion. A 3D
model, the .glb file those tutorials tell you to import, is a digital
sculpture of the product that a 3D artist builds over days.
That is the wall. A solo builder has neither. Getting the asset was always the expensive part, and the code was never the problem.
That is what changed. You can now generate the footage, and it costs less than a coffee. What follows is the exact path, including the parts where I got it wrong, which turned out to be the useful parts. Working like this, where you test a thing and let the measurement decide, is the method I teach in Build With AI 1.0.
I used Google's Veo 3.1 throughout, because it is what I had a key for and because the numbers in this guide had to come from somewhere specific. Nothing here depends on it.
Runway, Kling, Luma, Pika, Sora, Hailuo and Wan are all reasonable substitutes, and new ones appear constantly. The one thing your model must support is image-to-video, sometimes labelled "first frame" or "starting image". That is the feature the whole technique rests on, for the reason shown in Step 2.
If a tool only takes a text prompt and gives you back a video, it cannot do this job. The prices and the exact wording of the prompts will differ. The method does not.
- What you need: a Google account with billing enabled for the generation steps, and ffmpeg for the encoding step. Steps 1 and 2 can be done entirely in a browser, so Python is optional and every script in this guide is tucked behind a "prefer to script it?" toggle.
- Veo is not on the free tier at all. Not throttled, not limited. Not available. If your account has no billing, every generation in this guide fails.
- Tested on:
google-genai1.56.0, Veo 3.1, Chromium, on 31 July 2026. - The old model IDs are dead.
veo-3.0-generate-001andveo-3.0-fast-generate-001shut down on 30 June 2026, so any tutorial or sample still calling them will fail. Use theveo-3.1-*IDs below.
Step 1: make the product exist
Generate one still photograph first. Not a video. One image, and make it good, because everything after this inherits from it.
I call it a plate: one reference image that every later step is built from. The idea comes from my guide on keeping AI characters consistent across scenes, and the problem is identical. An image model has no memory of the last picture it made, so if you want the same product twice you have to give it a reference to work from.
The product is invented on purpose. It carries no logo and no brand marks, which keeps a published page out of trademark territory and costs nothing editorially.
Here is the whole thing. Everything that follows in this guide came out of these words:
The plate prompt
Widescreen studio product photograph, tight hero framing. A pair of matte black over-ear headphones fills the frame, occupying roughly 80 percent of the frame height, centred, floating. Large deep circular ear cups with thick dark grey memory-foam ear pads, visible brushed aluminium yokes and hinge hardware, a slim padded headband, fine panel seams and small visible screws on the cup housings. Clean generic industrial design with no logos, no text and no brand marks anywhere. Seamless light grey studio background. Soft large softbox key light from the upper left, gentle fill from the right, soft contact shadow beneath. Razor sharp focus, high mechanical detail, neutral colour, three-quarter view, landscape orientation.
You do not need to write any code for this step. Paste that prompt into Google AI Studio, pick the image model, and set the aspect ratio to 16:9 in the settings panel beside the prompt.
Setting it there rather than asking for it in words is not a detail. It is the first mistake below, and it cost me $0.44.
Prefer to script it? The Python version
Same prompt, sent through the Gemini SDK. Useful if you are generating plates in bulk or want the aspect ratio pinned in code rather than in a UI.
from google import genai
from google.genai import types
client = genai.Client(api_key="YOUR_KEY")
PROMPT = "...the plate prompt above..."
resp = client.models.generate_content(
model="gemini-2.5-flash-image",
contents=PROMPT,
config=types.GenerateContentConfig(
image_config=types.ImageConfig(aspect_ratio="16:9")
),
)
for part in resp.candidates[0].content.parts:
if part.inline_data and part.inline_data.data:
open("plate.png", "wb").write(part.inline_data.data)
break
Either route costs $0.039 and returns a 1344x768 image. This is the one I kept, and every clip, frame and number in the rest of this guide comes from it:
My first plate prompt said "Widescreen 16:9 studio product photograph" and ended with "16:9 widescreen aspect ratio, landscape orientation". I got back a square image.
Aspect ratio is a setting, not prompt language. Asking for it in words does nothing.
You set it in the panel beside the prompt, or in the image_config field if
you are scripting it.
The cost of getting this wrong is not just one bad image. Feed a square plate to Veo and the clip comes back with the product in a 1:1 box and hard black bars down both sides, wasting about 44% of the frame. So the mistake propagates into the expensive step.
That cost me $0.44 in wasted plates and clips before I read the configuration properly.
Framing decides how much comes apart
This one surprised me, and it changes more than anything else in this guide.
My second plate was correct 16:9, but composed wide. The headphones occupy 53% of the frame height, with a lot of empty studio around them. My third plate is the same product, same lighting, same everything, framed so it fills 86%.
I sent both to Veo with an identical prompt. Press play and watch how differently they come apart:
Nothing changed but the framing. The model can only take apart what it can see, and a product sitting small in a wide frame simply has fewer legible parts to work with. If your clip comes back lazy, this is the first thing to fix. Fill the frame.
Step 2: make it come apart
Now hand the plate to Veo as the first frame. This is the whole trick, and the reason it works is worth a minute.
Most people's instinct is to describe the product to the video model and let it generate. That is called text-to-video. The alternative, image-to-video, hands the model a starting picture and asks it to animate that. It sounds like a small distinction. It is the difference between this technique working and not working.
I ran the same motion prompt twice. Once with no image, text only. Once with the plate as the first frame. Same words, same model, same settings.
Anchored against not anchored
Identical prompt and model. The only difference is whether the plate was passed as the first frame.
Text-to-video does not make a worse version of your product. It makes a different product. For a landscape that does not matter. For something a customer is supposed to recognise, it is the whole game.
Those are single frames. The clips make it plainer:
Here is the motion prompt. It is doing two jobs: naming every component that should separate, and saying when the motion starts and ends.
The motion prompt
The disassembly begins immediately in the very first frame and continues without pause until the final frame. The headphones separate into a full exploded view: the ear pads lift away as rings, the ear cups split open and separate outward from the yokes, the internal driver units emerge and float free, the hinge hardware drifts apart, and the headband rises. Each component drifts apart along its own axis and hangs suspended in mid-air, and by the final frame all components are fully separated and widely spaced. The camera holds a slow steady orbit. Every component keeps its exact shape, colour and material as it separates. Clean seamless light grey studio background, soft studio lighting, precision product visualisation.
Again, no code required. In Google AI Studio, choose the Veo model, upload your plate as the starting image, paste the motion prompt, and set the length to 8 seconds. Uploading the plate is the part that matters. Without it you are doing text-to-video, and you get the clip on the left above.
Every other tool that offers image-to-video works the same way: there is a slot for a starting frame, and that slot is the whole technique.
Prefer to script it? The Python version
Video generation is asynchronous, so this starts a job and polls it. About 45 seconds of waiting for 8 seconds of video.
import time
from pathlib import Path
from google import genai
from google.genai import types
client = genai.Client(api_key="YOUR_KEY")
MOTION = "...the motion prompt above..."
op = client.models.generate_videos(
model="veo-3.1-lite-generate-preview",
prompt=MOTION,
image=types.Image(
image_bytes=Path("plate.png").read_bytes(),
mime_type="image/png",
),
config=types.GenerateVideosConfig(
duration_seconds=8,
resolution="1080p",
),
)
while not op.done:
time.sleep(10)
op = client.operations.get(op)
video = op.response.generated_videos[0]
client.files.download(file=video.video)
video.video.save("hero.mp4")
About 45 seconds of waiting for 8 seconds of video.
My instinct for a scroll animation was to demand even motion, so I wrote "at a slow, steady, completely constant speed from the first frame to the last, never pausing and never speeding up". It sounds exactly right. It produced a mess: the product blew apart early and the rest of the clip was a cluster of anonymous black rings.
What worked was naming the parts and saying only that the motion begins in the first frame and completes in the last. Describe what comes apart, not how fast. The timing instruction fights the model. The parts list helps it.
Four settings that look available and are not
The Python SDK exposes a rich config object. Most of it is rejected by the Gemini Developer API, and the errors only arrive when you call. Rejections are not billed, so mapping this cost me nothing, but it is worth knowing before you design around a field.
| Setting | What happens | What it means for you |
|---|---|---|
seed | Rejected on every tier | You cannot reproduce a clip. Ever. A roll you like is the only copy that will ever exist. |
generate_audio | Rejected on every tier | Every clip ships an AAC track you did not ask for. Strip it later. |
last_frame | Rejected | No forcing the clip to land back where it started, so no free loop. |
reference_images | Rejected on Lite and Fast | The multi-reference version of the plate trick is not available. First-frame is the anchor you get. |
negative_prompt | Rejected on Lite | Works on Fast and Standard. This is a real reason to move up a tier, unlike quality. |
image (first frame) | Works everywhere | The one anchor that exists. Use it. |
The seed line has a consequence that shapes the rest of this guide, so it is
worth sitting with. When you get a roll you love, that file is the only copy that will ever
exist. You cannot regenerate it at a higher resolution afterwards. Generate at the
resolution you intend to ship.
Step 3: the cheap tier probably wins
Veo 3.1 comes in three tiers and the price gap is large: $0.05, $0.10 and $0.40 per second of output at 720p. The obvious assumption is that hero footage deserves the top one. So I ran the same plate and the same prompt through all three and looked at the same frame.
Standard looks the most coherent. Fast produced more parts but duplicated some of them. Lite is the sparsest. Read that quickly and you conclude the expensive tier is worth it.
Then I ran the control that actually mattered. Three runs of Lite. Same plate, same prompt, same settings, nothing changed between them.
Three runs of the same model, same everything
Veo 3.1 Lite, 8s, 1080p, identical plate and prompt. Frame 185 of 192 in each.
Run 3 came off the $0.05 tier and is better than the $3.20 Standard clip. Run 2 is a write-off. The spread between two runs of the same model is wider than the spread between models, which means the tier is not the variable that matters. The roll is.
Put that together with the missing seed and the strategy falls out on its own.
You cannot re-roll a good result and you cannot avoid a bad one, so buy more rolls instead of
more model. Three Lite rolls cost $1.20 and produced the asset on this page. One
Standard roll costs $3.20 and is still one roll.
I ran Lite three times, and Fast and Standard once each. That is enough to show the variance is large.
It is not enough to rank the tiers against each other, and I am not going to pretend otherwise on a sample of one.
If you need a specific look rather than a good look, the negative_prompt
support on Fast and Standard is a better argument for paying more than image quality is.
Step 4: the keyframe trap
You have a clip. Two things need fixing before it goes near a page.
Both are done with ffmpeg, a free command-line tool for video. If you have never used a terminal, this is the one place in this guide where you will need one. The commands are copy-paste, and if any of them puzzle you, paste it into an AI assistant and ask what each flag does. That is how I read unfamiliar ffmpeg lines too.
First, the audio. Veo hands you an AAC track whether you want one or not, and a scroll hero is silent by definition. This command copies the picture across untouched and drops the sound:
ffmpeg -i hero.mp4 -an -c:v copy hero-silent.mp4
That took my clip from 4,850 KB to 4,695 KB. A free 155 KB, and it costs nothing because the picture is copied across untouched rather than re-encoded.
Second, and this is the one that decides whether the whole approach feels good or terrible: count your keyframes.
ffprobe -v error -select_streams v -show_entries frame=key_frame -of csv=p=0 hero.mp4 | grep -c '^1'
That asks the file how many of its frames are keyframes, and counts them.
My eight second clip came back with one. One keyframe, 192 frames.
A keyframe is a frame stored in full. Every other frame only stores the difference from the one before it. So when you jump to frame 150 and there is no keyframe behind it, the decoder has to walk all the way from frame 0 to build the picture. Every scroll seek pays that.
That is the theory. I have not seen anyone put a number on it, so here is one. Both files fully downloaded first, so nothing but decoding is being timed, then 40 seek positions visited once each in random order on a CPU throttled to a quarter of its speed:
What one keyframe costs you per seek
Same 8s clip at 720p, same crf, fully buffered, 4x CPU throttle. Median of 40 unique seeks.
A screen refreshes every 16.7 ms. The dense encode lands inside that budget and the sparse one misses it by more than three frames, on every single seek. I ran it twice and got 57 against 14, then 56 against 10.
That is the whole argument for spending the bytes. The 379 KB you save by skipping this step buys you a scrub that cannot keep up with the display.
You fix it by forcing keyframes. It costs bytes, and not in a straight line:
Keyframe spacing against file size
The shipped clip, re-encoded from one master at 720p, h264 crf 28, changing nothing but -g (the gap between forced keyframes).
Even making every single frame a keyframe lands under
the 4,008 KB that the equivalent image sequence weighs. The floor for this technique is
lower than most people assume. Note that -g 1 is not just five times the
size, it also scores worse on SSIM than -g 8: at a fixed crf, spending
every frame on a full picture buys you nothing.
This is the command that produced the file running at the top of this page. It scales the clip to 720p, compresses it, and forces a keyframe every eight frames:
ffmpeg -i hero-silent.mp4 -an -vf scale=1280:-2 -c:v libx264 -crf 28 -preset slow -g 8 -keyint_min 8 -sc_threshold 0 -movflags +faststart -pix_fmt yuv420p hero.mp4
-movflags +faststart matters more than it looks. It moves the index to the front
of the file so the browser can start seeking before the whole thing has arrived.
High resolution, dense keyframes, small file. Pick two.
I encoded the same 1080p master at -g 4 and got 3,436 KB,
which is heavier than the image sequence it was supposed to beat. Dropping to 720p at
-g 8 gave 847 KB, and seeks got faster rather than slower.
A scroll hero is displayed at whatever height the viewport allows, and it is constantly in motion. It needs the keyframes far more than it needs the pixels.
Step 5: wire it to the scrollbar
Now the easy half. A tall track, a sticky stage inside it, and scroll position mapped onto
currentTime. You do not have to write any of it by hand.
But you cannot just ask for it either, and this is the part worth slowing down for.
The obvious way to write this is one line: work out how far down the page you are, multiply
by the video's duration, assign it to currentTime. Almost every tutorial does
exactly that, so it is what an AI assistant will hand you by default. That version is
broken. It looks fine while you test it slowly and falls apart the moment a real person
scrolls at a real speed, for a reason I measured and put below.
So the useful thing here is not the code. It is knowing what to insist on. Give your assistant this:
The build prompt
Build me a scroll-scrubbed video hero as a single HTML file.
Structure: a tall section (400vh) containing a sticky stage that is 100vh tall and pins to the top of the viewport. Inside the stage, a video element holding my clip, scaled to fit without cropping.
Behaviour: as the reader scrolls through the tall section, map scroll progress to a number from 0 to 1 and set the video's currentTime to that fraction of its duration. The video must never play on its own. Scroll position drives it entirely.
These five requirements are not optional, and most examples online get the first one wrong:
1. Do NOT assign currentTime directly inside the scroll handler. A video element services one seek at a time and silently discards any currentTime you write while its `seeking` property is true. Instead, store the latest target position in a variable; if a seek is already in flight, do nothing, and re-apply the stored target from the video's `seeked` event.
2. Throttle the scroll handler with requestAnimationFrame behind a boolean gate, so you never queue more seeks than the screen can actually draw.
3. Register the scroll listener with { passive: true }.
4. The video element must carry the `muted` and `playsinline` attributes, or mobile browsers will refuse to decode it without a tap.
5. Give the video a `poster` image, so there is something on screen in the milliseconds before the clip is drawable.
Finally, respect prefers-reduced-motion: if the reader has it set, skip the scrubbing entirely and show a single static frame instead.
Why hand over a spec instead of a description? Because every one of those five points is something the model will otherwise get wrong, and four of them fail quietly. You will not see a broken build. You will see a page that works when you test it and looks frozen to your readers.
Read the two mistake boxes below before you accept whatever comes back. They are the failures that cost me the most time on this build, and both of them looked like success.
Want the finished code? Here it is
This is what the prompt above should produce, and what runs at the top of this page. Useful as a reference to check the output against.
<section id="track" style="height: 400vh">
<div id="stage" style="position: sticky; top: 0; height: 100vh;
display: grid; place-items: center; overflow: hidden">
<video id="hero" src="hero.mp4" poster="poster.jpg"
muted playsinline preload="auto"
style="width: 100%; height: auto; max-height: 100vh"></video>
</div>
</section>
<script>
const v = document.getElementById('hero');
const track = document.getElementById('track');
let ticking = false;
// A video services one seek at a time. Keep the latest target and re-apply it
// when the current seek finishes, or fast scrolling throws most of them away.
let target = 0, queued = false;
function seek() {
if (!v.duration) return;
if (v.seeking) { queued = true; return; }
queued = false;
v.currentTime = target;
}
v.addEventListener('seeked', () => { if (queued) seek(); });
addEventListener('scroll', () => {
if (ticking) return;
ticking = true;
requestAnimationFrame(() => {
const r = track.getBoundingClientRect();
const p = Math.min(1, Math.max(0, -r.top / (r.height - innerHeight)));
if (v.duration) { target = p * v.duration; seek(); }
ticking = false;
});
}, { passive: true });
</script>
The first version of this page shipped without the seek() wrapper. It
assigned currentTime straight from the scroll handler, which is what almost
every example does. Hasan opened it and told me the labels were moving but
the image was frozen.
A video element services one seek at a time. Assign
currentTime while seeking is still true and the browser
silently discards it. Scroll slowly and each seek finishes before the next one arrives,
so it looks fine. Scroll at a normal speed and most updates are thrown away, and the
picture parks on whichever seek last completed. Meanwhile the caption and the progress
bar, which are ordinary DOM writes, keep updating perfectly. That gap between moving
text and a frozen frame is the tell.
Measured on the broken version in Chrome and Edge: with 120 ms of settling between
steps, 2 of 9 seeks were dropped. Under a continuous scroll,
8 of 40 updates arrived while a seek was already in flight. Holding the
latest target and re-applying it on seeked takes both to zero, because the
final position is always honoured even when every update before it was discarded.
Checking what it gives you
Five things are doing real work in that file. Read whatever your assistant produces and confirm each one is present, because four of the five fail silently:
- The seek queue. Look for a stored target and a
seekedlistener. IfcurrentTimeis assigned straight from the scroll handler, you have the broken version. Symptom: the picture freezes while everything else on the page keeps moving. - The
requestAnimationFramegate. Scroll events fire far more often than the screen redraws. Without thetickingflag you queue up seeks the browser will never show. Symptom: heat and battery drain, no visible fault. { passive: true }. Tells the browser this listener will never callpreventDefault, so it can keep scrolling smoothly instead of waiting to find out. Symptom: scrolling feels slightly sticky.mutedandplaysinline. Without both, mobile browsers refuse to load or decode the video without a tap. Symptom: works on your laptop, blank on a phone.- The
poster. What people see in the milliseconds before the video is drawable. Use the assembled first frame, never an exploded one. Symptom: a black rectangle on first paint.
That list is the real deliverable of this step. The code is fifteen lines and any assistant can write it. Knowing which fifteen lines are wrong is the part that took me a day.
This one cost me the most time, and it will hit you too. I built the demo, it worked, Hasan opened it locally and the picture was frozen on the first frame while the captions kept updating.
Seeking a video needs HTTP Range requests. The browser asks for
bytes 1000-2000 and expects 206 Partial Content. A server that instead
replies 200 with the whole file leaves the browser reporting
video.seekable as [0, 0]. The clip downloads,
it is fully buffered, readyState is 4, and every single
currentTime you assign is clamped straight back to zero.
Django's development static server does not support Range. So with
DEBUG=True this technique cannot work, no matter how correct your code is.
Check with one command:
curl -s -D - -o /dev/null -r 1000-2000 http://localhost:8000/static/your-hero.mp4 | head -1
206 Partial Content means you are fine. 200 OK means you
will spend an afternoon debugging JavaScript that was never broken. WhiteNoise, nginx
and every CDN answer 206. Serve the clip from one of those, or from a CDN, and it works
immediately.
The frame budget nobody mentions
Your clip has a fixed number of frames. Veo returns 24 frames per second, so eight seconds is 192 frames. That is every distinct picture the reader can ever see, and it has to cover the whole scroll distance you gave the track.
At height: 400vh on a 900px viewport, that is roughly 2,700 pixels of scrolling
spread across 192 frames: about 14 pixels of scroll per frame. Comfortable. Push the track to
1000vh and you get one new frame every 35 pixels, and the animation visibly steps. Track height
and clip length are one decision, not two.
Hijacking the scrollbar is a strong move and some people physically cannot tolerate it.
Wrap the pinning in @media (prefers-reduced-motion: reduce), drop the sticky
behaviour, and show a single still instead. The demo at the top of this page does exactly
that. It costs about six lines.
The measurement, on a throttled phone
The alternative approach is to export the clip as stills and draw them into a canvas. It is the technique most people reach for. I built both against the same clip and measured them with Playwright: desktop, then an emulated mid-range phone at 412x915 with a 4x CPU throttle, driving an identical 60 step scroll through the pinned track. Every timing below is the median of five runs, because a single run of this cannot tell a real difference from noise.
| On the throttled phone | Canvas + WebP sequence | Scrubbed video | Difference |
|---|---|---|---|
| Transferred | 4,010 KB | 849 KB | 4.7x lighter |
| Requests | 193 | 2 | 96x fewer |
| Hero drawable after | 585 ms | 117 ms | 5x faster |
| 60 step scroll took | 2,081 ms | 1,884 ms | 9% faster |
The weight and the time to first picture are the clear wins. The scroll time is the interesting one, and the median undersells it.
Across those five runs the video's scroll time stayed inside a 43 ms band, from 1,848 to 1,891. The canvas swung 626 ms, from 1,982 to 2,608. That spread is the number that matters, because nobody perceives an average. What a reader notices is the run where it stutters, and the sequence has one of those and the video does not.
Be straight about the other half of this: on desktop the two are a dead heat. Scroll times came out within 1% of each other, and there the canvas was marginally the steadier. This is a mobile advantage, not a universal one. It is also the half that matters, because the phone is where these pages fall over.
What each approach weighs
The shipped clip, 192 frames, 720p, every format encoded from the same master.
Cutting the sequence to every fourth frame still lands at 1,904 KB, and it costs you three quarters of your frames. There is no version of the image sequence that wins on weight.
- Safari and iOS. Chromium only. Video seek behaviour differs meaningfully on WebKit and I have no numbers for it, so treat iOS as unmeasured rather than as covered.
- A real phone. An emulated viewport with a 4x CPU throttle is directionally useful, not a substitute for a device in your hand.
- A real thumb. My 60 step scroll is perfectly even. Human scrolling is burstier, and bursts are where seeks pile up.
What the whole thing cost
Every generation, including the mistakes and the ones that failed:
| What | Detail | Cost |
|---|---|---|
| Access probe | 1 clip, just to prove the key could call Veo at all | $0.20 |
| Reference plates | 4 images, 3 of them wrong before the aspect ratio clicked | $0.16 |
| Prompt discovery | 9 clips on Lite 720p, finding the framing and the wording | $3.30 |
| Tier comparison | 4 clips: Fast, Standard, and two capability probes | $6.00 |
| Variance control | 2 extra Lite runs, the experiment that changed the advice | $0.80 |
| Final hero | 3 rolls at 1080p, one failed and was not billed | $1.28 |
| Frame weights, benchmarks, encoding | ffmpeg and Playwright | $0.00 |
| Total | 18 billed clips, 4 images | $11.74 |
If you already know what you are doing, which you now roughly do, the cost of one hero is the plate plus three rolls: $1.24 at 720p, or $1.96 at 1080p. The $11.74 is what it cost to find the path, and most of it went on the tier comparison that turned out not to matter.
Six of my findings cost nothing at all, because rejected and failed generations are not billed. Every unsupported setting in that table earlier was discovered for free.
Where this breaks
I would not use this technique for everything, and it is worth being straight about the edges I hit.
- Text on the product. Anything with a model number, a logo or a label will come apart into gibberish. Video models cannot hold small type. If your product has words on it, render those in HTML over the video instead, which is the generate it with code, not diffusion rule applied to a different surface.
- Engineering accuracy. This is a plausible teardown, not a true one. The driver units are convincing and they are invented. Do not use it for documentation, repair guides or anything a customer might take literally.
- An existing real product. If you need your product, you need your product in the plate, and a generated photo of a real item is a different problem with trademark attached. Photograph the real thing and use that as the plate.
- Long clips. Everything here is 8 seconds. Longer means more frames, more keyframes, more bytes, and the model back-loads the motion more the longer it runs.
- Reproducibility. Worth repeating because it is genuinely unusual. With
no
seed, your good clip cannot be regenerated. Back it up like a photograph you cannot retake.
Questions people actually ask
Can AI video really generate a product exploded view?
Yes, if you anchor it with a first frame. Text-to-video invented a different pair of headphones from the one I described. The same prompt with the plate attached kept the shape, the materials and the softbox reflection, and separated it into pads, drivers and hinge hardware.
Which Veo tier do I need?
Probably the cheapest. Three runs of Lite at $0.05 per second produced one unusable clip, one
moderate one, and the best asset in the project, which beat a single Standard run at eight times
the price. Buy more rolls, not more model. The real reason to pay for Fast or Standard is
negative_prompt support, which Lite does not have.
Image sequence or video scrubbing?
Video, provided you re-encode with dense keyframes. On a CPU-throttled phone the sequence was 4,010 KB across 193 requests and took 585 ms to become drawable. The video was 849 KB across 2 requests and took 117 ms. Across five runs the video's scroll time stayed inside a 43 ms band while the sequence swung 626 ms.
Why does scrubbing a video feel janky?
Keyframe spacing, almost always. A default encode of an 8 second clip can contain a single
keyframe, so every seek decodes from the start. On a throttled CPU that measured 57 ms per seek
against 14 ms for the same clip at -g 8, and a display refreshes every 16.7 ms.
Re-encode with -g 8.
Can I get the same clip twice?
No. The SDK has a seed field and the Gemini Developer API rejects it. Generate at
the resolution you plan to ship, because you cannot go back for a bigger version.
Does this work on iPhone?
Unmeasured. I tested Chromium only. WebKit handles video seeking differently and I would test on a real device before shipping this to an audience that skews iOS.
The useful part here was not the prompt. It was running the control: three rolls of the same model, which turned a confident conclusion into the opposite one and saved 8x on every hero after it.
Building that reflex, where you check the thing you assumed instead of shipping it, is what Build With AI 1.0 is really about. 12 days, one real product, nothing skipped.
Related
Tested on: Veo 3.1 (lite / fast / standard) · Gemini 2.5 Flash
Image · google-genai 1.56.0 · ffmpeg 7 · Chromium via
Playwright, desktop and emulated 412x915 at 4x CPU throttle.
Lab run: 2026-07-31. Last verified: 2026-07-31. Every
price, byte count and timing on this page came from that run. 18 billed clips,
4 images, $11.74 total.
Get the free Vibe Engineering Blocks guide
The exact building blocks I use to ship real products with AI — yours as a free PDF.
Questions & Discussion
Ask a question about this guide →Have a question? Ask it in the community — it's tagged #guide and linked back here. Reading is open to everyone; posting needs a free account.
Loading questions…