Odori vs Remotion vs HyperFrames
How the three authoring models in this repository differ in timeline, discovery, components, preview, and export.
All three tools render frame-accurate video with Chrome and FFmpeg, and all three are deterministic: a frame number in, the same pixels out. They differ in what they own. HyperFrames owns a document and a capture loop. Remotion owns a React frame runtime. Odori owns the frame runtime and the conventions above it: discovery, layout inheritance, a component registry, contract tests, and an export protocol.
The comparisons below are drawn from building the same product video three ways.
At a glance
| HyperFrames | Remotion | Odori | |
|---|---|---|---|
| Authoring surface | HTML, CSS, GSAP | React and frame arithmetic | React with <Video> and <Scene> |
| Composition exists because | A .html file is in the project |
You registered it in Root.tsx |
A videos/**/video.tsx file exists |
| Timeline | data-start / data-duration attributes |
<Sequence from durationInFrames> |
<Scene duration="4s">, offsets derived |
| Timing unit | Seconds | Frames | Durations ("4s") resolved to frames |
| Format and fps | Attributes on the composition element | Props on each <Composition> |
Inherited from videos/layout.tsx |
| Brand tokens | CSS custom properties you maintain | Whatever you build | defineBrand, read with useBrand |
| Component library | Registry blocks copied in | npm packages you assemble | odori add, source copied to videos/components/ |
| Typed inputs | None | Zod schema per composition | schema on defineVideoMetadata, --input on every command |
| Preview | CLI preview server | Remotion Studio | Studio, with component fixtures alongside videos |
| Automated checks | hyperframes check / lint |
Your own tests | odori test: blank frames, overflow, unreadable text, contracts |
| Export | hyperframes render |
remotion render |
odori export, jobs and a frozen manifest |
| Build step | None | Bundler | Bundler |
HyperFrames: the raw-web option
The timeline lives in the document. Sections carry their own timing, and a paused GSAP timeline is handed to the renderer to seek:
<section id="opening" class="clip scene" data-start="0" data-duration="3">…</section>
<section id="proof" class="clip scene" data-start="3" data-duration="3.5">…</section>
const tl = gsap.timeline({ paused: true });
tl.fromTo("#opening h1", { opacity: 0, y: 42 }, { opacity: 1, y: 0, duration: 0.7 }, 0.15);
window.__timelines.main = tl;
Strengths. No build step, no framework, and any web technique is available immediately. A designer who knows CSS can ship a cut.
Costs. Timing is duplicated between markup attributes and the GSAP timeline, so a scene can drift out of sync with its own animation. There is no type checking across scenes, no component contract, and reuse is copy-paste.
Remotion: React on a frame clock
Remotion gives you useCurrentFrame, interpolate, and <Sequence>, and you
build the rest. Composition metadata is registration, not a file convention:
<Composition id="DirectRemotion" component={DirectRemotion} durationInFrames={270} fps={30} width={1920} height={1080} />
<Sequence durationInFrames={90} name="Opening">…</Sequence>
<Sequence from={90} durationInFrames={105} name="Proof">…</Sequence>
<Sequence from={195} durationInFrames={75} name="Resolution">…</Sequence>
Strengths. A mature, well-documented runtime with a large ecosystem, a capable studio, and hosted rendering options.
Costs. Absolute frame offsets are computed by hand, so inserting a scene means renumbering the ones after it. Format, fps, and brand are per-composition props rather than an inherited layout, and there is no built-in component system or authored-frame linting. You assemble those yourself.
Odori: the framework layer
A video exists because its file exists. Metadata is static, so discovery never runs your component:
export const metadata = defineVideoMetadata({
id: "launch",
duration: "12s",
layout: productLayout,
schema: launchInput,
});
export default function LaunchVideo({headline}: {headline: string}) {
return (
<Video>
<Audio src="/audio/product-cinematic.m4a" gain={0.75} duckUnder />
<Scene id="opening" duration="4s">
<Stage>
<TitleReveal title={headline} />
</Stage>
</Scene>
<Scene id="proof" duration="5s">…</Scene>
</Video>
);
}
What the framework adds on top of a frame clock:
- Derived offsets. Scenes are ordered, not numbered. Reordering or retiming a scene never touches its neighbours.
- Inherited layout. Format, fps, and brand come from
videos/layout.tsx, so one change re-formats every video. - Source-owned components.
odori add @odori/title-revealcopies real source intovideos/components/. You edit it;odori diffandodori updatecompare it with upstream later. - Temporal contracts. Registry components declare aspect ratios, minimum
and recommended durations, entrance and exit frames, and content limits, and
odori testfails a cut that violates them. - Authored-frame checks.
odori testsamples representative frames and reports blank frames, content escaping the canvas, and text too small to read at delivery size. - Typed inputs. Every command takes
--input, validated against the video’s schema, so one composition renders many variants. - One frozen manifest. Preview and export resolve the same manifest, so the approved cut is the rendered cut.
Choosing
- Choose HyperFrames for a one-off cut, when the team is CSS-first and no build step is worth more than reuse.
- Choose Remotion when you want an established ecosystem and are happy to own the conventions above the runtime yourself.
- Choose Odori when video is ongoing work: many cuts, several formats, a brand to hold, components to share, and agents or teammates who need the structure to be discoverable and checkable.
The three are not mutually exclusive ideas. Odori’s position is that the
runtime was never the hard part; the conventions around it are. See Alternative structures for the models Odori
considered before settling on videos/ plus JSX.