REEL

Edit video
without a window.

Reel is two programs in one binary. reel <file> opens the player; everything below runs headless — no display needed — so a script, a CI job or an agent can cut, caption and render video with it.

The short version

A .reel project is plain JSON. The whole workflow is: make one, change it, render it.

reel new cut.reel --size 1080x1920 --fps 30
reel add cut.reel intro.mp4 --in 2 --duration 5   # 5s of intro.mp4, from 0:02
reel add cut.reel main.mp4                        # whole file, after the last clip
reel captions cut.reel                            # transcribed on this machine
reel title add cut.reel --text "Hello" --at 0 --duration 3
reel music set cut.reel bed.mp3 --gain-db -14     # ducks under the speech
reel render cut.reel out.mp4 --preset tiktok

Built to be driven

Everything speaks JSON

Add --json to any command and you get exactly one object on stdout. Progress and logs go to stderr, so reel render … --json 2>/dev/null | jq is always safe. Failures are JSON too, and always exit non-zero.

The manual is machine-readable

reel commands --json describes every command, argument and flag. It's generated from the same table that parses the arguments, so it cannot drift from what the binary actually does. Read it at runtime instead of hard-coding anything.

Ids, not positions

reel inspect gives every clip a stable id. Pass it to trim, move, gain and the rest — ids survive edits elsewhere in the timeline, indexes don't.

Mistakes are refused

An unknown flag is an error, not a shrug, so a render never quietly differs from what you asked for. A mistyped command exits 2 instead of opening a window — which matters on the headless machines where automation actually runs.

Times, positions and sizes

secondsEvery time value, as a float
--atWhere a clip sits on the timeline
--inWhere it starts inside the source file
--x --y --sizeFractions of the frame, not pixels

Because titles are stored as fractions, one composed against a 720p preview lands in exactly the same place in a 4K render.

A worked example

Two shots, a crossfade, colour, captions, a title and a ducked music bed — rendered for TikTok, start to finish.

set -e
reel new promo.reel --size 1080x1920 --fps 30
reel add promo.reel shot-a.mp4 --duration 4
reel add promo.reel shot-b.mp4 --duration 3

# Ids are stable; read them back rather than guessing.
read -r A B <<< "$(reel inspect promo.reel --json | jq -r '[.clips[].id] | @tsv')"

reel transition promo.reel --clip "$B" --seconds 0.5
reel effects   promo.reel --clip "$A" --saturation 1.2 --fade-in 0.5
reel captions  promo.reel --model base
reel title add promo.reel --text "New drop" --at 0.2 --duration 2.5 \
     --y 0.15 --size 0.11 --color ffcc00
reel music set promo.reel bed.mp3 --gain-db -14

reel render promo.reel promo.mp4 --preset tiktok --json

Captions, locally

reel captions cut.reel transcribes speech on the machine it runs on. No account, no API key, nothing uploaded.

The engine (~9 MB) and model (~75 MB) are fetched automatically on first use and cached in ~/.cache/reel; after that it works offline. Cues are generated against the source and mapped through the edit, so a line spanning a cut appears in both halves, and speech you trimmed away captions nowhere. Add --srt out.srt to get the subtitle file as well.

Every command

Generated from reel commands --json. Every one also takes --json.

reel info MEDIA

Duration, frame size and rate of a media file

reel new PROJECT

Create an empty .reel project

--name TEXTProject name
--size WxHFrame size, e.g. 1920x1080
--fps NFrame rate

reel inspect PROJECT

The whole project — clips with their ids, titles, captions, music, markers

reel add PROJECT MEDIA

Append a piece of media to the timeline

--at SECONDSTimeline position (default: after the last clip)
--in SECONDSStart point inside the source (default 0)
--duration SECONDSHow much of the source to use (default: all of it)
--track KINDvideo, overlay (picture-in-picture) or audio

reel split PROJECT

Cut every clip that crosses a point in the timeline

--at SECONDSWhere to cut

reel trim PROJECT

Change a clip's source window or position

--clip IDClip id (from `reel inspect`)
--in SECONDSNew start point inside the source
--duration SECONDSNew length
--start SECONDSNew timeline position

reel move PROJECT

Move a clip along the timeline

--clip IDClip id
--to SECONDSNew timeline position

reel roll PROJECT

Roll a cut: one clip grows, the other shrinks, the timeline stays put

--clip IDThe cut between this clip and its left neighbour moves
--by SECONDSPositive = the neighbour grows; total length never changes

reel slip PROJECT

Slip a clip: change WHAT plays without moving WHEN

--clip IDClip id
--by SECONDSShift the clip's window through its source

reel slide PROJECT

Slide a clip between its neighbours; the combined span is unchanged

--clip IDClip id (must touch neighbours on both sides)
--by SECONDSMove the clip; neighbours absorb the motion

reel remove PROJECT

Delete a clip

--clip IDClip id
--rippleClose the gap left behind

reel gap PROJECT

Close every gap between clips

reel gain PROJECT

Set a clip's audio level

--clip IDClip id
--db DECIBELSLevel change, e.g. -6 or 3

reel effects PROJECT

Colour, fades and reframing for one clip

