Edit Spec v1
An edit spec is a JSON document that describes a finished edit: one base
source, a stack of overlay templates, cameras, and sound. It is the only
input an agent needs to produce a video. Zod schemas live in
src/spec/types.ts; full validation (shape + template cross-checks) is
validateSpec / parseSpec from src/spec/validate.ts.
{
"version": 1,
"format": "landscape",
"fps": 60,
"durationSec": 20,
"source": { "type": "video", "src": "your-footage.mp4" },
"overlays": [
{
"template": "speaker-card",
"region": "lower-third",
"time": { "start": "2s", "duration": "9s", "appear": 0.7 },
"props": { "name": "Ana", "role": "Founder", "photo": "ana.png" }
}
]
}The two axes: region and time
Natural language like "on the bottom, at 2/3 of the video, for 3s" compiles
to region: "lower-third", time: { start: "66%", duration: "3s" }.
region(space): a named region (fullscreen,top-banner,lower-third,upper-third,caption-zone,left-panel,right-panel,center,corner-tl/tr/bl/br) or a custom rect in percentages ({ "x": 31, "y": 70, "w": 38, "h": 18 }). Unset uses the template's preferred region.time(timeline): values are seconds (3or"3s"), percentages of the owner timeline ("66%"), or negative seconds from the end ("-2s"). A window is{ start, duration, appear, hold }:appearis how long the entrance choreography takes,holdis how long the finished overlay stays after that.durationabsent +holdpresent makes the windowappear + hold.
Source and the source contract
source is the base layer: { type: "video", src } (options: muted,
fit, position, flipHorizontal, footage-only camera, time-windowed
reframes), { type: "audio", src }, or
{ type: "none" }.
Subject-safe source reframes
source.reframes moves the base footage into a percentage rectangle during a
time window while overlays stay locked to the composition. This is editorial
layout, not camera motion. It is intended for split screens where simply
covering half of the fullscreen source would hide a face or demonstrated
object. Reframe windows may not overlap.
{
"type": "video",
"src": "speaker.mp4",
"fit": "cover",
"position": "center 30%",
"reframes": [
{
"time": { "start": "9s", "duration": "8s" },
"region": { "x": 4, "y": 8, "w": 44, "h": 84 },
"position": "60% center",
"transitionSec": 0.3
}
]
}fit defaults to cover, position to center, and transitionSec to 0.3.
Resolve the position by inspecting the subject across the full window.
Every template declares one relationship with that base source, and validation enforces it:
| Contract | Meaning | Requires |
|---|---|---|
overlay |
Self-contained; draws over whatever the source is. Most templates. | nothing |
annotates-video |
Only makes sense over footage (captions, ticker, recording chrome). | source.type: "video" |
wraps-video |
Renders the base video inside its own layout (video-card). One per spec. | source.type: "video" |
visualizes-audio |
Driven by a standalone audio file (audiogram); receives it as sourceSrc. |
source.type: "audio" |
A template that ships its own footage via props is still overlay: the
contract describes the BASE source only.
Camera motion vs object motion
A camera moves the frame around finished content; object motion is the content moving, and it belongs to the template. A camera never choreographs entrances or exits.
Cameras live at three scopes, location = scope:
spec.camera: the scene; source and every overlay move as one shot.source.camera: footage only; overlays stay locked to their regions.overlay.camera: one overlay's region viewport, on the overlay's own timeline.
Presets: push-in, push-in-out, push-in-fast-out, pull-out,
pan-left/right/up/down, handheld (fields frequency, seed), with
amount, focus {x,y}, time, easing. Scene scope accepts an array of
windows. Full grammar: docs/camera-motion-spec.md.
The motion language
Small on purpose, so mixed templates read as one design system. All three knobs are optional; unset keeps the template's native motion.
reveal(text entrance hint):"fade-up","blur-in","typewriter". Text templates map it to their nearest native mode and may expose richer extensions via props (quote-cardanimateInadds"lines"/"words"; tweet-card adds"paragraphs"/"none").enter(card entrance, renderer-provided, identical on every template):"slide-left","slide-right","spring","mask".exit:"blur-out"(blur + lift, the signature departure),"fade-down","shrink". Templates with their ownexitprop (speaker-card) treat the spec value as default, not override.
Sound
sound at the spec root sets defaults; each overlay may override with its
own sound block. Cues resolve to built-in names, arbitrary audio paths, or
false to silence one cue. The curated core palette (reach for these
first): click, pop, whoosh, ding, typewriter (CORE_SFX in
src/sound/config.ts).
Brand theme
Templates never hardcode style; they read tokens from the active
BrandTheme: colors (primary, onPrimary, surface, onSurface, muted,
background, optional secondary/accent), fonts (heading, body), radius,
logoText, and optional style (surface "solid" | "glass", blur up to
100, opacity, gradients, borderColor). Same spec + another theme =
rebranded video.
Formats
vertical 1080×1920, horizontal 1920×1080, landscape 1620×1080,
square 1080×1080. Templates scale through the shared rem() helper, so
one spec renders correctly in all four.
Validation
parseSpec(raw) (throws) or validateSpec(raw) (safe result) from
src/spec/validate.ts. Beyond shape, they enforce: known template slugs,
source-contract requirements, at most one wraps-video overlay, and
non-overlapping source-reframe windows. Overlay and source-reframe windows may
not extend past the composition duration.
editSpec.parse alone checks shape only; prefer the checked variants.
