OverlayMotion

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 (3 or "3s"), percentages of the owner timeline ("66%"), or negative seconds from the end ("-2s"). A window is { start, duration, appear, hold }: appear is how long the entrance choreography takes, hold is how long the finished overlay stays after that. duration absent + hold present makes the window appear + 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-card animateIn adds "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 own exit prop (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.