Skip to content

build

Assemble a reel from a storyboard: render each card to a frame, crossfade them into a 1080×1920 H.264 video, and (unless --silent) mix the narration over the music bed. Rendering shells out to ffmpeg behind the Renderer interface.

Usage

# Workspace build (the authoring loop): --workspace is a slug under the reel root
keryx reel build --workspace my-post

# Render without audio (cards use their storyboard `dur`)
keryx reel build --workspace my-post --silent

# Standalone build from a loose storyboard file
keryx reel build --storyboard board.json --out promo.mp4

Flags

Flag Description
--workspace Reel workspace slug under the reel root (reels/<slug>), the same slug used by reel new and storyboard draft. The storyboard, VO takes, music bed, and cover are read from that workspace.
--storyboard A standalone storyboard.json path, used as-is. Overrides the workspace board when both are given.
--theme Reel theme keyword (palette, fonts, music gain). Defaults to the configured reel default.
--out Output .mp4 path. Defaults to reels/<slug>/reel-<slug>.mp4 for a workspace build, or reel.mp4 for a standalone build.
--cover Cover image path. Defaults to reels/<slug>/cover.png if present.
--aspect Frame aspect as W:H. Defaults to the workspace's own aspect, then config reel.aspect, then 9:16. Rejected beyond 3:1 in either direction.
--silent Render video only — no narration, no music. Each card must carry a dur.

Media hydrate

With an object store configured (spec 0039), reel build first pulls any media that media.lock pins but is missing locally — a fresh CI clone renders without an explicit keryx media pull. Local edits are never overwritten; unconfigured storage skips the step.

Timing

  • With VO (promoted vo/NN.mp3 clips exist, not --silent): each clip is bound to its own card by line number — a card with a promoted clip gets a duration derived from the clip length plus lead and tail; cards crossfade. The music bed is mixed in under the VO at the theme's gain with an end fade.
  • Mixed boards (some cards VO'd, some not — the normal studio mid-authoring state): VO-less cards fall back to their storyboard dur, rendered silent. A card with neither VO nor a dur is an error naming the card.
  • Silent / no VO: each card uses its storyboard dur. A card with no dur is an error in this mode.

When narration outlasts the reel

The reel's length is the video timeline — the cards laid end to end, less one crossfade per join. The mix is assembled separately: each clip is delayed to its own card's start and runs for its own length. Those two numbers can cross, and when they do reel build refuses before any render begins:

mix ends at 130.973s, 1.200s past the 129.773s timeline — a card's padding is
trimmed shorter than its own narration

A card's duration is its narration plus lead and tail, so the usual cause is padding trimmed to nothing on the last card — reasonable when a hand-recorded read carries its own pauses, but it leaves the narration with nowhere to finish. The other cause is a card carrying an explicit dur shorter than the clip it also has.

The check is refused rather than patched over on purpose. Left to a render backend the outcome depends on the engine: one that enforces its output bound cuts the narration short, one that does not renders long and says nothing. Neither is what the board asked for, and only a refusal names which card to change. Give the card back its tail (or raise its dur), or re-record the line shorter.

Progress: when does the percentage appear?

On an interactive terminal, reel build shows a live rendering… NN% line while the reel assembles, measured against the reel's total duration. It is written to stderr (so stdout and --output json stay clean) and cleared when the render finishes; piped or non-TTY runs show nothing at all.

Both render backends stream progress — ffmpeg parses its own -progress output, and afmpeg reads the NDJSON progress records the ffmpeg-wasi engine writes. A negative percentage means the backend cannot determine completion, which is deliberately distinct from 0%.

Engine advisories on a successful render

A render can be degraded and green at once: the engine reports a codec falling back, timestamps being reconstructed, frames dropped or an option ignored, and still exits zero. When the afmpeg backend's engine writes anything on a run that succeeded, reel build logs it:

INFO the render engine reported advisories engine="..."

At info rather than debug, because these are the lines that explain a file which decodes cleanly but is not what was asked for, and having to re-run under --debug to see them means the reel has already shipped. Nothing is logged when the engine was quiet, or for a backend that has no advisories to report.

Reproducibility

After rendering, reel build reports how many selected assets are locked (approved) vs merely selected. When every selected asset is locked, the reel is reproducible from committed inputs — see reel lock. The counts are also in the --output json payload (selected, locked).

See the Testing page for the BDD scenario that exercises a silent workspace render end to end.