@officexapp/vidfarm-devcli 0.21.18 → 0.21.20
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.
- package/.agents/skills/vidfarm/SKILL.md +17 -0
- package/.agents/skills/vidfarm/recipes/cutout-graphics-for-explainers.md +89 -0
- package/.agents/skills/vidfarm/recipes/onboard-a-new-director.md +2 -1
- package/.agents/skills/vidfarm/references/automation-and-local-dev.md +3 -1
- package/.agents/skills/vidfarm/references/editor-workflows.md +26 -0
- package/SKILL.director.md +138 -2
- package/dist/src/cli.js +434 -1
- package/dist/src/devcli/greenscreen-local.js +149 -0
- package/dist/src/devcli/timeline-edit.js +24 -1
- package/package.json +1 -1
|
@@ -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,10 @@ 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
|
+
|
|
142
|
+
**Cost-saving move — mask illustrations OUT of a source image the director already has.** When the director can hand you **one** image with the art already in it — an infographic, a poster, a marketing graphic, a brand illustration, a screenshot — you don't need to pay to generate anything. `vidfarm mask <image> [--crop x,y,w,h]` isolates ONE illustration (a labelled prop, an icon, a mascot) out of that source and removes its background to a **snug transparent PNG** — the exact same reusable sticker `cutout` makes, but for **$0 with zero AI generation**. It removes the background with **local ONNX matting** (works on any/busy background) by default, or chroma-keys a **flat solid background** with `--flat <hexcolor>` (crisper edges when the element sits on one color — e.g. the cream paper behind an infographic's icons). Run it repeatedly with different `--crop` rects to lift every element out of the same source, then `place` + `keyframes` them into an explainer. **Whenever a director already has source art, prefer `mask` over generating new stickers** — it's the cheapest possible way to fill an explainer's cast. Same recipe: `recipes/cutout-graphics-for-explainers.md` (“Mask from an image you already have”).
|
|
143
|
+
|
|
127
144
|
**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
145
|
|
|
129
146
|
**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,89 @@
|
|
|
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.** Three paths, cheapest first:
|
|
12
|
+
|
|
13
|
+
- **Mask it out of a source image the director already has** (FREE, no account, no generation) — *the cheapest path, and the one to prefer whenever source art exists.* If the director hands you one image with the art already in it — an infographic, a poster, a marketing graphic, a brand illustration, a screenshot — lift each illustration straight out of it with `vidfarm mask`:
|
|
14
|
+
```
|
|
15
|
+
vidfarm mask ./infographic.png --crop 8%,52%,26%,22% --out blue-ticket-stack.png
|
|
16
|
+
vidfarm mask ./poster.png --crop 40,120,300,260 --out mascot.png
|
|
17
|
+
vidfarm mask https://…/graphic.png --out hero.png # whole-image foreground, no crop
|
|
18
|
+
```
|
|
19
|
+
`--crop x,y,w,h` (pixels **or** percents) isolates ONE element from a multi-illustration source **before** masking; run `mask` again with a different rect to grab the next one. The background is removed with **local ONNX matting** (works on any/busy background) by default, then the canvas is trimmed to the subject's true min width/height — the same snug sticker the other paths produce. If the element sits on a **flat solid color** (e.g. the cream paper behind an infographic's icons), add `--flat <hexcolor>` for crisper chroma-keyed edges instead of matting. `--keep-region <file>` saves the pre-mask crop so you can re-tune the box. See **"Mask from an image you already have"** below for the guided loop.
|
|
20
|
+
- **Generate + cut in one shot** (needs an image key; billed ~$0.01–0.05 per graphic) — when no source art exists:
|
|
21
|
+
```
|
|
22
|
+
vidfarm cutout --generate "a friendly robot mascot waving, flat vector style" --out robot.png
|
|
23
|
+
```
|
|
24
|
+
`--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.
|
|
25
|
+
- **Cut an existing graphic on a solid background** (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:
|
|
26
|
+
```
|
|
27
|
+
vidfarm cutout ./raw-graphic.png --out robot.png
|
|
28
|
+
vidfarm cutout https://…/graphic.png --preset white --out logo.png
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Knobs (`cutout`): `--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`. `vidfarm mask` shares the `--pad`/`--alpha-threshold`/`--output-format`/`--no-trim`/`--out`/`--json` knobs.
|
|
32
|
+
|
|
33
|
+
**Both are image-only.** A moving subject has no single bounding box — matte a video clip with `vidfarm remove-background <video>` or key a flat backdrop with `vidfarm remove-greenscreen <video>` (→ transparent WebM/mov).
|
|
34
|
+
|
|
35
|
+
**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`.
|
|
36
|
+
|
|
37
|
+
**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:
|
|
38
|
+
```
|
|
39
|
+
vidfarm place ./work --src ./robot.png --kind image --x 30 --y 20 --width 40 --at 0 --duration 6
|
|
40
|
+
vidfarm place ./work --src ./arrow.png --kind image --x 62 --y 45 --width 12 --at 1.5 --duration 4
|
|
41
|
+
```
|
|
42
|
+
`--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.
|
|
43
|
+
|
|
44
|
+
**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:
|
|
45
|
+
```
|
|
46
|
+
vidfarm keyframes ./work --layer robot --preset pop-in # enter: scale-up + fade
|
|
47
|
+
vidfarm keyframes ./work --layer arrow --preset shake # attention jitter → "look here"
|
|
48
|
+
vidfarm keyframes ./work --layer badge --preset grow --duration 0.6 # emphasis grow
|
|
49
|
+
vidfarm keyframes ./work --layer robot --preset float # idle "alive" bob
|
|
50
|
+
vidfarm keyframes ./work --layer icon --preset slide-in-left # slide in from offstage
|
|
51
|
+
```
|
|
52
|
+
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).
|
|
53
|
+
|
|
54
|
+
**Step 5 — Render & approve** as usual (`vidfarm render` → `vidfarm approve`).
|
|
55
|
+
|
|
56
|
+
### Mask from an image you already have (the $0 path)
|
|
57
|
+
|
|
58
|
+
This is the biggest cost saver in the whole explainer workflow: **if the director can give you one source image with the art already in it, you never pay to generate stickers.** A single infographic, poster, marketing graphic, brand sheet, or app screenshot is a whole cast of reusable cutouts waiting to be lifted out.
|
|
59
|
+
|
|
60
|
+
**When to reach for it.** Any time source art exists. Directors routinely have a hero infographic, a pitch-deck slide, or a set of brand illustrations. Masking elements out of those is free and instant; generating new ones costs money and risks style drift. Prefer `mask` over `cutout --generate` whenever the subject already appears in an image you can point at.
|
|
61
|
+
|
|
62
|
+
**The loop:**
|
|
63
|
+
|
|
64
|
+
1. **Look at the source and pick the elements.** Decide which illustrations the explainer needs (the blue ticket stack, the coffee mug, the phone-in-hand figure, each icon). Each will become its own transparent sticker.
|
|
65
|
+
2. **Isolate each one with `--crop`.** The rect is `x,y,w,h` in pixels **or** percents — percents are easiest by eye ("the blue stack is about 8% in from the left, 52% down, ~26% wide, ~22% tall"):
|
|
66
|
+
```
|
|
67
|
+
vidfarm mask ./infographic.png --crop 8%,52%,26%,22% --out blue-stack.png
|
|
68
|
+
vidfarm mask ./infographic.png --crop 38%,52%,26%,22% --out green-stack.png
|
|
69
|
+
vidfarm mask ./infographic.png --crop 68%,52%,26%,22% --out red-stack.png
|
|
70
|
+
```
|
|
71
|
+
Add `--keep-region region.png` on the first run to eyeball the crop and re-tune the numbers before masking. Skip `--crop` entirely to mask the whole image's foreground.
|
|
72
|
+
3. **Choose the background remover.** Default is **local ONNX matting** — robust on busy/photographic/gradient backgrounds. If the element sits on a **flat solid color** (an infographic's cream or white paper, a solid brand fill), `--flat <hexcolor>` chroma-keys it for **cleaner, sharper edges** — often the better call for flat-design illustrations:
|
|
73
|
+
```
|
|
74
|
+
vidfarm mask ./infographic.png --crop 8%,52%,26%,22% --flat "#f6efe0" --out blue-stack.png
|
|
75
|
+
```
|
|
76
|
+
`--tolerance`/`--softness` tune the flat key if edges fringe or the fill isn't perfectly uniform.
|
|
77
|
+
4. **Confirm each is clean.** Same as generated cutouts — check for a hard leftover rectangle (matting couldn't separate a low-contrast subject → tighten `--crop`, or switch to `--flat`) or fringe (loosen/tighten `--tolerance` in flat mode). Cutouts are cheap to redo.
|
|
78
|
+
5. **Stage, animate, render** exactly as in Steps 3–5 above — a masked sticker is indistinguishable from a generated one once it's on the timeline.
|
|
79
|
+
|
|
80
|
+
Everything here is **local and free** — crop, matte/key, and trim are all `ffmpeg` + the bundled ONNX engine. No wallet, no account, no generation call.
|
|
81
|
+
|
|
82
|
+
### Why cutouts beat full-frame AI images for explainers
|
|
83
|
+
|
|
84
|
+
- **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.
|
|
85
|
+
- **Reusable & cheap** — generate the mascot once, reuse it in every scene; the cut+trim is free and local.
|
|
86
|
+
- **Composable motion** — each element animates independently (the arrow shakes while the hero floats and a label grows), which is exactly the explainer feel.
|
|
87
|
+
- **Small files** — trimming a mostly-empty plate typically cuts 80–95% of the pixel area, so the composition stays light.
|
|
88
|
+
|
|
89
|
+
`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.
|
|
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,8 @@ 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`. |
|
|
147
|
+
| `vidfarm mask <image\|url> [--crop x,y,w,h] [--flat <hex>] [--pad <px>] [--alpha-threshold <n>] [--no-trim] [--output-format png\|webp] [--keep-region <f>] [--out <f>]` | **local, free** (no job) — ffmpeg crop + ONNX matting (or ffmpeg chroma-key) + `cropdetect` trim | **Lift an illustration OUT of an image you already have** (infographic / poster / marketing graphic / brand sheet / screenshot) → snug transparent PNG, the same reusable explainer sticker `cutout` makes but with **$0 and zero AI generation** — the cost-saving move whenever source art exists. `--crop x,y,w,h` (pixels **or** %) isolates ONE element from a multi-illustration source before masking (re-run with different rects to grab each). Background removed by **local ONNX matting** (any/busy background) by default, or **`--flat <hexcolor>`** chroma-keys a solid fill for crisper edges (an infographic's cream/white paper); then trims to the subject's true min width/height. **IMAGE-only** (matte a clip with `remove-background`). Aliases: `isolate`, `extract`. See recipe `cutout-graphics-for-explainers.md` → "Mask from an image you already have". |
|
|
146
148
|
| `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
149
|
| `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
150
|
| `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 +205,7 @@ The licensed harness also carries the **generative build workflow** guidance (ch
|
|
|
203
205
|
|
|
204
206
|
## Cost mode — the devcli's money-saving guardrail
|
|
205
207
|
|
|
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.
|
|
208
|
+
`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
209
|
|
|
208
210
|
- **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
211
|
- **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,10 @@ 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
|
+
|
|
142
|
+
**Cost-saving move — mask illustrations OUT of a source image the director already has.** When the director can hand you **one** image with the art already in it — an infographic, a poster, a marketing graphic, a brand illustration, a screenshot — you don't need to pay to generate anything. `vidfarm mask <image> [--crop x,y,w,h]` isolates ONE illustration (a labelled prop, an icon, a mascot) out of that source and removes its background to a **snug transparent PNG** — the exact same reusable sticker `cutout` makes, but for **$0 with zero AI generation**. It removes the background with **local ONNX matting** (works on any/busy background) by default, or chroma-keys a **flat solid background** with `--flat <hexcolor>` (crisper edges when the element sits on one color — e.g. the cream paper behind an infographic's icons). Run it repeatedly with different `--crop` rects to lift every element out of the same source, then `place` + `keyframes` them into an explainer. **Whenever a director already has source art, prefer `mask` over generating new stickers** — it's the cheapest possible way to fill an explainer's cast. Same recipe: `recipes/cutout-graphics-for-explainers.md` (“Mask from an image you already have”).
|
|
143
|
+
|
|
127
144
|
**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
145
|
|
|
129
146
|
**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 +938,32 @@ Beyond the Ken Burns / transition / animated-caption presets, the copilot can ha
|
|
|
921
938
|
|
|
922
939
|
**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
940
|
|
|
941
|
+
### Cutout graphics for explainers
|
|
942
|
+
|
|
943
|
+
**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.
|
|
944
|
+
|
|
945
|
+
**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.
|
|
946
|
+
|
|
947
|
+
**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.
|
|
948
|
+
|
|
949
|
+
**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:
|
|
950
|
+
|
|
951
|
+
| Move | Preset | Feel / use |
|
|
952
|
+
|---|---|---|
|
|
953
|
+
| **Zoom** | `zoom-in` / `zoom-out` | slow scale creep over the clip (subtle life, or a push-in for focus) |
|
|
954
|
+
| **Grow** | `grow` / `shrink` | scale 0.85→1.25 emphasis (a badge/number that swells on entry) |
|
|
955
|
+
| **Pop entry** | `pop-in` | scale-up + fade — the default "element appears" |
|
|
956
|
+
| **Shake** | `shake` | quick horizontal jitter that settles → "look here" / error buzz |
|
|
957
|
+
| **Float** | `float` | gentle vertical bob — an idle "alive" sticker on stage |
|
|
958
|
+
| **Wobble** | `wobble` | rotate ±4° oscillation — playful mascot idle |
|
|
959
|
+
| **Move across** | `drift-left` / `drift-right` | slide across the layer's own footprint (chain/scale for bigger travel) |
|
|
960
|
+
| **Slide in** | `slide-in-left` / `slide-in-right` | enter from offstage with a fade |
|
|
961
|
+
| **Spin in** | `spin-in` | rotate + scale entry |
|
|
962
|
+
|
|
963
|
+
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.
|
|
964
|
+
|
|
965
|
+
**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.
|
|
966
|
+
|
|
924
967
|
### Local file paths as media (skip the S3 upload)
|
|
925
968
|
|
|
926
969
|
`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 +1249,8 @@ The licensed harness also carries the **generative build workflow** guidance (ch
|
|
|
1206
1249
|
| `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
1250
|
| `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
1251
|
| `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`. |
|
|
1252
|
+
| `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`. |
|
|
1253
|
+
| `vidfarm mask <image\|url> [--crop x,y,w,h] [--flat <hex>] [--pad <px>] [--alpha-threshold <n>] [--no-trim] [--output-format png\|webp] [--keep-region <f>] [--out <f>]` | **local, free** (no job) — ffmpeg crop + ONNX matting (or ffmpeg chroma-key) + `cropdetect` trim | **Lift an illustration OUT of an image you already have** (infographic / poster / marketing graphic / brand sheet / screenshot) → snug transparent PNG, the same reusable explainer sticker `cutout` makes but with **$0 and zero AI generation** — the cost-saving move whenever source art exists. `--crop x,y,w,h` (pixels **or** %) isolates ONE element from a multi-illustration source before masking (re-run with different rects to grab each). Background removed by **local ONNX matting** (any/busy background) by default, or **`--flat <hexcolor>`** chroma-keys a solid fill for crisper edges (an infographic's cream/white paper); then trims to the subject's true min width/height. **IMAGE-only** (matte a clip with `remove-background`). Aliases: `isolate`, `extract`. See recipe `cutout-graphics-for-explainers.md` → "Mask from an image you already have". |
|
|
1209
1254
|
| `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
1255
|
| `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
1256
|
| `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 +1311,7 @@ The licensed harness also carries the **generative build workflow** guidance (ch
|
|
|
1266
1311
|
|
|
1267
1312
|
## Cost mode — the devcli's money-saving guardrail
|
|
1268
1313
|
|
|
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.
|
|
1314
|
+
`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
1315
|
|
|
1271
1316
|
- **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
1317
|
- **hybrid** *(default recommendation)* — billed ops run but print a one-line cost notice each.
|
|
@@ -1777,6 +1822,96 @@ Use this when a coding agent is doing the work locally or the user wants a repro
|
|
|
1777
1822
|
|
|
1778
1823
|
Prefer this path for batch work, CI-like edits, or when the user wants free local rendering through `vidfarm serve`.
|
|
1779
1824
|
|
|
1825
|
+
## Recipe: Cutout graphics for explainer videos
|
|
1826
|
+
|
|
1827
|
+
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).
|
|
1828
|
+
|
|
1829
|
+
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.
|
|
1830
|
+
|
|
1831
|
+
### The guided sequence (prompt harness)
|
|
1832
|
+
|
|
1833
|
+
**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.
|
|
1834
|
+
|
|
1835
|
+
**Step 1 — Make each cutout.** Three paths, cheapest first:
|
|
1836
|
+
|
|
1837
|
+
- **Mask it out of a source image the director already has** (FREE, no account, no generation) — *the cheapest path, and the one to prefer whenever source art exists.* If the director hands you one image with the art already in it — an infographic, a poster, a marketing graphic, a brand illustration, a screenshot — lift each illustration straight out of it with `vidfarm mask`:
|
|
1838
|
+
```
|
|
1839
|
+
vidfarm mask ./infographic.png --crop 8%,52%,26%,22% --out blue-ticket-stack.png
|
|
1840
|
+
vidfarm mask ./poster.png --crop 40,120,300,260 --out mascot.png
|
|
1841
|
+
vidfarm mask https://…/graphic.png --out hero.png # whole-image foreground, no crop
|
|
1842
|
+
```
|
|
1843
|
+
`--crop x,y,w,h` (pixels **or** percents) isolates ONE element from a multi-illustration source **before** masking; run `mask` again with a different rect to grab the next one. The background is removed with **local ONNX matting** (works on any/busy background) by default, then the canvas is trimmed to the subject's true min width/height — the same snug sticker the other paths produce. If the element sits on a **flat solid color** (e.g. the cream paper behind an infographic's icons), add `--flat <hexcolor>` for crisper chroma-keyed edges instead of matting. `--keep-region <file>` saves the pre-mask crop so you can re-tune the box. See **"Mask from an image you already have"** below for the guided loop.
|
|
1844
|
+
- **Generate + cut in one shot** (needs an image key; billed ~$0.01–0.05 per graphic) — when no source art exists:
|
|
1845
|
+
```
|
|
1846
|
+
vidfarm cutout --generate "a friendly robot mascot waving, flat vector style" --out robot.png
|
|
1847
|
+
```
|
|
1848
|
+
`--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.
|
|
1849
|
+
- **Cut an existing graphic on a solid background** (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:
|
|
1850
|
+
```
|
|
1851
|
+
vidfarm cutout ./raw-graphic.png --out robot.png
|
|
1852
|
+
vidfarm cutout https://…/graphic.png --preset white --out logo.png
|
|
1853
|
+
```
|
|
1854
|
+
|
|
1855
|
+
Knobs (`cutout`): `--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`. `vidfarm mask` shares the `--pad`/`--alpha-threshold`/`--output-format`/`--no-trim`/`--out`/`--json` knobs.
|
|
1856
|
+
|
|
1857
|
+
**Both are image-only.** A moving subject has no single bounding box — matte a video clip with `vidfarm remove-background <video>` or key a flat backdrop with `vidfarm remove-greenscreen <video>` (→ transparent WebM/mov).
|
|
1858
|
+
|
|
1859
|
+
**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`.
|
|
1860
|
+
|
|
1861
|
+
**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:
|
|
1862
|
+
```
|
|
1863
|
+
vidfarm place ./work --src ./robot.png --kind image --x 30 --y 20 --width 40 --at 0 --duration 6
|
|
1864
|
+
vidfarm place ./work --src ./arrow.png --kind image --x 62 --y 45 --width 12 --at 1.5 --duration 4
|
|
1865
|
+
```
|
|
1866
|
+
`--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.
|
|
1867
|
+
|
|
1868
|
+
**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:
|
|
1869
|
+
```
|
|
1870
|
+
vidfarm keyframes ./work --layer robot --preset pop-in # enter: scale-up + fade
|
|
1871
|
+
vidfarm keyframes ./work --layer arrow --preset shake # attention jitter → "look here"
|
|
1872
|
+
vidfarm keyframes ./work --layer badge --preset grow --duration 0.6 # emphasis grow
|
|
1873
|
+
vidfarm keyframes ./work --layer robot --preset float # idle "alive" bob
|
|
1874
|
+
vidfarm keyframes ./work --layer icon --preset slide-in-left # slide in from offstage
|
|
1875
|
+
```
|
|
1876
|
+
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).
|
|
1877
|
+
|
|
1878
|
+
**Step 5 — Render & approve** as usual (`vidfarm render` → `vidfarm approve`).
|
|
1879
|
+
|
|
1880
|
+
### Mask from an image you already have (the $0 path)
|
|
1881
|
+
|
|
1882
|
+
This is the biggest cost saver in the whole explainer workflow: **if the director can give you one source image with the art already in it, you never pay to generate stickers.** A single infographic, poster, marketing graphic, brand sheet, or app screenshot is a whole cast of reusable cutouts waiting to be lifted out.
|
|
1883
|
+
|
|
1884
|
+
**When to reach for it.** Any time source art exists. Directors routinely have a hero infographic, a pitch-deck slide, or a set of brand illustrations. Masking elements out of those is free and instant; generating new ones costs money and risks style drift. Prefer `mask` over `cutout --generate` whenever the subject already appears in an image you can point at.
|
|
1885
|
+
|
|
1886
|
+
**The loop:**
|
|
1887
|
+
|
|
1888
|
+
1. **Look at the source and pick the elements.** Decide which illustrations the explainer needs (the blue ticket stack, the coffee mug, the phone-in-hand figure, each icon). Each will become its own transparent sticker.
|
|
1889
|
+
2. **Isolate each one with `--crop`.** The rect is `x,y,w,h` in pixels **or** percents — percents are easiest by eye ("the blue stack is about 8% in from the left, 52% down, ~26% wide, ~22% tall"):
|
|
1890
|
+
```
|
|
1891
|
+
vidfarm mask ./infographic.png --crop 8%,52%,26%,22% --out blue-stack.png
|
|
1892
|
+
vidfarm mask ./infographic.png --crop 38%,52%,26%,22% --out green-stack.png
|
|
1893
|
+
vidfarm mask ./infographic.png --crop 68%,52%,26%,22% --out red-stack.png
|
|
1894
|
+
```
|
|
1895
|
+
Add `--keep-region region.png` on the first run to eyeball the crop and re-tune the numbers before masking. Skip `--crop` entirely to mask the whole image's foreground.
|
|
1896
|
+
3. **Choose the background remover.** Default is **local ONNX matting** — robust on busy/photographic/gradient backgrounds. If the element sits on a **flat solid color** (an infographic's cream or white paper, a solid brand fill), `--flat <hexcolor>` chroma-keys it for **cleaner, sharper edges** — often the better call for flat-design illustrations:
|
|
1897
|
+
```
|
|
1898
|
+
vidfarm mask ./infographic.png --crop 8%,52%,26%,22% --flat "#f6efe0" --out blue-stack.png
|
|
1899
|
+
```
|
|
1900
|
+
`--tolerance`/`--softness` tune the flat key if edges fringe or the fill isn't perfectly uniform.
|
|
1901
|
+
4. **Confirm each is clean.** Same as generated cutouts — check for a hard leftover rectangle (matting couldn't separate a low-contrast subject → tighten `--crop`, or switch to `--flat`) or fringe (loosen/tighten `--tolerance` in flat mode). Cutouts are cheap to redo.
|
|
1902
|
+
5. **Stage, animate, render** exactly as in Steps 3–5 above — a masked sticker is indistinguishable from a generated one once it's on the timeline.
|
|
1903
|
+
|
|
1904
|
+
Everything here is **local and free** — crop, matte/key, and trim are all `ffmpeg` + the bundled ONNX engine. No wallet, no account, no generation call.
|
|
1905
|
+
|
|
1906
|
+
### Why cutouts beat full-frame AI images for explainers
|
|
1907
|
+
|
|
1908
|
+
- **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.
|
|
1909
|
+
- **Reusable & cheap** — generate the mascot once, reuse it in every scene; the cut+trim is free and local.
|
|
1910
|
+
- **Composable motion** — each element animates independently (the arrow shakes while the hero floats and a label grows), which is exactly the explainer feel.
|
|
1911
|
+
- **Small files** — trimming a mostly-empty plate typically cuts 80–95% of the pixel area, so the composition stays light.
|
|
1912
|
+
|
|
1913
|
+
`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.
|
|
1914
|
+
|
|
1780
1915
|
## Recipe: Onboard a New Director
|
|
1781
1916
|
|
|
1782
1917
|
Use this only when the director signals they do not know where to start.
|
|
@@ -1787,6 +1922,7 @@ Use this only when the director signals they do not know where to start.
|
|
|
1787
1922
|
4. Ask about brand assets, demos, and recurring characters; organize them in My Files.
|
|
1788
1923
|
5. Ask about budget and map it to the cost spectrum before recommending expensive generation.
|
|
1789
1924
|
6. Search for the best matching templates and fork one strong default.
|
|
1790
|
-
7.
|
|
1925
|
+
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.
|
|
1926
|
+
8. Transition into the ordinary template-editing workflow.
|
|
1791
1927
|
|
|
1792
1928
|
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, cropImageRegion } 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,37 @@ 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)
|
|
239
|
+
mask <image|url> Lift an illustration OUT of an image you already have
|
|
240
|
+
(an infographic, poster, marketing image, screenshot)
|
|
241
|
+
→ snug transparent PNG you can reuse as a FREE explainer
|
|
242
|
+
sticker. Local, free, no account. Image-only.
|
|
243
|
+
(aliases: isolate, extract)
|
|
244
|
+
--crop x,y,w,h Isolate ONE element first (pixels or %, e.g.
|
|
245
|
+
40,120,300,260 or 8%,55%,26%,22%) before masking
|
|
246
|
+
(default) Remove the background with local ONNX matting — works on
|
|
247
|
+
any/busy background
|
|
248
|
+
--flat <hexcolor> Instead chroma-key a FLAT solid background (crisper edges
|
|
249
|
+
when the region sits on one color; --preset/--key-color too)
|
|
250
|
+
--tolerance/--softness Key radius / edge feather (flat-key mode)
|
|
251
|
+
--pad <px> Keep N px of transparent breathing room around the subject
|
|
252
|
+
--alpha-threshold <n> Alpha 0..255 below which edges count as empty (default 8)
|
|
253
|
+
--no-trim Mask only; don't shrink to the bounding box
|
|
254
|
+
--output-format png|webp Sticker format (default png)
|
|
255
|
+
--keep-region <file> Also save the cropped (pre-mask) region
|
|
256
|
+
--out <file> Write the sticker here (default <source>.cutout.png)
|
|
219
257
|
place <dir|composition.html> Insert media into a local composition
|
|
220
258
|
(fill a gap, replace a scene, or overlay an AUDIO
|
|
221
259
|
track) — same clip markup the browser editor makes;
|
|
@@ -800,6 +838,15 @@ async function main() {
|
|
|
800
838
|
case "greenscreen":
|
|
801
839
|
await runRemoveGreenscreenCommand(rest);
|
|
802
840
|
return;
|
|
841
|
+
case "cutout":
|
|
842
|
+
case "sticker":
|
|
843
|
+
await runCutoutCommand(rest);
|
|
844
|
+
return;
|
|
845
|
+
case "mask":
|
|
846
|
+
case "isolate":
|
|
847
|
+
case "extract":
|
|
848
|
+
await runMaskCommand(rest);
|
|
849
|
+
return;
|
|
803
850
|
case "tts":
|
|
804
851
|
await runTtsCommand(rest);
|
|
805
852
|
return;
|
|
@@ -4601,6 +4648,378 @@ async function runLocalGreenscreen(ctx, values, sourceArg, presetRaw) {
|
|
|
4601
4648
|
rmSync(downloadDir, { recursive: true, force: true });
|
|
4602
4649
|
}
|
|
4603
4650
|
}
|
|
4651
|
+
// ── cutout: generate → key greenscreen → alpha-trim to min bounding box ───────
|
|
4652
|
+
// The one-shot "make a transparent explainer sticker" verb. Three mechanical
|
|
4653
|
+
// steps that used to be run by hand (generate an AI graphic on a green plate →
|
|
4654
|
+
// `remove-greenscreen` → shrink the mostly-empty canvas to the tight cutout)
|
|
4655
|
+
// fused into one local, free, ffmpeg-only pipeline. IMAGE-only (a moving subject
|
|
4656
|
+
// has no single bounding box — key a clip with `vidfarm remove-greenscreen`).
|
|
4657
|
+
//
|
|
4658
|
+
// --generate "<prompt>" AI-generates the source graphic first, auto-appending a
|
|
4659
|
+
// flat chroma-plate instruction that matches the chosen key color so the keyer
|
|
4660
|
+
// has a clean edge. Without --generate it keys+trims an existing file/url.
|
|
4661
|
+
// Output is a snug PNG/WebP whose pixel dimensions ARE the cutout — ready to
|
|
4662
|
+
// scale/position precisely on an explainer composition (see the skill's
|
|
4663
|
+
// "Cutout graphics for explainers" recipe for placement + zoom/grow/shake/move).
|
|
4664
|
+
/** Build a chroma-plate instruction so an AI-generated graphic keys cleanly.
|
|
4665
|
+
* Matches the color to the chosen key so the keyer has a flat edge to cut. */
|
|
4666
|
+
function cutoutPlateInstruction(keyColorHex) {
|
|
4667
|
+
const named = /^#?00ff00$/i.test(keyColorHex) ? "pure chroma-key green"
|
|
4668
|
+
: /^#?0047bb$/i.test(keyColorHex) ? "solid chroma-key blue"
|
|
4669
|
+
: /^#?ff00ff$/i.test(keyColorHex) ? "solid magenta"
|
|
4670
|
+
: `a solid flat ${keyColorHex}`;
|
|
4671
|
+
return (`Isolated subject on a completely flat, evenly-lit ${named} (${keyColorHex}) background — ` +
|
|
4672
|
+
`like a green-screen plate. The subject must contain NONE of that background color anywhere on it. ` +
|
|
4673
|
+
`Center the subject with generous empty margin on all sides, no drop shadow or reflection cast on the ` +
|
|
4674
|
+
`background, crisp clean edges, sticker/cutout style, single subject, no text.`);
|
|
4675
|
+
}
|
|
4676
|
+
async function runCutoutCommand(argv) {
|
|
4677
|
+
const parsed = parseArgs({
|
|
4678
|
+
args: argv,
|
|
4679
|
+
allowPositionals: true,
|
|
4680
|
+
options: {
|
|
4681
|
+
...commonOptions(),
|
|
4682
|
+
source: { type: "string" },
|
|
4683
|
+
// Optional AI generation of the source graphic (billed image primitive).
|
|
4684
|
+
generate: { type: "string" },
|
|
4685
|
+
ref: { type: "string", multiple: true },
|
|
4686
|
+
"aspect-ratio": { type: "string" },
|
|
4687
|
+
provider: { type: "string" },
|
|
4688
|
+
model: { type: "string" },
|
|
4689
|
+
// Keyer knobs (mirror remove-greenscreen).
|
|
4690
|
+
preset: { type: "string" },
|
|
4691
|
+
"key-color": { type: "string" },
|
|
4692
|
+
tolerance: { type: "string" },
|
|
4693
|
+
softness: { type: "string" },
|
|
4694
|
+
"no-despill": { type: "boolean", default: false },
|
|
4695
|
+
// Trim knobs.
|
|
4696
|
+
"no-trim": { type: "boolean", default: false },
|
|
4697
|
+
pad: { type: "string" },
|
|
4698
|
+
"alpha-threshold": { type: "string" },
|
|
4699
|
+
"output-format": { type: "string" },
|
|
4700
|
+
out: { type: "string" },
|
|
4701
|
+
"keep-plate": { type: "string" },
|
|
4702
|
+
tracer: { type: "string" }
|
|
4703
|
+
}
|
|
4704
|
+
});
|
|
4705
|
+
const ctx = commonContext(parsed.values);
|
|
4706
|
+
// Cutout is a local, ffmpeg-only pipeline (key + trim). Fail early & clearly.
|
|
4707
|
+
if (!(await localGreenscreenAvailable())) {
|
|
4708
|
+
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`.");
|
|
4709
|
+
}
|
|
4710
|
+
const presetRaw = parsed.values.preset?.trim().toLowerCase();
|
|
4711
|
+
if (presetRaw && !(presetRaw in GREENSCREEN_PRESETS)) {
|
|
4712
|
+
throw new Error(`Unknown --preset "${presetRaw}". Choose one of: ${Object.keys(GREENSCREEN_PRESETS).join(", ")}, or pass --key-color <hex>.`);
|
|
4713
|
+
}
|
|
4714
|
+
const preset = presetRaw ? GREENSCREEN_PRESETS[presetRaw] : undefined;
|
|
4715
|
+
const keyColor = parsed.values["key-color"] ?? preset?.key_color ?? "#00FF00";
|
|
4716
|
+
const tolerance = parsed.values.tolerance !== undefined ? Number(parsed.values.tolerance) : preset?.tolerance;
|
|
4717
|
+
const softness = parsed.values.softness !== undefined ? Number(parsed.values.softness) : preset?.softness;
|
|
4718
|
+
const despill = !parsed.values["no-despill"];
|
|
4719
|
+
const outputFormat = parsed.values["output-format"]?.trim().toLowerCase();
|
|
4720
|
+
const outExt = outputFormat === "webp" ? "webp" : "png";
|
|
4721
|
+
const generatePrompt = parsed.values.generate?.trim();
|
|
4722
|
+
const sourceArg = parsed.values.source ?? parsed.positionals[0];
|
|
4723
|
+
if (!generatePrompt && !sourceArg) {
|
|
4724
|
+
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.");
|
|
4725
|
+
}
|
|
4726
|
+
const work = mkdtempSync(path.join(tmpdir(), "vidfarm-cutout-"));
|
|
4727
|
+
let sourcePath;
|
|
4728
|
+
let stemHint;
|
|
4729
|
+
try {
|
|
4730
|
+
// ---- 1. Obtain the source graphic (generate OR existing file/url) --------
|
|
4731
|
+
if (generatePrompt) {
|
|
4732
|
+
guardBilled(ctx, {
|
|
4733
|
+
label: "AI image generation (cutout plate)",
|
|
4734
|
+
estimate: "cheap, ~$0.01–$0.05",
|
|
4735
|
+
freeAlternative: "pass an existing graphic instead — `vidfarm cutout <image|url>` keys+trims for $0.00"
|
|
4736
|
+
});
|
|
4737
|
+
const refs = await resolveReferenceUrls(ctx, parsed.values.ref);
|
|
4738
|
+
const fullPrompt = `${generatePrompt}. ${cutoutPlateInstruction(keyColor)}`;
|
|
4739
|
+
const payload = { prompt: fullPrompt };
|
|
4740
|
+
payload.aspect_ratio = parsed.values["aspect-ratio"] ?? "1:1";
|
|
4741
|
+
if (parsed.values.provider)
|
|
4742
|
+
payload.provider = parsed.values.provider;
|
|
4743
|
+
if (parsed.values.model)
|
|
4744
|
+
payload.model = parsed.values.model;
|
|
4745
|
+
if (refs.length)
|
|
4746
|
+
payload.prompt_attachments = refs.slice(0, 16);
|
|
4747
|
+
const tracer = parsed.values.tracer ?? `devcli-cutout-${Date.now().toString(36)}`;
|
|
4748
|
+
if (!ctx.json)
|
|
4749
|
+
console.log(`${DIM}Generating the cutout graphic on a ${keyColor} plate…${RESET}`);
|
|
4750
|
+
const submit = await apiRequest({ method: "POST", host: ctx.host, path: "/api/v1/primitives/images/generate", auth: ctx.auth, body: { tracer, payload } });
|
|
4751
|
+
assertApiOk(submit, "cutout generate");
|
|
4752
|
+
const jobId = submit.json?.job_id;
|
|
4753
|
+
if (!jobId)
|
|
4754
|
+
throw new Error("cutout generate: no job id returned.");
|
|
4755
|
+
const job = await pollPrimitiveJob(ctx, jobId);
|
|
4756
|
+
const mediaUrl = resolveJobMediaUrl(job);
|
|
4757
|
+
if (!mediaUrl)
|
|
4758
|
+
throw new Error(`cutout generate: job ${jobId} produced no image (${String(job?.status ?? "unknown")}).`);
|
|
4759
|
+
sourcePath = path.join(work, "plate.png");
|
|
4760
|
+
await downloadUrlToFile(mediaUrl, sourcePath);
|
|
4761
|
+
stemHint = "cutout";
|
|
4762
|
+
if (parsed.values["keep-plate"]) {
|
|
4763
|
+
const keepPath = path.resolve(process.cwd(), String(parsed.values["keep-plate"]));
|
|
4764
|
+
mkdirSync(path.dirname(keepPath), { recursive: true });
|
|
4765
|
+
await downloadUrlToFile(mediaUrl, keepPath);
|
|
4766
|
+
if (!ctx.json)
|
|
4767
|
+
console.log(`${DIM}Saved the pre-key plate to ${keepPath}${RESET}`);
|
|
4768
|
+
}
|
|
4769
|
+
}
|
|
4770
|
+
else {
|
|
4771
|
+
const directPath = path.resolve(process.cwd(), sourceArg);
|
|
4772
|
+
const isLocalFile = !/^https?:\/\//i.test(sourceArg) && existsSync(directPath);
|
|
4773
|
+
if (isLocalFile) {
|
|
4774
|
+
if (/\.(mp4|mov|webm|m4v|mkv)(\?|#|$)/i.test(directPath)) {
|
|
4775
|
+
throw new Error("cutout is image-only (a moving subject has no single bounding box). Key a video with `vidfarm remove-greenscreen <video>`.");
|
|
4776
|
+
}
|
|
4777
|
+
sourcePath = directPath;
|
|
4778
|
+
stemHint = path.basename(directPath).replace(/\.[^.]+$/, "");
|
|
4779
|
+
}
|
|
4780
|
+
else {
|
|
4781
|
+
const sourceUrl = await resolveSingleMediaUrl(ctx, sourceArg);
|
|
4782
|
+
if (/\.(mp4|mov|webm|m4v|mkv)(\?|#|$)/i.test(sourceUrl)) {
|
|
4783
|
+
throw new Error("cutout is image-only. Key a video with `vidfarm remove-greenscreen <video>`.");
|
|
4784
|
+
}
|
|
4785
|
+
sourcePath = path.join(work, path.basename(new URL(sourceUrl).pathname) || "source.png");
|
|
4786
|
+
if (!ctx.json)
|
|
4787
|
+
console.log(`${DIM}Downloading source…${RESET}`);
|
|
4788
|
+
await downloadUrlToFile(sourceUrl, sourcePath);
|
|
4789
|
+
stemHint = "cutout";
|
|
4790
|
+
}
|
|
4791
|
+
}
|
|
4792
|
+
// ---- 2. Key out the flat plate → transparent PNG ------------------------
|
|
4793
|
+
const keyedPath = path.join(work, "keyed.png");
|
|
4794
|
+
if (!ctx.json)
|
|
4795
|
+
console.log(`${DIM}Keying out the ${presetRaw ?? keyColor} background locally with ffmpeg (free)…${RESET}`);
|
|
4796
|
+
await removeGreenscreenLocal({ sourcePath, mediaType: "image", outputPath: keyedPath, keyColor, tolerance, softness, despill });
|
|
4797
|
+
// ---- 3. Trim to the tight cutout bounding box ---------------------------
|
|
4798
|
+
const outPath = parsed.values.out
|
|
4799
|
+
? path.resolve(process.cwd(), String(parsed.values.out))
|
|
4800
|
+
: path.resolve(process.cwd(), `${stemHint}.cutout.${outExt}`);
|
|
4801
|
+
mkdirSync(path.dirname(outPath), { recursive: true });
|
|
4802
|
+
const noTrim = Boolean(parsed.values["no-trim"]);
|
|
4803
|
+
const padPx = parsed.values.pad !== undefined ? Number(parsed.values.pad) : 0;
|
|
4804
|
+
const alphaThreshold = parsed.values["alpha-threshold"] !== undefined ? Number(parsed.values["alpha-threshold"]) : undefined;
|
|
4805
|
+
let trim;
|
|
4806
|
+
if (noTrim) {
|
|
4807
|
+
// Straight copy of the keyed still into the requested format (no shrink).
|
|
4808
|
+
trim = await trimTransparentBorders({ sourcePath: keyedPath, outputPath: outPath, padPx, alphaThreshold: 999 });
|
|
4809
|
+
}
|
|
4810
|
+
else {
|
|
4811
|
+
if (!ctx.json)
|
|
4812
|
+
console.log(`${DIM}Trimming transparent margins to the cutout's true size…${RESET}`);
|
|
4813
|
+
trim = await trimTransparentBorders({ sourcePath: keyedPath, outputPath: outPath, padPx, alphaThreshold });
|
|
4814
|
+
}
|
|
4815
|
+
const shrinkPct = trim.origWidth && trim.origHeight && trim.width && trim.height
|
|
4816
|
+
? Math.round((1 - (trim.width * trim.height) / (trim.origWidth * trim.origHeight)) * 100)
|
|
4817
|
+
: null;
|
|
4818
|
+
if (ctx.json) {
|
|
4819
|
+
printJson({
|
|
4820
|
+
ok: true,
|
|
4821
|
+
target: "local",
|
|
4822
|
+
out: trim.outputPath,
|
|
4823
|
+
format: trim.format,
|
|
4824
|
+
width: trim.width,
|
|
4825
|
+
height: trim.height,
|
|
4826
|
+
source_width: trim.origWidth,
|
|
4827
|
+
source_height: trim.origHeight,
|
|
4828
|
+
trimmed: trim.trimmed,
|
|
4829
|
+
area_reduced_pct: shrinkPct,
|
|
4830
|
+
bytes: safeSize(trim.outputPath),
|
|
4831
|
+
key_color: keyColor
|
|
4832
|
+
});
|
|
4833
|
+
}
|
|
4834
|
+
else {
|
|
4835
|
+
console.log(`${GREEN}Cutout ready:${RESET} ${trim.outputPath} ${DIM}(${trim.width}×${trim.height}, ${formatBytes(safeSize(trim.outputPath))}${trim.trimmed && shrinkPct !== null ? `, −${shrinkPct}% area` : ""})${RESET}`);
|
|
4836
|
+
if (!trim.trimmed && !noTrim)
|
|
4837
|
+
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}`);
|
|
4838
|
+
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}`);
|
|
4839
|
+
}
|
|
4840
|
+
}
|
|
4841
|
+
finally {
|
|
4842
|
+
rmSync(work, { recursive: true, force: true });
|
|
4843
|
+
}
|
|
4844
|
+
}
|
|
4845
|
+
// ── mask: isolate an illustration OUT of an existing image → transparent sticker
|
|
4846
|
+
// The "reuse what you already have" sibling of `cutout`. Where `cutout` keys a
|
|
4847
|
+
// FLAT chroma plate (a graphic made on a solid background, or AI-generated on
|
|
4848
|
+
// one), `mask` lifts a subject out of an ARBITRARY, busy source the user already
|
|
4849
|
+
// owns — an infographic, a poster, a marketing image, a screenshot — so its
|
|
4850
|
+
// illustrations become free, reusable explainer stickers instead of paying to
|
|
4851
|
+
// AI-generate each one. Two mechanisms:
|
|
4852
|
+
// • default → local ONNX matting (the `remove-background` engine): separates
|
|
4853
|
+
// the foreground from any background, flat or busy. Free, local.
|
|
4854
|
+
// • --flat <c> → local ffmpeg chroma-key of a solid fill (e.g. the cream paper
|
|
4855
|
+
// behind an infographic's icons): crisper edges when the region's
|
|
4856
|
+
// background really is one flat color.
|
|
4857
|
+
// --crop x,y,w,h first isolates ONE element from a multi-illustration source
|
|
4858
|
+
// (pixels or %), then the background is removed and the canvas trimmed to the
|
|
4859
|
+
// subject's true min width/height — a snug PNG you `place` + `keyframes` exactly
|
|
4860
|
+
// like a `cutout` sticker (same "Cutout graphics for explainers" recipe).
|
|
4861
|
+
// IMAGE-only (a moving subject has no single bounding box → remove-background /
|
|
4862
|
+
// remove-greenscreen a video instead).
|
|
4863
|
+
async function runMaskCommand(argv) {
|
|
4864
|
+
const parsed = parseArgs({
|
|
4865
|
+
args: argv,
|
|
4866
|
+
allowPositionals: true,
|
|
4867
|
+
options: {
|
|
4868
|
+
...commonOptions(),
|
|
4869
|
+
source: { type: "string" },
|
|
4870
|
+
// Isolate ONE illustration from a busy source before masking.
|
|
4871
|
+
crop: { type: "string" },
|
|
4872
|
+
"keep-region": { type: "string" },
|
|
4873
|
+
// Flat-key mode (chroma-key a solid background) instead of ONNX matting.
|
|
4874
|
+
flat: { type: "string" },
|
|
4875
|
+
preset: { type: "string" },
|
|
4876
|
+
"key-color": { type: "string" },
|
|
4877
|
+
tolerance: { type: "string" },
|
|
4878
|
+
softness: { type: "string" },
|
|
4879
|
+
"no-despill": { type: "boolean", default: false },
|
|
4880
|
+
// Trim knobs (mirror cutout).
|
|
4881
|
+
"no-trim": { type: "boolean", default: false },
|
|
4882
|
+
pad: { type: "string" },
|
|
4883
|
+
"alpha-threshold": { type: "string" },
|
|
4884
|
+
"output-format": { type: "string" },
|
|
4885
|
+
out: { type: "string" }
|
|
4886
|
+
}
|
|
4887
|
+
});
|
|
4888
|
+
const ctx = commonContext(parsed.values);
|
|
4889
|
+
// Crop + trim are pure ffmpeg; matting rides the bundled ONNX engine — both
|
|
4890
|
+
// need ffmpeg present. Fail early & clearly.
|
|
4891
|
+
if (!(await localGreenscreenAvailable())) {
|
|
4892
|
+
throw new Error("mask needs ffmpeg (for the free local crop + alpha trim), which wasn't found. Install ffmpeg (or `npm i -g ffmpeg-static`).");
|
|
4893
|
+
}
|
|
4894
|
+
const sourceArg = parsed.values.source ?? parsed.positionals[0];
|
|
4895
|
+
if (!sourceArg) {
|
|
4896
|
+
throw new Error("mask needs a source image: `vidfarm mask <image|url> [--crop x,y,w,h] [--flat <hexcolor>]`. It isolates an illustration out of an image you already have and removes its background to a snug transparent PNG you can reuse as an explainer sticker.");
|
|
4897
|
+
}
|
|
4898
|
+
// Resolve flat-key mode (solid background) vs matting (default, arbitrary bg).
|
|
4899
|
+
const presetRaw = parsed.values.preset?.trim().toLowerCase();
|
|
4900
|
+
if (presetRaw && !(presetRaw in GREENSCREEN_PRESETS)) {
|
|
4901
|
+
throw new Error(`Unknown --preset "${presetRaw}". Choose one of: ${Object.keys(GREENSCREEN_PRESETS).join(", ")}, or pass --flat/--key-color <hex>.`);
|
|
4902
|
+
}
|
|
4903
|
+
const preset = presetRaw ? GREENSCREEN_PRESETS[presetRaw] : undefined;
|
|
4904
|
+
const flatColor = parsed.values.flat ?? parsed.values["key-color"] ?? preset?.key_color;
|
|
4905
|
+
const useFlatKey = Boolean(flatColor);
|
|
4906
|
+
const tolerance = parsed.values.tolerance !== undefined ? Number(parsed.values.tolerance) : preset?.tolerance;
|
|
4907
|
+
const softness = parsed.values.softness !== undefined ? Number(parsed.values.softness) : preset?.softness;
|
|
4908
|
+
const despill = !parsed.values["no-despill"];
|
|
4909
|
+
const outputFormat = parsed.values["output-format"]?.trim().toLowerCase();
|
|
4910
|
+
const outExt = outputFormat === "webp" ? "webp" : "png";
|
|
4911
|
+
const work = mkdtempSync(path.join(tmpdir(), "vidfarm-mask-"));
|
|
4912
|
+
let stemHint = "mask";
|
|
4913
|
+
try {
|
|
4914
|
+
// ---- 1. Obtain the source image (local file or url; reject video) -------
|
|
4915
|
+
let sourcePath;
|
|
4916
|
+
const directPath = path.resolve(process.cwd(), sourceArg);
|
|
4917
|
+
const isLocalFile = !/^https?:\/\//i.test(sourceArg) && existsSync(directPath);
|
|
4918
|
+
if (isLocalFile) {
|
|
4919
|
+
if (/\.(mp4|mov|webm|m4v|mkv)(\?|#|$)/i.test(directPath)) {
|
|
4920
|
+
throw new Error("mask is image-only (a moving subject has no single bounding box). Matte a video with `vidfarm remove-background <video>` or key a flat backdrop with `vidfarm remove-greenscreen <video>`.");
|
|
4921
|
+
}
|
|
4922
|
+
sourcePath = directPath;
|
|
4923
|
+
stemHint = path.basename(directPath).replace(/\.[^.]+$/, "");
|
|
4924
|
+
}
|
|
4925
|
+
else {
|
|
4926
|
+
const sourceUrl = await resolveSingleMediaUrl(ctx, sourceArg);
|
|
4927
|
+
if (/\.(mp4|mov|webm|m4v|mkv)(\?|#|$)/i.test(sourceUrl)) {
|
|
4928
|
+
throw new Error("mask is image-only. Matte a video with `vidfarm remove-background <video>`.");
|
|
4929
|
+
}
|
|
4930
|
+
sourcePath = path.join(work, path.basename(new URL(sourceUrl).pathname) || "source.png");
|
|
4931
|
+
if (!ctx.json)
|
|
4932
|
+
console.log(`${DIM}Downloading source…${RESET}`);
|
|
4933
|
+
await downloadUrlToFile(sourceUrl, sourcePath);
|
|
4934
|
+
}
|
|
4935
|
+
// ---- 2. Optionally isolate ONE illustration from a busy source ----------
|
|
4936
|
+
let regionPath = sourcePath;
|
|
4937
|
+
let cropInfo = null;
|
|
4938
|
+
const cropArg = parsed.values.crop?.trim();
|
|
4939
|
+
if (cropArg) {
|
|
4940
|
+
const parts = cropArg.split(/[,\s]+/).filter(Boolean);
|
|
4941
|
+
if (parts.length !== 4) {
|
|
4942
|
+
throw new Error(`--crop wants four values "x,y,w,h" — pixels (40,120,300,260) or percents (8%,55%,26%,22%). Got "${cropArg}".`);
|
|
4943
|
+
}
|
|
4944
|
+
const cropped = path.join(work, `region.${outExt}`);
|
|
4945
|
+
cropInfo = await cropImageRegion({ sourcePath, outputPath: cropped, x: parts[0], y: parts[1], width: parts[2], height: parts[3] });
|
|
4946
|
+
regionPath = cropped;
|
|
4947
|
+
if (!ctx.json)
|
|
4948
|
+
console.log(`${DIM}Isolated a ${cropInfo.width}×${cropInfo.height} region from the ${cropInfo.sourceWidth}×${cropInfo.sourceHeight} source.${RESET}`);
|
|
4949
|
+
if (parsed.values["keep-region"]) {
|
|
4950
|
+
const keepPath = path.resolve(process.cwd(), String(parsed.values["keep-region"]));
|
|
4951
|
+
mkdirSync(path.dirname(keepPath), { recursive: true });
|
|
4952
|
+
copyFileSync(cropped, keepPath);
|
|
4953
|
+
if (!ctx.json)
|
|
4954
|
+
console.log(`${DIM}Saved the pre-mask region to ${keepPath}${RESET}`);
|
|
4955
|
+
}
|
|
4956
|
+
}
|
|
4957
|
+
// ---- 3. Remove the background → transparent PNG -------------------------
|
|
4958
|
+
const maskedPath = path.join(work, "masked.png");
|
|
4959
|
+
if (useFlatKey) {
|
|
4960
|
+
if (!ctx.json)
|
|
4961
|
+
console.log(`${DIM}Keying out the flat ${presetRaw ?? flatColor} background locally with ffmpeg (free)…${RESET}`);
|
|
4962
|
+
await removeGreenscreenLocal({ sourcePath: regionPath, mediaType: "image", outputPath: maskedPath, keyColor: flatColor, tolerance, softness, despill });
|
|
4963
|
+
}
|
|
4964
|
+
else {
|
|
4965
|
+
if (!ctx.json)
|
|
4966
|
+
console.log(`${DIM}Masking the illustration off its background with the local ONNX matting model (free, no account)…${RESET}`);
|
|
4967
|
+
const run = await runHyperframesCommand("remove-background", [regionPath, "-o", maskedPath, ...(ctx.json ? ["--json"] : [])], { stdio: ctx.json ? "capture" : "inherit" });
|
|
4968
|
+
if (run.code !== 0 || !existsSync(maskedPath)) {
|
|
4969
|
+
const summary = parseHyperframesJson(run.stdout) ?? parseHyperframesJson(run.stderr);
|
|
4970
|
+
const reason = (typeof summary?.error === "string" && summary.error) || run.stderr.trim().split("\n").pop() || "";
|
|
4971
|
+
throw new Error(`mask (matting) failed (exit ${run.code})${reason ? `: ${reason}` : ""}. If the region sits on a flat, solid background, retry with --flat <hexcolor> for a cleaner ffmpeg key.`);
|
|
4972
|
+
}
|
|
4973
|
+
}
|
|
4974
|
+
// ---- 4. Trim to the tight bounding box ----------------------------------
|
|
4975
|
+
const outPath = parsed.values.out
|
|
4976
|
+
? path.resolve(process.cwd(), String(parsed.values.out))
|
|
4977
|
+
: path.resolve(process.cwd(), `${stemHint}.cutout.${outExt}`);
|
|
4978
|
+
mkdirSync(path.dirname(outPath), { recursive: true });
|
|
4979
|
+
const noTrim = Boolean(parsed.values["no-trim"]);
|
|
4980
|
+
const padPx = parsed.values.pad !== undefined ? Number(parsed.values.pad) : 0;
|
|
4981
|
+
const alphaThreshold = parsed.values["alpha-threshold"] !== undefined ? Number(parsed.values["alpha-threshold"]) : undefined;
|
|
4982
|
+
let trim;
|
|
4983
|
+
if (noTrim) {
|
|
4984
|
+
trim = await trimTransparentBorders({ sourcePath: maskedPath, outputPath: outPath, padPx, alphaThreshold: 999 });
|
|
4985
|
+
}
|
|
4986
|
+
else {
|
|
4987
|
+
if (!ctx.json)
|
|
4988
|
+
console.log(`${DIM}Trimming transparent margins to the illustration's true size…${RESET}`);
|
|
4989
|
+
trim = await trimTransparentBorders({ sourcePath: maskedPath, outputPath: outPath, padPx, alphaThreshold });
|
|
4990
|
+
}
|
|
4991
|
+
const shrinkPct = trim.origWidth && trim.origHeight && trim.width && trim.height
|
|
4992
|
+
? Math.round((1 - (trim.width * trim.height) / (trim.origWidth * trim.origHeight)) * 100)
|
|
4993
|
+
: null;
|
|
4994
|
+
if (ctx.json) {
|
|
4995
|
+
printJson({
|
|
4996
|
+
ok: true,
|
|
4997
|
+
target: "local",
|
|
4998
|
+
mode: useFlatKey ? "flat-key" : "matting",
|
|
4999
|
+
out: trim.outputPath,
|
|
5000
|
+
format: trim.format,
|
|
5001
|
+
width: trim.width,
|
|
5002
|
+
height: trim.height,
|
|
5003
|
+
source_width: trim.origWidth,
|
|
5004
|
+
source_height: trim.origHeight,
|
|
5005
|
+
cropped: cropInfo ? { x: cropInfo.x, y: cropInfo.y, width: cropInfo.width, height: cropInfo.height } : null,
|
|
5006
|
+
trimmed: trim.trimmed,
|
|
5007
|
+
area_reduced_pct: shrinkPct,
|
|
5008
|
+
bytes: safeSize(trim.outputPath)
|
|
5009
|
+
});
|
|
5010
|
+
}
|
|
5011
|
+
else {
|
|
5012
|
+
console.log(`${GREEN}Masked illustration ready:${RESET} ${trim.outputPath} ${DIM}(${trim.width}×${trim.height}, ${formatBytes(safeSize(trim.outputPath))}${trim.trimmed && shrinkPct !== null ? `, −${shrinkPct}% area` : ""})${RESET}`);
|
|
5013
|
+
if (!trim.trimmed && !noTrim) {
|
|
5014
|
+
console.log(`${YELLOW}Note:${RESET} ${DIM}no transparent margin to trim — the subject reached the crop's edges, or the background didn't key. If matting left a hard rectangle behind, the background may be busy — try tightening --crop; if it's a flat fill, pass --flat <hexcolor>.${RESET}`);
|
|
5015
|
+
}
|
|
5016
|
+
console.log(`${DIM}Reuse it as a FREE explainer sticker: 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. Isolate more illustrations from the same source with different --crop rects.${RESET}`);
|
|
5017
|
+
}
|
|
5018
|
+
}
|
|
5019
|
+
finally {
|
|
5020
|
+
rmSync(work, { recursive: true, force: true });
|
|
5021
|
+
}
|
|
5022
|
+
}
|
|
4604
5023
|
// Poll a greenscreen job through the same backend it was submitted to (local
|
|
4605
5024
|
// in-process app or cloud). Mirrors pollPrimitiveJob's terminal conditions.
|
|
4606
5025
|
async function pollGreenscreenJob(ctx, target, jobId) {
|
|
@@ -6007,6 +6426,12 @@ async function runVisibilityCommand(argv) {
|
|
|
6007
6426
|
assertApiOk(result, "visibility");
|
|
6008
6427
|
const templateId = result.json?.template_id;
|
|
6009
6428
|
emitResult(result, ctx.json, templateId ? [["Open editor ", editorFrontendUrl(ctx.host, templateId, forkId)]] : undefined);
|
|
6429
|
+
// Going public makes this a reusable template a teammate can pull onto their
|
|
6430
|
+
// own machine — surface the exact "pull the template" command to share.
|
|
6431
|
+
if (!ctx.json && visibility === "public" && templateId) {
|
|
6432
|
+
console.log("");
|
|
6433
|
+
console.log(` ${BOLD}Teammates pull it with${RESET} vidfarm serve ${templateId}`);
|
|
6434
|
+
}
|
|
6010
6435
|
}
|
|
6011
6436
|
async function runCloneCommand(argv) {
|
|
6012
6437
|
const parsed = parseArgs({ args: argv, allowPositionals: true, options: { ...commonOptions(), version: { type: "string" }, title: { type: "string" }, from: { type: "string" } } });
|
|
@@ -6044,6 +6469,14 @@ async function runShareLinkCommand(argv) {
|
|
|
6044
6469
|
});
|
|
6045
6470
|
assertApiOk(result, "share-link");
|
|
6046
6471
|
emitResult(result, ctx.json, [["Share ", result.json?.share_url]]);
|
|
6472
|
+
// A share link is how a teammate pulls this work-in-progress fork onto their
|
|
6473
|
+
// own machine to edit locally — spell out the "pull the template" command.
|
|
6474
|
+
const shareToken = (result.json?.token ?? result.json?.share_token);
|
|
6475
|
+
if (!ctx.json) {
|
|
6476
|
+
const suffix = shareToken ? ` --share ${shareToken}` : " --share <token-from-url>";
|
|
6477
|
+
console.log("");
|
|
6478
|
+
console.log(` ${BOLD}Teammate pulls it with${RESET} vidfarm serve --fork ${forkId}${suffix}`);
|
|
6479
|
+
}
|
|
6047
6480
|
}
|
|
6048
6481
|
// ── Approve & schedule ──────────────────────────────────────────────────────
|
|
6049
6482
|
async function runApproveCommand(argv) {
|
|
@@ -182,6 +182,155 @@ 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
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Cut a rectangular sub-region out of a still — the "isolate ONE illustration
|
|
289
|
+
* from a busy source image" step used by `vidfarm mask` when the caller wants to
|
|
290
|
+
* grab a single element (a labelled prop, an icon, a mascot) out of a composite
|
|
291
|
+
* (an infographic, a poster, a screenshot) before removing its background. Pure
|
|
292
|
+
* ffmpeg (bundle-safe). Values accept pixels or a `%` of the source dimension,
|
|
293
|
+
* and are clamped into the frame so a slightly-oversized box can't error out.
|
|
294
|
+
*/
|
|
295
|
+
export async function cropImageRegion(input) {
|
|
296
|
+
if (!existsSync(input.sourcePath))
|
|
297
|
+
throw new Error(`No such source file: ${input.sourcePath}`);
|
|
298
|
+
const dims = await probeImageDimensions(input.sourcePath);
|
|
299
|
+
if (!dims)
|
|
300
|
+
throw new Error(`Couldn't read image dimensions for ${input.sourcePath}.`);
|
|
301
|
+
const resolveValue = (raw, span, label) => {
|
|
302
|
+
const t = String(raw).trim();
|
|
303
|
+
const pct = /^(-?\d+(?:\.\d+)?)%$/.exec(t);
|
|
304
|
+
const val = pct ? (Number(pct[1]) / 100) * span : Number(t);
|
|
305
|
+
if (!Number.isFinite(val))
|
|
306
|
+
throw new Error(`Invalid crop ${label} "${raw}" — use pixels (120) or a percent (15%).`);
|
|
307
|
+
return Math.round(val);
|
|
308
|
+
};
|
|
309
|
+
let x = resolveValue(input.x, dims.width, "x");
|
|
310
|
+
let y = resolveValue(input.y, dims.height, "y");
|
|
311
|
+
let w = resolveValue(input.width, dims.width, "width");
|
|
312
|
+
let h = resolveValue(input.height, dims.height, "height");
|
|
313
|
+
// Clamp the rect into the frame so an over-generous box still yields a crop.
|
|
314
|
+
x = Math.min(Math.max(0, x), Math.max(0, dims.width - 1));
|
|
315
|
+
y = Math.min(Math.max(0, y), Math.max(0, dims.height - 1));
|
|
316
|
+
w = Math.min(Math.max(1, w), dims.width - x);
|
|
317
|
+
h = Math.min(Math.max(1, h), dims.height - y);
|
|
318
|
+
const ffmpeg = await resolveFfmpeg();
|
|
319
|
+
const isWebp = /\.webp$/i.test(input.outputPath);
|
|
320
|
+
const args = [
|
|
321
|
+
"-hide_banner", "-y",
|
|
322
|
+
"-i", input.sourcePath,
|
|
323
|
+
"-frames:v", "1",
|
|
324
|
+
"-vf", `crop=${w}:${h}:${x}:${y},format=rgba`,
|
|
325
|
+
...(isWebp ? ["-c:v", "libwebp", "-lossless", "1", "-pix_fmt", "rgba"] : []),
|
|
326
|
+
input.outputPath
|
|
327
|
+
];
|
|
328
|
+
const run = await runFfmpeg(ffmpeg, args);
|
|
329
|
+
if (run.code !== 0 || !existsSync(input.outputPath)) {
|
|
330
|
+
throw new Error(`Crop failed (ffmpeg exit ${run.code})${tail(run.stderr)}.`);
|
|
331
|
+
}
|
|
332
|
+
return { outputPath: input.outputPath, x, y, width: w, height: h, sourceWidth: dims.width, sourceHeight: dims.height };
|
|
333
|
+
}
|
|
185
334
|
/** Default output path next to a source, matching remove-background's naming. */
|
|
186
335
|
export function defaultGreenscreenOutPath(sourcePath, mediaType, outputFormat) {
|
|
187
336
|
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.
|
|
3
|
+
"version": "0.21.20",
|
|
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": {
|