The duration your video API accepts is not the duration it renders A developer discovered that video generation APIs from latent video models often return clips shorter than the requested duration, causing compounding errors in multi-clip edits. The issue stems from temporal compression in video diffusion models, which snap durations to a hidden grid of legal frame counts. The developer provides a quantization function and recommends displaying, pricing, and storing the resolved duration to avoid mismatches. A sequence I cut to a music bed was three frames out at the first transition, nine at the second, and by the sixth segment nothing lined up with anything. I had asked every generation for ten seconds. Every generation had returned a file that was not ten seconds. Nothing in the API said so. The request took duration: 10 , returned 200 , and produced an MP4 whose container duration was 8.708 . No warning field, no note in the response body, and — the part that actually cost me the afternoon — no mention of it on the docs page I had read three times. This is a general property of latent video models rather than a bug in one provider, and once you know the shape of it you can handle it in about twenty lines. Here is the shape. A video diffusion model does not work on frames. It works on a compressed latent tensor, and the compression is temporal as well as spatial: a causal 3D autoencoder folds a run of input frames into a single latent frame. Because the encoder is causal, the first frame is kept whole and everything after it is compressed in groups. With a temporal stride of s , a clip of F frames becomes latent frames = F - 1 / s + 1 which only divides evenly when F ≡ 1 mod s . Frame counts that miss that condition get padded or truncated, so implementations pick the nearest legal count and render that instead. Stack a second constraint on top — many of these models generate in fixed blocks of latent frames rather than one at a time — and the set of renderable lengths collapses into a short arithmetic progression: F = head + block · n n ∈ ℕ Every legal duration is one of those F values divided by the frame rate. Nothing between them is reachable. duration: 10 is not a request. It is a hint that gets snapped to a grid you were never shown. Three separate problems, and only the first is obvious. The output is not the length you promised. Your UI said 10s, the file is 8.708s, so your UI lied. Not by much, and not in a way anyone notices on one clip. The error compounds. Nobody makes one clip. They make six and cut them together. Six segments each 1.3 seconds short is eight seconds of drift, which is the difference between "cuts on the beat" and "re-render the sequence." Your cost estimate is wrong in the direction that generates tickets. These APIs bill per second of output. Estimate from the requested duration, let the model render a longer legal block, and you have quoted one number and charged another. Users find that one on their own. The fix is to stop treating duration as a free variable at the edge of your system. Resolve it to a legal value before anything is displayed, priced or persisted. type Grid = { fps: number; block: number; head: number; min: number; max: number }; / Legal frame counts are head + block·n, clamped to the provider's range. / export function legalFrames g: Grid : number { const out: number = ; for let n = 0; ; n++ { const f = g.head + g.block n; if f g.max break; if f = g.min out.push f ; } return out; } export function quantise seconds: number, g: Grid { const target = Math.round seconds g.fps ; const frames = legalFrames g .reduce best, f = Math.abs f - target < Math.abs best - target ? f : best, ; return { requested: seconds, frames, seconds: frames / g.fps }; } Then three rules for the resolved value: Show it instead of the requested one. The number in the duration control should change to the number you are going to get, at the moment the user picks it. A slider that snaps is honest. A slider that accepts anything and rounds in private is not. Price it. frames / fps × rate , from the resolved frames. Never from the requested seconds. Store it on the job. When somebody asks why their six clips do not add up, you want the answer in a column, not in a reconstruction. The bug was reachable from an integration test I had simply not thought to write: js it "returns the duration it promised", async = { for const requested of 4, 5, 6, 7, 8, 9, 10 { const job = await client.create { prompt: "a still grey card", duration: requested } ; const meta = await ffprobe await client.download job.id ; expect meta.duration .toBeCloseTo job.resolvedDuration, 2 ; } } ; Note what it asserts. Not that the file matches the request — that is a test you cannot pass and should not want to — but that it matches what the API told you it resolved to . That is a contract you can hold a provider to, and if the provider returns no resolved value at all, the absence is itself the finding. Running the loop is also the cheapest way to discover the grid empirically. Seven requests, ffprobe on each, and you have the progression whether or not anyone documented it. For anyone about to build on one of these APIs: That last point is not hypothetical. On the model I work with daily, the block arithmetic is tight enough that across its entire published 4-to-15 second range precisely one setting comes out even — and it is not a number anyone would think to type. Someone worked the progression out frame by frame and showed why only one lands on a whole second https://minimax-h3ai.video/blog/192-frames-is-the-only-whole-second , which is ten minutes well spent if you are about to pick a default your users will inherit. The general lesson is smaller than the arithmetic: when a generative API accepts a continuous parameter the model can only satisfy discretely, the rounding is part of the contract. Ask where the grid is before you let a I help run minimax-h3ai.video, an independent third-party interface for MiniMax H3. Not affiliated with MiniMax. Everything above is checked against the published docs and linked where it isn't.