{"slug": "the-duration-your-video-api-accepts-is-not-the-duration-it-renders", "title": "The duration your video API accepts is not the duration it renders", "summary": "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.", "body_md": "A sequence I cut to a music bed was three frames out at the first transition,\n\nnine at the second, and by the sixth segment nothing lined up with anything. I\n\nhad asked every generation for ten seconds. Every generation had returned a\n\nfile that was not ten seconds.\n\nNothing in the API said so. The request took `duration: 10`\n\n, returned `200`\n\n,\n\nand produced an MP4 whose container duration was `8.708`\n\n. No warning field, no\n\nnote in the response body, and — the part that actually cost me the afternoon —\n\nno mention of it on the docs page I had read three times.\n\nThis is a general property of latent video models rather than a bug in one\n\nprovider, and once you know the shape of it you can handle it in about twenty\n\nlines. Here is the shape.\n\nA video diffusion model does not work on frames. It works on a compressed\n\nlatent tensor, and the compression is temporal as well as spatial: a causal 3D\n\nautoencoder folds a run of input frames into a single latent frame.\n\nBecause the encoder is causal, the first frame is kept whole and everything\n\nafter it is compressed in groups. With a temporal stride of `s`\n\n, a clip of `F`\n\nframes becomes\n\n```\nlatent_frames = (F - 1) / s + 1\n```\n\nwhich only divides evenly when `F ≡ 1 (mod s)`\n\n. Frame counts that miss that\n\ncondition get padded or truncated, so implementations pick the nearest legal\n\ncount and render that instead.\n\nStack a second constraint on top — many of these models generate in fixed\n\nblocks of latent frames rather than one at a time — and the set of renderable\n\nlengths collapses into a short arithmetic progression:\n\n```\nF = head + block · n          n ∈ ℕ\n```\n\nEvery legal duration is one of those `F`\n\nvalues divided by the frame rate.\n\nNothing between them is reachable. `duration: 10`\n\nis not a request. It is a\n\nhint that gets snapped to a grid you were never shown.\n\nThree separate problems, and only the first is obvious.\n\n**The output is not the length you promised.** Your UI said 10s, the file is\n\n8.708s, so your UI lied. Not by much, and not in a way anyone notices on one\n\nclip.\n\n**The error compounds.** Nobody makes one clip. They make six and cut them\n\ntogether. Six segments each 1.3 seconds short is eight seconds of drift, which\n\nis the difference between \"cuts on the beat\" and \"re-render the sequence.\"\n\n**Your cost estimate is wrong in the direction that generates tickets.** These\n\nAPIs bill per second of output. Estimate from the requested duration, let the\n\nmodel render a longer legal block, and you have quoted one number and charged\n\nanother. Users find that one on their own.\n\nThe fix is to stop treating duration as a free variable at the edge of your\n\nsystem. Resolve it to a legal value before anything is displayed, priced or\n\npersisted.\n\n```\ntype Grid = { fps: number; block: number; head: number; min: number; max: number };\n\n/** Legal frame counts are head + block·n, clamped to the provider's range. */\nexport function legalFrames(g: Grid): number[] {\n  const out: number[] = [];\n  for (let n = 0; ; n++) {\n    const f = g.head + g.block * n;\n    if (f > g.max) break;\n    if (f >= g.min) out.push(f);\n  }\n  return out;\n}\n\nexport function quantise(seconds: number, g: Grid) {\n  const target = Math.round(seconds * g.fps);\n  const frames = legalFrames(g).reduce((best, f) =>\n    Math.abs(f - target) < Math.abs(best - target) ? f : best,\n  );\n  return { requested: seconds, frames, seconds: frames / g.fps };\n}\n```\n\nThen three rules for the resolved value:\n\n**Show it instead of the requested one.** The number in the duration control\n\nshould change to the number you are going to get, at the moment the user picks\n\nit. A slider that snaps is honest. A slider that accepts anything and rounds\n\nin private is not.\n\n**Price it.** `frames / fps × rate`\n\n, from the resolved frames. Never from the\n\nrequested seconds.\n\n**Store it on the job.** When somebody asks why their six clips do not add up,\n\nyou want the answer in a column, not in a reconstruction.\n\nThe bug was reachable from an integration test I had simply not thought to\n\nwrite:\n\n``` js\nit(\"returns the duration it promised\", async () => {\n  for (const requested of [4, 5, 6, 7, 8, 9, 10]) {\n    const job  = await client.create({ prompt: \"a still grey card\", duration: requested });\n    const meta = await ffprobe(await client.download(job.id));\n    expect(meta.duration).toBeCloseTo(job.resolvedDuration, 2);\n  }\n});\n```\n\nNote what it asserts. Not that the file matches the **request** — that is a\n\ntest you cannot pass and should not want to — but that it matches what the API\n\n**told you it resolved to**. That is a contract you can hold a provider to, and\n\nif the provider returns no resolved value at all, the absence is itself the\n\nfinding.\n\nRunning the loop is also the cheapest way to discover the grid empirically.\n\nSeven requests, `ffprobe`\n\non each, and you have the progression whether or not\n\nanyone documented it.\n\nFor anyone about to build on one of these APIs:\n\nThat last point is not hypothetical. On the model I work with daily, the block\n\narithmetic is tight enough that across its entire published 4-to-15 second\n\nrange precisely one setting comes out even — and it is not a number anyone\n\nwould think to type. Someone\n\n[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),\n\nwhich is ten minutes well spent if you are about to pick a default your users\n\nwill inherit.\n\nThe general lesson is smaller than the arithmetic: **when a generative API\naccepts a continuous parameter the model can only satisfy discretely, the\nrounding is part of the contract.** Ask where the grid is before you let a\n\n*I help run minimax-h3ai.video, an independent\nthird-party interface for MiniMax H3. Not affiliated with MiniMax. Everything\nabove is checked against the published docs and linked where it isn't.*", "url": "https://wpnews.pro/news/the-duration-your-video-api-accepts-is-not-the-duration-it-renders", "canonical_source": "https://dev.to/whereisthisplace/the-duration-your-video-api-accepts-is-not-the-duration-it-renders-4j86", "published_at": "2026-08-21 15:38:33+00:00", "updated_at": "2026-08-21 15:45:21.206947+00:00", "lang": "en", "topics": ["generative-ai", "ai-products", "developer-tools"], "entities": [], "alternates": {"html": "https://wpnews.pro/news/the-duration-your-video-api-accepts-is-not-the-duration-it-renders", "markdown": "https://wpnews.pro/news/the-duration-your-video-api-accepts-is-not-the-duration-it-renders.md", "text": "https://wpnews.pro/news/the-duration-your-video-api-accepts-is-not-the-duration-it-renders.txt", "jsonld": "https://wpnews.pro/news/the-duration-your-video-api-accepts-is-not-the-duration-it-renders.jsonld"}}