Skip to main content
ralphy render <project-id> is the only render path. It reads scenario.json and asset-manifest.json, drives HyperFrames (Puppeteer + FFmpeg), and writes the result to workspace/projects/<id>/render/final.mp4. No direct bunx hyperframes render calls, no ad-hoc ffmpeg shells — every recipe lives behind a ralphy verb so the cost rollup, the gen-log, and the asset manifest all stay consistent. That’s AGENTS invariant #2.

What render does

A render call walks through the project in order:
  1. Reads workspace/projects/<id>/scenario.json and asset-manifest.json to know which slots exist and where their files live.
  2. Resolves the composition. workspace/projects/<id>/index.html is the root composition HyperFrames seeks deterministically via its paused GSAP timeline.
  3. Calls HyperFrames headlessly. Puppeteer rasterizes each frame, FFmpeg muxes the mp4.
  4. Writes the result to workspace/projects/<id>/render/final.mp4 (or wherever you pointed --output).
  5. Optionally post-processes with ffmpeg if --loudnorm is set — EBU R128 two-pass normalization to -16 LUFS.
  6. Appends an entry to generations.jsonl with the model hyperframes, the latency, and the output path. Renders are first-class citizens in the gen-log.
The whole call is one command:
ralphy render syrup-001
{
  "projectId": "syrup-001",
  "output": "workspace/projects/syrup-001/render/final.mp4",
  "durationSec": 15.0,
  "fileSizeBytes": 4321987,
  "latencyMs": 287443
}

The flags that matter

--output
string
Output mp4 path. Default workspace/projects/<id>/render/final.mp4.
--fps
number
Frames per second. Default 30. Bump to 60 for high-motion content.
--quality
string
Render quality preset (draft, standard, high). Default standard.
--resolution
string
Resolution preset (portrait, landscape, square) — drives the canvas DPR.
--loudnorm
boolean
Apply EBU R128 loudnorm post-render via ffmpeg. Target -16 LUFS, two-pass. The TikTok / Reels / Shorts loudness standard. On by default for batch renders; off by default for single renders so you can iterate faster.
--dry-run
boolean
Print the resolved render plan (composition path, output path, flags) and exit. No render.
The full per-flag detail (every option, every default, every edge case) lives in CLI: rendering verbs.

The loudnorm pass

TikTok, Reels, and Shorts all target -16 LUFS integrated loudness. A video that’s quieter sounds amateur; a video that’s louder gets compressed to mush by the platform’s normalizer. --loudnorm runs a two-pass EBU R128 loudnorm via ffmpeg after HyperFrames writes the mp4:
ralphy render syrup-001 --loudnorm
The pass is documented in detail in the editor playbook. Behind the scenes it shells out to cli/lib/ffmpeg-recipes.ts — the only sanctioned place ffmpeg gets called from. Never run ffmpeg directly on a project file; use ralphy audio loudnorm or pass --loudnorm on render. If you forgot --loudnorm and want to normalize an existing render:
ralphy audio loudnorm \
  --in workspace/projects/syrup-001/render/final.mp4 \
  --out workspace/projects/syrup-001/render/final.loudnorm.mp4

The iteration loop

The normal cadence:
  1. Render with --dry-run first if you want to see the plan.
  2. Render for real. Wait until HyperFrames + ffmpeg finishes.
  3. Watch the mp4. Decide what’s off — wrong scene timing, captions out of sync, hook too slow, music too loud.
  4. Iterate on the underlying assets via the art-director playbook (regenerate a slot) or the scenarist playbook (rework a scene’s pacing).
  5. Render again. The new render lands at final.<iter>.mp4 so the previous take is preserved.
The render history pattern parallels asset versioning: every new render writes a new file, never overwrites. Pattern on disk:
workspace/projects/syrup-001/render/
  final.mp4         ← first render
  final.2.mp4       ← second render, after fixing scene-03 timing
  final.3.mp4       ← third render, after loudnorm pass
AGENTS invariant #13: no overwriting, no deleting, the history stays.

Preview vs final render

There’s no auto-launched preview in Ralphy. AGENTS invariant #5: no background processes, chat is the interface. If you want to preview frames live, run it foreground in a separate terminal:
bunx hyperframes preview workspace/projects/syrup-001
Don’t ask Ralphy to launch it for you — it won’t. Preview is a developer tool; the production path is ralphy render.

Perf targets

docs/perf-targets.md sets two numbers:
  • Cold-start single video ≤ 8 minutes end-to-end (intake → render).
  • Batch of 10 ≤ 25 minutes total.
If a render exceeds 50% of these targets, the producer playbook says report before continuing. The numbers exist so you can spot a stuck queue or a misconfigured model before it eats your afternoon.

Troubleshooting

Common render failures break down by stage:
  • E_NOT_FOUND on the project. The project ID is wrong, or you’re in the wrong workspace. Check ralphy project list -p.
  • Manifest references a missing asset. Re-run ralphy project verify <id> to see what’s broken. The fix is usually to regenerate the missing slot.
  • HyperFrames lint fails. Missing class="clip", missing data-composition-id, timeline registration key mismatch. Run bunx hyperframes lint <project> to see the exact issue.
  • ffmpeg loudnorm fails. Usually a corrupt mp4 from a failed render. Inspect with ffprobe, then re-render.
  • The render is silent. Check that asset-manifest.json has voiceover slots and that they point at non-empty files. The composition mounts them via <audio data-start="..." data-volume="...">; an empty file becomes silence.
The full error catalog is at CLI: error catalog. For env health (missing keys, missing deps), the first move is always ralphy doctor — see Setup and doctor.