cinecode

Rendering: the command line

node bin/cinecode.mjs <command> films/<slug> [options]

Run it from the repository root. The film argument is a folder holding a film.json (films/my-film, or any path), a path to a film.json, or a bare slug (my-film → films/my-film). Options take --name=value or --name value; cinecode <command> --help (or cinecode help <command>) prints a command's options, cinecode --help the list, and cinecode --version the version.

Exit status: 0 done, 1 failed (including lint finding errors), 2 a usage error, 130 interrupted (Ctrl-C stops the workers and encoders and cleans up; a second Ctrl-C exits at once).

Commands

command does writes
new <slug> scaffold a film from templates/ films/<slug>/film.json, screenplay.md, custom.mjs
check <film> validate film.json and load custom.mjs nothing
plan <film> compile and print the shot list nothing
voice <film> voice new or changed lines, with QA voices/<id>.wav + .json
sheet <film> contact sheet: the middle frame of every shot out/sheet.jpg
lint <film> staging and editing checks out/lint.json, out/lint.jpg
music <film> compose the score out/music.wav
mix <film> score + dialogue + effects + ambience, mastered out/music.wav, out/mix.wav
animatic <film> quick preview with sound out/animatic.mp4 (+ the mix)
render <film> the final film out/<slug>.mp4, out/<slug>.srt (+ the mix)
cast-sheet [film] [kinds…] every rig in its poses and expressions out/cast-sheet.jpg
set-sheet [film] [kits…] every kit with anchors, footprints and walk regions out/set-sheet.jpg
verify-rig <file|kind> a new rig's gate: checks and review images out/verify/<kind>.jpg, <kind>-lineup.jpg
verify-kit <file|name> a new kit's gate: checks and review images out/verify/<name>.jpg, <name>-times.jpg
verify-effect <film> [names…] a film's effects' gate: checks and a review strip out/verify/effects[-<names>].jpg

out/ is the film's own films/<slug>/out/, except for cast-sheet and set-sheet without a film, which write to out/ in the current folder, and verify-rig and verify-kit given a file, which write to out/ beside the folder that holds it: films/<slug>/out/verify/ for a film's rigs/ or kits/, cinecode/out/verify/ for a project's (--out=DIR puts them elsewhere; given a registered name, out/verify/ in the current folder).

new

node bin/cinecode.mjs new my-film --title="The Lantern Moth"

Creates films/my-film/ (or the path given, when it contains a /) from the files in templates/: a starter film.json (one scene of six shots, about 39 s with its title cards: a girl, a hare and an owl in the meadow), screenplay.md with the sections to fill in, and custom.mjs with a commented custom beat, overlay and sound. Every {{title}} in them becomes the title (default: from the slug, my-film → "My Film"). It refuses to overwrite an existing film.json and skips existing files unless --force is given.

check and plan

node bin/cinecode.mjs check films/my-film
node bin/cinecode.mjs plan films/my-film

check lists every problem with its path in film.json (storyboard.md). When the film is valid it names the extension files it loaded (rigs/kite.mjs, cinecode/kits/harbour.mjs), marking one that replaces a built-in rig, kit or effect of the same name: cinecode/effects/rows.mjs (replaces the built-in custom beat "sizerow"). plan compiles the film and prints its length and the shot list: id, set, size, start, length and the first line of each shot. Unvoiced lines use an estimate, marked ~. Neither prints warnings; the Claude Code skill's node .claude/skills/cinecode/scripts/warnings.mjs films/my-film does. Nor does plan print when each beat starts and ends: node .claude/skills/cinecode/scripts/timeline.mjs films/my-film [shot ids] prints every beat's start and end (in the shot and in the film), a move that runs on past its cut and into which shot, every mark's film time (for sheet --t=), and a pose or gesture that starts while its actor is still moving (where it is lost).

compile  0:38.8, 6 shots, 0/5 lines voiced                        0.4 s
         (5 lines not voiced yet: timing uses estimates; run `cinecode voice`)

My Film — 0:38.8 · 6 shots · 1 scene · 1920×1080 @ 24 fps

shot      set       size             start    dur   lines
1.1       meadow    establishing    0:04.0   7.8s   ~narrator: "At the edge of the old forest, a girl named…
1.2       meadow    full            0:11.8   5.8s   ~pip: "Hello? Is anybody out here?"
1.3       meadow    medium          0:17.6   3.8s   ~hare: "Only me. I was having a nap."
1.4       meadow    full            0:21.4   4.3s   ~pip: "Then wake up! Race you to the pond!"
1.5       meadow    wide            0:25.7   2.3s
1.6       meadow    full            0:28.0   6.8s   ~narrator: "And that is how the summer began."

5 lines (5 estimated, marked ~) · 17 sfx · 1 music cues · 5 captions

The first shot starts at 0:04 because the opening title card comes first; the total (0:38.8) includes both title cards, 4 s each here, so for a short film they are a big share of its length. Don't cut them below 3 s: a shorter card never reaches full strength (titles).

voice

Needs GEMINI_API_KEY. Sends only lines missing from the cache, checks every take, and prints each line's length and word error rate (sound.md).

sheet

node bin/cinecode.mjs sheet films/my-film                     # middle frame of every shot
node bin/cinecode.mjs sheet films/my-film --t=12,0:40.5 --cols=2 --tile=960 -o films/my-film/out/two.jpg
option meaning default
--t=T,T,… draw these times (seconds or m:ss) instead of one frame per shot
--cols=N columns 4
--tile=PX tile width in pixels 480
-o, --out=PATH output file out/sheet.jpg

