Skip to content

kiyeonjeon21/reframe

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

235 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

reframe

npm CI license: MIT

Declarative motion graphics that AI can write, humans can tweak, and the human's edits survive an AI regeneration.

One-shot generation ("prompt to mp4") puts reframe roughly on par with other LLM-driven video tools. The difference shows on the second turn: what you get back is not freeform code but an addressable document, so you can keep tweaking, regenerating, and scaling without changes being silently lost.

A human-edited scene (top) and the same edits surviving a full AI redesign of the base scene (bottom)

Top: a scene with human overlay edits applied (brand color, retimed reveal, watermark). Bottom: an AI fully regenerated the base scene with a different layout and different timing, and the same overlay reapplied by stable id. 4 edits survived; the one renamed node was reported as an orphan, never silently dropped.

The reframe demo, made with reframe itself

The demo above is a reframe scene (examples/scenes/reframe-demo.ts). Render it yourself: pnpm reframe render examples/scenes/reframe-demo.ts.

The loop

scene.ts  ──(written by you, or by an AI given `pnpm reframe guide`)──▶  IR (plain JSON data)
   │                                                                        │
   ▼                                                                        ▼
preview: scrub + knobs ──▶ edits recorded as an overlay JSON (non-destructive)
   │                                                                        │
   ▼                                                                        ▼
render: deterministic mp4 (same input → byte-identical frames)   ◀── overlay reapplies
                                                                     even after an AI
                                                                     regenerates the base

Everything is a pure function of time: evaluate(scene, t) with no wall clocks and no unseeded randomness. Scrubbing and distributed rendering work for free, and the purity is enforced: reframe lint compiles the scene twice and flags any IR that differs (a Math.random() or Date baked into a prop), so a scene that would silently render differently each time fails the gate before you ship it.

Generative choreography at scale

The kind of scene that is hand-rolled timeline math in GSAP or per-element interpolate() plumbing in React is ~100 lines here, because the host language generates the nodes, states, and phase-shifted behaviors. The output is still data: every dot, glyph, and moon keeps a stable id you can tweak in the preview.

Bloom
bloom.ts: 300 dots on a golden-angle spiral. Radial bloom, traveling breath wave, chromatic ripple, vortex collapse
Wavefield
wavefield.ts: physical interference. 1,152 phase-shifted oscillators on a 32×18 grid, second ripple source joins mid-scene
Orbit
orbit.ts: nested transform composition. Moons orbit planets orbit a sun, the whole system tilts (three nested groups, three linear tweens)
Typewave
typewave.ts: character-level kinetic type. Cascade, standing wave, shatter with spin, and a second phrase assembling from the debris
Glyph reveal
glyph-reveal.ts: the archival stop-motion format. AI-generated plates as image nodes, ~7fps hard cuts, push-in, camera shake, a tick per cut. Swap any plate from an overlay or batch row: nodes.frame-3.src

Motion vocabulary: name a motion, don't keyframe it

Motion is hard to put into words and tedious to hand-key, so reframe ships a small set of named motion presets: draw-bloom, punch-in, rise-settle, slide-bank, reveal-orbit, spin-forge. Each is a seeded generator, not a canned template. The same name yields a family of distinct takes, and a seed varies it within that family (an 8-seed spread stays measurably different yet recognizably the same motion, gated in the tests).

motionPreset("spin-forge", { target: logo, energy: 0.8, seed: 3 })

energy (clean to springy) and speed are universal knobs. The preset emits a beat you can retime, and a waypoint you drag on its path becomes an overlay edit that survives a knob-driven regeneration: knobs regenerate the base, hand craft persists. Two primitives back it:

  • path node: a true vector SVG shape with a progress draw-on, so the outline draws itself, stays crisp at any zoom, and recolors by animating fill.
  • motionPath: drives a node's x/y along a Catmull-Rom curve (with tangent autoRotate), for curved motion that straight tweens cannot do.

