Open Edit AI Agent
Open-source, agent-driven editing pipeline: create subtitles, motion graphics, slides, edit and render videos.
Open Edit is an open-source ai agent by veedstudio, written mainly in TypeScript, with 1,763 GitHub stars and the Apache-2.0 license. Its code was last updated Nov 1, 2026. The latest npm release is @veedstudio/openedit-cli 0.10.0, published Nov 13, 2026, with 900 downloads last week.
@veedstudio/openedit-cli
The Open Edit command-line tool: agent-driven video creation and editing, powered by VEED.
The package carries the agent skill that init installs into a workspace;
everything else is a command below. The source is public at
veedstudio/open-edit, whose README
covers what Open Edit is and what it makes; this page is the command surface.
Usage
npx @veedstudio/openedit-cli --help
Commands
login
Log in with VEED (OAuth 2.1 + PKCE via the browser; the token is stored locally and refreshed automatically):
npx @veedstudio/openedit-cli login
Pass --manual (or set VEED_LOGIN_MANUAL=1) when no reachable browser exists:
the URL is printed, and the redirect is pasted back instead of caught on a
localhost loopback.
token
Print a valid VEED access token for other tools to consume, refreshing it
first when stale (VEED_ACCESS_TOKEN, when set, is passed through as-is). The
value is printed only when the output is captured; run it straight in a terminal
and it reports that a token exists without putting one in your scrollback:
npx @veedstudio/openedit-cli token
Exits non-zero when no login is stored. --path prints the token store
location instead of a token.
transcribe
Transcribe videos, writing runs/<key>/transcript.json. The provider is the
user's choice, recorded once with --record: a bare transcribe video.mp4 runs
the recorded one (and refuses, saying why, when nothing usable is recorded), and
--provider names another for one run. Locally with WhisperX (free, offline,
nothing billed anywhere):
npx @veedstudio/openedit-cli transcribe --provider whisperx video.mp4 [...] [--model medium] [--language de] [--force]
A transcript already on disk is left alone (it may have been retimed onto an
edit); --force transcribes it again.
Or hosted by VEED (premium quality; requires the one-time login, and spends
the workspace's VEED transcription credits — --workspace <id> names which,
required only when the account has several):
npx @veedstudio/openedit-cli transcribe --provider veed video.mp4 [...]
--record <veed|whisperx|custom> [--model <id>] records the transcription
provider choice in .open-edit-prefs.json instead of running anything.
whisper
Map a speech-to-text JSON produced by your own service or a fal model into the
same runs/<key>/transcript.json: Whisper's segments with word times
(WhisperX, openai-whisper, whisper-timestamped, mlx-whisper), the OpenAI API's
verbose_json with word granularity, whisper.cpp -oj -ml 1, a timed word
list, fal's whisper and wizper chunks, a voice model's character timestamps, or
the fal-result.json that fal run keeps a model's answer in:
npx @veedstudio/openedit-cli whisper transcription.json media.mp4 [...] [--force]
Word timestamps are required; the media argument may be a video or an audio
file. Every command that reads a transcript (review, retime-transcript,
shots, veed-project, words) takes the same formats, mapped in memory, and
prints one line naming the format it read.
words
Print a transcript as timed lines, one per word with its start and end in
seconds, or one per cue with --cues; --from/--to keep the lines that
overlap that stretch. It reads a transcript.json or any format whisper maps,
and writes nothing:
npx @veedstudio/openedit-cli words runs/<key>/transcript.json [--from <sec>] [--to <sec>] [--cues]
generate / generate-set / sample-presenter
Fabric generation — source footage from a script, when there is no video to
caption. Generating draws the named workspace's AI Playground credits twice —
the speech is synthesized, then lip-synced — so it is two commands: the first
quotes the sum and records the approval, the second
spends exactly what was approved (no --script on the spend pass — the
recorded, hashed script is what bills):
npx @veedstudio/openedit-cli generate --script "spoken words" --key my-run --workspace <id>
npx @veedstudio/openedit-cli generate --key my-run --yes
--resume collects a job already created and paid for; --abandon <sessionId>
clears one abandoned charge record. generate-set --shots shots.json covers
several shots under one approval, and sample-presenter proposes a
deterministic character/voice pair for a run key (listing costs nothing).
background-removal / lipsync
Remove a video's background, or re-lipsync a video to a new audio track.
Background removal tries VEED's free route first; when that route cannot run,
it stops having spent nothing and prints what fal's model would cost and the
command to run with --fal. Only --fal (the full model, uploaded to fal's own
storage) or --fast (fal's fast model) buys anything, and lipsync always goes
through fal. Every fal call bills your own fal key, never a VEED workspace; a
re-run of the same inputs resumes the paid job instead of buying it again:
npx @veedstudio/openedit-cli background-removal video.mp4 [--fal|--fast] [--mask-only] [--no-refine] [--out <path>]
npx @veedstudio/openedit-cli lipsync video.mp4 narration.mp3 [--out <path>]
The fal key comes from FAL_KEY, or OPEN_EDIT_FAL_KEY_FILE pointing at a
file that holds it.
veed-project / veed-pull
Hand a finished edit to VEED's editor as a project a person can take apart:
footage cuts, text, captions, images and each audio track become the editor's
own items, and each page element the plan names becomes a transparent clip cut
to its box. It uses your VEED login and spends no VEED credits and nothing on your
fal key. The last step runs in
your browser: click the "OpenEdit to VEED" bookmark on VEED. The way back is the
"Send to Claude" bookmark on a project open in the editor, and veed-pull:
npx @veedstudio/openedit-cli veed-project --install-bookmark
npx @veedstudio/openedit-cli veed-project plan.json [--workspace <id>] [--local]
npx @veedstudio/openedit-cli veed-pull ~/Downloads/openedit-<project id>.json --out <dir>
veed-project --help lists the plan's fields; veed-pull writes the project
back as such a plan.
fal
Run any fal model on your own key. Local file paths inside the input are
uploaded for you (multipart above 90 MB), outputs are downloaded, and each job
ends with a cost line: the billed figure when your key can read fal's billing,
otherwise cost: unknown beside fal's listed price. An identical request is
bought once, even across processes and crashes:
npx @veedstudio/openedit-cli fal schema <model>
npx @veedstudio/openedit-cli fal run <model> --input '{"prompt":"…"}' [--out <dir>]
npx @veedstudio/openedit-cli fal run --batch jobs.json [--concurrency N] # [{model, input, name?}]
install-ffmpeg / install-whisperx
install-ffmpeg looks for an existing FFmpeg first (OPENEDIT_FFMPEG and
OPENEDIT_FFPROBE, then a previous install, then PATH) and only downloads when
there is none — a checksum-verified static build into the app-data dir (the
download route is Windows-only; macOS points at brew install ffmpeg, Linux at
apt). A set OPENEDIT_FFMPEG or OPENEDIT_FFPROBE is run first, and fails
the command, naming the variable, when its binary does not start, since nothing
else is used while it is set; --check runs whichever pair every command would.
--force reinstalls the app-data copy, and is refused while either override is
set, since no command would run the copy it installs.
install-whisperx installs the local transcription provider into an isolated
uv/pipx tool environment; it never touches the system Python.
npx @veedstudio/openedit-cli install-ffmpeg [--check|--force]
npx @veedstudio/openedit-cli install-whisperx [<version>]
brief
Record what the user asked of a piece once, in its folder: how long it runs,
where it is posted and their own words. review compares the video's length
with it and the numbers on screen with the words, render and apply-edl print
a line when what they made misses that length, safe-zone takes its zone from
the platform, render gives that zone to a page that links none, and reframe
takes its shape; a value given to the tool itself wins, and the tool says which
it used. A tool finds
brief.json from the file it works on, in that file's folder or the nearest
one above it inside the workspace. Each call sets the fields it is given and
keeps the others:
npx @veedstudio/openedit-cli brief runs/<key> --length 60 --platform tiktok --words "Doors open at 7"
npx @veedstudio/openedit-cli brief runs/<key> --platform "linkedin 1:1"
npx @veedstudio/openedit-cli brief runs/<key> # prints the brief that applies there
render / fonts / safe-zone / install-browser
Render an HTML page to video in headless Chrome, frame-exact. The page runs on a
virtual clock (timers, requestAnimationFrame, Date.now and a seeded
Math.random advance one frame per frame); CSS and Web Animations, GSAP's
global timeline and an optional window.__seek(t) are set to each frame's time,
and <video> elements show the picture at that time. The output is silent H.264
(or ProRes 4444 with --transparent), identical whatever the worker count:
npx @veedstudio/openedit-cli render page.html --out out.mp4 --fps 30000/1001 --duration 12 [--workers N]
npx @veedstudio/openedit-cli render page.html --out out.mp4 --footage clip.mp4 # the clip's rate and length
npx @veedstudio/openedit-cli render page.html --out out.mp4 --from 4 --to 6 # re-render only that stretch
npx @veedstudio/openedit-cli render page.html --out stills --stills 0.5,2,4 --sheet sheet.jpg
Segments and a manifest are cached in <out>.render/, so --from/--to
re-renders the segments it overlaps, and any other whose page or files changed
since it was drawn (a cue that moved, a clip replaced at its path), and rejoins
the rest by stream copy; every frame of the joined file is checked against its
time. A page marked
data-stateless is checked on one frame before the workers start, and rendered
as an unmarked page, with a line saying why, when the mark does not hold.
A page's long-GOP videos, and any in a codec Chrome has no decoder for (HEVC
or ProRes, say), are served from all-intra copies, made once per source, keyed
by its content, in the workspace's runs/.media, which stills into any
directory, renders and patches share (render --help says how much it keeps).
It fails naming the cause when Chrome does not start or stops answering, the
page throws, or every frame is one flat colour, and a signal stops it with the
Chromes and ffmpegs it started. A video render then looks at what it made, as the
review's blank-frames and dead-air checks do, and prints a line, failing
nothing, for a first frame that is blank, near-black or partway into a fade-in,
and for each still held longer than its words allow; a patch looks only near
the range it re-rendered. fonts downloads Google Fonts as local woff2 files
with a fonts.css; install-browser fetches the pinned Chrome Headless Shell,
which render also does on first use. safe-zone writes the stylesheet for where
a video will be posted: the stage at its shape's size, and a [data-safe]
container on that platform's safe zone as the page's text layer. With no zone
named, it takes the platform in the piece's brief. A zone is the app the user
names (tiktok, reels, shorts, youtube, x, linkedin, facebook).
With none named, or several, it is the shape: 9:16, 1:1 and 4:5 are each clear
of every app's UI listed for them, while 16:9 is YouTube's player, so a 16:9 piece
for a feed names that feed's app (4:3 has one zone). For Instagram, name where it
goes: reels for a 9:16 Reel, the feed shape (1:1 or 4:5) or the app with its
shape (instagram 4:5), or pass the canvas with --canvas; LinkedIn and Facebook
are named or take --canvas the same way. Add
--ad when a 9:16 video is an ad: ads reserve more of the frame than an ordinary
post's UI takes. Ordinary posts and feeds are kept clear of the UI each app draws
over them, ads of the platforms' published ad safe zones, and 4:3 has television
title-safe sides (safe-zone --help says exactly what each zone keeps clear). The
container is a size container, so cqw and cqh measure the zone for what is
inside it. Its other defaults lose to any rule the page writes: text inside starts
at the shape's minimum (and so do small, sub, sup, h5, h6 and form
controls), a word too long for its line breaks, boxes inside keep within its width
unless they set max-width: none (a ticker's track also sets width: max-content), and nothing is clipped:
npx @veedstudio/openedit-cli fonts "Inter:wght@400;700" Anton --out fonts
npx @veedstudio/openedit-cli safe-zone --out runs/<key> # the platform in the piece's brief
npx @veedstudio/openedit-cli safe-zone tiktok --out runs/<key>
npx @veedstudio/openedit-cli safe-zone 9:16 --ad --out runs/<key>
npx @veedstudio/openedit-cli safe-zone instagram --canvas 1080x1350 --out runs/<key>
npx @veedstudio/openedit-cli install-browser [--check|--force]
captions
Caption every spoken word of a transcript on the page. captions writes
captions.js beside the transcript (--out names another file): its words and
the code that shows them as cues on the page's clock. The page loads it with a
<script> tag and gives it an empty element marked data-captions where the
captions go, inside [data-safe] when the page has a safe zone. Each cue comes
up with its first word and stays half a second past its last, held until the
next when the gap is under half a second, so the screen clears only in a silence
of a second or more. Cues break where the speech does (a sentence end, a pause,
a clause), speak for at most 7 seconds, and take at most two lines of 42
characters, fitted to the element's width in the page's own font. With
faces.js loaded the cues are placed off the faces and the graphics burned into
the footage, inside the safe zone, at one place through each shot: they can move
only at a cut, and inside a shot only where no single place stays clear for every
cue up in it, however briefly, as few times as that takes, which the console
says. The script sets
structure and state only, never a look: the cue
on screen is a [data-cue] of [data-line]s of [data-word] spans
(punctuation in a [data-punct] of its own), each word's data-state is
next, now or said, and --since, --progress, --cue-since,
--cue-until and --cue-progress give the page's CSS each frame's timing to
animate from. data-lines,
data-words (1 for one word at a time), data-chars, data-at (where the
transcript's clip starts on the page), data-from and data-to on the element
change the cues; captions --help lists them all. A speech-to-text JSON is read
as whisper reads it:
npx @veedstudio/openedit-cli captions runs/<key>/transcript.json [--out <file>]
mux-audio / mix-audio
Put sound on a render. mux-audio lays a track (the source clip, or a built
soundtrack) onto a silent render, levelled to -14 LUFS / -1 dBTP
(--no-loudnorm keeps the source level; the line it prints says which
correction ran), starts a clip that render --footage-at placed later in the
piece at that same moment, and prints a silent opening or ending longer than a
second in what it wrote. --music <file> in place of --audio lays a music
file alone, run to the picture's last frame;
mix-audio first builds one track from many pieces — narration, music,
effects — per the run's mix spec, with music and effects ducked under the voice,
effects given no gain set under it by measured loudness (give an effect a
gain for a deliberate loud hit), and the music bed run to the end of the film:
looped with a crossfade when its sound ends sooner, faded out at the end.
--video takes the mix's length from the render, and the sound of each
<video> its page plays is laid in where the render played it, trims, loops
and speed included, under the voice; the spec's pageVideos leaves one out or
lays it otherwise:
npx @veedstudio/openedit-cli mix-audio runs/<key> --video runs/<key>/out.mp4 # → runs/<key>/audio/mix.m4a
npx @veedstudio/openedit-cli mux-audio --video runs/<key>/out.mp4 --audio runs/<key>/audio/mix.m4a --out runs/<key>/final.mp4
npx @veedstudio/openedit-cli mux-audio --video runs/<key>/out.mp4 --music runs/<key>/assets/music.mp3 --out runs/<key>/final.mp4
review
Run every delivery check on the video about to be delivered and print one
report. It changes nothing, and exits 0 whatever it finds. The blank-frames
check reports a flash, a frame or two of one flat colour (a blank white page
with only a caption on it, say) between other pictures; any flat colour held
with nothing on it for more than a second; and a first frame that is blank,
near-black or partway into a fade-in, since platforms show it as the cover.
The dead-tail check reports a picture that runs on more than a second past its
last sound or its last spoken word. The words come from the transcript.json
in the video's folder, or the one --transcript names, in any format whisper
maps; the review never transcribes, and without a transcript it says what it
skipped. A picture that runs on past the last word with sound under it to the
end is an end card when
new text is on screen through it: text that came up from the pause before the
last spoken phrase on, in an element that showed no other words before then.
It passes unless it is held longer than its words take to read: 0.2 seconds a
word, plus 2 seconds to find the text (3 from ten words) and a one-second beat,
never less than 4 seconds and never more than 4.5. So a card brought up with
the spoken call to action counts, while a caption left on into the music, a
title up since earlier or a watermark does not. A run-on with more than a
second of no end card is reported as background left running; without the
record of on-screen text (below) it is reported, and the report says the end
card could not be judged. The dead-air check reports a stretch with nothing
happening anywhere in the video. In the picture, that is a still, frozen or
nearly so (a slow zoom or a drift over a held card), held longer than a still
may hold, end cards included: as long as an end card of as many words when it
has words on screen, 3 seconds when it has none. Footage that moves almost
everywhere, such as a slow pan, is not still. A talking head can measure as
still as a held card, so a nearly still picture passes while a voice talks over
it without pausing more than 2 seconds; without a transcript it is reported,
and the report says the voice could not be checked. A frozen picture is
reported whoever speaks over it. In the sound, it is a first sound more than a
second after the first frame, and more than 2 seconds of silence between two
sounds; a silence after the last sound is the dead-tail check's to report, so
no silence is reported twice. The length check reports a picture that runs longer
or shorter than the length asked for, in seconds (--length, else the length in
the piece's brief), by more than a second or 5% of that length, whichever is
more; without either it is skipped. The
canvas check reports a picture nearly one of the platform shapes (9:16, 1:1, 4:5,
16:9) but not exactly it, such as a 606x1080 crop of 1080p footage, and one under
1080 px on its short side; a picture of another shape is judged on its short
side alone.
The text, captions, numbers and graphics checks read the record of on-screen text that render
writes beside each video, <video>.text.json: the text the page draws in its
HTML and SVG (not text inside an image, a canvas or the footage), read about
every 100 ms of the page's time, with its words, its box and font size in the
video's pixels, its colour, its opacity and how opaque the backgrounds painted
under it are, and each <video> the page shows, with its clip, the second of
it on screen and where its picture lies. mux-audio carries the record to
the file it writes, and a patch with --from/--to keeps it whole. The text
check reports text smaller than a phone's body text, measured against the
frame's shorter side, and text under WCAG's 3:1 contrast for large text against
what is really behind it, measured on the video's pixels around the glyphs, so a
backing, a stroke or a shadow counts; it skips the contrast of a transparent
render, and of text that is translucent or has no solid fill of its own. The
captions check reports a second or more of speech with no caption on screen. A
caption is a text in the page's caption track (an element marked
data-captions, which captions.js fills), a text that shows the words being
spoken, or, in another language than the voice, one of a run of texts in one
place that change with the spoken phrases; a title is not one. It is skipped when the page captions too little of
the speech to be captioning it, or there is no transcript. The numbers check
reports a number on screen that neither the transcript nor the user's own words
say, with where and when it shows: a time, a price, a percentage or an amount
worked out or guessed while the video was made, such as "Ready in 15 min" over
narration of "about 10 minutes". Numbers are compared by value, in digits or in
English words, so "15 min", "fifteen minutes" and "0:15" agree, as do "$5" and
"five dollars", "50%" and "half", "2018" and "twenty eighteen", and a rounding
such as "1.4M" for 1,423,000; a range or a score is two numbers. A list's own
numbering ("01", "Tip 2") and the count of its items, a clock or a count running
in one place, a number up for under a quarter of a second, and digits in a name
("MP4"), an address or a handle are set aside. The user's words come from
--brief, their message itself or a file holding it, repeatable, else from the
words in the piece's brief (brief --words); without either the numbers are
compared with the transcript only, without a transcript with the user's words
only, and with neither the check is skipped. It cannot see the
footage, so a number read off the picture (a scoreboard, a page in shot) is
reported too. The graphics check reports text drawn over a graphic burned into
the footage while it is up: the record names the clip --footage laid under the
page, and each <video> the page plays with the second of its clip on screen
and where its picture lies. It reads the graphics faces found on a clip, or
looks for them itself when faces has not run on it, and counts a text only
where the video still shows the clip's picture around it, so text on a card or
band that covers the graphic passes. A video without a record, or with one for a
picture of another size or length or older than the video, skips all four:
npx @veedstudio/openedit-cli review runs/<key>/final.mp4 [--transcript <transcript.json>] [--length <sec>] [--brief <text|file>]
Frames and joins
Tools for looking at and joining footage:
npx @veedstudio/openedit-cli concat-videos [--canvas WxH] [--fit crop|letterbox] <out> <in1> <in2> [...]
npx @veedstudio/openedit-cli frames <video> --at 12.5,1:02 --frame 300-306 --every 0.5 --from 8 --to 11 [--sheet] # stills at the moments you name
npx @veedstudio/openedit-cli frames --images <image|dir> [...] [--width N] [--cols N] # pictures that already exist, on one sheet
npx @veedstudio/openedit-cli shots <video> [--threshold 0.1] [--min-shot 0.333] [--faces <faces.json>] [--json] # a video as a shot list: cuts, lengths, speech from an existing transcript, shot size from existing face boxes, storyboard sheets
The cut tools, for an edit made before captioning: measure where speech starts,
stops and pauses; assemble the kept ranges of an EDL (edit decision list) with
crossfaded joins, each range snapped to the frame grid and optionally at its own
speed (slow motion by repeated or interpolated frames, sound time-stretched or
silenced) and grade (exposure, contrast, saturation, temperature, a LUT, grain),
a source at another frame rate converted to the first's and each source's speech
levelled to the quietest's, moving the per-word timings, the face boxes and the burned-in graphics the
sources already have onto the cut instead of finding them again. Each range's picture is kept in <cut>.ranges/ under
a name made of its source's content and everything applied to it, so a second run
that changes one range encodes only that range. A path in the EDL is read beside
it, else from the working directory or the workspace when the file is there.
After the cut, apply-edl prints what it removed from each source, with times,
each range whose picture runs on past its last sound, and each stretch held
still with nothing heard, measured on the cut, and a line when the cut misses
the length in the piece's brief.
--gap and --crossfade are milliseconds; the EDL is seconds.
npx @veedstudio/openedit-cli speech-probe <video> [--range a:b] [--gap 250] [--window 10] [--json]
npx @veedstudio/openedit-cli apply-edl --edl edl.json --out cut.mp4 [--crossfade 40] [--crf 20] [--fit crop|letterbox] [--keep-levels]
npx @veedstudio/openedit-cli retime-transcript --edl edl.json --out <transcript.json>
Both joins put every clip on one canvas, sized as the clips are shown (a phone
clip stored sideways counts as upright). A clip of another shape is cut to the
canvas's shape with reframe's own code, around its faces when faces has run
on it and from the middle otherwise, and a line says which; --fit letterbox
keeps it whole and pads it instead. render --footage refuses footage of
another shape than the stage and names the reframe command that cuts it.
The retimed transcript goes where a transcription of the cut would land,
runs/<key>/transcript.json under the workspace, so the transcription routes
refuse to overwrite it and the cut is never transcribed. retime-transcript
writes the same transcript from an EDL alone, to any path.
faces / reframe / install-mediapipe
Find the faces in each shot of a clip and write their boxes, for laying text
out around them and for reframe (and the joins, which cut a clip of another
shape as it does). They go to runs/<key>/faces-<hash>/, a folder of the clip's
own named by its real path, so two clips of one name never share one; faces
prints the path, and every reader looks there (and in a runs/<key>/faces.json
an older faces wrote, when it names that same clip). One decode finds the cuts, those between two
shots of one set that change the picture little included, and samples four
frames a second (--every 0.25), each labelled by its presentation time, so
variable-frame-rate footage is sampled in time; a shot sampled fewer than four
times also gets its first frame and the first at or past the middle of each
quarter of it;
MediaPipe's full-range BlazeFace model finds the faces on CPU. The same
sampled frames, small and grey, show the graphics burned into the footage (a
lower third, a score bar, old subtitles, a logo), without OCR or a model: sharp
edges, mostly square to the frame, that hold their value while most of the frame
changes around them, at a cut or as the camera moves (a step in the camera's
exposure is no change), or that come up over, or give way to, other steady
content in a still shot, each with the seconds it is up. faces.json
holds the boxes per sampled frame of each shot and the graphics; faces.js holds them too, with
openeditFaces.place(el, from, to), which moves a laid-out element the least
distance that clears every face on screen, and every graphic up, between those seconds of the clip.
For elements shown one after another in one spot, such as caption cues,
openeditFaces.placer(spans, measure) gives them one place through each shot,
moving them only at a cut, or where no single place stays clear for every element
up in a shot, however briefly. measure(i) gives the box {x, y, w, h} element
i is laid out in, in viewport pixels, and is called by prepare(i), which the
page runs while none of the elements is on screen, before showing one; a page
that never calls prepare still works, with place() measuring each time.
place(el, i, sec) takes an element's index and one second of the clip rather
than a span. part(i, sec) changes at each cut, and the page places an element
up across it again there. captions.js places its cues with it.
install-mediapipe puts MediaPipe, every package it runs on at the versions
verified together (OpenCV's headless build) and the model into an environment
of their own under the app-data directory, never the system Python: with uv
when present, which fetches a Python it can use, else with python3 -m venv
when that python3 is 3.12 or newer. MediaPipe 1.1.1 has no Intel Mac build; on
an Apple Silicon Mac run an arm64 Node, and on Windows on ARM install uv and set
OPEN_EDIT_MEDIAPIPE_PYTHON=cpython-3.12-windows-x86_64-none:
npx @veedstudio/openedit-cli install-mediapipe [<version>]
npx @veedstudio/openedit-cli faces video.mp4 [--out <dir>] [--every 0.25] [--threshold 10] [--confidence 0.5] [--margin 0.25]
reframe cuts a clip to another shape (16:9 to 9:16, say). With those boxes,
and without guessing who is who, each shot, which can be split again where a
face seen alone for four samples jumps further than its own width and is seen
alone there for four more (a dissolve or a cut the cut detection missed), is
followed when it shows one face only (never two at once, each found
overlapping the one before), the crop holding still while the face stays inside
it and easing to a new place only when it must, and held at one place when it
shows more, where the faces wholly inside add up to the most height; at a cut
or a split it jumps. A face less than half the height of the tallest found with
it is someone in the background and does not count. Without them it cuts the
clip from the middle. Where a face does not lie wholly inside the crop, larger
than it or beside another face a held shot keeps, it prints those seconds. The
crop is
scaled (lanczos) to 1080 px on its short side, the platforms' canvas for their
shapes: 1080x1920 for 9:16, 1080x1080 for 1:1, 1080x1350 for 4:5, 1920x1080 for
16:9. --size names another size of the shape, such as 2160x3840 for 4K, and
--size native keeps the crop in the source's own pixels. It always re-encodes:
upright, in square pixels, H.264, a clip already in that shape included and
scaled the same way. With the clip's boxes it also writes the output's own, in
the output's folder where faces would write them, each box and burned-in
graphic moved and scaled with the crop and cut where the crop cuts it, so
faces need not run on the clip it made.
When the crop cuts in part a graphic faces found (old subtitles, a lower
third), a last line says so, with the rows it is in and what erasing them
costs. --erase erases those rows, across the whole width and for the whole
clip, since the search misses a line of text that is up for under a second and
a half. The first run cuts the clip into parts of at most 6 s for the eraser,
a video editor on fal told to remove the subtitles and keep the rest, beside
--out, and prints the price of every part as one total: an estimate, each
run's seconds, in whole seconds as fal bills them, at fal's price per second,
with a quote id. Nothing is spent, and no run exists, until --approve passes
that price and quote back once the user has said yes to it. That run writes
each part's input and prints the fal runs to make: one batch for openedit fal on a fal key, or, with no key set, one run a part for the environment's
own fal command, one whose help says it runs a model with its input. A quote
approves once, so one yes prints each run once: running a part again may buy
it twice, so a part that is not back is priced again, under a quote of its
own, and its run printed only for an --approve of that. reframe never runs
the eraser itself. Once the parts are back, the same command lays only the
erased rows over the clip, keeping every other pixel, every frame and the
sound, writes that clip with its boxes and its graphics searched for again,
and reframes it. Text the crop still cuts is said in a last line; the parts it
is in that last 2 s or more are cut in two for the eraser again, and sending
them is a second spend, with a price and an --approve of its own. A shorter
part is left as it is, and the line says so.
npx @veedstudio/openedit-cli reframe video.mp4 --aspect 9:16 --out tall.mp4 [--faces <faces.json>] [--size WxH|native] [--erase [--approve <usd:quote>]]
npx @veedstudio/openedit-cli reframe video.mp4 --out runs/<key>/tall.mp4 # the shape of the platform in the piece's brief
zoom
Find the clicks in a screen recording and write zoom.js, a camera that holds
each click zoomed until the text there reaches the minimum text size of the
stage it is shown on. One decode compares small grey frames a tenth of a second
apart, with no model: the drawn cursor is followed where it leaves a patch of its
own outline and shows the same outline elsewhere, and a click is a rest of the
cursor that the screen answers near it, or a page changing wholesale soon after
it arrives; with no cursor drawn, the first change after half a second of none.
The text nearest each click is measured on its frame, from the line's top to its
baseline. --clicks names points of your own instead. The page loads zoom.js
and calls openeditZoom.apply(el, t) each frame on the element showing the clip,
whose parent the camera fills; zoom.json beside it also lists the stretches
in which nothing changes.
npx @veedstudio/openedit-cli zoom recording.mp4 [--clicks <clicks.json>] [--aspect 16:9,9:16] [--out <dir>] [--every 0.1] [--json]
stills
stills fetches licensed pictures from Wikimedia Commons, recording each
file's terms beside it (search / show / save):
npx @veedstudio/openedit-cli stills search "berlin skyline" --limit 10
sfx
sfx takes recorded sound effects from a stock library, where the environment
serves one at OPEN_EDIT_LIBRARY_URL. A search takes a plain description and
lists each sound's id, length and title. With --run, it leaves out the sounds
that run already saved, so the next search finds a different one. save
downloads a sound into the run's assets/ and records it in the asset manifest:
npx @veedstudio/openedit-cli sfx search "a door slamming in an empty hallway" --run runs/<key> --max-seconds 3
npx @veedstudio/openedit-cli sfx save <id> --run runs/<key> --as door-slam
init
init is the workspace setup: it checks the machine dependencies (Node,
ffmpeg) and npm-ifies the workspace (a minimal private package.json when none
exists, this CLI exact-pinned as a devDependency and installed, git init when
git is available, runs/ gitignored, the skill refreshed from packaged content
into .claude/skills/ for Claude Code and .agents/skills/ for Codex and Gemini
CLI). In a clone whose node_modules lacks the pinned CLI, init asks for
approval to run the project's install (with lifecycle scripts off), since until
then npx would run the registry's latest rather than the pinned version.
It adds nothing to agent settings, and removes the SessionStart hooks earlier
versions added. Bare init applies only safe,
workspace-local setup; --dry reports without writing; --auto-approve also
applies the machine-global installs init can run on the system (Node and FFmpeg
only through Homebrew on macOS; on Windows bare init already fetches FFmpeg into
the app-data dir, no admin rights needed; the rest it prints for the user to
run), and is only for after a person has approved every reported
action. Exit 10 means something is awaiting
that approval; on success the workspace root is printed on stdout.
The CLI never updates itself. About once a day per machine a command asks the
registry whether a newer release is out; from the next command on, until the
update, each prints update available with both versions and the exact
init --update <version> that installs that version and refreshes the skill.
NO_UPDATE_NOTIFIER=1 turns the lookup off.
npx @veedstudio/openedit-cli init --dry --workspace <dir>
npx @veedstudio/openedit-cli init --workspace <dir>
npx @veedstudio/openedit-cli init --update <version> --workspace <dir>
Configuration
| Environment variable | Effect |
|---|---|
VEED_ORIGIN | Overrides the default https://www.veed.io origin. |
OPENEDIT_STATE_DIR | Overrides where login state, the update-check cache and init's lease are stored. |
OPEN_EDIT_ROOT | Where runs/<key>/ outputs and .open-edit-prefs.json are written (default: the nearest project above the working directory, else the app-data directory below). |
OPENEDIT_PACKAGE_SOURCE | Overrides what init pins into a scaffolded workspace (a packed tarball path in tests and CI). |
OPENEDIT_REGISTRY | Overrides the registry the update notice reads the latest version from (default: https://registry.npmjs.org); installs use npm's own registry config. |
NO_UPDATE_NOTIFIER | Any non-empty value stops the once-a-day lookup and the update available notice. CI runs skip both already. |
OPENEDIT_FFMPEG / OPENEDIT_FFPROBE | ffmpeg/ffprobe binaries (default: the copy install-ffmpeg put in the app-data directory, else PATH; ffprobe defaults beside a configured ffmpeg; an empty value counts as unset). |
OPENEDIT_CHROME | The Chrome or Chromium render, behind and veed-project drive when render --chrome is not given (default: the pinned Chrome Headless Shell; an empty value counts as unset). |
WHISPERX_BIN / WHISPERX_MODEL | WhisperX binary and fallback model tier (defaults: whisperx on PATH, small.en). |
OPEN_EDIT_WHISPERX_DEVICE / OPEN_EDIT_WHISPERX_COMPUTE | WhisperX device/compute (defaults: cpu/int8). |
OPEN_EDIT_LIBRARY_URL | The stock sound library sfx searches and saves from (unset: sfx says there is none, and effects come from fal). |
OPEN_EDIT_MEDIAPIPE_PYTHON | The Python version install-mediapipe asks uv for (default: 3.12; the pinned packages need 3.12 or newer). |
Login state lives in the platform's per-user app-data directory:
~/Library/Application Support/veed-openedit on macOS, %APPDATA%\veed-openedit
on Windows, and $XDG_CONFIG_HOME/veed-openedit (default ~/.config/veed-openedit)
on Linux.
License
Apache-2.0. See the bundled LICENSE and NOTICE files.
Open Edit alternatives
View more →Similar ai agents in Design & Media, ranked by GitHub stars. Compare licence, recent activity and scan status before you choose.
| Name | Stars | Licence | Last updated | Scan |
|---|---|---|---|---|
| Open Edit (this page) veedstudio | 1.8k | Apache-2.0 | Nov 1, 2026 | Scanned, no flagged issues |
| Taste Skill Leonxlnx | 107k | MIT | Oct 31, 2026 | Scanned, no flagged issues |
| Archify tt-a1i | 92k | Apache-2.0 | Oct 31, 2026 | Scanned, no flagged issues |
| Hyperframes heygen-com | 77k | Apache-2.0 | Nov 1, 2026 | Scanned, no flagged issues |
| OpenMontage calesthio | 74k | AGPL-3.0 | Oct 28, 2026 | Scanned, no flagged issues |
| CopilotKit CopilotKit | 38k | MIT | Oct 31, 2026 | Scanned, no flagged issues |