--clip IDClip id
--exposure N1.0 = unchanged
--contrast N1.0 = unchanged
--saturation N1.0 = unchanged
--fade-in SECONDSFade up from black
--fade-out SECONDSFade down to black
--zoom N1.0 = whole frame; used for reframing
--pan-x N-1..1, where the zoom sits
--pan-y N-1..1
--key-color RRGGBBChroma key: knock this colour out (e.g. 00b140)
--key-similarity 0..1How far from the key colour still counts (default 0.3)
--key-softness 0..1Soft edge width beyond similarity (default 0.1)
--key-offStop keying
--resetBack to no effects

reel keyframe PROJECT

Animate a parameter over time — evaluated per frame at render

--clip IDClip id (from `reel inspect`)
--param NAMEexposure, contrast, saturation, zoom, pan-x, pan-y, opacity, pip-x, pip-y, pip-scale
--at SECONDSTIMELINE time of the keyframe
--value NThe value at that moment
--interp MODElinear (default), hold or ease
--removeRemove the keyframe nearest --at instead
--listShow every keyframe on the clip

reel pip PROJECT

Place a picture-in-picture overlay in the frame

--clip IDAn overlay clip's id
--x 0..1Centre of the inset across the frame
--y 0..1Centre of the inset down the frame
--scale 0..1Inset width as a fraction of the frame

reel speed PROJECT

Change how fast a clip plays

--clip IDClip id
--rate N2 = twice as fast, 0.5 = half. Audio follows.
--keep-lengthKeep the timeline slot; use more or less source

reel transition PROJECT

Crossfade from the previous clip into this one

--clip IDClip id — the fade runs INTO this clip
--seconds SECONDSTransition length (0 = hard cut)
--kind NAMEfade, dip, wipe-left/right/up/down, slide-left/right

reel title ACTION PROJECT

ACTION is add, list or remove — text placed on the picture

--text TEXTThe words
--at SECONDSWhen it appears
--duration SECONDSHow long it stays
--x 0..1Horizontal centre, as a fraction of the frame
--y 0..1Vertical centre, as a fraction of the frame
--size 0..1Text height as a fraction of the frame
--color RRGGBBHex colour, e.g. ffcc00
--no-boldRegular weight
--no-outlineNo dark outline
--index NWhich title (for remove)

reel music ACTION PROJECT AUDIO?

ACTION is set or clear — a music bed under the whole edit

--gain-db DECIBELSLevel (default -12)
--no-duckDon't pull the music down under speech
--fade SECONDSFade in/out (default 1)

reel marker PROJECT

Flag a position in the timeline

--at SECONDSWhere to put it
--removeTake it away instead
--listShow the markers

reel align PROJECT

Sync one clip to another by their AUDIO — multicam without clap sticks

--clip IDThe clip to move
--to IDThe clip to sync against
--window SECONDSLargest offset to search (default 90)

reel tighten PROJECT

Cut the silent air out of the edit and close up — the podcast jump-cut

--threshold 0..1Quiet = below this fraction of the source's own peak (default 0.06)
--min-gap SECONDSOnly cut silences at least this long (default 0.6)
--pad SECONDSBreathing room kept on each side of a cut (default 0.15)

reel captions TARGET

Transcribe speech locally. TARGET is a .reel project or a media file

--model NAMEtiny, base or small (default base)
--size NCaption size (default 20)
--srt FILEAlso write the captions to this .srt
--source MEDIATranscribe this instead of the project's first clip
--quietDon't print progress

reel frame TARGET

Export one frame as PNG. TARGET is a .reel (rendered with effects, overlays, animation) or a media file

--at SECONDSWhich moment (default 0)
--out FILE.pngWhere to write the PNG (default beside the target)
--overwriteReplace the output if it exists

reel render PROJECT OUTPUT

Render the edit — captions, titles and music included

--preset NAMEA social preset: see `reel presets`
--codec NAMEh264, h265, av1, vp9, remux, mp3, m4a, opus, flac, wav, png, jpeg, webp
--quality NAMEhigh, balanced, small, or a CRF number
--resolution HEIGHTsource, 2160, 1080, 720, 480
--fit MODEletterbox, crop or blur (how a mismatched aspect is filled)
--audio MODEcopy (pass the source audio through) or encode
--loudness LUFSDeliver audio at this integrated loudness (e.g. -14); presets set it automatically
--no-hardwareForce the software encoder
--overwriteReplace the output file if it exists
--quietDon't print progress

reel convert MEDIA OUTPUT

Transcode one file, no project needed

--preset NAMEA social preset: see `reel presets`
--codec NAMEh264, h265, av1, vp9, remux, mp3, m4a, opus, flac, wav, png, jpeg, webp
--quality NAMEhigh, balanced, small, or a CRF number
--resolution HEIGHTsource, 2160, 1080, 720, 480
--fit MODEletterbox, crop or blur (how a mismatched aspect is filled)
--audio MODEcopy (pass the source audio through) or encode
--loudness LUFSDeliver audio at this integrated loudness (e.g. -14); presets set it automatically
--no-hardwareForce the software encoder
--overwriteReplace the output file if it exists
--quietDon't print progress

reel presets

The one-click destinations (YouTube, TikTok, Reels…)

reel commands

Every command, argument and flag — the machine-readable manual

Exit codes

0Success
1Ran and failed — bad arguments, missing clip, render error
2Not a command — a typo, or a file that doesn't exist