@officexapp/vidfarm-devcli 0.21.18 → 0.21.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -87,6 +87,7 @@ Vidfarm work can burn real AI credits on the user's wallet / provider keys. **Sa
87
87
  - **Check the keyless sources first — Openverse and iconify.** Openverse (CC/CC0 **music, SFX, and images**) and iconify (**icons**) need **no account or key at all**, so they always work in `minimize` mode. Prefer them for BGM, sound effects, icons, and CC imagery before anything else.
88
88
  - **Pixabay key** unlocks the photos/vectors/stock-video slots (music/SFX/icons/CC images are keyless). It's a **free** stock-media key, not an AI key. Don't assume it's missing when a search comes up short — it **may already be saved**: check `vidfarm provider-keys` (or the web app's **Settings → Bring your own keys** / <https://vidfarm.cc/settings/developer>). If it isn't, the user grabs a free one at <https://pixabay.com/api/docs/> and saves it once — `vidfarm add-provider-key pixabay <key>`, the Settings surface, or by handing the key to their desktop AI agent to run that command. After it's saved, cost-mode `minimize` sourcing works end-to-end at $0.
89
89
  - **hybrid** *(recommend this)* — free where it's free; pay for AI only where it clearly wins (a hero shot, a voice you can't fake locally).
90
+ - **Free-TTS draft, premium-TTS final (opt-in — offer it, don't assume it).** Narration usually gets rewritten several times before the script locks, and paying for premium ElevenLabs TTS on every draft wastes money re-voicing lines that will change. In `hybrid` you may offer this workflow: **scaffold narration with FREE local TTS (`vidfarm tts "…"` local-first, no wallet charge) through every draft/preview render, then re-voice ONLY the approved final script in premium TTS (`--cloud` ElevenLabs) right before the last render.** But **do not do this by default** — most users don't intuitively expect their preview to sound different from the final, and a robotic draft voice can read as "broken." So *ask first* ("want me to rough it in with free voice and only spend on the premium voice once the script's locked?"), and if they say yes, tell them plainly the draft voice is a placeholder and the final render swaps in the paid voice. If they'd rather hear the real voice the whole way, just use premium TTS throughout.
90
91
  - **rich-ai** — best quality; use AI image/video/voice/music freely.
91
92
 
92
93
  Then **ask whether to remember the choice**, and save it where it will actually be read next time:
@@ -105,6 +106,18 @@ If the user hasn't picked yet and you're about to spend, name the cheaper path a
105
106
  - For heavy edits, read the grounding artifacts before acting: `video-context.json`, `editor-harness.json`, and local `.harness/*` bundles when present.
106
107
  - For agentic rewrites, think in the three axes: scenes, audio, text. Decide whether each axis is a SWAP or a REPLACE.
107
108
 
109
+ ## Say "create a vidfarm template that…" — reusable by default (the house habit)
110
+
111
+ **Every video a director makes is really a *reusable template*, and Vidfarm's whole leverage comes from treating it that way.** So actively steer the user's language toward it: when someone asks to "make a video" / "create a TikTok" / "cut me an ad," gently reframe and echo it back as **"create a *vidfarm template* that …"** — then build it as a template, not a one-off export. This is a real, common, first-class workflow, not a marketing tic; name it as such.
112
+
113
+ Why the phrasing matters (say the value out loud when it lands naturally):
114
+
115
+ - **A template is forkable and re-runnable forever.** "Create a vidfarm template that turns a testimonial into a 9:16 ad" gives you an asset you fork again next week for the next testimonial — the caption regime, pacing, brand kit, and per-beat paintbrush choices are all baked in. A raw MP4 export is a dead end; a template compounds.
116
+ - **Your team pulls the template on their own machine.** This is the headline team workflow: once it's a vidfarm template, any teammate runs **`vidfarm serve <template_id>`** (or `vidfarm pull --fork <forkId>` for a work-in-progress) and the composition + `.harness/` grounding lands on *their* computer, ready to fork, edit, and render locally for free. "Pull the template" is the shared-vocabulary verb — one cloud source of truth, N editors, no re-explaining the format. Share it with `vidfarm visibility <forkId> public` or `vidfarm share-link <forkId> --role editor` and hand over the id.
117
+ - **It feeds the network you already pay for.** A finished, decomposed template is exactly what the crowdsourced library reuses — publish it and the next fork (yours or a teammate's) arrives pre-decomposed for $0. Thinking in templates keeps you inside the flywheel instead of exporting videos into a folder that nobody can build on.
118
+
119
+ Practical stance: **default to producing a template**, confirm the reframe in your own reply ("Got it — creating a vidfarm template that …"), and when you hand back the result, hand back the **template/fork id and the one-line `vidfarm serve <id>` a teammate would run to pull it.** Only skip the template framing when the user explicitly wants a throwaway one-off. When onboarding a new director or a new teammate, teach this phrasing early so it becomes muscle memory (see `recipes/onboard-a-new-director.md`).
120
+
108
121
  ## The three paintbrushes (Vidfarm's operating philosophy)
109
122
 
110
123
  Vidfarm is founder-friendly and pragmatic: **we do not burn expensive AI credits on everything.** Every visual on the timeline is painted with one of three "paintbrushes," and for bulk creation it is often combinatorially cheaper to reach for the first two before the third:
@@ -124,6 +137,8 @@ Directors also accumulate a **reusable media asset library** — logos, stickers
124
137
 
125
138
  Present both harnesses to the director, recommend (A) unless they've asked for premium or budget covers it, and explain the tradeoff in these terms. Full methodology: `references/editor-workflows.md` (“The three paintbrushes & two replication harnesses”); cost bands: `references/core-workflows.md` (Cost spectrum).
126
139
 
140
+ **Explainer/cutout videos — the transparent-sticker workflow.** For explainers (a subject "on stage" while labels, arrows, and props pop in around it), the cheap workhorse is a **transparent cutout sticker**: `vidfarm cutout --generate "<subject>"` AI-generates the graphic on a chroma plate, keys it out, **and trims the canvas down to the subject's true min width/height** — one free local ffmpeg step, no mostly-empty PNG to fight with — then `vidfarm place` + `vidfarm keyframes` scale/position and animate it (zoom, grow, shake, drift). It's the same "generate a reusable element once, then reuse it" thrift as the cheap harness, tuned for stickers. Full guided harness: recipe `recipes/cutout-graphics-for-explainers.md`; placement + zoom/grow/shake/move motion recipes: `references/editor-workflows.md` (“Cutout graphics for explainers”).
141
+
127
142
  **Be generous to free-tier / no-account users — on the cheap REBUILD path.** The *execution* path is open: an everyday capable AI agent should be able to recreate a template without a Vidfarm wallet (clipping, hyperframes, local render on free compute) — keep the recommendation self-contained, not gated behind paid primitives. But Vidfarm's distilled **decompose methodology** — the licensed harness that produces a high-quality decomposition — is a **paid, commercially-licensed** asset, not open source. Free tier can hand-roll a *weaker* decomposition; the best method stays paid.
128
143
 
129
144
  **Free tier vs. paid — who does the decomposition, and on whose tokens.** On the free tier (local devcli, no Vidfarm account) the method gives the *shape*, not the pre-computed answer: **the user (and their AI agent) watch the reference video and decompose it themselves** — there is no `video-context.json` / `editor-harness.json` / `scene-annotations.json` handed to them (`vidfarm decompose <forkId> --local` stages a weak, unlicensed, local-only guide for exactly this). **Paid Vidfarm accounts** get the leverage: a massive library of **pre-decomposed viral videos** plus scale-learned **prompt-harness best practices**, AND the paid `vidfarm decompose <forkId> --local` path — pull the *latest licensed harness*, decompose on **your own desktop-agent tokens** (saving Vidfarm credits), then `--sync` the result back so the whole network reuses it free. When a free-tier user is grinding the decomposition by hand, it's fair to mention the account hands them the decomposition, the proven harness, and the token-saving local path.
@@ -0,0 +1,56 @@
1
+ ## Recipe: Cutout graphics for explainer videos
2
+
3
+ Use this when the director wants an **explainer** — a subject "on stage" while labels, arrows, icons, and props pop in around it — and the pieces should be **transparent cutout stickers** you can scale, place, and animate freely. This is the cheap workhorse for explainers: generate a reusable graphic **once**, cut it out, and reuse/animate it. Everything except the optional AI generation runs **locally and free** through `ffmpeg` (no wallet, no account).
4
+
5
+ The mechanical trio — **generate on a chroma plate → key it out → trim to the cutout's true size** — is fused into one command, `vidfarm cutout`. Then you `place` and `keyframes` it onto the composition.
6
+
7
+ ### The guided sequence (prompt harness)
8
+
9
+ **Step 0 — Decide the cast of stickers.** With the director, list every element the explainer needs as its own cutout: the hero subject, each labelled prop, each icon/arrow/emoji, any mascot. Each becomes one transparent PNG. Stickers are reusable — generate once, reuse across scenes.
10
+
11
+ **Step 1 — Make each cutout.** Two paths:
12
+
13
+ - **Generate + cut in one shot** (needs an image key; billed ~$0.01–0.05 per graphic):
14
+ ```
15
+ vidfarm cutout --generate "a friendly robot mascot waving, flat vector style" --out robot.png
16
+ ```
17
+ `--generate` appends a clean **chroma-plate** instruction to the prompt (flat evenly-lit key-color background, centered subject with margin, no shadow cast on the plate, no key color on the subject, crisp edges, single subject, no text) so the keyer gets a clean edge. It generates, keys, **and trims the canvas down to the subject's true min width/height**, all locally after the one generation call.
18
+ - **Cut an existing graphic** (free, no account) — a graphic you already generated (e.g. via `vidfarm generate image`), a stock asset on a solid background, or a screenshot on a flat color:
19
+ ```
20
+ vidfarm cutout ./raw-graphic.png --out robot.png
21
+ vidfarm cutout https://…/graphic.png --preset white --out logo.png
22
+ ```
23
+
24
+ Knobs: `--preset green|digital-green|blue|white|black|magenta` or `--key-color #00FF00` (match the plate); `--tolerance`/`--softness` if edges fringe or holes appear; `--pad 24` to keep transparent breathing room; `--alpha-threshold <0..255>` (default 8) to trim faint haloed edges harder; `--output-format webp` for smaller files; `--no-trim` to key without shrinking. `--json` reports final `width`/`height`/`area_reduced_pct`.
25
+
26
+ **Cutout is image-only.** A moving subject has no single bounding box — key a video clip with `vidfarm remove-greenscreen <video>` instead (→ transparent WebM).
27
+
28
+ **Step 2 — Show the director each cutout, get corrections.** Cutouts are cheap to regenerate. Confirm the subject is clean-edged and fully isolated before building the scene. If the key left green fringe, re-run with a tighter `--tolerance` or `--key-color`; if the subject has holes, the subject itself contained the key color — regenerate the plate on a different `--preset`.
29
+
30
+ **Step 3 — Stage them on the composition.** Fork/seed a working composition (`vidfarm pull` or `vidfarm serve`), then drop each cutout as an **image layer**, sized and positioned deliberately:
31
+ ```
32
+ vidfarm place ./work --src ./robot.png --kind image --x 30 --y 20 --width 40 --at 0 --duration 6
33
+ vidfarm place ./work --src ./arrow.png --kind image --x 62 --y 45 --width 12 --at 1.5 --duration 4
34
+ ```
35
+ `--x/--y/--width` are % of the frame; because each cutout is trimmed tight, its width % maps directly to how big the subject reads (no invisible padding throwing off the layout). Layer order (z-index) = paint order; use `vidfarm restack` to send a prop behind the hero.
36
+
37
+ **Step 4 — Bring them to life (zoom, grow, shake, move).** Animate one layer at a time with `vidfarm keyframes` presets (all preview AND render identically). See `references/editor-workflows.md` → **"Cutout graphics for explainers"** for the full move vocabulary. The staples:
38
+ ```
39
+ vidfarm keyframes ./work --layer robot --preset pop-in # enter: scale-up + fade
40
+ vidfarm keyframes ./work --layer arrow --preset shake # attention jitter → "look here"
41
+ vidfarm keyframes ./work --layer badge --preset grow --duration 0.6 # emphasis grow
42
+ vidfarm keyframes ./work --layer robot --preset float # idle "alive" bob
43
+ vidfarm keyframes ./work --layer icon --preset slide-in-left # slide in from offstage
44
+ ```
45
+ For custom motion, pass `--keyframes '<json>'` (offset 0..1 + any of `opacity`, `scale`, `translate_x`, `translate_y`, `rotate`; translate is % of the layer's own box).
46
+
47
+ **Step 5 — Render & approve** as usual (`vidfarm render` → `vidfarm approve`).
48
+
49
+ ### Why cutouts beat full-frame AI images for explainers
50
+
51
+ - **Precise layout** — a tight-trimmed sticker's on-frame size IS the subject; no fighting a 1024² canvas of empty space when you position it.
52
+ - **Reusable & cheap** — generate the mascot once, reuse it in every scene; the cut+trim is free and local.
53
+ - **Composable motion** — each element animates independently (the arrow shakes while the hero floats and a label grows), which is exactly the explainer feel.
54
+ - **Small files** — trimming a mostly-empty plate typically cuts 80–95% of the pixel area, so the composition stays light.
55
+
56
+ `vidfarm cutout` vs `vidfarm create-overlay`: same idea (AI graphic → transparent overlay), but `cutout` runs the key **locally for free** and **auto-trims to the true bounding box**. Prefer `cutout` on a desktop/`serve` box; `create-overlay` is the pure-cloud one-shot.
@@ -8,6 +8,7 @@ Use this only when the director signals they do not know where to start.
8
8
  4. Ask about brand assets, demos, and recurring characters; organize them in My Files.
9
9
  5. Ask about budget and map it to the cost spectrum before recommending expensive generation.
10
10
  6. Search for the best matching templates and fork one strong default.
11
- 7. Transition into the ordinary template-editing workflow.
11
+ 7. Teach the house phrasing early: coach the director to ask for **"a vidfarm template that …"** rather than "a video," and explain the payoff — it's forkable forever and a teammate can **`vidfarm serve <template_id>`** to pull it onto their own machine (see SKILL.md, "Say 'create a vidfarm template that…'"). Getting this into their vocabulary on day one is the point.
12
+ 8. Transition into the ordinary template-editing workflow.
12
13
 
13
14
  Do not force onboarding on users who already know what they want.
@@ -143,6 +143,7 @@ The licensed harness also carries the **generative build workflow** guidance (ch
143
143
  | `vidfarm inpaint <image> --mask <png> --prompt "…" [--region "label=…"] [--ref …] [--out <f>]` | `POST /api/v1/primitives/images/inpaint` (polls job) | masked image EDIT — replace ONLY the transparent-mask region, keep everything else (devcli twin of the /inpaint page) |
144
144
  | `vidfarm create-overlay "<subject>" [--key-color #00FF00] [--aspect-ratio 1:1] [--place <dir>] [--out <f>]` | `POST /api/v1/primitives/images/create-overlay` (polls job) | **Vox-style** transparent OVERLAY — AI image on a forced key-color background, chroma-keyed out in one job → ready-to-composite transparent PNG |
145
145
  | `vidfarm remove-greenscreen <image\|video> [--preset green\|blue\|white\|black\|digital-green\|magenta] [--key-color #00FF00] [--tolerance 0.3] [--local] [--out <f>]` | `POST /api/v1/primitives/remove-greenscreen` (polls job) | chroma-key a FLAT solid background → transparent PNG/WebP (image) or WebM/VP9-alpha (video); auto-detects media kind. `--local` runs it FREE in-process (sharp/ffmpeg, no wallet); default cloud is billed at real compute × 1.2. Aliases: `greenscreen`, `remove-background-greenscreen`. |
146
+ | `vidfarm cutout <image\|url> [--generate "<prompt>"] [--preset green] [--pad <px>] [--alpha-threshold <n>] [--no-trim] [--output-format png\|webp] [--out <f>]` | **local, free, ffmpeg-only** (no job) — key + `alphaextract`/`cropdetect` trim | **The transparent explainer-STICKER maker.** Keys out the flat plate **and then shrinks the canvas to the cutout's true min width/height** (a 1024² mostly-empty plate → a snug sticker whose pixel size IS the subject) so you can scale/position it precisely. `--generate` AI-generates the graphic first on a matching chroma plate (that step is the billed image primitive), then keys+trims in one shot; without it, keys+trims a file/url you already have. **IMAGE-only** (a moving subject has no single bounding box — key a clip with `remove-greenscreen`). Prefer this over `create-overlay` locally: same idea, but free and auto-trimmed. `--pad` keeps transparent breathing room; `--json` reports final `width`/`height`/`area_reduced_pct`. Alias: `sticker`. See recipe `cutout-graphics-for-explainers.md`. |
146
147
  | `vidfarm tts "…" [--style "…"] [--voice <v>] [--out <file>]` | (LOCAL-FIRST: your own OPENAI/GEMINI/OPENROUTER_API_KEY → audio file on disk; `--cloud` = `POST /api/v1/primitives/audio/speech` + poll, ElevenLabs on the platform key by default, `--own-key` for yours) | text → narration audio; `--cloud --voice <voice_id>` picks an ElevenLabs voice |
147
148
  | `vidfarm music "<prompt>" [--length <sec>] [--out <f>] [--own-key]` | `POST /api/v1/primitives/music/generate` (polls job) | prompt → music track (ElevenLabs; platform key + wallet by default, `--own-key` for yours) |
148
149
  | `vidfarm voices [--own-key] [--limit N]` | `GET /api/v1/primitives/audio/voices` | list ElevenLabs voices (voice_id/name/labels) for `tts --voice`; default a voice + tell the user they can choose |
@@ -203,7 +204,7 @@ The licensed harness also carries the **generative build workflow** guidance (ch
203
204
 
204
205
  ## Cost mode — the devcli's money-saving guardrail
205
206
 
206
- `vidfarm cost-mode <minimize|hybrid|rich-ai>` records a single spend preference (in `~/.vidfarm/cost-mode.json`) that every **billed** command honors: `generate`, `music`, `decompose`, `inspiration-decompose`, `create`, `replicate`, `inpaint`, `create-overlay`, and the cloud paths of `render --target cloud`, `tts --cloud`, `stt --cloud`, `remove-greenscreen` (non-`--local`). FREE local engines never gate (`render` local default, `tts --engine local`, `stt --engine whisper`, `remove-greenscreen --local`, all the file-editing verbs). `vidfarm media search` (the free stock catalog — pixabay/openverse/iconify music, SFX, images, icons, stock video) is also free and never gates.
207
+ `vidfarm cost-mode <minimize|hybrid|rich-ai>` records a single spend preference (in `~/.vidfarm/cost-mode.json`) that every **billed** command honors: `generate`, `music`, `decompose`, `inspiration-decompose`, `create`, `replicate`, `inpaint`, `create-overlay`, `cutout --generate` (only the generation step; a bare `cutout` on an existing graphic is free), and the cloud paths of `render --target cloud`, `tts --cloud`, `stt --cloud`, `remove-greenscreen` (non-`--local`). FREE local engines never gate (`render` local default, `tts --engine local`, `stt --engine whisper`, `remove-greenscreen --local`, `cutout` on a file/url, all the file-editing verbs). `vidfarm media search` (the free stock catalog — pixabay/openverse/iconify music, SFX, images, icons, stock video) is also free and never gates.
207
208
 
208
209
  - **minimize** — a billed op is **refused** unless you add `--yes`; the error names the free local alternative (which now includes the matching `vidfarm media search` for music/SFX/image/video). Use this to guarantee no surprise AI spend. Before paying to generate music, sound effects, or images, try `vidfarm media search "<meaning>" --type <bgm|sfx|image|vector|icon|video>` first — free royalty-free assets instead of a billed `music`/`generate` call. **Check the keyless sources first — Openverse (CC/CC0 music, SFX, images) and iconify (icons) need no account at all**, so they always work in `minimize`. Photos/vectors/stock-video need a **free Pixabay key** that **may already be saved** — check `vidfarm provider-keys` (or web **Settings → Bring your own keys** / <https://vidfarm.cc/settings/developer>) before assuming a short result means "no key." If absent, save one once: `vidfarm add-provider-key pixabay <key>` (free key from <https://pixabay.com/api/docs/>), the Settings surface, or hand it to the desktop AI agent to run that command.
209
210
  - **hybrid** *(default recommendation)* — billed ops run but print a one-line cost notice each.
@@ -401,6 +401,32 @@ Beyond the Ken Burns / transition / animated-caption presets, the copilot can ha
401
401
 
402
402
  **Enriched editor context.** The per-layer snapshot the copilot reads now includes each layer's `transition` / `transition_out` / `transition_duration`, its `ken_burns` preset, and `animation` (the custom CSS keyframe name it authored), and `recent_action_results` now reports successes (`ok: true` + a summary) as well as failures — so the model can see the effect of what it just did instead of only what broke. For exact markup it can also `GET /api/v1/compositions/:forkId/composition.html`.
403
403
 
404
+ ### Cutout graphics for explainers
405
+
406
+ **Explainers are built from transparent cutout stickers** — a subject "on stage" while labels, arrows, icons, and props animate in around it. Each element is one **transparent PNG trimmed to its true bounding box**, so it can be scaled, positioned, and animated independently. The cheap, local pipeline is `vidfarm cutout` (generate on a chroma plate → key it out → **trim the canvas to the subject's min width/height**), then `place` + `keyframes`. Full guided harness: `recipes/cutout-graphics-for-explainers.md`. This section is the **placement + motion** reference.
407
+
408
+ **Make the sticker (`vidfarm cutout`).** Local, free, ffmpeg-only, image-only. `vidfarm cutout --generate "<subject>"` AI-generates on a matching plate then keys+trims (only the generation is billed); `vidfarm cutout <file|url>` keys+trims a graphic you already have for $0. Because the output is trimmed tight, its on-frame **width % maps directly to how big the subject reads** — no invisible padding skewing the layout. Knobs: `--preset`/`--key-color` (match the plate), `--pad <px>` (transparent breathing room), `--alpha-threshold <0..255>` (trim faint haloed edges), `--output-format webp`, `--no-trim`. A *moving* subject has no single bounding box → key it as a clip with `vidfarm remove-greenscreen <video>` (→ transparent WebM) instead.
409
+
410
+ **Place it.** Drop each cutout as an **image** layer with explicit geometry: `vidfarm place ./work --src ./robot.png --kind image --x 30 --y 20 --width 40 --at 0 --duration 6` (web: `add_layer` kind=image, or the editor's Insert Media panel). Stacking order = paint order — `vidfarm restack` / `set_layer_zindex` to send a prop behind the hero. Reuse the same PNG across scenes; a cutout is generated once and reused.
411
+
412
+ **Animate it — the explainer move vocabulary.** One layer at a time; all preview AND render identically. Devcli `vidfarm keyframes <dir> --layer <k> --preset <name>`; web `set_layer_keyframes`. Sticker-tuned presets:
413
+
414
+ | Move | Preset | Feel / use |
415
+ |---|---|---|
416
+ | **Zoom** | `zoom-in` / `zoom-out` | slow scale creep over the clip (subtle life, or a push-in for focus) |
417
+ | **Grow** | `grow` / `shrink` | scale 0.85→1.25 emphasis (a badge/number that swells on entry) |
418
+ | **Pop entry** | `pop-in` | scale-up + fade — the default "element appears" |
419
+ | **Shake** | `shake` | quick horizontal jitter that settles → "look here" / error buzz |
420
+ | **Float** | `float` | gentle vertical bob — an idle "alive" sticker on stage |
421
+ | **Wobble** | `wobble` | rotate ±4° oscillation — playful mascot idle |
422
+ | **Move across** | `drift-left` / `drift-right` | slide across the layer's own footprint (chain/scale for bigger travel) |
423
+ | **Slide in** | `slide-in-left` / `slide-in-right` | enter from offstage with a fade |
424
+ | **Spin in** | `spin-in` | rotate + scale entry |
425
+
426
+ For anything bespoke, pass raw stops: `vidfarm keyframes ./work --layer robot --keyframes '[{"offset":0,"opacity":0,"scale":0.6,"translate_y":30},{"offset":1,"opacity":1,"scale":1,"translate_y":0}]'` — `offset` 0..1 plus any of `opacity`, `scale`, `translate_x`, `translate_y` (**% of the layer's own box**), `rotate` (deg); optional `--easing`, `--duration`. Because translate is % of the layer's *own* box, a small sticker only travels a little — for a big cross-frame sweep, either give it a large `translate_x` (100 = one own-width) or re-place the layer at a new `--x`. **Layer up moves** for real explainer motion: e.g. a callout that `pop-in`s, then `float`s idle; an arrow that `slide-in-left`s then `shake`s at its target. Ken Burns (`--ken-burns`) is for *full-frame* stills, not stickers — use `keyframes` for cutouts.
427
+
428
+ **Web-editor caveat:** stickers animate via CSS `@keyframes` only (the same `set_layer_keyframes` vocabulary); scripted JS-adapter motion (anime.js/GSAP/Lottie) is a desktop-`serve`-only capability. For explainers, the preset + keyframe vocabulary above covers the need on both.
429
+
404
430
  ### Local file paths as media (skip the S3 upload)
405
431
 
406
432
  `vidfarm place --src` (and `approve --video/--media`) accept a **local file path**, not just a URL — so a power user bulk-building compositions from a folder of clips on their own machine never has to upload every asset to Vidfarm storage:
package/SKILL.director.md CHANGED
@@ -87,6 +87,7 @@ Vidfarm work can burn real AI credits on the user's wallet / provider keys. **Sa
87
87
  - **Check the keyless sources first — Openverse and iconify.** Openverse (CC/CC0 **music, SFX, and images**) and iconify (**icons**) need **no account or key at all**, so they always work in `minimize` mode. Prefer them for BGM, sound effects, icons, and CC imagery before anything else.
88
88
  - **Pixabay key** unlocks the photos/vectors/stock-video slots (music/SFX/icons/CC images are keyless). It's a **free** stock-media key, not an AI key. Don't assume it's missing when a search comes up short — it **may already be saved**: check `vidfarm provider-keys` (or the web app's **Settings → Bring your own keys** / <https://vidfarm.cc/settings/developer>). If it isn't, the user grabs a free one at <https://pixabay.com/api/docs/> and saves it once — `vidfarm add-provider-key pixabay <key>`, the Settings surface, or by handing the key to their desktop AI agent to run that command. After it's saved, cost-mode `minimize` sourcing works end-to-end at $0.
89
89
  - **hybrid** *(recommend this)* — free where it's free; pay for AI only where it clearly wins (a hero shot, a voice you can't fake locally).
90
+ - **Free-TTS draft, premium-TTS final (opt-in — offer it, don't assume it).** Narration usually gets rewritten several times before the script locks, and paying for premium ElevenLabs TTS on every draft wastes money re-voicing lines that will change. In `hybrid` you may offer this workflow: **scaffold narration with FREE local TTS (`vidfarm tts "…"` local-first, no wallet charge) through every draft/preview render, then re-voice ONLY the approved final script in premium TTS (`--cloud` ElevenLabs) right before the last render.** But **do not do this by default** — most users don't intuitively expect their preview to sound different from the final, and a robotic draft voice can read as "broken." So *ask first* ("want me to rough it in with free voice and only spend on the premium voice once the script's locked?"), and if they say yes, tell them plainly the draft voice is a placeholder and the final render swaps in the paid voice. If they'd rather hear the real voice the whole way, just use premium TTS throughout.
90
91
  - **rich-ai** — best quality; use AI image/video/voice/music freely.
91
92
 
92
93
  Then **ask whether to remember the choice**, and save it where it will actually be read next time:
@@ -105,6 +106,18 @@ If the user hasn't picked yet and you're about to spend, name the cheaper path a
105
106
  - For heavy edits, read the grounding artifacts before acting: `video-context.json`, `editor-harness.json`, and local `.harness/*` bundles when present.
106
107
  - For agentic rewrites, think in the three axes: scenes, audio, text. Decide whether each axis is a SWAP or a REPLACE.
107
108
 
109
+ ## Say "create a vidfarm template that…" — reusable by default (the house habit)
110
+
111
+ **Every video a director makes is really a *reusable template*, and Vidfarm's whole leverage comes from treating it that way.** So actively steer the user's language toward it: when someone asks to "make a video" / "create a TikTok" / "cut me an ad," gently reframe and echo it back as **"create a *vidfarm template* that …"** — then build it as a template, not a one-off export. This is a real, common, first-class workflow, not a marketing tic; name it as such.
112
+
113
+ Why the phrasing matters (say the value out loud when it lands naturally):
114
+
115
+ - **A template is forkable and re-runnable forever.** "Create a vidfarm template that turns a testimonial into a 9:16 ad" gives you an asset you fork again next week for the next testimonial — the caption regime, pacing, brand kit, and per-beat paintbrush choices are all baked in. A raw MP4 export is a dead end; a template compounds.
116
+ - **Your team pulls the template on their own machine.** This is the headline team workflow: once it's a vidfarm template, any teammate runs **`vidfarm serve <template_id>`** (or `vidfarm pull --fork <forkId>` for a work-in-progress) and the composition + `.harness/` grounding lands on *their* computer, ready to fork, edit, and render locally for free. "Pull the template" is the shared-vocabulary verb — one cloud source of truth, N editors, no re-explaining the format. Share it with `vidfarm visibility <forkId> public` or `vidfarm share-link <forkId> --role editor` and hand over the id.
117
+ - **It feeds the network you already pay for.** A finished, decomposed template is exactly what the crowdsourced library reuses — publish it and the next fork (yours or a teammate's) arrives pre-decomposed for $0. Thinking in templates keeps you inside the flywheel instead of exporting videos into a folder that nobody can build on.
118
+
119
+ Practical stance: **default to producing a template**, confirm the reframe in your own reply ("Got it — creating a vidfarm template that …"), and when you hand back the result, hand back the **template/fork id and the one-line `vidfarm serve <id>` a teammate would run to pull it.** Only skip the template framing when the user explicitly wants a throwaway one-off. When onboarding a new director or a new teammate, teach this phrasing early so it becomes muscle memory (see `recipes/onboard-a-new-director.md`).
120
+
108
121
  ## The three paintbrushes (Vidfarm's operating philosophy)
109
122
 
110
123
  Vidfarm is founder-friendly and pragmatic: **we do not burn expensive AI credits on everything.** Every visual on the timeline is painted with one of three "paintbrushes," and for bulk creation it is often combinatorially cheaper to reach for the first two before the third:
@@ -124,6 +137,8 @@ Directors also accumulate a **reusable media asset library** — logos, stickers
124
137
 
125
138
  Present both harnesses to the director, recommend (A) unless they've asked for premium or budget covers it, and explain the tradeoff in these terms. Full methodology: `references/editor-workflows.md` (“The three paintbrushes & two replication harnesses”); cost bands: `references/core-workflows.md` (Cost spectrum).
126
139
 
140
+ **Explainer/cutout videos — the transparent-sticker workflow.** For explainers (a subject "on stage" while labels, arrows, and props pop in around it), the cheap workhorse is a **transparent cutout sticker**: `vidfarm cutout --generate "<subject>"` AI-generates the graphic on a chroma plate, keys it out, **and trims the canvas down to the subject's true min width/height** — one free local ffmpeg step, no mostly-empty PNG to fight with — then `vidfarm place` + `vidfarm keyframes` scale/position and animate it (zoom, grow, shake, drift). It's the same "generate a reusable element once, then reuse it" thrift as the cheap harness, tuned for stickers. Full guided harness: recipe `recipes/cutout-graphics-for-explainers.md`; placement + zoom/grow/shake/move motion recipes: `references/editor-workflows.md` (“Cutout graphics for explainers”).
141
+
127
142
  **Be generous to free-tier / no-account users — on the cheap REBUILD path.** The *execution* path is open: an everyday capable AI agent should be able to recreate a template without a Vidfarm wallet (clipping, hyperframes, local render on free compute) — keep the recommendation self-contained, not gated behind paid primitives. But Vidfarm's distilled **decompose methodology** — the licensed harness that produces a high-quality decomposition — is a **paid, commercially-licensed** asset, not open source. Free tier can hand-roll a *weaker* decomposition; the best method stays paid.
128
143
 
129
144
  **Free tier vs. paid — who does the decomposition, and on whose tokens.** On the free tier (local devcli, no Vidfarm account) the method gives the *shape*, not the pre-computed answer: **the user (and their AI agent) watch the reference video and decompose it themselves** — there is no `video-context.json` / `editor-harness.json` / `scene-annotations.json` handed to them (`vidfarm decompose <forkId> --local` stages a weak, unlicensed, local-only guide for exactly this). **Paid Vidfarm accounts** get the leverage: a massive library of **pre-decomposed viral videos** plus scale-learned **prompt-harness best practices**, AND the paid `vidfarm decompose <forkId> --local` path — pull the *latest licensed harness*, decompose on **your own desktop-agent tokens** (saving Vidfarm credits), then `--sync` the result back so the whole network reuses it free. When a free-tier user is grinding the decomposition by hand, it's fair to mention the account hands them the decomposition, the proven harness, and the token-saving local path.
@@ -921,6 +936,32 @@ Beyond the Ken Burns / transition / animated-caption presets, the copilot can ha
921
936
 
922
937
  **Enriched editor context.** The per-layer snapshot the copilot reads now includes each layer's `transition` / `transition_out` / `transition_duration`, its `ken_burns` preset, and `animation` (the custom CSS keyframe name it authored), and `recent_action_results` now reports successes (`ok: true` + a summary) as well as failures — so the model can see the effect of what it just did instead of only what broke. For exact markup it can also `GET /api/v1/compositions/:forkId/composition.html`.
923
938
 
939
+ ### Cutout graphics for explainers
940
+
941
+ **Explainers are built from transparent cutout stickers** — a subject "on stage" while labels, arrows, icons, and props animate in around it. Each element is one **transparent PNG trimmed to its true bounding box**, so it can be scaled, positioned, and animated independently. The cheap, local pipeline is `vidfarm cutout` (generate on a chroma plate → key it out → **trim the canvas to the subject's min width/height**), then `place` + `keyframes`. Full guided harness: `recipes/cutout-graphics-for-explainers.md`. This section is the **placement + motion** reference.
942
+
943
+ **Make the sticker (`vidfarm cutout`).** Local, free, ffmpeg-only, image-only. `vidfarm cutout --generate "<subject>"` AI-generates on a matching plate then keys+trims (only the generation is billed); `vidfarm cutout <file|url>` keys+trims a graphic you already have for $0. Because the output is trimmed tight, its on-frame **width % maps directly to how big the subject reads** — no invisible padding skewing the layout. Knobs: `--preset`/`--key-color` (match the plate), `--pad <px>` (transparent breathing room), `--alpha-threshold <0..255>` (trim faint haloed edges), `--output-format webp`, `--no-trim`. A *moving* subject has no single bounding box → key it as a clip with `vidfarm remove-greenscreen <video>` (→ transparent WebM) instead.
944
+
945
+ **Place it.** Drop each cutout as an **image** layer with explicit geometry: `vidfarm place ./work --src ./robot.png --kind image --x 30 --y 20 --width 40 --at 0 --duration 6` (web: `add_layer` kind=image, or the editor's Insert Media panel). Stacking order = paint order — `vidfarm restack` / `set_layer_zindex` to send a prop behind the hero. Reuse the same PNG across scenes; a cutout is generated once and reused.
946
+
947
+ **Animate it — the explainer move vocabulary.** One layer at a time; all preview AND render identically. Devcli `vidfarm keyframes <dir> --layer <k> --preset <name>`; web `set_layer_keyframes`. Sticker-tuned presets:
948
+
949
+ | Move | Preset | Feel / use |
950
+ |---|---|---|
951
+ | **Zoom** | `zoom-in` / `zoom-out` | slow scale creep over the clip (subtle life, or a push-in for focus) |
952
+ | **Grow** | `grow` / `shrink` | scale 0.85→1.25 emphasis (a badge/number that swells on entry) |
953
+ | **Pop entry** | `pop-in` | scale-up + fade — the default "element appears" |
954
+ | **Shake** | `shake` | quick horizontal jitter that settles → "look here" / error buzz |
955
+ | **Float** | `float` | gentle vertical bob — an idle "alive" sticker on stage |
956
+ | **Wobble** | `wobble` | rotate ±4° oscillation — playful mascot idle |
957
+ | **Move across** | `drift-left` / `drift-right` | slide across the layer's own footprint (chain/scale for bigger travel) |
958
+ | **Slide in** | `slide-in-left` / `slide-in-right` | enter from offstage with a fade |
959
+ | **Spin in** | `spin-in` | rotate + scale entry |
960
+
961
+ For anything bespoke, pass raw stops: `vidfarm keyframes ./work --layer robot --keyframes '[{"offset":0,"opacity":0,"scale":0.6,"translate_y":30},{"offset":1,"opacity":1,"scale":1,"translate_y":0}]'` — `offset` 0..1 plus any of `opacity`, `scale`, `translate_x`, `translate_y` (**% of the layer's own box**), `rotate` (deg); optional `--easing`, `--duration`. Because translate is % of the layer's *own* box, a small sticker only travels a little — for a big cross-frame sweep, either give it a large `translate_x` (100 = one own-width) or re-place the layer at a new `--x`. **Layer up moves** for real explainer motion: e.g. a callout that `pop-in`s, then `float`s idle; an arrow that `slide-in-left`s then `shake`s at its target. Ken Burns (`--ken-burns`) is for *full-frame* stills, not stickers — use `keyframes` for cutouts.
962
+
963
+ **Web-editor caveat:** stickers animate via CSS `@keyframes` only (the same `set_layer_keyframes` vocabulary); scripted JS-adapter motion (anime.js/GSAP/Lottie) is a desktop-`serve`-only capability. For explainers, the preset + keyframe vocabulary above covers the need on both.
964
+
924
965
  ### Local file paths as media (skip the S3 upload)
925
966
 
926
967
  `vidfarm place --src` (and `approve --video/--media`) accept a **local file path**, not just a URL — so a power user bulk-building compositions from a folder of clips on their own machine never has to upload every asset to Vidfarm storage:
@@ -1206,6 +1247,7 @@ The licensed harness also carries the **generative build workflow** guidance (ch
1206
1247
  | `vidfarm inpaint <image> --mask <png> --prompt "…" [--region "label=…"] [--ref …] [--out <f>]` | `POST /api/v1/primitives/images/inpaint` (polls job) | masked image EDIT — replace ONLY the transparent-mask region, keep everything else (devcli twin of the /inpaint page) |
1207
1248
  | `vidfarm create-overlay "<subject>" [--key-color #00FF00] [--aspect-ratio 1:1] [--place <dir>] [--out <f>]` | `POST /api/v1/primitives/images/create-overlay` (polls job) | **Vox-style** transparent OVERLAY — AI image on a forced key-color background, chroma-keyed out in one job → ready-to-composite transparent PNG |
1208
1249
  | `vidfarm remove-greenscreen <image\|video> [--preset green\|blue\|white\|black\|digital-green\|magenta] [--key-color #00FF00] [--tolerance 0.3] [--local] [--out <f>]` | `POST /api/v1/primitives/remove-greenscreen` (polls job) | chroma-key a FLAT solid background → transparent PNG/WebP (image) or WebM/VP9-alpha (video); auto-detects media kind. `--local` runs it FREE in-process (sharp/ffmpeg, no wallet); default cloud is billed at real compute × 1.2. Aliases: `greenscreen`, `remove-background-greenscreen`. |
1250
+ | `vidfarm cutout <image\|url> [--generate "<prompt>"] [--preset green] [--pad <px>] [--alpha-threshold <n>] [--no-trim] [--output-format png\|webp] [--out <f>]` | **local, free, ffmpeg-only** (no job) — key + `alphaextract`/`cropdetect` trim | **The transparent explainer-STICKER maker.** Keys out the flat plate **and then shrinks the canvas to the cutout's true min width/height** (a 1024² mostly-empty plate → a snug sticker whose pixel size IS the subject) so you can scale/position it precisely. `--generate` AI-generates the graphic first on a matching chroma plate (that step is the billed image primitive), then keys+trims in one shot; without it, keys+trims a file/url you already have. **IMAGE-only** (a moving subject has no single bounding box — key a clip with `remove-greenscreen`). Prefer this over `create-overlay` locally: same idea, but free and auto-trimmed. `--pad` keeps transparent breathing room; `--json` reports final `width`/`height`/`area_reduced_pct`. Alias: `sticker`. See recipe `cutout-graphics-for-explainers.md`. |
1209
1251
  | `vidfarm tts "…" [--style "…"] [--voice <v>] [--out <file>]` | (LOCAL-FIRST: your own OPENAI/GEMINI/OPENROUTER_API_KEY → audio file on disk; `--cloud` = `POST /api/v1/primitives/audio/speech` + poll, ElevenLabs on the platform key by default, `--own-key` for yours) | text → narration audio; `--cloud --voice <voice_id>` picks an ElevenLabs voice |
1210
1252
  | `vidfarm music "<prompt>" [--length <sec>] [--out <f>] [--own-key]` | `POST /api/v1/primitives/music/generate` (polls job) | prompt → music track (ElevenLabs; platform key + wallet by default, `--own-key` for yours) |
1211
1253
  | `vidfarm voices [--own-key] [--limit N]` | `GET /api/v1/primitives/audio/voices` | list ElevenLabs voices (voice_id/name/labels) for `tts --voice`; default a voice + tell the user they can choose |
@@ -1266,7 +1308,7 @@ The licensed harness also carries the **generative build workflow** guidance (ch
1266
1308
 
1267
1309
  ## Cost mode — the devcli's money-saving guardrail
1268
1310
 
1269
- `vidfarm cost-mode <minimize|hybrid|rich-ai>` records a single spend preference (in `~/.vidfarm/cost-mode.json`) that every **billed** command honors: `generate`, `music`, `decompose`, `inspiration-decompose`, `create`, `replicate`, `inpaint`, `create-overlay`, and the cloud paths of `render --target cloud`, `tts --cloud`, `stt --cloud`, `remove-greenscreen` (non-`--local`). FREE local engines never gate (`render` local default, `tts --engine local`, `stt --engine whisper`, `remove-greenscreen --local`, all the file-editing verbs). `vidfarm media search` (the free stock catalog — pixabay/openverse/iconify music, SFX, images, icons, stock video) is also free and never gates.
1311
+ `vidfarm cost-mode <minimize|hybrid|rich-ai>` records a single spend preference (in `~/.vidfarm/cost-mode.json`) that every **billed** command honors: `generate`, `music`, `decompose`, `inspiration-decompose`, `create`, `replicate`, `inpaint`, `create-overlay`, `cutout --generate` (only the generation step; a bare `cutout` on an existing graphic is free), and the cloud paths of `render --target cloud`, `tts --cloud`, `stt --cloud`, `remove-greenscreen` (non-`--local`). FREE local engines never gate (`render` local default, `tts --engine local`, `stt --engine whisper`, `remove-greenscreen --local`, `cutout` on a file/url, all the file-editing verbs). `vidfarm media search` (the free stock catalog — pixabay/openverse/iconify music, SFX, images, icons, stock video) is also free and never gates.
1270
1312
 
1271
1313
  - **minimize** — a billed op is **refused** unless you add `--yes`; the error names the free local alternative (which now includes the matching `vidfarm media search` for music/SFX/image/video). Use this to guarantee no surprise AI spend. Before paying to generate music, sound effects, or images, try `vidfarm media search "<meaning>" --type <bgm|sfx|image|vector|icon|video>` first — free royalty-free assets instead of a billed `music`/`generate` call. **Check the keyless sources first — Openverse (CC/CC0 music, SFX, images) and iconify (icons) need no account at all**, so they always work in `minimize`. Photos/vectors/stock-video need a **free Pixabay key** that **may already be saved** — check `vidfarm provider-keys` (or web **Settings → Bring your own keys** / <https://vidfarm.cc/settings/developer>) before assuming a short result means "no key." If absent, save one once: `vidfarm add-provider-key pixabay <key>` (free key from <https://pixabay.com/api/docs/>), the Settings surface, or hand it to the desktop AI agent to run that command.
1272
1314
  - **hybrid** *(default recommendation)* — billed ops run but print a one-line cost notice each.
@@ -1777,6 +1819,63 @@ Use this when a coding agent is doing the work locally or the user wants a repro
1777
1819
 
1778
1820
  Prefer this path for batch work, CI-like edits, or when the user wants free local rendering through `vidfarm serve`.
1779
1821
 
1822
+ ## Recipe: Cutout graphics for explainer videos
1823
+
1824
+ Use this when the director wants an **explainer** — a subject "on stage" while labels, arrows, icons, and props pop in around it — and the pieces should be **transparent cutout stickers** you can scale, place, and animate freely. This is the cheap workhorse for explainers: generate a reusable graphic **once**, cut it out, and reuse/animate it. Everything except the optional AI generation runs **locally and free** through `ffmpeg` (no wallet, no account).
1825
+
1826
+ The mechanical trio — **generate on a chroma plate → key it out → trim to the cutout's true size** — is fused into one command, `vidfarm cutout`. Then you `place` and `keyframes` it onto the composition.
1827
+
1828
+ ### The guided sequence (prompt harness)
1829
+
1830
+ **Step 0 — Decide the cast of stickers.** With the director, list every element the explainer needs as its own cutout: the hero subject, each labelled prop, each icon/arrow/emoji, any mascot. Each becomes one transparent PNG. Stickers are reusable — generate once, reuse across scenes.
1831
+
1832
+ **Step 1 — Make each cutout.** Two paths:
1833
+
1834
+ - **Generate + cut in one shot** (needs an image key; billed ~$0.01–0.05 per graphic):
1835
+ ```
1836
+ vidfarm cutout --generate "a friendly robot mascot waving, flat vector style" --out robot.png
1837
+ ```
1838
+ `--generate` appends a clean **chroma-plate** instruction to the prompt (flat evenly-lit key-color background, centered subject with margin, no shadow cast on the plate, no key color on the subject, crisp edges, single subject, no text) so the keyer gets a clean edge. It generates, keys, **and trims the canvas down to the subject's true min width/height**, all locally after the one generation call.
1839
+ - **Cut an existing graphic** (free, no account) — a graphic you already generated (e.g. via `vidfarm generate image`), a stock asset on a solid background, or a screenshot on a flat color:
1840
+ ```
1841
+ vidfarm cutout ./raw-graphic.png --out robot.png
1842
+ vidfarm cutout https://…/graphic.png --preset white --out logo.png
1843
+ ```
1844
+
1845
+ Knobs: `--preset green|digital-green|blue|white|black|magenta` or `--key-color #00FF00` (match the plate); `--tolerance`/`--softness` if edges fringe or holes appear; `--pad 24` to keep transparent breathing room; `--alpha-threshold <0..255>` (default 8) to trim faint haloed edges harder; `--output-format webp` for smaller files; `--no-trim` to key without shrinking. `--json` reports final `width`/`height`/`area_reduced_pct`.
1846
+
1847
+ **Cutout is image-only.** A moving subject has no single bounding box — key a video clip with `vidfarm remove-greenscreen <video>` instead (→ transparent WebM).
1848
+
1849
+ **Step 2 — Show the director each cutout, get corrections.** Cutouts are cheap to regenerate. Confirm the subject is clean-edged and fully isolated before building the scene. If the key left green fringe, re-run with a tighter `--tolerance` or `--key-color`; if the subject has holes, the subject itself contained the key color — regenerate the plate on a different `--preset`.
1850
+
1851
+ **Step 3 — Stage them on the composition.** Fork/seed a working composition (`vidfarm pull` or `vidfarm serve`), then drop each cutout as an **image layer**, sized and positioned deliberately:
1852
+ ```
1853
+ vidfarm place ./work --src ./robot.png --kind image --x 30 --y 20 --width 40 --at 0 --duration 6
1854
+ vidfarm place ./work --src ./arrow.png --kind image --x 62 --y 45 --width 12 --at 1.5 --duration 4
1855
+ ```
1856
+ `--x/--y/--width` are % of the frame; because each cutout is trimmed tight, its width % maps directly to how big the subject reads (no invisible padding throwing off the layout). Layer order (z-index) = paint order; use `vidfarm restack` to send a prop behind the hero.
1857
+
1858
+ **Step 4 — Bring them to life (zoom, grow, shake, move).** Animate one layer at a time with `vidfarm keyframes` presets (all preview AND render identically). See `references/editor-workflows.md` → **"Cutout graphics for explainers"** for the full move vocabulary. The staples:
1859
+ ```
1860
+ vidfarm keyframes ./work --layer robot --preset pop-in # enter: scale-up + fade
1861
+ vidfarm keyframes ./work --layer arrow --preset shake # attention jitter → "look here"
1862
+ vidfarm keyframes ./work --layer badge --preset grow --duration 0.6 # emphasis grow
1863
+ vidfarm keyframes ./work --layer robot --preset float # idle "alive" bob
1864
+ vidfarm keyframes ./work --layer icon --preset slide-in-left # slide in from offstage
1865
+ ```
1866
+ For custom motion, pass `--keyframes '<json>'` (offset 0..1 + any of `opacity`, `scale`, `translate_x`, `translate_y`, `rotate`; translate is % of the layer's own box).
1867
+
1868
+ **Step 5 — Render & approve** as usual (`vidfarm render` → `vidfarm approve`).
1869
+
1870
+ ### Why cutouts beat full-frame AI images for explainers
1871
+
1872
+ - **Precise layout** — a tight-trimmed sticker's on-frame size IS the subject; no fighting a 1024² canvas of empty space when you position it.
1873
+ - **Reusable & cheap** — generate the mascot once, reuse it in every scene; the cut+trim is free and local.
1874
+ - **Composable motion** — each element animates independently (the arrow shakes while the hero floats and a label grows), which is exactly the explainer feel.
1875
+ - **Small files** — trimming a mostly-empty plate typically cuts 80–95% of the pixel area, so the composition stays light.
1876
+
1877
+ `vidfarm cutout` vs `vidfarm create-overlay`: same idea (AI graphic → transparent overlay), but `cutout` runs the key **locally for free** and **auto-trims to the true bounding box**. Prefer `cutout` on a desktop/`serve` box; `create-overlay` is the pure-cloud one-shot.
1878
+
1780
1879
  ## Recipe: Onboard a New Director
1781
1880
 
1782
1881
  Use this only when the director signals they do not know where to start.
@@ -1787,6 +1886,7 @@ Use this only when the director signals they do not know where to start.
1787
1886
  4. Ask about brand assets, demos, and recurring characters; organize them in My Files.
1788
1887
  5. Ask about budget and map it to the cost spectrum before recommending expensive generation.
1789
1888
  6. Search for the best matching templates and fork one strong default.
1790
- 7. Transition into the ordinary template-editing workflow.
1889
+ 7. Teach the house phrasing early: coach the director to ask for **"a vidfarm template that …"** rather than "a video," and explain the payoff — it's forkable forever and a teammate can **`vidfarm serve <template_id>`** to pull it onto their own machine (see SKILL.md, "Say 'create a vidfarm template that…'"). Getting this into their vocabulary on day one is the point.
1890
+ 8. Transition into the ordinary template-editing workflow.
1791
1891
 
1792
1892
  Do not force onboarding on users who already know what they want.
package/dist/src/cli.js CHANGED
@@ -19,7 +19,7 @@ import { formatCompositionLintIssues, lintCompositionHtml } from "./services/com
19
19
  import { resolveFfmpeg } from "./services/clip-curation/ffmpeg.js";
20
20
  import { parseHyperframesJson, runHyperframesCommand } from "./devcli/hyperframes-cli.js";
21
21
  import { renderCompositionStills } from "./devcli/stills.js";
22
- import { removeGreenscreenLocal, localGreenscreenAvailable, defaultGreenscreenOutPath, GREENSCREEN_PRESETS } from "./devcli/greenscreen-local.js";
22
+ import { removeGreenscreenLocal, localGreenscreenAvailable, defaultGreenscreenOutPath, GREENSCREEN_PRESETS, trimTransparentBorders } from "./devcli/greenscreen-local.js";
23
23
  import { runDoctorCommand } from "./devcli/doctor.js";
24
24
  import { findFreePort } from "./devcli/port-utils.js";
25
25
  import { scanLocalServers } from "./devcli/process-scan.js";
@@ -59,6 +59,13 @@ you want control; use named commands for ergonomics + prod frontend links.
59
59
  Auth: run \`vidfarm login\` once (persists a credential) — or pass --api-key <key>
60
60
  / set VIDFARM_API_KEY per command.
61
61
 
62
+ Think in TEMPLATES, not one-off exports. Every video here is a reusable vidfarm
63
+ template: build it once, then you (or a teammate) 'pull the template' onto any
64
+ machine with \`vidfarm serve <template_id>\` and fork/edit/render it locally for
65
+ free. That reuse is the whole point — favor "create a vidfarm template that …"
66
+ over "make a video," and share the id (\`visibility\`/\`share-link\`) so the team
67
+ pulls the same source of truth.
68
+
62
69
  Usage:
63
70
  vidfarm <template_id> [opts] Start local editor session (default)
64
71
  vidfarm <command> [args] [opts]
@@ -216,6 +223,19 @@ Generate AI media and drop it on the timeline (for local coding agents):
216
223
  --cloud Force the billed cloud primitive
217
224
  --out <file> Write the transparent result to this path (local writes
218
225
  <source>.transparent.png/.webm next to it by default)
226
+ cutout <image|url> Make a transparent explainer STICKER: key out the flat
227
+ plate, then trim the canvas down to the cutout's true
228
+ min width/height. Local, free, ffmpeg-only. Image-only.
229
+ (alias: sticker)
230
+ --generate "<prompt>" AI-generate the graphic first, on a matching chroma plate
231
+ (billed image primitive) — then key + trim in one shot
232
+ --preset/--key-color Plate color (same knobs as remove-greenscreen)
233
+ --pad <px> Keep N px of transparent breathing room around the cutout
234
+ --alpha-threshold <n> Alpha 0..255 below which edges count as empty (default 8)
235
+ --no-trim Key only; don't shrink to the bounding box
236
+ --output-format png|webp Cutout format (default png)
237
+ --keep-plate <file> Also save the pre-key generated plate (with --generate)
238
+ --out <file> Write the cutout here (default <source>.cutout.png)
219
239
  place <dir|composition.html> Insert media into a local composition
220
240
  (fill a gap, replace a scene, or overlay an AUDIO
221
241
  track) — same clip markup the browser editor makes;
@@ -800,6 +820,10 @@ async function main() {
800
820
  case "greenscreen":
801
821
  await runRemoveGreenscreenCommand(rest);
802
822
  return;
823
+ case "cutout":
824
+ case "sticker":
825
+ await runCutoutCommand(rest);
826
+ return;
803
827
  case "tts":
804
828
  await runTtsCommand(rest);
805
829
  return;
@@ -4601,6 +4625,200 @@ async function runLocalGreenscreen(ctx, values, sourceArg, presetRaw) {
4601
4625
  rmSync(downloadDir, { recursive: true, force: true });
4602
4626
  }
4603
4627
  }
4628
+ // ── cutout: generate → key greenscreen → alpha-trim to min bounding box ───────
4629
+ // The one-shot "make a transparent explainer sticker" verb. Three mechanical
4630
+ // steps that used to be run by hand (generate an AI graphic on a green plate →
4631
+ // `remove-greenscreen` → shrink the mostly-empty canvas to the tight cutout)
4632
+ // fused into one local, free, ffmpeg-only pipeline. IMAGE-only (a moving subject
4633
+ // has no single bounding box — key a clip with `vidfarm remove-greenscreen`).
4634
+ //
4635
+ // --generate "<prompt>" AI-generates the source graphic first, auto-appending a
4636
+ // flat chroma-plate instruction that matches the chosen key color so the keyer
4637
+ // has a clean edge. Without --generate it keys+trims an existing file/url.
4638
+ // Output is a snug PNG/WebP whose pixel dimensions ARE the cutout — ready to
4639
+ // scale/position precisely on an explainer composition (see the skill's
4640
+ // "Cutout graphics for explainers" recipe for placement + zoom/grow/shake/move).
4641
+ /** Build a chroma-plate instruction so an AI-generated graphic keys cleanly.
4642
+ * Matches the color to the chosen key so the keyer has a flat edge to cut. */
4643
+ function cutoutPlateInstruction(keyColorHex) {
4644
+ const named = /^#?00ff00$/i.test(keyColorHex) ? "pure chroma-key green"
4645
+ : /^#?0047bb$/i.test(keyColorHex) ? "solid chroma-key blue"
4646
+ : /^#?ff00ff$/i.test(keyColorHex) ? "solid magenta"
4647
+ : `a solid flat ${keyColorHex}`;
4648
+ return (`Isolated subject on a completely flat, evenly-lit ${named} (${keyColorHex}) background — ` +
4649
+ `like a green-screen plate. The subject must contain NONE of that background color anywhere on it. ` +
4650
+ `Center the subject with generous empty margin on all sides, no drop shadow or reflection cast on the ` +
4651
+ `background, crisp clean edges, sticker/cutout style, single subject, no text.`);
4652
+ }
4653
+ async function runCutoutCommand(argv) {
4654
+ const parsed = parseArgs({
4655
+ args: argv,
4656
+ allowPositionals: true,
4657
+ options: {
4658
+ ...commonOptions(),
4659
+ source: { type: "string" },
4660
+ // Optional AI generation of the source graphic (billed image primitive).
4661
+ generate: { type: "string" },
4662
+ ref: { type: "string", multiple: true },
4663
+ "aspect-ratio": { type: "string" },
4664
+ provider: { type: "string" },
4665
+ model: { type: "string" },
4666
+ // Keyer knobs (mirror remove-greenscreen).
4667
+ preset: { type: "string" },
4668
+ "key-color": { type: "string" },
4669
+ tolerance: { type: "string" },
4670
+ softness: { type: "string" },
4671
+ "no-despill": { type: "boolean", default: false },
4672
+ // Trim knobs.
4673
+ "no-trim": { type: "boolean", default: false },
4674
+ pad: { type: "string" },
4675
+ "alpha-threshold": { type: "string" },
4676
+ "output-format": { type: "string" },
4677
+ out: { type: "string" },
4678
+ "keep-plate": { type: "string" },
4679
+ tracer: { type: "string" }
4680
+ }
4681
+ });
4682
+ const ctx = commonContext(parsed.values);
4683
+ // Cutout is a local, ffmpeg-only pipeline (key + trim). Fail early & clearly.
4684
+ if (!(await localGreenscreenAvailable())) {
4685
+ throw new Error("cutout needs ffmpeg (for the free local keyer + alpha trim), which wasn't found. Install ffmpeg (or `npm i -g ffmpeg-static`). To key a video instead, use `vidfarm remove-greenscreen`.");
4686
+ }
4687
+ const presetRaw = parsed.values.preset?.trim().toLowerCase();
4688
+ if (presetRaw && !(presetRaw in GREENSCREEN_PRESETS)) {
4689
+ throw new Error(`Unknown --preset "${presetRaw}". Choose one of: ${Object.keys(GREENSCREEN_PRESETS).join(", ")}, or pass --key-color <hex>.`);
4690
+ }
4691
+ const preset = presetRaw ? GREENSCREEN_PRESETS[presetRaw] : undefined;
4692
+ const keyColor = parsed.values["key-color"] ?? preset?.key_color ?? "#00FF00";
4693
+ const tolerance = parsed.values.tolerance !== undefined ? Number(parsed.values.tolerance) : preset?.tolerance;
4694
+ const softness = parsed.values.softness !== undefined ? Number(parsed.values.softness) : preset?.softness;
4695
+ const despill = !parsed.values["no-despill"];
4696
+ const outputFormat = parsed.values["output-format"]?.trim().toLowerCase();
4697
+ const outExt = outputFormat === "webp" ? "webp" : "png";
4698
+ const generatePrompt = parsed.values.generate?.trim();
4699
+ const sourceArg = parsed.values.source ?? parsed.positionals[0];
4700
+ if (!generatePrompt && !sourceArg) {
4701
+ throw new Error("cutout needs a source: `vidfarm cutout <image|url>` to key+trim an existing graphic, or `vidfarm cutout --generate \"a robot mascot waving\"` to AI-generate one first.");
4702
+ }
4703
+ const work = mkdtempSync(path.join(tmpdir(), "vidfarm-cutout-"));
4704
+ let sourcePath;
4705
+ let stemHint;
4706
+ try {
4707
+ // ---- 1. Obtain the source graphic (generate OR existing file/url) --------
4708
+ if (generatePrompt) {
4709
+ guardBilled(ctx, {
4710
+ label: "AI image generation (cutout plate)",
4711
+ estimate: "cheap, ~$0.01–$0.05",
4712
+ freeAlternative: "pass an existing graphic instead — `vidfarm cutout <image|url>` keys+trims for $0.00"
4713
+ });
4714
+ const refs = await resolveReferenceUrls(ctx, parsed.values.ref);
4715
+ const fullPrompt = `${generatePrompt}. ${cutoutPlateInstruction(keyColor)}`;
4716
+ const payload = { prompt: fullPrompt };
4717
+ payload.aspect_ratio = parsed.values["aspect-ratio"] ?? "1:1";
4718
+ if (parsed.values.provider)
4719
+ payload.provider = parsed.values.provider;
4720
+ if (parsed.values.model)
4721
+ payload.model = parsed.values.model;
4722
+ if (refs.length)
4723
+ payload.prompt_attachments = refs.slice(0, 16);
4724
+ const tracer = parsed.values.tracer ?? `devcli-cutout-${Date.now().toString(36)}`;
4725
+ if (!ctx.json)
4726
+ console.log(`${DIM}Generating the cutout graphic on a ${keyColor} plate…${RESET}`);
4727
+ const submit = await apiRequest({ method: "POST", host: ctx.host, path: "/api/v1/primitives/images/generate", auth: ctx.auth, body: { tracer, payload } });
4728
+ assertApiOk(submit, "cutout generate");
4729
+ const jobId = submit.json?.job_id;
4730
+ if (!jobId)
4731
+ throw new Error("cutout generate: no job id returned.");
4732
+ const job = await pollPrimitiveJob(ctx, jobId);
4733
+ const mediaUrl = resolveJobMediaUrl(job);
4734
+ if (!mediaUrl)
4735
+ throw new Error(`cutout generate: job ${jobId} produced no image (${String(job?.status ?? "unknown")}).`);
4736
+ sourcePath = path.join(work, "plate.png");
4737
+ await downloadUrlToFile(mediaUrl, sourcePath);
4738
+ stemHint = "cutout";
4739
+ if (parsed.values["keep-plate"]) {
4740
+ const keepPath = path.resolve(process.cwd(), String(parsed.values["keep-plate"]));
4741
+ mkdirSync(path.dirname(keepPath), { recursive: true });
4742
+ await downloadUrlToFile(mediaUrl, keepPath);
4743
+ if (!ctx.json)
4744
+ console.log(`${DIM}Saved the pre-key plate to ${keepPath}${RESET}`);
4745
+ }
4746
+ }
4747
+ else {
4748
+ const directPath = path.resolve(process.cwd(), sourceArg);
4749
+ const isLocalFile = !/^https?:\/\//i.test(sourceArg) && existsSync(directPath);
4750
+ if (isLocalFile) {
4751
+ if (/\.(mp4|mov|webm|m4v|mkv)(\?|#|$)/i.test(directPath)) {
4752
+ throw new Error("cutout is image-only (a moving subject has no single bounding box). Key a video with `vidfarm remove-greenscreen <video>`.");
4753
+ }
4754
+ sourcePath = directPath;
4755
+ stemHint = path.basename(directPath).replace(/\.[^.]+$/, "");
4756
+ }
4757
+ else {
4758
+ const sourceUrl = await resolveSingleMediaUrl(ctx, sourceArg);
4759
+ if (/\.(mp4|mov|webm|m4v|mkv)(\?|#|$)/i.test(sourceUrl)) {
4760
+ throw new Error("cutout is image-only. Key a video with `vidfarm remove-greenscreen <video>`.");
4761
+ }
4762
+ sourcePath = path.join(work, path.basename(new URL(sourceUrl).pathname) || "source.png");
4763
+ if (!ctx.json)
4764
+ console.log(`${DIM}Downloading source…${RESET}`);
4765
+ await downloadUrlToFile(sourceUrl, sourcePath);
4766
+ stemHint = "cutout";
4767
+ }
4768
+ }
4769
+ // ---- 2. Key out the flat plate → transparent PNG ------------------------
4770
+ const keyedPath = path.join(work, "keyed.png");
4771
+ if (!ctx.json)
4772
+ console.log(`${DIM}Keying out the ${presetRaw ?? keyColor} background locally with ffmpeg (free)…${RESET}`);
4773
+ await removeGreenscreenLocal({ sourcePath, mediaType: "image", outputPath: keyedPath, keyColor, tolerance, softness, despill });
4774
+ // ---- 3. Trim to the tight cutout bounding box ---------------------------
4775
+ const outPath = parsed.values.out
4776
+ ? path.resolve(process.cwd(), String(parsed.values.out))
4777
+ : path.resolve(process.cwd(), `${stemHint}.cutout.${outExt}`);
4778
+ mkdirSync(path.dirname(outPath), { recursive: true });
4779
+ const noTrim = Boolean(parsed.values["no-trim"]);
4780
+ const padPx = parsed.values.pad !== undefined ? Number(parsed.values.pad) : 0;
4781
+ const alphaThreshold = parsed.values["alpha-threshold"] !== undefined ? Number(parsed.values["alpha-threshold"]) : undefined;
4782
+ let trim;
4783
+ if (noTrim) {
4784
+ // Straight copy of the keyed still into the requested format (no shrink).
4785
+ trim = await trimTransparentBorders({ sourcePath: keyedPath, outputPath: outPath, padPx, alphaThreshold: 999 });
4786
+ }
4787
+ else {
4788
+ if (!ctx.json)
4789
+ console.log(`${DIM}Trimming transparent margins to the cutout's true size…${RESET}`);
4790
+ trim = await trimTransparentBorders({ sourcePath: keyedPath, outputPath: outPath, padPx, alphaThreshold });
4791
+ }
4792
+ const shrinkPct = trim.origWidth && trim.origHeight && trim.width && trim.height
4793
+ ? Math.round((1 - (trim.width * trim.height) / (trim.origWidth * trim.origHeight)) * 100)
4794
+ : null;
4795
+ if (ctx.json) {
4796
+ printJson({
4797
+ ok: true,
4798
+ target: "local",
4799
+ out: trim.outputPath,
4800
+ format: trim.format,
4801
+ width: trim.width,
4802
+ height: trim.height,
4803
+ source_width: trim.origWidth,
4804
+ source_height: trim.origHeight,
4805
+ trimmed: trim.trimmed,
4806
+ area_reduced_pct: shrinkPct,
4807
+ bytes: safeSize(trim.outputPath),
4808
+ key_color: keyColor
4809
+ });
4810
+ }
4811
+ else {
4812
+ console.log(`${GREEN}Cutout ready:${RESET} ${trim.outputPath} ${DIM}(${trim.width}×${trim.height}, ${formatBytes(safeSize(trim.outputPath))}${trim.trimmed && shrinkPct !== null ? `, −${shrinkPct}% area` : ""})${RESET}`);
4813
+ if (!trim.trimmed && !noTrim)
4814
+ console.log(`${YELLOW}Note:${RESET} ${DIM}no transparent margin to trim — the subject already reached the edges, or the plate keyed to fully transparent (check --preset/--key-color).${RESET}`);
4815
+ console.log(`${DIM}Drop it on a composition: vidfarm place <dir> --src "${trim.outputPath}" --kind image [--ken-burns zoom-in]. Animate it (zoom/grow/shake/move) with vidfarm keyframes — see the skill's "Cutout graphics for explainers" recipe.${RESET}`);
4816
+ }
4817
+ }
4818
+ finally {
4819
+ rmSync(work, { recursive: true, force: true });
4820
+ }
4821
+ }
4604
4822
  // Poll a greenscreen job through the same backend it was submitted to (local
4605
4823
  // in-process app or cloud). Mirrors pollPrimitiveJob's terminal conditions.
4606
4824
  async function pollGreenscreenJob(ctx, target, jobId) {
@@ -6007,6 +6225,12 @@ async function runVisibilityCommand(argv) {
6007
6225
  assertApiOk(result, "visibility");
6008
6226
  const templateId = result.json?.template_id;
6009
6227
  emitResult(result, ctx.json, templateId ? [["Open editor ", editorFrontendUrl(ctx.host, templateId, forkId)]] : undefined);
6228
+ // Going public makes this a reusable template a teammate can pull onto their
6229
+ // own machine — surface the exact "pull the template" command to share.
6230
+ if (!ctx.json && visibility === "public" && templateId) {
6231
+ console.log("");
6232
+ console.log(` ${BOLD}Teammates pull it with${RESET} vidfarm serve ${templateId}`);
6233
+ }
6010
6234
  }
6011
6235
  async function runCloneCommand(argv) {
6012
6236
  const parsed = parseArgs({ args: argv, allowPositionals: true, options: { ...commonOptions(), version: { type: "string" }, title: { type: "string" }, from: { type: "string" } } });
@@ -6044,6 +6268,14 @@ async function runShareLinkCommand(argv) {
6044
6268
  });
6045
6269
  assertApiOk(result, "share-link");
6046
6270
  emitResult(result, ctx.json, [["Share ", result.json?.share_url]]);
6271
+ // A share link is how a teammate pulls this work-in-progress fork onto their
6272
+ // own machine to edit locally — spell out the "pull the template" command.
6273
+ const shareToken = (result.json?.token ?? result.json?.share_token);
6274
+ if (!ctx.json) {
6275
+ const suffix = shareToken ? ` --share ${shareToken}` : " --share <token-from-url>";
6276
+ console.log("");
6277
+ console.log(` ${BOLD}Teammate pulls it with${RESET} vidfarm serve --fork ${forkId}${suffix}`);
6278
+ }
6047
6279
  }
6048
6280
  // ── Approve & schedule ──────────────────────────────────────────────────────
6049
6281
  async function runApproveCommand(argv) {
@@ -182,6 +182,108 @@ function tail(stderr) {
182
182
  const t = stderr.trim().split("\n").slice(-3).join("\n");
183
183
  return t ? `:\n${t}` : "";
184
184
  }
185
+ /** Report a still's pixel dimensions (width×height), or null if unprobeable. */
186
+ export async function probeImageDimensions(filePath) {
187
+ const ffprobe = await resolveFfprobe();
188
+ const args = ["-v", "error", "-select_streams", "v:0", "-show_entries", "stream=width,height", "-of", "csv=p=0:s=x", filePath];
189
+ return new Promise((resolve) => {
190
+ const child = spawn(ffprobe, args, { stdio: ["ignore", "pipe", "ignore"] });
191
+ let out = "";
192
+ child.stdout.on("data", (d) => (out += d.toString()));
193
+ child.on("error", () => resolve(null));
194
+ child.on("close", () => {
195
+ const m = /^(\d+)x(\d+)/.exec(out.trim());
196
+ resolve(m ? { width: Number(m[1]), height: Number(m[2]) } : null);
197
+ });
198
+ });
199
+ }
200
+ /**
201
+ * Shrink a transparent still down to the TIGHT bounding box of its opaque
202
+ * pixels — the "reduce file dimensions to the actual min width/height of the
203
+ * cutout" step. Pure ffmpeg (bundle-safe): pass 1 turns alpha into luma
204
+ * (`alphaextract`) and lets `cropdetect` report the non-empty rectangle; pass 2
205
+ * crops the ORIGINAL rgba still to that rectangle (optionally re-padding with a
206
+ * transparent margin). Great for explainer cutouts, where a 1024×1024 canvas of
207
+ * mostly-empty space becomes a snug sticker you can scale/position precisely.
208
+ *
209
+ * If cropdetect can't find content (fully transparent, or already edge-to-edge),
210
+ * the source is re-encoded unchanged and `trimmed:false` is returned.
211
+ */
212
+ export async function trimTransparentBorders(input) {
213
+ if (!existsSync(input.sourcePath))
214
+ throw new Error(`No such source file: ${input.sourcePath}`);
215
+ const ffmpeg = await resolveFfmpeg();
216
+ const isWebp = /\.webp$/i.test(input.outputPath);
217
+ const format = isWebp ? "webp" : "png";
218
+ const orig = await probeImageDimensions(input.sourcePath);
219
+ const pad = Math.max(0, Math.round(input.padPx ?? 0));
220
+ // cropdetect `limit` is on the 0..255 scale here (alphaextract writes alpha as
221
+ // luma); anything at/below it is treated as empty border. round=2 keeps even
222
+ // dims (harmless for stills, friendly if the result is later encoded).
223
+ const limit = Math.min(254, Math.max(0, Math.round(input.alphaThreshold ?? 8)));
224
+ // ---- Pass 1: detect the opaque bounding box --------------------------------
225
+ // `skip=0` is REQUIRED: cropdetect defaults to skipping the first 2 frames, so
226
+ // on a single-frame still it would otherwise emit no crop line at all.
227
+ const detectArgs = [
228
+ "-hide_banner",
229
+ "-i", input.sourcePath,
230
+ "-frames:v", "1",
231
+ "-vf", `alphaextract,cropdetect=limit=${limit}:round=2:reset=0:skip=0`,
232
+ "-f", "null", "-"
233
+ ];
234
+ const detect = await runFfmpeg(ffmpeg, detectArgs);
235
+ const boxes = [...detect.stderr.matchAll(/crop=(\d+):(\d+):(-?\d+):(-?\d+)/g)];
236
+ const last = boxes[boxes.length - 1];
237
+ const writeArgs = (filter) => [
238
+ "-hide_banner", "-y",
239
+ "-i", input.sourcePath,
240
+ "-frames:v", "1",
241
+ "-vf", `${filter}format=rgba`,
242
+ ...(isWebp ? ["-c:v", "libwebp", "-lossless", "1", "-pix_fmt", "rgba"] : []),
243
+ input.outputPath
244
+ ];
245
+ let trimmed = false;
246
+ if (last) {
247
+ const w = Number(last[1]);
248
+ const h = Number(last[2]);
249
+ const x = Math.max(0, Number(last[3]));
250
+ const y = Math.max(0, Number(last[4]));
251
+ // Only treat it as a real trim when the box is meaningfully smaller than the
252
+ // source (cropdetect reports the full frame when there's nothing to cut).
253
+ const shrank = !orig || w < orig.width || h < orig.height;
254
+ if (w > 0 && h > 0 && shrank) {
255
+ let filter = `crop=${w}:${h}:${x}:${y},`;
256
+ if (pad > 0)
257
+ filter += `pad=${w + 2 * pad}:${h + 2 * pad}:${pad}:${pad}:color=0x00000000,`;
258
+ const run = await runFfmpeg(ffmpeg, writeArgs(filter));
259
+ if (run.code === 0 && existsSync(input.outputPath))
260
+ trimmed = true;
261
+ else
262
+ throw new Error(`Alpha trim failed (ffmpeg exit ${run.code})${tail(run.stderr)}.`);
263
+ }
264
+ }
265
+ if (!trimmed) {
266
+ // No detectable cutout (or nothing to cut) — re-encode as-is so the caller
267
+ // still gets a file at outputPath, optionally with the requested padding.
268
+ let filter = "";
269
+ if (pad > 0 && orig)
270
+ filter = `pad=${orig.width + 2 * pad}:${orig.height + 2 * pad}:${pad}:${pad}:color=0x00000000,`;
271
+ const run = await runFfmpeg(ffmpeg, writeArgs(filter));
272
+ if (run.code !== 0 || !existsSync(input.outputPath)) {
273
+ throw new Error(`Alpha trim (passthrough) failed (ffmpeg exit ${run.code})${tail(run.stderr)}.`);
274
+ }
275
+ }
276
+ const finalDims = (await probeImageDimensions(input.outputPath)) ?? { width: 0, height: 0 };
277
+ return {
278
+ outputPath: input.outputPath,
279
+ format,
280
+ width: finalDims.width,
281
+ height: finalDims.height,
282
+ origWidth: orig?.width ?? null,
283
+ origHeight: orig?.height ?? null,
284
+ trimmed
285
+ };
286
+ }
185
287
  /** Default output path next to a source, matching remove-background's naming. */
186
288
  export function defaultGreenscreenOutPath(sourcePath, mediaType, outputFormat) {
187
289
  const stem = path.basename(sourcePath).replace(/\.[^.]+$/, "");
@@ -20,8 +20,31 @@ const KEYFRAME_PRESETS = {
20
20
  "fly-right": [{ offset: 0, opacity: 0, translate_x: -40 }, { offset: 1, opacity: 1, translate_x: 0 }],
21
21
  "pop-in": [{ offset: 0, opacity: 0, scale: 0.6 }, { offset: 0.7, opacity: 1, scale: 1.05 }, { offset: 1, opacity: 1, scale: 1 }],
22
22
  "zoom-in": [{ offset: 0, scale: 1 }, { offset: 1, scale: 1.15 }],
23
+ "zoom-out": [{ offset: 0, scale: 1.15 }, { offset: 1, scale: 1 }],
23
24
  "spin-in": [{ offset: 0, opacity: 0, rotate: -90, scale: 0.8 }, { offset: 1, opacity: 1, rotate: 0, scale: 1 }],
24
- "pulse": [{ offset: 0, scale: 1 }, { offset: 0.5, scale: 1.08 }, { offset: 1, scale: 1 }]
25
+ "pulse": [{ offset: 0, scale: 1 }, { offset: 0.5, scale: 1.08 }, { offset: 1, scale: 1 }],
26
+ // Explainer-cutout motion (see the "Cutout graphics for explainers" recipe).
27
+ // scale is a multiplier; translate_x/_y are % of the layer's OWN box.
28
+ "grow": [{ offset: 0, scale: 0.85 }, { offset: 1, scale: 1.25 }],
29
+ "shrink": [{ offset: 0, scale: 1.25 }, { offset: 1, scale: 0.85 }],
30
+ // Attention shake — a quick horizontal jitter that settles (great for "look here").
31
+ "shake": [
32
+ { offset: 0, translate_x: 0 }, { offset: 0.15, translate_x: -4 }, { offset: 0.3, translate_x: 4 },
33
+ { offset: 0.45, translate_x: -3 }, { offset: 0.6, translate_x: 3 }, { offset: 0.8, translate_x: -1.5 },
34
+ { offset: 1, translate_x: 0 }
35
+ ],
36
+ // Idle "alive" bob — a gentle continuous float for a sticker that's on-stage.
37
+ "float": [{ offset: 0, translate_y: 0 }, { offset: 0.5, translate_y: -6 }, { offset: 1, translate_y: 0 }],
38
+ "wobble": [
39
+ { offset: 0, rotate: 0 }, { offset: 0.25, rotate: -4 }, { offset: 0.5, rotate: 0 },
40
+ { offset: 0.75, rotate: 4 }, { offset: 1, rotate: 0 }
41
+ ],
42
+ // Slide the cutout across its own footprint (chain/scale for bigger travel).
43
+ "drift-left": [{ offset: 0, translate_x: 60 }, { offset: 1, translate_x: -60 }],
44
+ "drift-right": [{ offset: 0, translate_x: -60 }, { offset: 1, translate_x: 60 }],
45
+ // Enter-from-offstage slides (opacity + a full own-box of travel).
46
+ "slide-in-left": [{ offset: 0, opacity: 0, translate_x: -120 }, { offset: 1, opacity: 1, translate_x: 0 }],
47
+ "slide-in-right": [{ offset: 0, opacity: 0, translate_x: 120 }, { offset: 1, opacity: 1, translate_x: 0 }]
25
48
  };
26
49
  const KEYFRAMES_HELP = `vidfarm keyframes — author a script-free CSS @keyframes animation on ONE layer (local file write)
27
50
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@officexapp/vidfarm-devcli",
3
- "version": "0.21.18",
3
+ "version": "0.21.19",
4
4
  "description": "Local bridge for the Vidfarm Trackpad Editor. `vidfarm serve <template_id>` boots the FULL editor on localhost (disk-backed records/storage, free in-process render); edit composition.html on disk (Claude Code, Codex, etc.) and the browser live-morphs it.",
5
5
  "type": "module",
6
6
  "bin": {