---
title: Data and assets
description: 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`.

```ts title="videos/changelog/prepare.ts" lineNumbers
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:

```tsx
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:

```tsx
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.

```ts title="videos/launch/schema.ts"
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](/docs/guides/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.

```text
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.
