Your 60-second video is 57 seconds. Your agent now fixes it.
I planned a 60-second video. The render came back 57 seconds long, followed by three seconds of black. Nothing errored. Here is the breakdown, and what I shipped to stop it happening again.
Today @di-atomic/video-director goes live on the OPVS marketplace. Point your agent at a script and it returns a shot list your renderer accepts without a single edit. Every row names its engine. Every clip length is one the model can actually produce. The timeline adds up to the video you asked for.
The plan a person writes is the plan that fails
Here is what you write when you want sixty seconds. Six eight-second clips, then two tens:
8 + 8 + 8 + 8 + 8 + 10 + 10 = 60That is correct arithmetic and the wrong arithmetic. Transitions overlap the shots on either side of them. Seven rows means six cuts, and at the default half-second transition you lose three seconds:
content = sum(len) - (n - 1) x transition_seconds
= 60 - (6 x 0.5)
= 57.0The renderer does not complain. It composes your clips, runs out of content at 57 seconds, and fills the rest with black.
I measured it before I believed it. I submitted a 6.000-second job: three two-second scenes, two half-second transitions. Content ended at exactly t=5.000s. Frames 150 to 179 came back pure black. That is one full second, or 16.7% of the output. Then I rendered a control with the frame count corrected to 150, and the tail was gone.
Scale that up. At a half-second transition, a ten-scene video ends with four and a half seconds of black.
The fix is not a longer last clip. It is solving for content instead of for the sum:
need sum = target + (n - 1) x t = 60 + 3.0 = 63
8 x 6 + 15 = 63 -> content 60.00s PASS
You cannot ask a model for 6.3 seconds
This is the second constraint, and it is why the first one is hard to fix by hand.
Generated clips come in fixed lengths. veo3 makes eight seconds. sora2 makes ten or fifteen. You do not get to request 6.3 seconds and you do not get it if you ask. So when your arithmetic comes up three seconds short, you cannot stretch a clip to close the gap. You have to change the shot count.
Screen recordings and motion compositions are different. Those are elastic, any length you like, and they are where the slack belongs. In a mixed video you put the remainder on a screen row. In an all-generated video you are solving a small search problem, not doing subtraction.
That is the reframe. A shot list is not a wish list with durations attached. It is a constraint problem with two coupled variables, and it has to be solved before you pay for a single clip.

What your agent now decides for you
The mode, and it will not guess. single_video means each row is a shot inside one timeline. post_batch means each row is a whole separate post. Nothing in a script tells you which one somebody wants. Get it wrong and you ship one 24-second video where three 8-second posts were ordered, with no error anywhere to tell you. So mode is required, it has no default, and a list without it is rejected.
The engine on every row. ai for a generated clip, capped at 5 to 15 seconds. avatar for lipsync. screen for a real screen recording, any length. motion for a composition, any length. The engine is how the asset gets made. The type the renderer sees is derived from it, never chosen.
Every cut, on purpose. The renderer defaults transitions to fade. A crossfade carries nothing across the cut, so the eye has nothing to follow, and the craft rule says never use one. Which means accepting the default is itself the defect. Your agent sets the transition on every row, matching axis and direction and speed across each seam. The gate counts any row left on the default.

Four verifiers, before you spend anything
Rendering costs money. Checking a plan does not. So the shot list gets graded first:
node scripts/verify-shot-list.mjs shot-list.json --target 60
node scripts/verify-projectability.mjs shot-list.json
node scripts/verify-seam-ledger.mjs shot-list.json
node scripts/verify-prompt-grammar.mjs shot-list.jsonThey print PASS or FAIL with numbers. Run them against the naive 60-second plan and you get this:
shot 6: len 10s is NOT native for veo3 (native: 8s)
content 57.00s misses target 60s by 3.00s. Solve for sum len = 63.00s
RESULT: FAIL (2 violations)Two problems, named, before a clip is bought.
The gate failed my own template first
Worth telling you, because it is the only reason I trust the gate at all.
The first time I ran the seam verifier against the shot list I was shipping as the worked example, it failed. Shot 5 changed its motion axis with no declared reason, while shots 4 and 6 both declared theirs. My template, my rule, my mistake. I fixed the shot list and left the verifier alone.
Then I threw twelve deliberately broken shot lists at it, including an empty object and an empty scene array. All twelve were rejected. A gate that passes garbage is decoration.
What it does not do
It decides and composes. It renders nothing. It does not generate clips, record screens, or write your script. It produces the one document all three of those read.
Two more limits worth stating plainly. The tutorial recipe is reasoned from first principles, because our prompt corpus is short-form only and has no tutorial prior art at all. Its structure is gated; its pacing is not, because there are no examples to grade rhythm against. And v0.1.0 ships no duration solver. It tells you the procedure for hitting a stubborn target and leaves you to run it.
Both are written into the skill where you will read them, not buried here.
Install it
opvs-skills install @di-atomic/video-directorGuidance-only, so there is no backend to configure and nothing to authenticate. Your agent reads it and does the work.
Point it at a script. Ask for sixty seconds. Count the frames.