Data and assets
Resolve async work before playback, freeze mutable inputs, and keep frame rendering deterministic.
React frame rendering must be pure: the same frame and props should produce the
same pixels. Put network, database, filesystem, and secret-backed work in
prepare.ts.
import {definePrepare} from "odori";
export const prepare = definePrepare(async ({input, assets, cache, signal}) => {
const release = await cache.getOrSet(
`release:${input.tag}`,
() => fetchRelease(input.tag, {signal}),
);
const logo = await assets.resolve("brand:product-mark");
return {release, logo};
});
The prepared result becomes a prop of the video entry:
export default function ChangelogVideo({prepared}) {
return (
<Video>
<Scene duration="6s">
<ReleaseTitle release={prepared.release} logo={prepared.logo} />
</Scene>
</Video>
);
}
Because the result is frozen into the manifest, a video keeps a sensible fallback for the case where preparation has not run:
export default function WorkflowVideo({prepared}: {prepared?: {commands: Step[]}}) {
return <Terminal steps={prepared?.commands ?? FALLBACK} />;
}
Inputs
All preview and render inputs must be serializable and schema-valid. One contract powers Studio controls, embedded Player props, CLI input, and export jobs.
import {defineInputSchema} from "odori";
export const launchInput = defineInputSchema({
headline: {type: "text", defaultValue: "Author the story.", maxLength: 64, multiline: true},
});
defineInputSchema validates, fills defaults, and describes itself so Studio
can generate controls. Any zod-compatible object with a parse() method is
accepted instead.
Readiness and integrity
Fonts and images resolve before the first frame is captured, and
audio is collected into a track the encoder mixes.
Assets declared in odori.config.ts are addressable by reference through
useAssets().
Every font, asset, and audio source in the manifest carries a real content
hash. Local files hash their bytes; remote files are fetched once and cached by
URL under .odori/cache/integrity.json. A source that cannot be read is
recorded as unresolved rather than pretending to be verified.
font Geist Sans /fonts/Geist-Variable.woff2 sha256-o2n89WKOoqpOG54uxqWzYk42W9pYjh8PLxK1ZPco+7g=
Cache keys
Prepared data is cached on disk under .odori/cache/prepare/, keyed by video
source hash, validated input, and prepare version. Changing scene styling does
not refetch source data; changing a data dependency invalidates deterministically.
Repeated stills and exports of an approved cut reuse the cached result, and a
retry never reruns preparation at all because it replays the frozen manifest.