# Rendering to MP4 (Remotion)

Preview happens live in the app — you only render when the operator wants the final file. Rendering uses
Remotion. The render bundle installs its own dependencies (Remotion + React) into its `render/` folder the
**first time you render** (Remotion is a project-scoped toolchain, so it can't be a global install). Nothing
heavy is pulled in until then. Remotion ships its own ffmpeg.

## Run it

From the agent workspace (needs the `shell` capability):

```bash
node <skill-folder>/render/render.mjs projects/spot.mvideo.json projects/spot.mp4
```

- Arg 1: the project (workspace-relative). Arg 2 (optional): output path — defaults to the project name
  with `.mp4`.
- The script bundles `render/index.jsx` (a Remotion composition mapping the project to Remotion primitives),
  derives duration/size/fps from the project, and writes the MP4.
- Asset paths in the project are workspace-relative; the renderer resolves them against the workspace root.
- `branding: { "$ref": "auto" }` is resolved to the nearest `branding.json` (folder → workspace root), so
  `brand/<style>` text styles render with the right colors/fonts.

`<skill-folder>` is this skill's install location in the agent's `skills/` directory (e.g.
`skills/video-editor`).

## Mapping (what export does)

| Project | Remotion |
|---|---|
| video/image clip | `<Sequence from durationInFrames>` + `<OffthreadVideo>` / `<Img>` (cover-fit, transform, fade) |
| text clip | `<Sequence>` + styled `<AbsoluteFill>` (brand/inline style, fade/slide-up) |
| audio clip | `<Sequence>` + `<Audio startFrom volume>` (gain → linear volume) |
| project fps/size/duration | composition metadata (via `calculateMetadata`) |

## Notes & limits

- The preview engine in the app is an approximation; this render is the source of truth for the final file.
- Transitions: `clip.transition.{in,out}` ("fade"/"dissolve"/"crossfade", `frames` = ramp length; "cut"/
  absent = hard cut) ramp the clip's opacity at its edges. Overlap two clips (or use the `crossfade` edit
  op: `crossfade{clipId,frames?}` — overlaps a clip with its previous neighbour and ripples later clips) to
  get a cross-dissolve. Preview and renderer interpolate identically (shared sampler).
- Audio gain automation: `clip.gainKeyframes` ({t,db}, clip-local, interpolated) drives smooth level moves
  (e.g. music ducking under dialogue) beyond the constant `gain` + edge fades.
- Trimming uses `trimIn` as the source start (`startFrom`); `trimOut` is honored by the clip's timeline length.
- If a model produced silent video and you want sound, add a music/SFX/voice track — the renderer mixes all
  audio tracks.
- First render in a fresh workspace also triggers Remotion's one-time Chromium download.
