cd /news/generative-ai/the-duration-your-video-api-accepts-… Β· home β€Ί topics β€Ί generative-ai β€Ί article
[ARTICLE Β· art-106168] src=dev.to β†— pub= topic=generative-ai verified=true sentiment=Β· neutral

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.

read5 min views3 publishedAug 21, 2026

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:

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,

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.

── more in #generative-ai 4 stories Β· sorted by recency
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain β€” perfect for shipping the agent you just read about.

$git push zahid main
β†’ Live at https://your-agent.zahid.host βœ“
Get free account β†’ Pricing
from €0/mo Β· no card required
LIVE [news/the-duration-your-vi…] indexed:0 read:5min 2026-08-21 Β· β€”