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.
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
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.
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.
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.
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.
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 pixelsBecause titles are stored as fractions, one composed against a 720p preview lands in exactly the same place in a 4K render.
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
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.
Generated from reel commands --json. Every one also takes
--json.
reel info MEDIADuration, frame size and rate of a media file
reel new PROJECTCreate an empty .reel project
--name TEXTProject name--size WxHFrame size, e.g. 1920x1080--fps NFrame ratereel inspect PROJECTThe whole project — clips with their ids, titles, captions, music, markers
reel add PROJECT MEDIAAppend 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 audioreel split PROJECTCut every clip that crosses a point in the timeline
--at SECONDSWhere to cutreel trim PROJECTChange 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 positionreel move PROJECTMove a clip along the timeline
--clip IDClip id--to SECONDSNew timeline positionreel roll PROJECTRoll 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 changesreel slip PROJECTSlip a clip: change WHAT plays without moving WHEN
--clip IDClip id--by SECONDSShift the clip's window through its sourcereel slide PROJECTSlide 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 motionreel remove PROJECTDelete a clip
--clip IDClip id--rippleClose the gap left behindreel gap PROJECTClose every gap between clips
reel gain PROJECTSet a clip's audio level
--clip IDClip id--db DECIBELSLevel change, e.g. -6 or 3reel effects PROJECTColour, 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 effectsreel keyframe PROJECTAnimate 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 clipreel pip PROJECTPlace 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 framereel speed PROJECTChange 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 sourcereel transition PROJECTCrossfade 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/rightreel title ACTION PROJECTACTION 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 PROJECTFlag a position in the timeline
--at SECONDSWhere to put it--removeTake it away instead--listShow the markersreel align PROJECTSync 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 PROJECTCut 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 TARGETTranscribe 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 progressreel frame TARGETExport 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 existsreel render PROJECT OUTPUTRender 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 progressreel convert MEDIA OUTPUTTranscode 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 progressreel presetsThe one-click destinations (YouTube, TikTok, Reels…)
reel commandsEvery command, argument and flag — the machine-readable manual
0Success1Ran and failed — bad arguments, missing clip, render error2Not a command — a typo, or a file that doesn't exist