The plan lifecycle¶
A plan goes through up to five stages between "some JSON arrived" and "frames are being written". Each stage owns a different class of problem. Knowing which stage owns what is the difference between a refine loop that converges in one round and one that plays whack-a-mole.
from_dict() → repair() → normalize_dimensions() → check() → run_to_file()
shape mechanics concat geometry everything execution
Parse owns shape, validation owns numbers¶
VideoEdit.from_dict() enforces the shape: field types, required fields, unknown
ops, extra fields, and op-local structural rules (resize needs at least one dimension).
Anything wrong there is a Pydantic ValidationError.
The numeric bounds of the plan skeleton — segment start/end, effect window
ranges — are deliberately not enforced at parse. A negative window.start parses fine.
A segment with start >= end parses fine. Validation reports them instead.
That looks backwards until you consider who writes plans. If bounds raised at parse, a
model that emitted window.start: -0.5 would produce an exception with one message,
while a model that emitted a segment ending past the source would produce a structured
PlanError — two different error channels for the same class of mistake, and only one of
them repairable. Deferring numbers to validation gives every numeric violation the same
treatment: structured, collectable, and often auto-fixable.
Validation and caller input¶
validate() stops at the first failure. check() collects independent plan errors,
so a caller can correct several faults in one round. A failed operation can prevent
checks that depend on its predicted output. Both work without decoding video frames.
Supplied metadata avoids video probes, but referenced assets can still be inspected.
Repair and dimension normalization return new plans with change records. They do not replace validation. All metadata-taking methods require an entry for each source; an incomplete map raises before the plan can be checked.
See the validation reference
for signatures and return types. Branch on PlanError.code and structured fields,
not diagnostic message text.
What repair() will and will not do¶
It clamps only what has one obvious correct answer, and records every change in a
PlanRepair changelog:
- effect
window.start/window.stopinto[0, duration]— negatives to0, overruns to the duration — in segments and inpost_operations; - time-valued op parameters past the clip end (
freeze_frame.timestampand friends), generically, via each op's declared time fields; - a negative segment
startto0; - a transition overlap to just below the shorter adjacent segment;
- with
clamp_segment_end=True, a segmentendpast the source to the source end. Off by default, because shortening a segment changes editorial intent.
It never invents intent. A concat dimension mismatch or an end <= start range is
left exactly as it was, for check() to report and for you to re-prompt about. It also
never raises on an operation it cannot repair — it leaves it for check(). So always
check() the returned plan before running it.
Why normalize_dimensions() exists separately¶
CONCAT_MISMATCH is the one error class you cannot cleanly fix in your own layer.
Detecting it needs each segment's predicted post-operation dimensions; fixing it needs a
per-segment resize inserted before the concat. Both require the engine's own
prediction machinery.
So it is a first-class method. Given a target — an explicit (width, height), "first",
"largest", or "match" (the lowest common resolution, the same policy
match_to_lowest_resolution applies in-stream) — it appends a resize to every segment
whose predicted output differs, and returns the usual changelog. The "all segments share
dimensions" invariant becomes satisfiable by construction.
It is best-effort: a segment it cannot predict yet is left untouched and deferred.
The one thing the runner tolerates¶
A duration-shrinking operation (speed_change, silence_removal) ordered before a
windowed effect can leave its endpoints past the now-shorter clip. This is common,
harmless, and unambiguous, so run_to_file() clamps them rather than failing. A window
that starts at or after the new duration becomes an empty no-op.
To keep the reports consistent with that, validate(clamp_windows=True) and
check(..., clamp_windows=True) do not report these overruns either, and repair()
clamps them in the returned plan.
The whole loop¶
edit = VideoEdit.from_dict(plan) # shape enforced
edit, repairs = edit.repair(source_metadata) # mechanics clamped
edit, dim_repairs = edit.normalize_dimensions(source_metadata, "largest")
errors = edit.check(source_metadata) # whatever is left, all at once
if errors:
... # re-prompt with the previous plan + the full structured list
else:
edit.run_to_file("out.mp4")
source_metadata leads every signature in the family, so the calls read the same way. The
practical version of this loop, with provider code around it, is in Author edit plans
with your own LLM.