Turn any SVG into a share-worthy animated sting in one command, no clone needed (a local file, or any of simple-icons' brands):

npx reframe-video logo react --motion spin-forge

Six real brand logos, six motions

Six real logos (React, Figma, Vercel, GitHub, Notion, Stripe), each a different preset, one command each. Pulled from simple-icons by slug, or drop in your own .svg.

Device mockups: one call, a framed screen

Promo and product shots almost always need the app inside something (a phone, a browser, a laptop). devicePreset bakes in ten parametric vector frames, each with a clipped screen "content slot", so a mockup is a single call:

devicePreset("phone", { id: "hero", content: [ /* ...your UI nodes */ ] })

Ten frames: phone, tablet, laptop, browser, watch, monitor, tv, foldable, terminal, car. All pure vector primitives (no assets), deterministic. deviceScreen, deviceScreenCenter, and deviceBounds hand back the screen bounds and the frame footprint, so content scrolls inside the clip and many devices tile onto a grid. Drive the device group with tweens or a motionPreset. See examples/scenes/device-presets.ts (three devices, a scrolling feed) and device-teardown.ts (all ten, each with its own signature move).

Why reframe instead of HTML output?

Tools that generate arbitrary HTML/React code produce output that is great to generate once and impossible to safely operate on afterwards. reframe's output is data with stable addresses, and everything below falls out of that one difference:

the second turn HTML / React output reframe
"tweak just the color and timing" edit code by hand, or re-prompt and hope nothing else changes (no visual editor is possible over arbitrary code) turn knobs in the preview, no code
"now redesign it" after my tweaks your hand edits live inside the code; regeneration overwrites them or you merge diffs (silent loss is the default) edits live in an overlay; they reapply onto the regenerated scene (measured 100% across 23 regenerations/turns), breaks are reported loudly
"make 50 personalized versions" only what the author pre-parameterized (props) any address, post-hoc: nodes.name.content
"is it wrong before I render?" semantic failures are invisible until pixels (wrong text, off-frame) structure validates pre-render with actionable errors; motion is computable from the IR

If your video is fire-and-forget, use the simpler tool. If it's an asset that will be tweaked, regenerated, and multiplied, that loop is what reframe is for.

Quickstart

No clone needed. reframe-video is on npm:

brew install ffmpeg                  # system dep (or apt install ffmpeg)
npx playwright install chromium      # one-time browser download
npx reframe-video new hello          # scaffold hello.ts in any directory
npx reframe-video render hello.ts    # → out/hello.mp4

Using Claude Code? Install the skill and just describe the video you want:

/plugin marketplace add kiyeonjeon21/reframe
/plugin install reframe@kiyeonjeon21

To hack on reframe itself, clone-based setup:

brew install ffmpeg                          # 0. system dep (or apt install ffmpeg)
pnpm install                                 # 1.
pnpm exec playwright install chromium        # 2. one-time browser download
pnpm reframe render examples/scenes/lower-third.ts   # 3. → out/lower-third.mp4

Then open the editor and render with your edits:

pnpm reframe preview     # scrub, play, and edit any scene with knobs
# edits accumulate in an overlay; click "download", save to examples/overlays/
pnpm reframe render examples/scenes/logo-reveal.ts \
  --overlay examples/overlays/brand-edits.json

Saving edits: the preview never modifies your scene file. Edits live in an overlay document. Download it from the panel, then pass it to render with --overlay. That same file keeps working after the scene is redesigned.

The edit-survival demo

pnpm reframe demo    # renders out/demo-{1,2,3}-*.mp4

Renders the base scene, the base + a human overlay, and then an AI-regenerated base + the same overlay. Watch the console: surviving edits apply, the deliberately renamed node orphans loudly with a diagnosis. This is the project's core claim, reproducible in one command. (Measured with real agents: 100% id/state/label retention across 8 regenerations; benchmark/regen/REGEN-ANALYSIS.md.)

Sound that follows the motion

Because choreography is data, audio cues anchor to timeline labels, not to seconds on a waveform:

audio: {
  bgm: { synth: "ambient-pad", gain: 0.3, duck: { depth: 0.5 } },
  cues: [{ at: "enter", sfx: "whoosh" }, { at: "shatter", offset: 0.18, sfx: "thud" }],
}

Retime a step with an overlay (or let an AI regenerate the scene) and the sound design moves with it (verified: a +1.8s hold patch shifted the anchored cues by exactly +1.8s). SFX are procedurally synthesized (deterministic, zero assets) with CC0 samples in assets/sfx/ for organic sounds like real mechanical keypresses; the bed auto-ducks under cues. --no-audio to skip.

Batch rendering: data in, videos out

A scene is a template; every data row becomes an overlay. Row keys are overlay addresses with no new schema:

// examples/data/team.json
[{ "_name": "alice",
   "nodes.name.content": "Alice Park",
   "nodes.role.content": "Chief Technology Officer",
   "nodes.bar.fill": "#00C2A8" }, ...]
pnpm reframe batch examples/scenes/lower-third.ts examples/data/team.json
# → out/batch/alice.mp4, ben.mp4, ... + batch-report.json

Rows render in parallel; a row with a bad address renders with a loud orphan warning instead of killing the batch. CSV works too (headers = addresses).

Writing a scene

import { scene, text, seq, to, wait } from "@reframe/core";

export default scene({
  id: "hello",
  size: { width: 1920, height: 1080 },
  fps: 30,
  background: "#101014",
  nodes: [
    text({ id: "title", x: 960, y: 540, anchor: "center",
      content: "Hello", fontFamily: "Inter", fontSize: 120, fontWeight: 800, fill: "#FFF" }),
  ],
  // base props are the finished design; states are sparse overrides
  states: {
    hidden: { title: { opacity: 0, y: 580 } },
    shown:  { title: { opacity: 1, y: 540 } },
  },
  initial: "hidden",
  // labels are stable addresses for overlay timing edits
  timeline: seq(
    to("shown", { duration: 0.6, ease: "easeOutCubic", label: "enter" }),
    wait(2, "hold"),
  ),
});

Scaffold one with pnpm reframe new my-scene. Full syntax (node types, states, timeline operators, behaviors): pnpm reframe guide. This is the guide an LLM reads to write valid scenes; the benchmark recorded 33/33 first-attempt renders across generation models.

A scene is a single self-contained file. It can live in any directory with no package.json or node_modules next to it. render bundles it on the fly (resolving @reframe/core and any relative imports beside it), and preview lists scenes from the directory you launched it in alongside the repo's examples/scenes/. Keep overlays and batch data files right next to your scene.

Recently shipped: backdrop (live "liquid glass")

backdrop: { blur, saturate, brightness } on a rect or ellipse makes the shape sample what is already drawn behind it and redraw it blurred and graded inside its outline. The node's translucent fill is the glass tint; the stroke is the rim. Animate the node's x/width/height/opacity and the frosted area re-samples each frame, so a panel can glide or resize and the backdrop tracks the content beneath it live. Works in both render (to mp4) and player (live in any browser). Use a gradient for the tint and rim (a solid color string loses its alpha):

rect({ id: "panel", x: 960, y: 540, width: 480, height: 320, anchor: "center",
  fill: linearGradient(["#FFFFFF24", "#FFFFFF0A"], { angle: 90 }),   // translucent tint
  stroke: linearGradient(["#FFFFFFE6", "#FFFFFF33"], { angle: 125 }), strokeWidth: 1.5,
  backdrop: { blur: 24, saturate: 1.4 },
})

See examples/scenes/liquid-glass.ts (animated glass card over moving blobs) and liquid-glass-nav.ts (nav bar over a photo).

CLI

command what it does
pnpm reframe render <scene.ts|.json|.html> [--overlay f]... [--theme brand.json] [--supersample N] [-o out] deterministic mp4 (output defaults to out/); --theme re-skins token() colors; --supersample 2 renders 2× and downscales for crisp anti-aliasing (smooths moving text)
pnpm reframe batch <scene.ts> <data.json|csv> [-o dir] [--overlay f]... one mp4 per data row (rows = overlays; a design.<token.path> column = one mp4 per brand), parallel, with a per-row report
pnpm reframe compile <scene.ts|.json> [-o out.json] [--json] bundle + validate a scene to SceneIR JSON, no render (fast; no ffmpeg/chromium); --json returns {ok, kind, issues}
pnpm reframe frame <scene.ts|.json> [--t <sec>] [--overlay f]... [-o out.png] render one frame at time t to a PNG (chromium only, no mux); --overlay previews edits
pnpm reframe compose <scene.ts|.json> --overlay f... [-o out.json] [--json] compose overlay(s) onto a scene and emit composed SceneIR, no render (feed to player/frame for live preview)
pnpm reframe manifest <scene.ts|.json> [--json] dump the addressable surface: every node, state, timeline label, and beat with the overlay address that reaches it
pnpm reframe geometry <scene.ts|.json> [--t <sec>] [--json] where each node + motionPath waypoint is on screen at time t (the spatial analog of manifest; pure, no chromium)
pnpm reframe lint <scene.ts|.json> [--json] [--strict] flag un-addressable motion + verify the scene is a pure function of time; --strict exits non-zero
pnpm reframe verify-overlay <base> <overlay>... [--json] compose an overlay onto a base and report applied-vs-orphaned, no render (non-zero exit on orphans)
pnpm reframe labels <scene.ts|.json> print the compiled event clock (every timeline label → exact seconds)
pnpm reframe assemble <media...> [-o name] probe images/videos (ffprobe) and scaffold an editable montage scene .ts wired with photoMontage
pnpm reframe narrate <scene.ts|.json> [--voice <name>] [--max-speed n] [--dry-run] scene-fitted Kokoro voiceover: synth each audio.narration line and auto-fit its rate to the slot (needs python + kokoro)
pnpm reframe player <scene.ts|.json> [--overlay f]... [--edit] [-o out.html] bundle a scene into one self-contained HTML that plays live in any browser; --edit adds window.__reframe + postMessage for embedding
pnpm reframe logo <logo.svg|brand-slug> [--motion <preset>] animate a logo (or a simple-icons brand) into a sting
pnpm reframe diff <ref-image> [scene.ts] [--t <sec>] [--mode side|blend|diff|grid] compare a render against a reference image
pnpm reframe preview scrub/play/edit UI; edits export as overlay JSON
pnpm reframe new <name> scaffold a documented starter scene
pnpm reframe motion <mp4|framesDir> calibrated motion profile (speeds, easing, discontinuities)
pnpm reframe trace <ref.mp4> [--apply scene.ts] extract a video's motion structure; --apply emits a timeline that re-tells it on your own nodes
pnpm reframe guide [--directing|--regen|--html] print a guide: eDSL syntax (default), directing workflow, regeneration contract, or HTML/GSAP scenes
pnpm reframe skill [--path] print the authoring skill for an agent; --path prints the plugin dir
pnpm reframe demo the edit-survival demo (three mp4s into out/)

How edits survive regeneration

Overlays address the scene by node id, state name, and timeline label (or beat name), never by position or index. When an AI regenerates a scene it follows one contract (docs/guides/regen-contract.md, or pnpm reframe guide --regen): keep those names stable for every concept that survives the redesign. When the contract is broken anyway, composeScene skips the affected edits and reports them with a diagnosis naming the likely rename. The failure hierarchy:

  1. Contract followed (the measured common case): edits survive.
  2. Contract broken: loud orphan report.
  3. Never: silent edit loss, or a render failure caused by base drift.

Restructure, not just re-skin

An overlay can also change the structure of the cut, keyed by the same stable addresses. You can reorder a beat (timeline.<beat>.order), remove one (removeTimeline: ["shot-3"]), or insert a whole new unit (insertNodes + insertTimeline { into: "montage", after: "shot-2" }). A montage builds each shot as the self-contained beat shot-${i}, so an overlay can drop, reorder, or splice in a card without touching the base:

// reorder two cards, drop one, retitle another — all survive an AI regen
{ "removeTimeline": ["shot-3"],
  "timeline": { "shot-1": { "order": 2 }, "shot-2": { "order": 1 } },
  "nodes": { "shot-0-title": { "content": "REORDERED" } } }

Reproduce it with the pure-vector demo (no assets):

pnpm reframe verify-overlay examples/scenes/vector-montage.ts \
  examples/overlays/vector-montage-restructure.json   # 4 applied, 0 orphaned
pnpm reframe render examples/scenes/vector-montage.ts \
  --overlay examples/overlays/vector-montage-insert.json   # a new card spliced into the cut

The preview editor: knobs write into a non-destructive overlay

Embedding reframe (in-process API)

The CLI is a thin shell over an importable, server-side API. Load, validate, and check a scene without spawning a process. From reframe-video/compile:

import { loadScene, loadSceneFromCode, checkDeterminism } from "reframe-video/compile";

const compiled = await loadScene("scene.ts");        // bundle + validate → CompiledScene
// On failure a SceneLoadError carries .kind ("bundle" | "eval" | "validation")
// and .issues: { code, path, message }[] — structured, not prose to parse.
const { deterministic, findings } = await checkDeterminism("scene.ts");

reframe-video/renderer exposes renderFrame / drawDisplayList (DisplayList to Canvas 2D) for live browser playback. reframe compile --json returns the same structured shape on the CLI: { ok: false, kind, issues: [{ code, path, message }] }, the feedback loop an agent or a UI reads to point at the exact broken node.

Documentation

docs.reframe-video.com is the full documentation site. The docs/ folder is its Mintlify source (docs/docs.json):

page what
Introduction · Quickstart · The loop the pitch, install, and the AI-write / human-edit / deterministic-render model
Gallery a curated visual reel of scenes
Examples example scenes, by category
Guides the eDSL, directing, HTML/GSAP, and regeneration-contract guides (also pnpm reframe guide)

Curated renders live in docs/assets/gallery/ and accumulate via pnpm gallery (the committed home; out/ stays scratch).

Repo map

path what
packages/core the eDSL, IR, determinism kernel, overlay composition (zero deps)
packages/renderer-canvas DisplayList to Canvas 2D (browser + capture shared)
packages/render-cli Playwright capture + ffmpeg encode; also renders arbitrary HTML/GSAP deterministically via a virtual clock
packages/preview the Vite editor
examples/ example scenes (see examples/README.md), overlays, compositions, the edit-survival demo
labs/ experiments and product probes (live-data to baked scene to render), kept out of examples/ so it stays purely demonstrative
docs/ the Mintlify-ready docs site + the authoring guides (also pnpm reframe guide)
benchmark/ measurement artifacts, not product code: LLM generation benchmark (RESULTS/ANALYSIS.md), regeneration-contract experiment (regen/), calibrated motion profiler (harness/motion/, MOTION.md)

Requirements and troubleshooting

  • Node >= 20, pnpm >= 9, ffmpeg on PATH.
  • Executable doesn't exist at …/ms-playwright/…: run pnpm exec playwright install chromium (the workspace blocks postinstall scripts, so the browser is not fetched automatically).
  • spawn ffmpeg ENOENT: install ffmpeg (step 0).
  • Fonts: only Inter 400/700/800 are bundled; other families silently fall back.
  • A scene importing npm packages beyond @reframe/core only bundles if those packages are resolvable from the scene's directory. Scenes are meant to be dependency-free documents.

Status

Early alpha (reframe-video on npm, Claude Code skill in this repo). The research phase is closed: every design hypothesis is measured, not assumed. LLM generation parity with HTML+GSAP, deterministic byte-identical rendering, edit survival across AI regeneration, and a five-turn natural-language iteration loop with zero silent edit loss. Receipts: benchmark/ANALYSIS.md, benchmark/MOTION.md, benchmark/regen/REGEN-ANALYSIS.md, benchmark/nl-loop/NL-LOOP.md. What "alpha" means honestly: it has not met strangers yet. Surface area is intentionally small (8 node types, one font, Canvas 2D) and the IR/overlay schema has no compatibility promise before 1.0.

About

Declarative motion graphics AI can write, humans can tweak — edits survive AI regeneration. Deterministic mp4 from plain-data scenes. npm: reframe-video

Topics

Resources

License

Code of conduct

Contributing

Stars

21 stars

Watchers

0 watching

Forks

Packages

 
 
 

Contributors

Languages