Each tile is labelled with its shot id, time and the caption or line at that moment.

For closer work there is a second tool with shot ids, points inside a shot, a debug overlay and full-size stills:

node engine/film/sheet.mjs films/<slug> films/<slug>/out/s01.jpg --shots=1.2,1.3 --cols=2 --tile=640
node engine/film/sheet.mjs films/<slug> films/<slug>/out/strip.jpg --shots=1.5@0.1,1.5@0.5,1.5@0.9 --cols=3
node engine/film/sheet.mjs films/<slug> films/<slug>/out/debug.jpg --shots=1.4 --debug
node engine/film/sheet.mjs films/<slug> films/<slug>/out/still.jpg --shots=1.6 --frame

--shots= takes shot ids, each at its middle or at @u (0..1 through the shot; --u= changes the default); --t= takes seconds. --debug draws actor boxes (yellow while speaking), head boxes, contact points, solid footprints and the caption box. --frame writes full-size stills instead of a sheet (one file per time).

lint

--step=S sets the sampling step in seconds (default 1/6). See lint.md.

music and mix

music composes the score to out/music.wav. mix composes it and mixes it with the cached dialogue, the effects and the ambience, mastering to −16 LUFS in out/mix.wav (sound.md). Unvoiced lines are left out with a warning. To check the score without listening, the skill's node .claude/skills/cinecode/scripts/levels.mjs films/my-film [music|mix] prints its level per shot in 0.5 s windows and flags a second or more of near-silence under a cue (a whole-shot average hides it); it reads the WAV itself, no ffmpeg needed.

animatic

node bin/cinecode.mjs animatic films/my-film
node bin/cinecode.mjs animatic films/my-film --from=0:20 --to=0:40

Half resolution, 12 fps, x264 crf 23 veryfast, with sound. It mixes first when out/mix.wav is missing or older than any file of the film (film.json, custom.mjs, voices/, …), otherwise reuses it.

option meaning default
-j, --jobs=N worker processes CPUs − 1
--from=T, --to=T a part of the film (seconds or m:ss) the whole film
-o, --out=PATH output file out/animatic.mp4

render

node bin/cinecode.mjs render films/my-film
node bin/cinecode.mjs render films/my-film --from=1:20 --to=1:40     # → out/my-film-80s.mp4

The steps, each printed as it finishes:

  1. voices: from the cache. Missing lines are voiced first when GEMINI_API_KEY is set; without it the render stops and lists them.
  2. music and mix: the score, the mix, and the master to −16 LUFS / −1.5 dBTP (out/mix.wav).
  3. srt: every caption (dialogue, narration and sounds) to out/<slug>.srt.
  4. render: the frames are split into contiguous ranges, one per worker process; each worker loads the film itself and pipes its frames into its own x264 encoder (yuv420p, BT.709, a keyframe every 2 s). The segments are joined without re-encoding, then the master audio (AAC 192 kbit/s, 48 kHz stereo) and the captions as soft subtitles (mov_text, off by default) are added.
  5. verify: ffprobe checks the result (frame count, size, frame rate, duration, audio and subtitle streams) before it replaces the output file.
option meaning default
-j, --jobs=N worker processes CPUs − 1
--from=T, --to=T render only this part, at full quality the whole film
-o, --out=PATH output file out/<slug>.mp4, or out/<slug>-<from>s.mp4 for a part
--crf=N x264 quality (lower is better and bigger) 17
--preset=NAME x264 preset slow
--keep keep the per-worker segment files off

Work in progress lives in a hidden .<name>.mp4.parts/ folder next to the output and is removed at the end (kept with --keep). The burned-in captions follow format.captions (dialogue by default); the .srt and the subtitle track always hold all of them.

cast-sheet and set-sheet

node bin/cinecode.mjs cast-sheet hare,tortoise -o out/two.jpg
node bin/cinecode.mjs set-sheet films/my-film meadow

With a film first, its own rigs and kits from custom.mjs are included and the sheet goes to the film's out/. Names may be comma-separated; without names every rig or kit is drawn. See cast.md and sets.md.

Environment

variable used for
GEMINI_API_KEY voicing lines (voice, and render when lines are missing). Nothing else uses the network.
CINECODE_FFMPEG, CINECODE_FFPROBE the ffmpeg and ffprobe binaries for video (default: from PATH). Mastering the mix always runs ffmpeg from PATH.
CINECODE_DEBUG print stack traces on errors (and a worker's stack when a worker fails)

Performance

Measured on a 10-core Mac (Node 22, ffmpeg 7.1):

Seventy-Six Summers, load and compile 2–3 s
Seventy-Six Summers, one 1920×1080 frame in one process about 0.7–0.85 s on average
the starter film, one 1920×1080 frame about 0.12–0.13 s
the starter's animatic (466 frames, 9 workers) 31 s in all, including the mix

A frame's cost depends on what is in it: painted sets are cached after the first frame a worker draws, while depth of field, night grades, many actors and fields of particles cost more. The render scales with cores (-j). For quick looks, use contact sheets and strips (seconds) and the animatic (a quarter of the pixels at half the frame rate) before a full render, and --from/--to to check one sequence at full quality.

This page on GitHub