@officexapp/vidfarm-devcli 0.21.30 → 0.21.31
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 +2 -0
- package/.agents/skills/vidfarm/recipes/cutout-graphics-for-explainers.md +18 -4
- package/.agents/skills/vidfarm/references/assets-and-sourcing.md +17 -0
- package/.agents/skills/vidfarm/references/automation-and-local-dev.md +4 -3
- package/.agents/skills/vidfarm/references/editor-workflows.md +1 -1
- package/.agents/skills/vidfarm/references/primitives.md +80 -1
- package/SKILL.director.md +122 -9
- package/SKILL.md +2 -1
- package/dist/src/cli.js +192 -11
- package/dist/src/devcli/handoff.js +6 -2
- package/dist/src/devcli/sticker-pack.js +196 -2
- package/package.json +3 -1
|
@@ -210,6 +210,7 @@ Present both harnesses to the director, recommend (A) unless they've asked for p
|
|
|
210
210
|
- **Stickers are usually animated, not pasted.** Once placed, animate each one with `vidfarm keyframes` presets (`pop-in`, `float`, `shake`, `grow`, `slide-in-left`, `drift`) — that's HTML/CSS canvas motion, deterministic, free, and identical in preview and render. Layer moves up (pop-in, then idle float) for real life. See `references/editor-workflows.md` → "Cutout graphics for explainers".
|
|
211
211
|
- **A sticker can carry its OWN motion too.** A *moving* subject has no single bounding box, so it isn't a PNG: key the clip with `vidfarm remove-greenscreen <video>` → transparent WebM (browser/editor-playable, the right choice on a composition).
|
|
212
212
|
- **The plate must be a color the ART DOESN'T USE — green is a default, not a law.** A chroma key removes exactly ONE color, so a green plate under green artwork (leaves, frogs, plants, money, an "eco" icon set) keys holes straight through the stickers. `sticker-pack`/`cutout` handle this for you in two ways, and you should still say it out loud when it matters: **(a)** when generating, they read the subject and move the plate off any hue it mentions — green → magenta (`#FF00FF`) → blue (`#0047BB`) → black → white — and print which plate they picked and why; **(b)** when splitting a sheet you already have, they **detect the plate from the sheet's four corners**, so a red/purple/blue sheet handed back from a web generator just works. Pin it yourself with `--key-color "#FF00FF"` / `--preset magenta` (that disables the auto-pick), or `--no-auto-key` to force plain green. If a pack's palette collides with *every* standard plate, split it into two sheets on two different plates rather than fighting the key.
|
|
213
|
+
- **The ART must be CLOSED and SOLIDLY FILLED — this is the other half of surviving the key, and the #1 way stickers come back broken.** Ask an image model for "icons on a green plate" and it will happily draw **outline art**: a colored stroke with the shape's interior left as bare plate. It looks perfect on the sheet, and after the key each sticker is a **rim floating around a see-through hole** (an apple-shaped outline with nothing inside it). Same outcome from a *near-plate* fill (the keyer works on tolerance, not exact match), a translucent/glassy/glowing material, or a soft glow fading into the plate. **You cannot key those pixels back — it has to be in the prompt:** *"every object is a closed, solidly filled shape; outlines must enclose an opaque fill of a different color; no outline-only or hollow art; nothing on the art in the plate color or any near-shade of it; fully opaque, no translucency, glow or drop shadow."* `cutout --generate`, `sticker-pack --generate`, `handoff image` and the `create-overlay` primitive **append that clause for you** with the chosen plate hex — write it yourself only when you prompt a generator directly. After the key, both commands report per-item `hole_pct`/`hollow` (console `⚠ N% hollow`, `--json`, `stickers.json`) — a ring or picture frame reads the same way, so it **warns, never blocks**. Flagged and it shouldn't be? Re-generate with the fill clause; a *near*-plate fill can sometimes be rescued with a lower `--tolerance`; one stubborn item can be lifted with `vidfarm mask --crop …` (ONNX matting ignores fill color).
|
|
213
214
|
- **Transparent GIF is supported, for GIF-only surfaces.** `vidfarm sticker-pack … --output-format gif` (stills) and `vidfarm remove-greenscreen <video> --gif` (animated) emit transparent GIFs. GIF alpha is **1-bit**, so edges go hard — fine for chat/forum/Notion sticker surfaces, worse than PNG/WebP/WebM for compositing on a timeline. Prefer PNG/WebP/WebM unless the destination only eats GIF.
|
|
214
215
|
|
|
215
216
|
**Explainer house style — the defaults to build with unless told otherwise.** **White background / light mode** (plain white stage, no gradients, no dark mode, no photo backdrop), **kinetic word-by-word captions** in dark ink on the light stage (`vidfarm captions generate --style word-pop --color "#111111" --active-color "#7C3AED" --background-style plain` — skip outlines/shadows, they're only needed over busy footage), and **female TTS narration** (`vidfarm tts --voice coral` on OpenAI — `nova` for energy, `sage` for calm; `Kore`/`Leda` on Gemini; any ElevenLabs voice via `vidfarm voices`). **Keep it clean and simple** — one idea on screen at a time, two or three cutouts per beat, one accent color, one font, lots of white space; remove before you add. **Illustrations default to simplicity**: flat vector, simple shapes, minimal detail, 2–3 flat colors, no baked-in text — simple art keys cleanly, trims tight, and stays on-style across the whole cast. State the defaults once so the director can override any of them. Full detail: recipe `recipes/cutout-graphics-for-explainers.md` (“House style — the explainer defaults”).
|
|
@@ -317,6 +318,7 @@ Choose the narrowest path that satisfies the request.
|
|
|
317
318
|
3. If the task is “change this video,” read `references/editor-workflows.md`.
|
|
318
319
|
4. If the task is “find footage” or “use our existing assets,” read `references/assets-and-sourcing.md`.
|
|
319
320
|
4b. If the task is **“download this video/audio off a website”** (a pasted YouTube / TikTok / Instagram / X post URL the user wants the actual file from), Vidfarm does that for you on a **paid plan** — `POST /api/v1/primitives/videos/download` (or `/audio/download`), devcli `vidfarm download-video <url>` / `vidfarm download-audio <url>`. **Free plan → do not call it; walk the user through opening the URL in Chrome and downloading it from the page, then `vidfarm put-file` the local file in for $0.** Details in `references/assets-and-sourcing.md` → `references/primitives.md`.
|
|
321
|
+
4c. If the task is **“turn this Reddit/X thread, subreddit, or account into a video”** — “tweet to TikTok”, “Reddit to TikTok”, “make a video from this thread”, “what are the top comments saying” — run `vidfarm recycle <source>` (or `POST /api/v1/primitives/social/recycle`) with the URL. It **decomposes** the source into raw JSON (text, comment tree, media URLs, author pics, stats) and hands it back unranked so YOU pick what to remix. **Paid plan; `max_records` is the spend ceiling.** Brokers the reddit-lead-gen / x-lead-gen OfficeX apps, so it waits out their async job for you. Details in `references/assets-and-sourcing.md` → `references/primitives.md`.
|
|
320
322
|
5. If the task is scripted, local, CI-driven, or `vidfarm serve`-based, read `references/automation-and-local-dev.md`.
|
|
321
323
|
6. If the task explicitly asks for a primitive or needs specialized generation/transcription work, read `references/primitives.md`.
|
|
322
324
|
7. If the task is the MARKETPLACE (ordering videos from specialist agents): browsing is web-only for paying customers — send the human to https://vidfarm.cc/marketplace, never render it locally. Placing/listing orders is the thin REST wrapper in `references/core-workflows.md` (§ Marketplace); anything deeper on a gig (inbox, proofs, payouts) needs the external Dollar Platoon skill — `npx skills add https://github.com/OfficeXApp/dollarplatoon-skill` — the same way FlockPoster work beyond scheduling needs `npx skills add https://github.com/OfficeXApp/flockposter-skill`.
|
|
@@ -18,7 +18,7 @@ The mechanical trio — **generate on a chroma plate → key it out → trim to
|
|
|
18
18
|
- **Female TTS narration.** Default to a warm, friendly **female** voice and say which one you picked: local-first `vidfarm tts "<script>" --voice coral` (OpenAI — `nova` if the script wants more energy, `sage` for calmer), `--voice Kore` or `Leda` on Gemini, or `vidfarm voices` → `vidfarm tts --cloud --voice <voice_id>` on ElevenLabs. Tell the director they can swap it in one flag.
|
|
19
19
|
- **Clean and simple wins.** One idea on screen at a time. Two or three cutouts per beat, not eight. Generous white space, one accent color, one font. When in doubt, remove an element rather than add one.
|
|
20
20
|
|
|
21
|
-
**Illustrations default to simplicity.** Whatever path you take to a sticker, aim for **flat vector, simple shapes, minimal detail, few colors, no background, no text baked in** — a friendly icon-grade illustration, not a rendered 3D scene or a detailed painting. Simple art keys cleanly, trims tight, scales without mush, animates readably at 9:16, and stays on-style across a whole cast. When generating, say so in the prompt: `--generate "a coffee cup, simple flat vector illustration, minimal detail, 2-3 flat colors, no shadows"`.
|
|
21
|
+
**Illustrations default to simplicity — and to SOLID FILLS.** Whatever path you take to a sticker, aim for **flat vector, simple shapes, minimal detail, few colors, solid opaque fills, no background, no text baked in** — a friendly icon-grade illustration, not a rendered 3D scene or a detailed painting. Simple art keys cleanly, trims tight, scales without mush, animates readably at 9:16, and stays on-style across a whole cast. "Solid fills" is the load-bearing word: **outline-only art has its interior keyed away and comes back as a rim around a transparent hole** (see "Then make the ART key-safe too" below). When generating, say so in the prompt: `--generate "a coffee cup, simple flat vector illustration, minimal detail, 2-3 flat colors, solid filled shapes (not outline-only), no shadows"`.
|
|
22
22
|
|
|
23
23
|
**In cost-saving mode, don't generate illustrations at all — mask them out of images the director already has.** If `vidfarm cost-mode` is `minimize` (or the director says "without burning credits"), the default for adding an illustration is `vidfarm mask <their-image> --crop …` — lifting art out of an infographic, poster, deck slide, brand sheet, or screenshot for **$0 and zero AI calls**. Ask for source art before you ask for a generation budget; the guided loop is **"Mask from an image you already have"** below. **If no source art exists and the graphic must be custom, you still don't have to spend** — hand the director a prompt for a **free** image generator (meta.ai / free ChatGPT / a Hugging Face Space) and cut the returned sheet into stickers locally: **"Free manual image-gen"** below.
|
|
24
24
|
|
|
@@ -63,6 +63,20 @@ Out comes `./stickers/sticker-01-red-barn.png`, `sticker-02-tractor.png`, … ea
|
|
|
63
63
|
|
|
64
64
|
Override whenever you know better: `--key-color "#FF00FF"` or `--preset magenta` pins the plate (and turns the auto-pick off); `--no-auto-key` forces plain green. If a pack's palette collides with *every* standard plate — a rainbow icon set — split it across two sheets on two different plates rather than fighting `--tolerance`. And when a sticker is a *green* subject, never let the user generate it on green: the hand-off brief (`vidfarm handoff image`) already picks the plate for them and carries the matching `--key-color` into the follow-up command.
|
|
65
65
|
|
|
66
|
+
**Then make the ART key-safe too — CLOSED, SOLIDLY FILLED shapes.** Picking the right plate is only half of surviving the key. The other half is the failure you actually see in the wild: you ask for "sticker icons on a green plate" and the model draws **outline art** — a colored stroke with the shape's *interior left as bare plate*. On the sheet it looks perfect. After the key, the interior is gone with the background, and every sticker composites as a **rim floating around a see-through hole** (an apple that's just an apple-shaped outline, a clock with no face). Same result from three cousins: a fill that's a *near-shade* of the plate (the keyer works on tolerance, not exact match), a **translucent/glassy/glowing** material that lets the plate through, and a **soft glow or drop shadow** that fades into the plate at the edges.
|
|
67
|
+
|
|
68
|
+
None of that is fixable after generation — you cannot key back pixels that were never drawn. So it belongs in the **prompt**, every time:
|
|
69
|
+
|
|
70
|
+
> Every object must be a CLOSED, SOLIDLY FILLED shape — outlines and strokes must enclose an opaque fill of a different color. No outline-only, hollow, or line-art objects; never leave a shape's interior as bare background. No part of any object — fill, outline, highlight, gradient, shading or detail — may be `<PLATE HEX>` or any near-shade, tint or tone of it. No transparent, translucent, glassy, glowing or ghosted materials; every pixel of every object is fully opaque. No soft glows, blurs or drop shadows fading into the background. Keep the whole palette in strong contrast to `<PLATE HEX>`. The background must be visible ONLY around the outside of the objects, never showing through inside them.
|
|
71
|
+
|
|
72
|
+
`cutout --generate`, `sticker-pack --generate`, `vidfarm handoff image`, and the `create-overlay` REST primitive all append that clause automatically with the chosen plate hex substituted in — **you only have to write it yourself when you're prompting a generator directly** (a free web tool by hand, or your own `POST /api/v1/primitives/images/generate` call). Do write it. It costs one paragraph and it is the difference between a usable pack and a re-generation.
|
|
73
|
+
|
|
74
|
+
**And it's checked after the key.** `sticker-pack` and `cutout` measure, per item, the transparent area **fully enclosed by that item's own art** and report it as `hole_pct` (plus `holes`, and `hollow: true` at ≥20%) in `--json`, in `stickers.json`, and as a `⚠ N% hollow` flag on the console line. A ring, donut, picture frame or letter "O" reads the same way, so it **warns, never blocks** — the judgement is yours. When items you expected to be solid come back flagged:
|
|
75
|
+
|
|
76
|
+
- **Re-generate the sheet** with the fill clause above spelled out. This is the fix ~90% of the time.
|
|
77
|
+
- If the fill was merely *close* to the plate rather than absent, a lower `--tolerance` can rescue the sheet you already have. Nothing rescues a genuinely empty interior.
|
|
78
|
+
- Last resort for one stubborn item: `vidfarm mask ./sheet.png --crop x,y,w,h` — ONNX matting doesn't care what color the fill is, so it lifts art the chroma key can't.
|
|
79
|
+
|
|
66
80
|
**Generation is the failure point, not the cutting.** The sheet prompt is auto-appended with the important instruction — *every item fully separated by clear plate-colored background, nothing touching or overlapping, wide margins, no text, no shadows, one consistent style* — because **touching items segment as one sticker**. If a pack comes back merged, re-run the generation asking for more spacing before you fight the `--gap` knob.
|
|
67
81
|
|
|
68
82
|
### Stickers are not necessarily small — and they usually move
|
|
@@ -179,15 +193,15 @@ Everything here is **local and free** — crop, matte/key, and trim are all `ffm
|
|
|
179
193
|
|
|
180
194
|
1. **List the cast.** Name every illustration the explainer needs, in the house style (flat vector, 2–3 flat colors, no baked-in text).
|
|
181
195
|
2. **Hand the director this prompt**, filled in:
|
|
182
|
-
> A flat vector illustration sheet on a solid pure green background (#00FF00), no gradient, no shadow, no text, no labels. Arranged in a 3×2 grid with generous even spacing, each object fully separated and not touching any other object or the image edge: (1) `<object one>`, (2) `<object two>`, (3) `<object three>`, … Simple flat shapes, 2–3 flat colors, minimal detail, consistent style and line weight across all objects, front-facing, centered in its own cell. Square image, high resolution.
|
|
196
|
+
> A flat vector illustration sheet on a solid pure green background (#00FF00), no gradient, no shadow, no text, no labels. Arranged in a 3×2 grid with generous even spacing, each object fully separated and not touching any other object or the image edge: (1) `<object one>`, (2) `<object two>`, (3) `<object three>`, … Simple flat shapes, 2–3 flat colors, minimal detail, consistent style and line weight across all objects, front-facing, centered in its own cell. **Every object is a closed, solidly filled shape — outlines must enclose an opaque fill of a different color; no outline-only or hollow objects, and no object's interior left as bare green. Nothing on any object may be green or any near-shade of green; no transparent, translucent, glassy or glowing materials; no soft glows or drop shadows fading into the background.** Square image, high resolution.
|
|
183
197
|
|
|
184
|
-
**No text in the image, ever** — rendered text never keys or scales cleanly; add copy as a caption or HTML layer instead.
|
|
198
|
+
**No text in the image, ever** — rendered text never keys or scales cleanly; add copy as a caption or HTML layer instead. **And don't drop the bolded fill sentence** — the green gets deleted, so anything green *on* the art gets deleted with it; hollow outline art is the single most common way a hand-generated sheet comes back broken. (`vidfarm handoff image --theme … --items …` mints this whole brief with the plate and the fill clause already filled in — prefer it over typing the prompt yourself.)
|
|
185
199
|
3. **Bring the sheet in:** `vidfarm put-file ./sheet.png` (or drag into **My Files** in the web app).
|
|
186
200
|
4. **Split it into stickers — one command, all free:**
|
|
187
201
|
```
|
|
188
202
|
vidfarm sticker-pack ./sheet.png --items "<object one>,<object two>,…" --out-dir ./stickers
|
|
189
203
|
```
|
|
190
|
-
It keys the flat green, finds each object in the alpha channel, and writes one trimmed transparent sticker per object (named from `--items`) plus `stickers.json`. Run it with `--dry-run` first to check the boxes. Two objects merged into one? The generator drew them too close — lower `--gap`, or grab that pair by hand:
|
|
204
|
+
It keys the flat green, finds each object in the alpha channel, and writes one trimmed transparent sticker per object (named from `--items`) plus `stickers.json`. Run it with `--dry-run` first to check the boxes. Any item flagged **`⚠ N% hollow`** came back as an outline around a transparent hole — unless it's genuinely a ring/frame, re-ask the director for that sheet with the fill sentence, or lift that one item with `vidfarm mask ./sheet.png --crop x,y,w,h` (ONNX matting doesn't care what the fill color is). Two objects merged into one? The generator drew them too close — lower `--gap`, or grab that pair by hand:
|
|
191
205
|
```
|
|
192
206
|
vidfarm mask ./sheet.png --crop 4%,4%,29%,45% --flat "#00FF00" --out prop-a.png
|
|
193
207
|
```
|
|
@@ -15,6 +15,23 @@ Both are async primitive jobs — poll `GET /api/v1/primitives/jobs/:jobId` unti
|
|
|
15
15
|
|
|
16
16
|
**Don't reach for this when the real goal is clips.** If the user wants short clips mined out of a long video, `POST /raws/scan` with `source_url` already downloads the source for you as part of the hunt (below) — a separate download call is wasted spend.
|
|
17
17
|
|
|
18
|
+
## Recycle a Reddit or X source into content ("tweet to TikTok", "Reddit to TikTok")
|
|
19
|
+
|
|
20
|
+
When the source isn't a video file but a **conversation** — a Reddit thread and its comments, a subreddit's front page, an X thread and its replies, an account's timeline — the download routes are the wrong tool. Use `vidfarm recycle <source>` (devcli) or `POST /api/v1/primitives/social/recycle` (alias `/recycle-social`), which **decomposes** the source into JSON you can remix:
|
|
21
|
+
|
|
22
|
+
| Paste this | You get back |
|
|
23
|
+
| --- | --- |
|
|
24
|
+
| `reddit.com/r/<sub>/comments/<id>/...` | the post + its whole comment tree |
|
|
25
|
+
| `reddit.com/r/<sub>` or `r/<sub>` | that subreddit's threads |
|
|
26
|
+
| `x.com/<user>/status/<id>` | the tweet + self-thread + replies |
|
|
27
|
+
| `x.com/<user>` or `@<user>` | that account's posts, pinned first |
|
|
28
|
+
|
|
29
|
+
Every item carries text, author + `avatar_url`, timestamps, permalink, engagement stats, and **media URLs** (X video resolves to the top-bitrate MP4). It never summarizes or ranks — you read `items[]` and choose the hook, the punchline comment, the stat worth a card. `max_records` is your spend ceiling; `nextCursor` resumes. Paid plans only.
|
|
30
|
+
|
|
31
|
+
Under the hood it brokers the **reddit-lead-gen** and **x-lead-gen** OfficeX apps rather than holding Reddit/X keys itself — so it's a job wrapping a job, and the primitive waits out the upstream job for you. Full payload/response reference in `references/primitives.md` → *Primitive: social/recycle*.
|
|
32
|
+
|
|
33
|
+
Pipe it forward: any `media[].url` goes straight into a layer or through `videos/download`; `profile.avatar_url` makes the "who said this" badge. **Recycled text is source material, not a script** — rewrite it to the hook/loop/payoff standard instead of pasting a comment thread on screen.
|
|
34
|
+
|
|
18
35
|
## Public raws catalog — browse BY CATEGORY (check here first)
|
|
19
36
|
|
|
20
37
|
Before you hunt footage and long before you generate it, check what the platform **already has**. The public raws catalog is a curated, pre-tagged, free-to-use pool of short clips, organised into **categories** (shelves) — and browsing it by category is one of the highest-leverage moves available to you, for two reasons:
|
|
@@ -214,9 +214,9 @@ The licensed harness also carries the **generative build workflow** guidance (ch
|
|
|
214
214
|
| `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) |
|
|
215
215
|
| `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 |
|
|
216
216
|
| `vidfarm remove-greenscreen <image\|video> [--preset green\|blue\|white\|black\|digital-green\|magenta] [--key-color #00FF00] [--tolerance 0.3] [--local] [--gif] [--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. **`--gif` writes a transparent GIF instead** (ANIMATED for a clip; `--gif-fps`/`--gif-width`/`--gif-alpha`) — local-only, 1-bit alpha, for GIF-only sticker surfaces; prefer PNG/WebP/WebM for compositing. Aliases: `greenscreen`, `remove-background-greenscreen`. |
|
|
217
|
-
| `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
|
|
217
|
+
| `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`, plus `hole_pct`/`hollow` — the "the key ate the fill" check (outline-only art keys into a rim around a transparent hole; `--generate` prompts against it automatically, and the console prints a `Hollow:` warning with the fix). Alias: `sticker`. See recipe `cutout-graphics-for-explainers.md`. |
|
|
218
218
|
| `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". |
|
|
219
|
-
| `vidfarm sticker-pack [sheet\|url] [--generate "<theme>"] [--items "a,b,c"] [--count <n>] [--dry-run] [--gap <pct>] [--min-area <pct>] [--output-format png\|webp\|gif] [--out-dir <d>]` | **local, free, ffmpeg-only** (no job; only `--generate` bills, ONCE for the whole set) — key + alpha-channel segmentation + per-item trim | **The STICKER-PACK maker — the answer whenever a director asks for "a sticker pack" / prop set / icon set.** A pack is ONE greenscreen sheet holding every item, keyed once and then masked apart: 1/N the cost of N `cutout` calls, and the only way a cast stays on-style. Finds each item **automatically** by segmenting the keyed sheet's alpha into connected islands — no hand-measured `--crop` rects — and writes one snug transparent file per item (named from `--items`, reading order) plus a `stickers.json` manifest. `--dry-run` prints the detected boxes first; `--gap` merges (lower) or splits (raise) items that came out joined/broken; items have **no maximum size** — a full-frame landscape/backdrop is as valid a sticker as a 3% icon. **Plate color is chosen for you:** when generating it reads the subject and moves the plate off any hue the art uses (green → magenta → blue → black → white — a pack of leaves/frogs/money on GREEN would key holes through the art), and when splitting an existing sheet it DETECTS the plate from the sheet's four corners, so a red/purple sheet handed back from a web tool just works. Pin it with `--key-color`/`--preset`, or `--no-auto-key` for plain green. `--output-format gif` emits 1-bit-alpha GIFs for GIF-only surfaces. IMAGE-only. Aliases: `stickers`, `sticker-sheet`. See recipe `cutout-graphics-for-explainers.md` → "A sticker pack". |
|
|
219
|
+
| `vidfarm sticker-pack [sheet\|url] [--generate "<theme>"] [--items "a,b,c"] [--count <n>] [--dry-run] [--gap <pct>] [--min-area <pct>] [--output-format png\|webp\|gif] [--out-dir <d>]` | **local, free, ffmpeg-only** (no job; only `--generate` bills, ONCE for the whole set) — key + alpha-channel segmentation + per-item trim | **The STICKER-PACK maker — the answer whenever a director asks for "a sticker pack" / prop set / icon set.** A pack is ONE greenscreen sheet holding every item, keyed once and then masked apart: 1/N the cost of N `cutout` calls, and the only way a cast stays on-style. Finds each item **automatically** by segmenting the keyed sheet's alpha into connected islands — no hand-measured `--crop` rects — and writes one snug transparent file per item (named from `--items`, reading order) plus a `stickers.json` manifest. `--dry-run` prints the detected boxes first; `--gap` merges (lower) or splits (raise) items that came out joined/broken; items have **no maximum size** — a full-frame landscape/backdrop is as valid a sticker as a 3% icon. **Plate color is chosen for you:** when generating it reads the subject and moves the plate off any hue the art uses (green → magenta → blue → black → white — a pack of leaves/frogs/money on GREEN would key holes through the art), and when splitting an existing sheet it DETECTS the plate from the sheet's four corners, so a red/purple sheet handed back from a web tool just works. Pin it with `--key-color`/`--preset`, or `--no-auto-key` for plain green. **The ART is made key-safe too:** the generation prompt is auto-appended with "closed, solidly filled shapes, no outline-only/hollow art, nothing in the plate hue or a near-shade, fully opaque, no glow/translucency" — the fix for stickers that come back as a rim around a transparent hole — and after keying each item reports `holes`/`hole_pct`/`hollow` (console `⚠ N% hollow` at ≥20%, plus `--json` and `stickers.json`). It **warns, never blocks** (a ring/frame/donut reads identically); re-generate with the fill clause, or lift that one item with `vidfarm mask --crop …`. `--output-format gif` emits 1-bit-alpha GIFs for GIF-only surfaces. IMAGE-only. Aliases: `stickers`, `sticker-sheet`. See recipe `cutout-graphics-for-explainers.md` → "A sticker pack". |
|
|
220
220
|
| `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 |
|
|
221
221
|
| `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) |
|
|
222
222
|
| `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 |
|
|
@@ -245,7 +245,7 @@ The licensed harness also carries the **generative build workflow** guidance (ch
|
|
|
245
245
|
| `vidfarm whoami` | `GET /api/v1/user/me` | who am I (also prints the active cost mode) |
|
|
246
246
|
| `vidfarm cost-mode [minimize\|hybrid\|rich-ai\|pure-videogen]` | (local `~/.vidfarm/cost-mode.json`) | show/set the money-saving preference every billed command respects; no arg = show + explain each; `--clear` forgets it |
|
|
247
247
|
| `vidfarm mode [interactive\|autonomous]` | local (`~/.vidfarm/interaction-mode.json`) | **The SECOND preference axis — ask it alongside cost mode.** Is the human in the loop? `interactive` = at checkpoints you hand them a copy-paste brief (an image prompt for a FREE web generator; a keyword list for sourcing clips) and they hand files back — usually BETTER output, and in `minimize` the only way to get custom art at all. `autonomous` = you finish solo within the budget. Independent of cost mode: every cost mode runs either way. No arg prints the pair + the explainer to relay; `--clear` forgets; per-run `--interactive`/`--autonomous`. Aliases: `interaction-mode`, `interaction`. |
|
|
248
|
-
| `vidfarm handoff image --theme "<what>" [--items "a,b,c"] [--style …] [--single]` | local (pure text) | **The interactive-mode IMAGE brief.** Prints the exact prompt, the numbered steps, the free tools (meta.ai / ChatGPT / Gemini / HF Spaces) and the follow-up command. Defaults to a **sticker pack**: ONE sheet holding every item on a chroma plate → `vidfarm sticker-pack` splits it for $0. Picks a plate the art won't collide with (green art → magenta plate) and carries that `--key-color` into the follow-up. `--single` for one subject. |
|
|
248
|
+
| `vidfarm handoff image --theme "<what>" [--items "a,b,c"] [--style …] [--single]` | local (pure text) | **The interactive-mode IMAGE brief.** Prints the exact prompt, the numbered steps, the free tools (meta.ai / ChatGPT / Gemini / HF Spaces) and the follow-up command. Defaults to a **sticker pack**: ONE sheet holding every item on a chroma plate → `vidfarm sticker-pack` splits it for $0. Picks a plate the art won't collide with (green art → magenta plate), spells out the **key-safe fill rules** (closed solidly-filled shapes, no outline-only/hollow art, nothing in a near-plate shade, fully opaque) so the sheet doesn't come back as rims around holes, and carries that `--key-color` into the follow-up. `--single` for one subject. |
|
|
249
249
|
| `vidfarm handoff raws --keywords "a,b" [--platforms tiktok,youtube] [--count N] [--purpose "…"]` | local (pure text) | **The interactive-mode CLIP-SOURCING brief** — the bottom rung of the sourcing ladder (browser control → `clipper`/`raws scan --cloud` → public raws → the human). Prints what to search, how to download (a Google *search* for a downloader, never a link that rots), and the import command for when the folder is ready. |
|
|
250
250
|
| `vidfarm storyboard [dir] [--init] [--frames "Title\|scene,…"] [--json]` | local (`STORYBOARD.md` / `SCRIPT.md`) | **The plan pass, and a core part of the composition format.** Scaffolds/reads the project's `STORYBOARD.md`: ordered frames with `duration`/`status`/`src`/`scene`/`voiceover`. The Vidfarm editor renders this file in its **Storyboard** view (contact sheet + per-frame comments + `outline → built → animated` progress), so it's the cheapest place to get a director's approval before building. Not to be confused with `vidfarm sequence` (which GENERATES storyboard images for the pure-videogen pipeline). Alias: `plan`. |
|
|
251
251
|
| `vidfarm wallet [--job <id>\|--tracer <t>] [--limit <n>]` | `GET /api/v1/user/me/wallet` | cost log: balance + lifetime spend + recent charges. `--job <renderJobId>` prints **what that one video cost** (sums its charges); `--tracer <t>` sums a tracer. Cloud-only; readable on the free plan too (shows $0.00). Aliases: `spend`, `costs` |
|
|
@@ -254,6 +254,7 @@ The licensed harness also carries the **generative build workflow** guidance (ch
|
|
|
254
254
|
| `vidfarm download <url> [dest]` | (streams any URL to disk) | download media from a **direct** media URL. Free — no plan, no job |
|
|
255
255
|
| `vidfarm download-video <url> [--quality best\|hd\|full_hd]` | `POST /api/v1/primitives/videos/download` + poll | **download a video FROM A WEBSITE** (YouTube/TikTok/IG/X/other supported posts) into durable Vidfarm media; photo/carousel posts return an ordered slideshow. **PAID PLAN** (wallet-billed resolver; free plans get 402). Aliases: `download-post`, `download-url` |
|
|
256
256
|
| `vidfarm download-audio <url>` | `POST /api/v1/primitives/audio/download` + poll | download just the AUDIO track (music/voice) off a website post into a durable audio file. **PAID PLAN**. Alias: `download-post-audio` |
|
|
257
|
+
| `vidfarm recycle <source>` | `POST /api/v1/primitives/social/recycle` + poll | RECYCLE a Reddit/X source into reusable JSON ("tweet to tiktok", "reddit to tiktok"): thread + comments, subreddit threads, X thread + replies, or an account's posts — text, author + avatar, stats, media URLs. `--out` saves the full payload. **PAID PLAN**. Aliases: `recycle-social`, `tweet-to-video`, `reddit-to-video` |
|
|
257
258
|
|
|
258
259
|
> **Free plan / `cost-mode minimize`:** both download commands are gated by `guardBilled` and will refuse or warn. The free path is **Chrome**: open the URL in a real browser, save the file off the page (right-click → *Save video as*, downloads pane, or a Googled `"<platform> video downloader"` site), then `vidfarm put-file ./the-file.mp4 --folder <folder>` to bring it in for $0. The CLI prints these steps on a 402. Never tell a free user the video simply can't be downloaded.
|
|
259
260
|
| `vidfarm files [--folder <path>]` | `GET /api/v1/user/me/attachments` | list My Files assets + folders |
|
|
@@ -595,7 +595,7 @@ Beyond the Ken Burns / transition / animated-caption presets, the copilot can ha
|
|
|
595
595
|
|
|
596
596
|
**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.
|
|
597
597
|
|
|
598
|
-
**Make a whole PACK at once (`vidfarm sticker-pack`).** When the scene needs a *set* — props, icons, a cast, plus any backdrops — don't run `cutout` N times. Generate ONE greenscreen sheet holding every item and split it locally: `vidfarm sticker-pack --generate "<theme>" --items "a,b,c" --out-dir ./stickers` (one billed image job for the set), or `vidfarm sticker-pack ./sheet.png` for $0 on a sheet you already have. It keys the plate once, **segments the sheet's alpha channel to find each item automatically** (no hand-measured crop rects), trims each to its true box, and writes `stickers.json`. `--dry-run` to inspect the boxes; `--gap` to fix items that merged (lower) or split (raise). **Item size is unbounded** — a full-frame landscape/skyline/backdrop is as legitimate a sticker as a small icon, and belongs in the same sheet so it shares the cast's style. Recipe: `recipes/cutout-graphics-for-explainers.md` → "A sticker pack".
|
|
598
|
+
**Make a whole PACK at once (`vidfarm sticker-pack`).** When the scene needs a *set* — props, icons, a cast, plus any backdrops — don't run `cutout` N times. Generate ONE greenscreen sheet holding every item and split it locally: `vidfarm sticker-pack --generate "<theme>" --items "a,b,c" --out-dir ./stickers` (one billed image job for the set), or `vidfarm sticker-pack ./sheet.png` for $0 on a sheet you already have. It keys the plate once, **segments the sheet's alpha channel to find each item automatically** (no hand-measured crop rects), trims each to its true box, and writes `stickers.json`. `--dry-run` to inspect the boxes; `--gap` to fix items that merged (lower) or split (raise). **Ask for solidly filled art** — outline-only/hollow shapes (or fills in a near-plate shade) get their interiors keyed away and composite as rims around transparent holes; the generation prompt says so automatically, and each item reports `⚠ N% hollow` / `hole_pct` after the key so you catch it before placing. **Item size is unbounded** — a full-frame landscape/skyline/backdrop is as legitimate a sticker as a small icon, and belongs in the same sheet so it shares the cast's style. Recipe: `recipes/cutout-graphics-for-explainers.md` → "A sticker pack".
|
|
599
599
|
|
|
600
600
|
**Sticker formats.** Still sticker → **PNG** (or `--output-format webp` for smaller files). Sticker whose own artwork moves → **transparent WebM** via `vidfarm remove-greenscreen <video>` (ProRes `.mov` is the local fallback; `--cloud` always returns WebM). Sticker destined for a **GIF-only surface** (chat, forum, Notion, a shareable pack) → `--output-format gif` on `sticker-pack`, or `vidfarm remove-greenscreen <video> --gif` for an animated one; GIF alpha is 1-bit, so edges harden and soft shadows vanish — never the choice for compositing on a timeline.
|
|
601
601
|
|
|
@@ -35,7 +35,7 @@ curl -X POST "$VIDFARM_BASE/api/v1/primitives/images/remove-background" \
|
|
|
35
35
|
|
|
36
36
|
- `POST /api/v1/primitives/images/create-overlay` (flat alias: `POST /api/v1/primitives/create-media-overlay`)
|
|
37
37
|
- Body: `{ "tracer": "...", "payload": { "prompt": "a cartoon rocket ship, flat vector illustration", "provider"?: "...", "model"?: "...", "prompt_attachments"?: ["https://..."], "aspect_ratio"?: "1:1", "image_size"?: "1K"|"2K"|"4K", "key_color"?: "#00FF00", "tolerance"?: 0.3, "softness"?: 0.1, "despill"?: true, "output_format"?: "png"|"webp" }, "webhook_url"?: "..." }`
|
|
38
|
-
- Just describe the **subject** in `prompt` — do NOT describe the background; the primitive appends the flat key-color background requirement for you.
|
|
38
|
+
- Just describe the **subject** in `prompt` — do NOT describe the background; the primitive appends the flat key-color background requirement for you, **and** the key-safe *artwork* requirement (closed solidly-filled shapes, no outline-only/hollow art, nothing on the subject in the key hue or a near-shade, fully opaque, no glow/translucency). That second clause is what stops the classic failure: outline art whose interior is bare plate keys into a **rim around a transparent hole**. If you build the prompt yourself for `images/generate` instead of using this primitive, write that clause in by hand.
|
|
39
39
|
- `key_color` lets you key against something other than green if your subject is itself green-heavy (e.g. key against `#FF00FF` magenta for a green frog).
|
|
40
40
|
- The chroma-key params (`tolerance`/`softness`/`despill`) behave exactly as in `image_remove_background_greenscreen`.
|
|
41
41
|
- Response: `primary_file_url` is the finished transparent overlay; `greenscreen_source_url` is the raw pre-key frame (kept so you can re-key at a different tolerance without paying for another generation).
|
|
@@ -136,6 +136,85 @@ curl -X POST "$VIDFARM_BASE/api/v1/primitives/audio/download" \
|
|
|
136
136
|
-d '{"tracer": "demo-download-audio", "payload": {"source_url": "https://www.youtube.com/watch?v=example"}}'
|
|
137
137
|
```
|
|
138
138
|
|
|
139
|
+
## Primitive: social/recycle
|
|
140
|
+
|
|
141
|
+
**"Tweet to TikTok" and "Reddit to TikTok" live here.** The text-and-thread sibling of `videos/download`: point it at a Reddit or X/Twitter source and it **decomposes** that source into reusable JSON — the post text, the comment/reply thread, every image/video media URL, the author's profile pic and follower count, and the engagement stats — so YOU decide what to recycle into a new video. It deliberately does **not** summarize, rank, score, or filter. It dumps the data back raw.
|
|
142
|
+
|
|
143
|
+
**Vidfarm does not hold Reddit/X vendor keys.** This primitive is a thin broker over the two OfficeX apps that already own that integration — **reddit-lead-gen** and **x-lead-gen** — called through their public REST job API. They own the vendor keys, retries, rate-limit budget, auto-pagination, and 90-day result storage; vidfarm owns target resolution, normalization, and the wallet charge. Both are async job APIs, so a recycle is **a job wrapping a job** — but the nesting is invisible: this primitive polls upstream to completion server-side and only resolves when the data is in hand, exactly like the GhostCut leg of `videos/remove-captions`. You poll `GET /api/v1/primitives/jobs/:jobId` like any other primitive.
|
|
144
|
+
|
|
145
|
+
- `POST /api/v1/primitives/social/recycle` (alias: `POST /api/v1/primitives/recycle-social`)
|
|
146
|
+
- Body: `{ "tracer": "...", "payload": { "source_url": "https://..." }, "webhook_url"?: "..." }`
|
|
147
|
+
|
|
148
|
+
### The four shapes
|
|
149
|
+
|
|
150
|
+
| You pass | Mode (auto-detected) | You get |
|
|
151
|
+
| --- | --- | --- |
|
|
152
|
+
| `https://reddit.com/r/<sub>/comments/<id>/...` | `reddit_thread` | the post + its comment tree, flattened in reading order |
|
|
153
|
+
| `https://reddit.com/r/<sub>` (or `r/<sub>`) | `reddit_channel` | that subreddit's threads, paginated |
|
|
154
|
+
| `https://x.com/<user>/status/<id>` | `twitter_thread` | the tweet + the author's self-thread + its replies, paginated |
|
|
155
|
+
| `https://x.com/<user>` (or `@<user>`) | `twitter_channel` | that account's tweets (pinned first), paginated |
|
|
156
|
+
|
|
157
|
+
A fifth mode, `reddit_user` (`https://reddit.com/user/<name>` or `u/<name>`), returns a redditor's posts and comments. Pass `mode` explicitly to override auto-detection, or `handle` + `network` instead of a URL.
|
|
158
|
+
|
|
159
|
+
### Payload
|
|
160
|
+
|
|
161
|
+
| Field | Default | Notes |
|
|
162
|
+
| --- | --- | --- |
|
|
163
|
+
| `source_url` | — | Reddit/X URL. Required unless you pass `handle` + `network`. Aliases: `url`, `thread_url`, `profile_url` |
|
|
164
|
+
| `handle` + `network` | — | `{ "handle": "wallstreetbets", "network": "reddit" }`. Aliases for handle: `username`, `screenname`, `subreddit` |
|
|
165
|
+
| `mode` | auto | Override the detected shape |
|
|
166
|
+
| `max_records` | `100` | Cap on records (1–1000). The lead-gen apps auto-paginate to reach it and reserve credits for `ceil(max_records / 25)` pages — **this is your spend ceiling** |
|
|
167
|
+
| `cursor` | — | Resume token from a previous run's `nextCursor` |
|
|
168
|
+
| `prompt_filter` | — | Hand the lead-gen app a Gemini scoring prompt; adds `match_score` + `ai_notes` per item and costs slightly more per task upstream. We surface it and never act on it |
|
|
169
|
+
| `query` | — | Restrict a channel pull to matching posts (Reddit search / X `from:<user> <query>`) |
|
|
170
|
+
| `filter` / `sort` / `time_filter` | — | Reddit listing controls (`hot`, `new`, `top`, …) |
|
|
171
|
+
| `search_type` | — | X search mode (`Top`, `Latest`, `Photos`, `Videos`) |
|
|
172
|
+
| `include_profile` | `true` | One extra upstream job for the subreddit/account avatar, banner, bio, follower count |
|
|
173
|
+
| `include_comments` | `true` | Thread modes only — set `false` to skip the replies leg (and its cost) |
|
|
174
|
+
| `save_raw` | `true` | Persist the untouched upstream pages as `raw-pages.json` |
|
|
175
|
+
|
|
176
|
+
### Result
|
|
177
|
+
|
|
178
|
+
Poll `GET /api/v1/primitives/jobs/:jobId` until `status: "succeeded"`, then read:
|
|
179
|
+
|
|
180
|
+
- `items[]` — the normalized view. Each entry: `{ network, kind: "post" | "comment", id, title, text, created_at, permalink, link_url, author: { handle, name, avatar_url, banner_url, followers, profile_url, bio, verified }, stats: { … }, media: [{ type: "image"|"video"|"gif", url, thumbnail_url, width, height, duration_ms }] }`. X video media resolves to the **highest-bitrate MP4** variant, not the HLS manifest.
|
|
181
|
+
- `profile` — the subreddit/account itself (avatar, banner, bio, subscriber/follower count).
|
|
182
|
+
- `mediaUrls[]` — every media URL flattened, ready to drop into layers or feed to `videos/download`.
|
|
183
|
+
- `nextCursor` — pass it back as `payload.cursor` to page further. `truncated: true` means there IS more.
|
|
184
|
+
- `counts` — `{ items, posts, comments, media, pages, upstream_jobs, credits_charged }`.
|
|
185
|
+
- `upstreamJobs[]` — one entry per lead-gen job we ran: `{ network, endpoint, job_id, status, tasks, credits_charged, view_url }`. **`view_url` opens a NocoDB spreadsheet of that pull — hand it to the human when they want to browse results visually.**
|
|
186
|
+
- `recycled.json` (also `primary_file_url`) — the **complete** normalized payload. `items` in the job result is trimmed when it would blow the job-record size limit; `itemsTruncatedInline: true` tells you to read the artifact instead.
|
|
187
|
+
- `raw-pages.json` — every untouched upstream page, so you can reach fields this primitive doesn't model.
|
|
188
|
+
|
|
189
|
+
- Billing: wallet, metered on the **OfficeX credits the lead-gen apps actually consumed** (`officex_social_recycle`), converted at `OFFICEX_CREDIT_USD`. A one-task thread costs a fraction of a 10-page timeline. Paid plans only — free plans get `402`, same as the download routes. If the *upstream* app runs out of OfficeX credits you get a `402` naming that app, which is an operator problem, not a customer one.
|
|
190
|
+
- devcli wrapper: `vidfarm recycle <source>` (aliases: `recycle-social`, `tweet-to-video`, `reddit-to-video`). Flags: `--max-records N`, `--cursor`, `--query`, `--filter hot|new|top`, `--mode`, `--network`, `--no-comments`, `--no-profile`, `--out ./recycled.json` (writes the FULL normalized payload to disk), `--no-wait`. It prints the counts, the upstream NocoDB `view_url`s, and the resume cursor.
|
|
191
|
+
- Legs: a recycle runs 1–3 upstream jobs (profile, feed, replies) **sequentially**, so a failure on the first never pays for the second. The optional profile leg degrades rather than sinking the pull.
|
|
192
|
+
- Timeouts: upstream polling gives up after `SOCIAL_RECYCLE_POLL_TIMEOUT_MS` (default 5 min); the upstream job may still finish and can be browsed in that app.
|
|
193
|
+
|
|
194
|
+
### Turning a recycle into a video
|
|
195
|
+
|
|
196
|
+
1. Recycle the source with a small `max_records` (25–50 is usually plenty for one video).
|
|
197
|
+
2. Read `items[]` yourself and pick the beat: the post's hook line, the two funniest comments, the stat everyone reacted to.
|
|
198
|
+
3. Pull any `media[].url` you want as real footage through `videos/download` or straight into a layer; use `profile.avatar_url` for the "who said this" badge.
|
|
199
|
+
4. Script and build as normal. **The recycled text is source material, not a script** — rewrite it to the hook/loop/payoff standard rather than pasting a comment thread on screen.
|
|
200
|
+
|
|
201
|
+
Example:
|
|
202
|
+
|
|
203
|
+
```bash
|
|
204
|
+
curl -X POST "$VIDFARM_BASE/api/v1/primitives/social/recycle" \
|
|
205
|
+
-H "vidfarm-api-key: $VIDFARM_API_KEY" \
|
|
206
|
+
-H "content-type: application/json" \
|
|
207
|
+
-d '{"tracer": "demo-recycle", "payload": {"source_url": "https://www.reddit.com/r/wallstreetbets/comments/p0esdp/slug/", "max_records": 50}}'
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
```bash
|
|
211
|
+
# An X account's recent posts, no replies, cheapest possible pull
|
|
212
|
+
curl -X POST "$VIDFARM_BASE/api/v1/primitives/recycle-social" \
|
|
213
|
+
-H "vidfarm-api-key: $VIDFARM_API_KEY" \
|
|
214
|
+
-H "content-type: application/json" \
|
|
215
|
+
-d '{"tracer": "demo-recycle-x", "payload": {"source_url": "https://x.com/elonmusk", "max_records": 40, "include_profile": true}}'
|
|
216
|
+
```
|
|
217
|
+
|
|
139
218
|
## Primitive: video_remove_captions
|
|
140
219
|
|
|
141
220
|
Remove burned-in captions/subtitles/on-screen text from any video URL (GhostCut-powered — the same pipeline auto-decompose uses for its caption-free mirror, exposed as a standalone reusable primitive). Result is a durable caption-free MP4 stored at a Vidfarm URL.
|
package/SKILL.director.md
CHANGED
|
@@ -210,6 +210,7 @@ Present both harnesses to the director, recommend (A) unless they've asked for p
|
|
|
210
210
|
- **Stickers are usually animated, not pasted.** Once placed, animate each one with `vidfarm keyframes` presets (`pop-in`, `float`, `shake`, `grow`, `slide-in-left`, `drift`) — that's HTML/CSS canvas motion, deterministic, free, and identical in preview and render. Layer moves up (pop-in, then idle float) for real life. See `references/editor-workflows.md` → "Cutout graphics for explainers".
|
|
211
211
|
- **A sticker can carry its OWN motion too.** A *moving* subject has no single bounding box, so it isn't a PNG: key the clip with `vidfarm remove-greenscreen <video>` → transparent WebM (browser/editor-playable, the right choice on a composition).
|
|
212
212
|
- **The plate must be a color the ART DOESN'T USE — green is a default, not a law.** A chroma key removes exactly ONE color, so a green plate under green artwork (leaves, frogs, plants, money, an "eco" icon set) keys holes straight through the stickers. `sticker-pack`/`cutout` handle this for you in two ways, and you should still say it out loud when it matters: **(a)** when generating, they read the subject and move the plate off any hue it mentions — green → magenta (`#FF00FF`) → blue (`#0047BB`) → black → white — and print which plate they picked and why; **(b)** when splitting a sheet you already have, they **detect the plate from the sheet's four corners**, so a red/purple/blue sheet handed back from a web generator just works. Pin it yourself with `--key-color "#FF00FF"` / `--preset magenta` (that disables the auto-pick), or `--no-auto-key` to force plain green. If a pack's palette collides with *every* standard plate, split it into two sheets on two different plates rather than fighting the key.
|
|
213
|
+
- **The ART must be CLOSED and SOLIDLY FILLED — this is the other half of surviving the key, and the #1 way stickers come back broken.** Ask an image model for "icons on a green plate" and it will happily draw **outline art**: a colored stroke with the shape's interior left as bare plate. It looks perfect on the sheet, and after the key each sticker is a **rim floating around a see-through hole** (an apple-shaped outline with nothing inside it). Same outcome from a *near-plate* fill (the keyer works on tolerance, not exact match), a translucent/glassy/glowing material, or a soft glow fading into the plate. **You cannot key those pixels back — it has to be in the prompt:** *"every object is a closed, solidly filled shape; outlines must enclose an opaque fill of a different color; no outline-only or hollow art; nothing on the art in the plate color or any near-shade of it; fully opaque, no translucency, glow or drop shadow."* `cutout --generate`, `sticker-pack --generate`, `handoff image` and the `create-overlay` primitive **append that clause for you** with the chosen plate hex — write it yourself only when you prompt a generator directly. After the key, both commands report per-item `hole_pct`/`hollow` (console `⚠ N% hollow`, `--json`, `stickers.json`) — a ring or picture frame reads the same way, so it **warns, never blocks**. Flagged and it shouldn't be? Re-generate with the fill clause; a *near*-plate fill can sometimes be rescued with a lower `--tolerance`; one stubborn item can be lifted with `vidfarm mask --crop …` (ONNX matting ignores fill color).
|
|
213
214
|
- **Transparent GIF is supported, for GIF-only surfaces.** `vidfarm sticker-pack … --output-format gif` (stills) and `vidfarm remove-greenscreen <video> --gif` (animated) emit transparent GIFs. GIF alpha is **1-bit**, so edges go hard — fine for chat/forum/Notion sticker surfaces, worse than PNG/WebP/WebM for compositing on a timeline. Prefer PNG/WebP/WebM unless the destination only eats GIF.
|
|
214
215
|
|
|
215
216
|
**Explainer house style — the defaults to build with unless told otherwise.** **White background / light mode** (plain white stage, no gradients, no dark mode, no photo backdrop), **kinetic word-by-word captions** in dark ink on the light stage (`vidfarm captions generate --style word-pop --color "#111111" --active-color "#7C3AED" --background-style plain` — skip outlines/shadows, they're only needed over busy footage), and **female TTS narration** (`vidfarm tts --voice coral` on OpenAI — `nova` for energy, `sage` for calm; `Kore`/`Leda` on Gemini; any ElevenLabs voice via `vidfarm voices`). **Keep it clean and simple** — one idea on screen at a time, two or three cutouts per beat, one accent color, one font, lots of white space; remove before you add. **Illustrations default to simplicity**: flat vector, simple shapes, minimal detail, 2–3 flat colors, no baked-in text — simple art keys cleanly, trims tight, and stays on-style across the whole cast. State the defaults once so the director can override any of them. Full detail: recipe `recipes/cutout-graphics-for-explainers.md` (“House style — the explainer defaults”).
|
|
@@ -317,6 +318,7 @@ Choose the narrowest path that satisfies the request.
|
|
|
317
318
|
3. If the task is “change this video,” read `references/editor-workflows.md`.
|
|
318
319
|
4. If the task is “find footage” or “use our existing assets,” read `references/assets-and-sourcing.md`.
|
|
319
320
|
4b. If the task is **“download this video/audio off a website”** (a pasted YouTube / TikTok / Instagram / X post URL the user wants the actual file from), Vidfarm does that for you on a **paid plan** — `POST /api/v1/primitives/videos/download` (or `/audio/download`), devcli `vidfarm download-video <url>` / `vidfarm download-audio <url>`. **Free plan → do not call it; walk the user through opening the URL in Chrome and downloading it from the page, then `vidfarm put-file` the local file in for $0.** Details in `references/assets-and-sourcing.md` → `references/primitives.md`.
|
|
321
|
+
4c. If the task is **“turn this Reddit/X thread, subreddit, or account into a video”** — “tweet to TikTok”, “Reddit to TikTok”, “make a video from this thread”, “what are the top comments saying” — run `vidfarm recycle <source>` (or `POST /api/v1/primitives/social/recycle`) with the URL. It **decomposes** the source into raw JSON (text, comment tree, media URLs, author pics, stats) and hands it back unranked so YOU pick what to remix. **Paid plan; `max_records` is the spend ceiling.** Brokers the reddit-lead-gen / x-lead-gen OfficeX apps, so it waits out their async job for you. Details in `references/assets-and-sourcing.md` → `references/primitives.md`.
|
|
320
322
|
5. If the task is scripted, local, CI-driven, or `vidfarm serve`-based, read `references/automation-and-local-dev.md`.
|
|
321
323
|
6. If the task explicitly asks for a primitive or needs specialized generation/transcription work, read `references/primitives.md`.
|
|
322
324
|
7. If the task is the MARKETPLACE (ordering videos from specialist agents): browsing is web-only for paying customers — send the human to https://vidfarm.cc/marketplace, never render it locally. Placing/listing orders is the thin REST wrapper in `references/core-workflows.md` (§ Marketplace); anything deeper on a gig (inbox, proofs, payouts) needs the external Dollar Platoon skill — `npx skills add https://github.com/OfficeXApp/dollarplatoon-skill` — the same way FlockPoster work beyond scheduling needs `npx skills add https://github.com/OfficeXApp/flockposter-skill`.
|
|
@@ -1247,7 +1249,7 @@ Beyond the Ken Burns / transition / animated-caption presets, the copilot can ha
|
|
|
1247
1249
|
|
|
1248
1250
|
**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.
|
|
1249
1251
|
|
|
1250
|
-
**Make a whole PACK at once (`vidfarm sticker-pack`).** When the scene needs a *set* — props, icons, a cast, plus any backdrops — don't run `cutout` N times. Generate ONE greenscreen sheet holding every item and split it locally: `vidfarm sticker-pack --generate "<theme>" --items "a,b,c" --out-dir ./stickers` (one billed image job for the set), or `vidfarm sticker-pack ./sheet.png` for $0 on a sheet you already have. It keys the plate once, **segments the sheet's alpha channel to find each item automatically** (no hand-measured crop rects), trims each to its true box, and writes `stickers.json`. `--dry-run` to inspect the boxes; `--gap` to fix items that merged (lower) or split (raise). **Item size is unbounded** — a full-frame landscape/skyline/backdrop is as legitimate a sticker as a small icon, and belongs in the same sheet so it shares the cast's style. Recipe: `recipes/cutout-graphics-for-explainers.md` → "A sticker pack".
|
|
1252
|
+
**Make a whole PACK at once (`vidfarm sticker-pack`).** When the scene needs a *set* — props, icons, a cast, plus any backdrops — don't run `cutout` N times. Generate ONE greenscreen sheet holding every item and split it locally: `vidfarm sticker-pack --generate "<theme>" --items "a,b,c" --out-dir ./stickers` (one billed image job for the set), or `vidfarm sticker-pack ./sheet.png` for $0 on a sheet you already have. It keys the plate once, **segments the sheet's alpha channel to find each item automatically** (no hand-measured crop rects), trims each to its true box, and writes `stickers.json`. `--dry-run` to inspect the boxes; `--gap` to fix items that merged (lower) or split (raise). **Ask for solidly filled art** — outline-only/hollow shapes (or fills in a near-plate shade) get their interiors keyed away and composite as rims around transparent holes; the generation prompt says so automatically, and each item reports `⚠ N% hollow` / `hole_pct` after the key so you catch it before placing. **Item size is unbounded** — a full-frame landscape/skyline/backdrop is as legitimate a sticker as a small icon, and belongs in the same sheet so it shares the cast's style. Recipe: `recipes/cutout-graphics-for-explainers.md` → "A sticker pack".
|
|
1251
1253
|
|
|
1252
1254
|
**Sticker formats.** Still sticker → **PNG** (or `--output-format webp` for smaller files). Sticker whose own artwork moves → **transparent WebM** via `vidfarm remove-greenscreen <video>` (ProRes `.mov` is the local fallback; `--cloud` always returns WebM). Sticker destined for a **GIF-only surface** (chat, forum, Notion, a shareable pack) → `--output-format gif` on `sticker-pack`, or `vidfarm remove-greenscreen <video> --gif` for an animated one; GIF alpha is 1-bit, so edges harden and soft shadows vanish — never the choice for compositing on a timeline.
|
|
1253
1255
|
|
|
@@ -1542,6 +1544,23 @@ Both are async primitive jobs — poll `GET /api/v1/primitives/jobs/:jobId` unti
|
|
|
1542
1544
|
|
|
1543
1545
|
**Don't reach for this when the real goal is clips.** If the user wants short clips mined out of a long video, `POST /raws/scan` with `source_url` already downloads the source for you as part of the hunt (below) — a separate download call is wasted spend.
|
|
1544
1546
|
|
|
1547
|
+
## Recycle a Reddit or X source into content ("tweet to TikTok", "Reddit to TikTok")
|
|
1548
|
+
|
|
1549
|
+
When the source isn't a video file but a **conversation** — a Reddit thread and its comments, a subreddit's front page, an X thread and its replies, an account's timeline — the download routes are the wrong tool. Use `vidfarm recycle <source>` (devcli) or `POST /api/v1/primitives/social/recycle` (alias `/recycle-social`), which **decomposes** the source into JSON you can remix:
|
|
1550
|
+
|
|
1551
|
+
| Paste this | You get back |
|
|
1552
|
+
| --- | --- |
|
|
1553
|
+
| `reddit.com/r/<sub>/comments/<id>/...` | the post + its whole comment tree |
|
|
1554
|
+
| `reddit.com/r/<sub>` or `r/<sub>` | that subreddit's threads |
|
|
1555
|
+
| `x.com/<user>/status/<id>` | the tweet + self-thread + replies |
|
|
1556
|
+
| `x.com/<user>` or `@<user>` | that account's posts, pinned first |
|
|
1557
|
+
|
|
1558
|
+
Every item carries text, author + `avatar_url`, timestamps, permalink, engagement stats, and **media URLs** (X video resolves to the top-bitrate MP4). It never summarizes or ranks — you read `items[]` and choose the hook, the punchline comment, the stat worth a card. `max_records` is your spend ceiling; `nextCursor` resumes. Paid plans only.
|
|
1559
|
+
|
|
1560
|
+
Under the hood it brokers the **reddit-lead-gen** and **x-lead-gen** OfficeX apps rather than holding Reddit/X keys itself — so it's a job wrapping a job, and the primitive waits out the upstream job for you. Full payload/response reference in `references/primitives.md` → *Primitive: social/recycle*.
|
|
1561
|
+
|
|
1562
|
+
Pipe it forward: any `media[].url` goes straight into a layer or through `videos/download`; `profile.avatar_url` makes the "who said this" badge. **Recycled text is source material, not a script** — rewrite it to the hook/loop/payoff standard instead of pasting a comment thread on screen.
|
|
1563
|
+
|
|
1545
1564
|
## Public raws catalog — browse BY CATEGORY (check here first)
|
|
1546
1565
|
|
|
1547
1566
|
Before you hunt footage and long before you generate it, check what the platform **already has**. The public raws catalog is a curated, pre-tagged, free-to-use pool of short clips, organised into **categories** (shelves) — and browsing it by category is one of the highest-leverage moves available to you, for two reasons:
|
|
@@ -1908,9 +1927,9 @@ The licensed harness also carries the **generative build workflow** guidance (ch
|
|
|
1908
1927
|
| `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) |
|
|
1909
1928
|
| `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 |
|
|
1910
1929
|
| `vidfarm remove-greenscreen <image\|video> [--preset green\|blue\|white\|black\|digital-green\|magenta] [--key-color #00FF00] [--tolerance 0.3] [--local] [--gif] [--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. **`--gif` writes a transparent GIF instead** (ANIMATED for a clip; `--gif-fps`/`--gif-width`/`--gif-alpha`) — local-only, 1-bit alpha, for GIF-only sticker surfaces; prefer PNG/WebP/WebM for compositing. Aliases: `greenscreen`, `remove-background-greenscreen`. |
|
|
1911
|
-
| `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
|
|
1930
|
+
| `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`, plus `hole_pct`/`hollow` — the "the key ate the fill" check (outline-only art keys into a rim around a transparent hole; `--generate` prompts against it automatically, and the console prints a `Hollow:` warning with the fix). Alias: `sticker`. See recipe `cutout-graphics-for-explainers.md`. |
|
|
1912
1931
|
| `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". |
|
|
1913
|
-
| `vidfarm sticker-pack [sheet\|url] [--generate "<theme>"] [--items "a,b,c"] [--count <n>] [--dry-run] [--gap <pct>] [--min-area <pct>] [--output-format png\|webp\|gif] [--out-dir <d>]` | **local, free, ffmpeg-only** (no job; only `--generate` bills, ONCE for the whole set) — key + alpha-channel segmentation + per-item trim | **The STICKER-PACK maker — the answer whenever a director asks for "a sticker pack" / prop set / icon set.** A pack is ONE greenscreen sheet holding every item, keyed once and then masked apart: 1/N the cost of N `cutout` calls, and the only way a cast stays on-style. Finds each item **automatically** by segmenting the keyed sheet's alpha into connected islands — no hand-measured `--crop` rects — and writes one snug transparent file per item (named from `--items`, reading order) plus a `stickers.json` manifest. `--dry-run` prints the detected boxes first; `--gap` merges (lower) or splits (raise) items that came out joined/broken; items have **no maximum size** — a full-frame landscape/backdrop is as valid a sticker as a 3% icon. **Plate color is chosen for you:** when generating it reads the subject and moves the plate off any hue the art uses (green → magenta → blue → black → white — a pack of leaves/frogs/money on GREEN would key holes through the art), and when splitting an existing sheet it DETECTS the plate from the sheet's four corners, so a red/purple sheet handed back from a web tool just works. Pin it with `--key-color`/`--preset`, or `--no-auto-key` for plain green. `--output-format gif` emits 1-bit-alpha GIFs for GIF-only surfaces. IMAGE-only. Aliases: `stickers`, `sticker-sheet`. See recipe `cutout-graphics-for-explainers.md` → "A sticker pack". |
|
|
1932
|
+
| `vidfarm sticker-pack [sheet\|url] [--generate "<theme>"] [--items "a,b,c"] [--count <n>] [--dry-run] [--gap <pct>] [--min-area <pct>] [--output-format png\|webp\|gif] [--out-dir <d>]` | **local, free, ffmpeg-only** (no job; only `--generate` bills, ONCE for the whole set) — key + alpha-channel segmentation + per-item trim | **The STICKER-PACK maker — the answer whenever a director asks for "a sticker pack" / prop set / icon set.** A pack is ONE greenscreen sheet holding every item, keyed once and then masked apart: 1/N the cost of N `cutout` calls, and the only way a cast stays on-style. Finds each item **automatically** by segmenting the keyed sheet's alpha into connected islands — no hand-measured `--crop` rects — and writes one snug transparent file per item (named from `--items`, reading order) plus a `stickers.json` manifest. `--dry-run` prints the detected boxes first; `--gap` merges (lower) or splits (raise) items that came out joined/broken; items have **no maximum size** — a full-frame landscape/backdrop is as valid a sticker as a 3% icon. **Plate color is chosen for you:** when generating it reads the subject and moves the plate off any hue the art uses (green → magenta → blue → black → white — a pack of leaves/frogs/money on GREEN would key holes through the art), and when splitting an existing sheet it DETECTS the plate from the sheet's four corners, so a red/purple sheet handed back from a web tool just works. Pin it with `--key-color`/`--preset`, or `--no-auto-key` for plain green. **The ART is made key-safe too:** the generation prompt is auto-appended with "closed, solidly filled shapes, no outline-only/hollow art, nothing in the plate hue or a near-shade, fully opaque, no glow/translucency" — the fix for stickers that come back as a rim around a transparent hole — and after keying each item reports `holes`/`hole_pct`/`hollow` (console `⚠ N% hollow` at ≥20%, plus `--json` and `stickers.json`). It **warns, never blocks** (a ring/frame/donut reads identically); re-generate with the fill clause, or lift that one item with `vidfarm mask --crop …`. `--output-format gif` emits 1-bit-alpha GIFs for GIF-only surfaces. IMAGE-only. Aliases: `stickers`, `sticker-sheet`. See recipe `cutout-graphics-for-explainers.md` → "A sticker pack". |
|
|
1914
1933
|
| `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 |
|
|
1915
1934
|
| `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) |
|
|
1916
1935
|
| `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 |
|
|
@@ -1939,7 +1958,7 @@ The licensed harness also carries the **generative build workflow** guidance (ch
|
|
|
1939
1958
|
| `vidfarm whoami` | `GET /api/v1/user/me` | who am I (also prints the active cost mode) |
|
|
1940
1959
|
| `vidfarm cost-mode [minimize\|hybrid\|rich-ai\|pure-videogen]` | (local `~/.vidfarm/cost-mode.json`) | show/set the money-saving preference every billed command respects; no arg = show + explain each; `--clear` forgets it |
|
|
1941
1960
|
| `vidfarm mode [interactive\|autonomous]` | local (`~/.vidfarm/interaction-mode.json`) | **The SECOND preference axis — ask it alongside cost mode.** Is the human in the loop? `interactive` = at checkpoints you hand them a copy-paste brief (an image prompt for a FREE web generator; a keyword list for sourcing clips) and they hand files back — usually BETTER output, and in `minimize` the only way to get custom art at all. `autonomous` = you finish solo within the budget. Independent of cost mode: every cost mode runs either way. No arg prints the pair + the explainer to relay; `--clear` forgets; per-run `--interactive`/`--autonomous`. Aliases: `interaction-mode`, `interaction`. |
|
|
1942
|
-
| `vidfarm handoff image --theme "<what>" [--items "a,b,c"] [--style …] [--single]` | local (pure text) | **The interactive-mode IMAGE brief.** Prints the exact prompt, the numbered steps, the free tools (meta.ai / ChatGPT / Gemini / HF Spaces) and the follow-up command. Defaults to a **sticker pack**: ONE sheet holding every item on a chroma plate → `vidfarm sticker-pack` splits it for $0. Picks a plate the art won't collide with (green art → magenta plate) and carries that `--key-color` into the follow-up. `--single` for one subject. |
|
|
1961
|
+
| `vidfarm handoff image --theme "<what>" [--items "a,b,c"] [--style …] [--single]` | local (pure text) | **The interactive-mode IMAGE brief.** Prints the exact prompt, the numbered steps, the free tools (meta.ai / ChatGPT / Gemini / HF Spaces) and the follow-up command. Defaults to a **sticker pack**: ONE sheet holding every item on a chroma plate → `vidfarm sticker-pack` splits it for $0. Picks a plate the art won't collide with (green art → magenta plate), spells out the **key-safe fill rules** (closed solidly-filled shapes, no outline-only/hollow art, nothing in a near-plate shade, fully opaque) so the sheet doesn't come back as rims around holes, and carries that `--key-color` into the follow-up. `--single` for one subject. |
|
|
1943
1962
|
| `vidfarm handoff raws --keywords "a,b" [--platforms tiktok,youtube] [--count N] [--purpose "…"]` | local (pure text) | **The interactive-mode CLIP-SOURCING brief** — the bottom rung of the sourcing ladder (browser control → `clipper`/`raws scan --cloud` → public raws → the human). Prints what to search, how to download (a Google *search* for a downloader, never a link that rots), and the import command for when the folder is ready. |
|
|
1944
1963
|
| `vidfarm storyboard [dir] [--init] [--frames "Title\|scene,…"] [--json]` | local (`STORYBOARD.md` / `SCRIPT.md`) | **The plan pass, and a core part of the composition format.** Scaffolds/reads the project's `STORYBOARD.md`: ordered frames with `duration`/`status`/`src`/`scene`/`voiceover`. The Vidfarm editor renders this file in its **Storyboard** view (contact sheet + per-frame comments + `outline → built → animated` progress), so it's the cheapest place to get a director's approval before building. Not to be confused with `vidfarm sequence` (which GENERATES storyboard images for the pure-videogen pipeline). Alias: `plan`. |
|
|
1945
1964
|
| `vidfarm wallet [--job <id>\|--tracer <t>] [--limit <n>]` | `GET /api/v1/user/me/wallet` | cost log: balance + lifetime spend + recent charges. `--job <renderJobId>` prints **what that one video cost** (sums its charges); `--tracer <t>` sums a tracer. Cloud-only; readable on the free plan too (shows $0.00). Aliases: `spend`, `costs` |
|
|
@@ -1948,6 +1967,7 @@ The licensed harness also carries the **generative build workflow** guidance (ch
|
|
|
1948
1967
|
| `vidfarm download <url> [dest]` | (streams any URL to disk) | download media from a **direct** media URL. Free — no plan, no job |
|
|
1949
1968
|
| `vidfarm download-video <url> [--quality best\|hd\|full_hd]` | `POST /api/v1/primitives/videos/download` + poll | **download a video FROM A WEBSITE** (YouTube/TikTok/IG/X/other supported posts) into durable Vidfarm media; photo/carousel posts return an ordered slideshow. **PAID PLAN** (wallet-billed resolver; free plans get 402). Aliases: `download-post`, `download-url` |
|
|
1950
1969
|
| `vidfarm download-audio <url>` | `POST /api/v1/primitives/audio/download` + poll | download just the AUDIO track (music/voice) off a website post into a durable audio file. **PAID PLAN**. Alias: `download-post-audio` |
|
|
1970
|
+
| `vidfarm recycle <source>` | `POST /api/v1/primitives/social/recycle` + poll | RECYCLE a Reddit/X source into reusable JSON ("tweet to tiktok", "reddit to tiktok"): thread + comments, subreddit threads, X thread + replies, or an account's posts — text, author + avatar, stats, media URLs. `--out` saves the full payload. **PAID PLAN**. Aliases: `recycle-social`, `tweet-to-video`, `reddit-to-video` |
|
|
1951
1971
|
|
|
1952
1972
|
> **Free plan / `cost-mode minimize`:** both download commands are gated by `guardBilled` and will refuse or warn. The free path is **Chrome**: open the URL in a real browser, save the file off the page (right-click → *Save video as*, downloads pane, or a Googled `"<platform> video downloader"` site), then `vidfarm put-file ./the-file.mp4 --folder <folder>` to bring it in for $0. The CLI prints these steps on a 402. Never tell a free user the video simply can't be downloaded.
|
|
1953
1973
|
| `vidfarm files [--folder <path>]` | `GET /api/v1/user/me/attachments` | list My Files assets + folders |
|
|
@@ -2225,7 +2245,7 @@ curl -X POST "$VIDFARM_BASE/api/v1/primitives/images/remove-background" \
|
|
|
2225
2245
|
|
|
2226
2246
|
- `POST /api/v1/primitives/images/create-overlay` (flat alias: `POST /api/v1/primitives/create-media-overlay`)
|
|
2227
2247
|
- Body: `{ "tracer": "...", "payload": { "prompt": "a cartoon rocket ship, flat vector illustration", "provider"?: "...", "model"?: "...", "prompt_attachments"?: ["https://..."], "aspect_ratio"?: "1:1", "image_size"?: "1K"|"2K"|"4K", "key_color"?: "#00FF00", "tolerance"?: 0.3, "softness"?: 0.1, "despill"?: true, "output_format"?: "png"|"webp" }, "webhook_url"?: "..." }`
|
|
2228
|
-
- Just describe the **subject** in `prompt` — do NOT describe the background; the primitive appends the flat key-color background requirement for you.
|
|
2248
|
+
- Just describe the **subject** in `prompt` — do NOT describe the background; the primitive appends the flat key-color background requirement for you, **and** the key-safe *artwork* requirement (closed solidly-filled shapes, no outline-only/hollow art, nothing on the subject in the key hue or a near-shade, fully opaque, no glow/translucency). That second clause is what stops the classic failure: outline art whose interior is bare plate keys into a **rim around a transparent hole**. If you build the prompt yourself for `images/generate` instead of using this primitive, write that clause in by hand.
|
|
2229
2249
|
- `key_color` lets you key against something other than green if your subject is itself green-heavy (e.g. key against `#FF00FF` magenta for a green frog).
|
|
2230
2250
|
- The chroma-key params (`tolerance`/`softness`/`despill`) behave exactly as in `image_remove_background_greenscreen`.
|
|
2231
2251
|
- Response: `primary_file_url` is the finished transparent overlay; `greenscreen_source_url` is the raw pre-key frame (kept so you can re-key at a different tolerance without paying for another generation).
|
|
@@ -2326,6 +2346,85 @@ curl -X POST "$VIDFARM_BASE/api/v1/primitives/audio/download" \
|
|
|
2326
2346
|
-d '{"tracer": "demo-download-audio", "payload": {"source_url": "https://www.youtube.com/watch?v=example"}}'
|
|
2327
2347
|
```
|
|
2328
2348
|
|
|
2349
|
+
## Primitive: social/recycle
|
|
2350
|
+
|
|
2351
|
+
**"Tweet to TikTok" and "Reddit to TikTok" live here.** The text-and-thread sibling of `videos/download`: point it at a Reddit or X/Twitter source and it **decomposes** that source into reusable JSON — the post text, the comment/reply thread, every image/video media URL, the author's profile pic and follower count, and the engagement stats — so YOU decide what to recycle into a new video. It deliberately does **not** summarize, rank, score, or filter. It dumps the data back raw.
|
|
2352
|
+
|
|
2353
|
+
**Vidfarm does not hold Reddit/X vendor keys.** This primitive is a thin broker over the two OfficeX apps that already own that integration — **reddit-lead-gen** and **x-lead-gen** — called through their public REST job API. They own the vendor keys, retries, rate-limit budget, auto-pagination, and 90-day result storage; vidfarm owns target resolution, normalization, and the wallet charge. Both are async job APIs, so a recycle is **a job wrapping a job** — but the nesting is invisible: this primitive polls upstream to completion server-side and only resolves when the data is in hand, exactly like the GhostCut leg of `videos/remove-captions`. You poll `GET /api/v1/primitives/jobs/:jobId` like any other primitive.
|
|
2354
|
+
|
|
2355
|
+
- `POST /api/v1/primitives/social/recycle` (alias: `POST /api/v1/primitives/recycle-social`)
|
|
2356
|
+
- Body: `{ "tracer": "...", "payload": { "source_url": "https://..." }, "webhook_url"?: "..." }`
|
|
2357
|
+
|
|
2358
|
+
### The four shapes
|
|
2359
|
+
|
|
2360
|
+
| You pass | Mode (auto-detected) | You get |
|
|
2361
|
+
| --- | --- | --- |
|
|
2362
|
+
| `https://reddit.com/r/<sub>/comments/<id>/...` | `reddit_thread` | the post + its comment tree, flattened in reading order |
|
|
2363
|
+
| `https://reddit.com/r/<sub>` (or `r/<sub>`) | `reddit_channel` | that subreddit's threads, paginated |
|
|
2364
|
+
| `https://x.com/<user>/status/<id>` | `twitter_thread` | the tweet + the author's self-thread + its replies, paginated |
|
|
2365
|
+
| `https://x.com/<user>` (or `@<user>`) | `twitter_channel` | that account's tweets (pinned first), paginated |
|
|
2366
|
+
|
|
2367
|
+
A fifth mode, `reddit_user` (`https://reddit.com/user/<name>` or `u/<name>`), returns a redditor's posts and comments. Pass `mode` explicitly to override auto-detection, or `handle` + `network` instead of a URL.
|
|
2368
|
+
|
|
2369
|
+
### Payload
|
|
2370
|
+
|
|
2371
|
+
| Field | Default | Notes |
|
|
2372
|
+
| --- | --- | --- |
|
|
2373
|
+
| `source_url` | — | Reddit/X URL. Required unless you pass `handle` + `network`. Aliases: `url`, `thread_url`, `profile_url` |
|
|
2374
|
+
| `handle` + `network` | — | `{ "handle": "wallstreetbets", "network": "reddit" }`. Aliases for handle: `username`, `screenname`, `subreddit` |
|
|
2375
|
+
| `mode` | auto | Override the detected shape |
|
|
2376
|
+
| `max_records` | `100` | Cap on records (1–1000). The lead-gen apps auto-paginate to reach it and reserve credits for `ceil(max_records / 25)` pages — **this is your spend ceiling** |
|
|
2377
|
+
| `cursor` | — | Resume token from a previous run's `nextCursor` |
|
|
2378
|
+
| `prompt_filter` | — | Hand the lead-gen app a Gemini scoring prompt; adds `match_score` + `ai_notes` per item and costs slightly more per task upstream. We surface it and never act on it |
|
|
2379
|
+
| `query` | — | Restrict a channel pull to matching posts (Reddit search / X `from:<user> <query>`) |
|
|
2380
|
+
| `filter` / `sort` / `time_filter` | — | Reddit listing controls (`hot`, `new`, `top`, …) |
|
|
2381
|
+
| `search_type` | — | X search mode (`Top`, `Latest`, `Photos`, `Videos`) |
|
|
2382
|
+
| `include_profile` | `true` | One extra upstream job for the subreddit/account avatar, banner, bio, follower count |
|
|
2383
|
+
| `include_comments` | `true` | Thread modes only — set `false` to skip the replies leg (and its cost) |
|
|
2384
|
+
| `save_raw` | `true` | Persist the untouched upstream pages as `raw-pages.json` |
|
|
2385
|
+
|
|
2386
|
+
### Result
|
|
2387
|
+
|
|
2388
|
+
Poll `GET /api/v1/primitives/jobs/:jobId` until `status: "succeeded"`, then read:
|
|
2389
|
+
|
|
2390
|
+
- `items[]` — the normalized view. Each entry: `{ network, kind: "post" | "comment", id, title, text, created_at, permalink, link_url, author: { handle, name, avatar_url, banner_url, followers, profile_url, bio, verified }, stats: { … }, media: [{ type: "image"|"video"|"gif", url, thumbnail_url, width, height, duration_ms }] }`. X video media resolves to the **highest-bitrate MP4** variant, not the HLS manifest.
|
|
2391
|
+
- `profile` — the subreddit/account itself (avatar, banner, bio, subscriber/follower count).
|
|
2392
|
+
- `mediaUrls[]` — every media URL flattened, ready to drop into layers or feed to `videos/download`.
|
|
2393
|
+
- `nextCursor` — pass it back as `payload.cursor` to page further. `truncated: true` means there IS more.
|
|
2394
|
+
- `counts` — `{ items, posts, comments, media, pages, upstream_jobs, credits_charged }`.
|
|
2395
|
+
- `upstreamJobs[]` — one entry per lead-gen job we ran: `{ network, endpoint, job_id, status, tasks, credits_charged, view_url }`. **`view_url` opens a NocoDB spreadsheet of that pull — hand it to the human when they want to browse results visually.**
|
|
2396
|
+
- `recycled.json` (also `primary_file_url`) — the **complete** normalized payload. `items` in the job result is trimmed when it would blow the job-record size limit; `itemsTruncatedInline: true` tells you to read the artifact instead.
|
|
2397
|
+
- `raw-pages.json` — every untouched upstream page, so you can reach fields this primitive doesn't model.
|
|
2398
|
+
|
|
2399
|
+
- Billing: wallet, metered on the **OfficeX credits the lead-gen apps actually consumed** (`officex_social_recycle`), converted at `OFFICEX_CREDIT_USD`. A one-task thread costs a fraction of a 10-page timeline. Paid plans only — free plans get `402`, same as the download routes. If the *upstream* app runs out of OfficeX credits you get a `402` naming that app, which is an operator problem, not a customer one.
|
|
2400
|
+
- devcli wrapper: `vidfarm recycle <source>` (aliases: `recycle-social`, `tweet-to-video`, `reddit-to-video`). Flags: `--max-records N`, `--cursor`, `--query`, `--filter hot|new|top`, `--mode`, `--network`, `--no-comments`, `--no-profile`, `--out ./recycled.json` (writes the FULL normalized payload to disk), `--no-wait`. It prints the counts, the upstream NocoDB `view_url`s, and the resume cursor.
|
|
2401
|
+
- Legs: a recycle runs 1–3 upstream jobs (profile, feed, replies) **sequentially**, so a failure on the first never pays for the second. The optional profile leg degrades rather than sinking the pull.
|
|
2402
|
+
- Timeouts: upstream polling gives up after `SOCIAL_RECYCLE_POLL_TIMEOUT_MS` (default 5 min); the upstream job may still finish and can be browsed in that app.
|
|
2403
|
+
|
|
2404
|
+
### Turning a recycle into a video
|
|
2405
|
+
|
|
2406
|
+
1. Recycle the source with a small `max_records` (25–50 is usually plenty for one video).
|
|
2407
|
+
2. Read `items[]` yourself and pick the beat: the post's hook line, the two funniest comments, the stat everyone reacted to.
|
|
2408
|
+
3. Pull any `media[].url` you want as real footage through `videos/download` or straight into a layer; use `profile.avatar_url` for the "who said this" badge.
|
|
2409
|
+
4. Script and build as normal. **The recycled text is source material, not a script** — rewrite it to the hook/loop/payoff standard rather than pasting a comment thread on screen.
|
|
2410
|
+
|
|
2411
|
+
Example:
|
|
2412
|
+
|
|
2413
|
+
```bash
|
|
2414
|
+
curl -X POST "$VIDFARM_BASE/api/v1/primitives/social/recycle" \
|
|
2415
|
+
-H "vidfarm-api-key: $VIDFARM_API_KEY" \
|
|
2416
|
+
-H "content-type: application/json" \
|
|
2417
|
+
-d '{"tracer": "demo-recycle", "payload": {"source_url": "https://www.reddit.com/r/wallstreetbets/comments/p0esdp/slug/", "max_records": 50}}'
|
|
2418
|
+
```
|
|
2419
|
+
|
|
2420
|
+
```bash
|
|
2421
|
+
# An X account's recent posts, no replies, cheapest possible pull
|
|
2422
|
+
curl -X POST "$VIDFARM_BASE/api/v1/primitives/recycle-social" \
|
|
2423
|
+
-H "vidfarm-api-key: $VIDFARM_API_KEY" \
|
|
2424
|
+
-H "content-type: application/json" \
|
|
2425
|
+
-d '{"tracer": "demo-recycle-x", "payload": {"source_url": "https://x.com/elonmusk", "max_records": 40, "include_profile": true}}'
|
|
2426
|
+
```
|
|
2427
|
+
|
|
2329
2428
|
## Primitive: video_remove_captions
|
|
2330
2429
|
|
|
2331
2430
|
Remove burned-in captions/subtitles/on-screen text from any video URL (GhostCut-powered — the same pipeline auto-decompose uses for its caption-free mirror, exposed as a standalone reusable primitive). Result is a durable caption-free MP4 stored at a Vidfarm URL.
|
|
@@ -2680,7 +2779,7 @@ The mechanical trio — **generate on a chroma plate → key it out → trim to
|
|
|
2680
2779
|
- **Female TTS narration.** Default to a warm, friendly **female** voice and say which one you picked: local-first `vidfarm tts "<script>" --voice coral` (OpenAI — `nova` if the script wants more energy, `sage` for calmer), `--voice Kore` or `Leda` on Gemini, or `vidfarm voices` → `vidfarm tts --cloud --voice <voice_id>` on ElevenLabs. Tell the director they can swap it in one flag.
|
|
2681
2780
|
- **Clean and simple wins.** One idea on screen at a time. Two or three cutouts per beat, not eight. Generous white space, one accent color, one font. When in doubt, remove an element rather than add one.
|
|
2682
2781
|
|
|
2683
|
-
**Illustrations default to simplicity.** Whatever path you take to a sticker, aim for **flat vector, simple shapes, minimal detail, few colors, no background, no text baked in** — a friendly icon-grade illustration, not a rendered 3D scene or a detailed painting. Simple art keys cleanly, trims tight, scales without mush, animates readably at 9:16, and stays on-style across a whole cast. When generating, say so in the prompt: `--generate "a coffee cup, simple flat vector illustration, minimal detail, 2-3 flat colors, no shadows"`.
|
|
2782
|
+
**Illustrations default to simplicity — and to SOLID FILLS.** Whatever path you take to a sticker, aim for **flat vector, simple shapes, minimal detail, few colors, solid opaque fills, no background, no text baked in** — a friendly icon-grade illustration, not a rendered 3D scene or a detailed painting. Simple art keys cleanly, trims tight, scales without mush, animates readably at 9:16, and stays on-style across a whole cast. "Solid fills" is the load-bearing word: **outline-only art has its interior keyed away and comes back as a rim around a transparent hole** (see "Then make the ART key-safe too" below). When generating, say so in the prompt: `--generate "a coffee cup, simple flat vector illustration, minimal detail, 2-3 flat colors, solid filled shapes (not outline-only), no shadows"`.
|
|
2684
2783
|
|
|
2685
2784
|
**In cost-saving mode, don't generate illustrations at all — mask them out of images the director already has.** If `vidfarm cost-mode` is `minimize` (or the director says "without burning credits"), the default for adding an illustration is `vidfarm mask <their-image> --crop …` — lifting art out of an infographic, poster, deck slide, brand sheet, or screenshot for **$0 and zero AI calls**. Ask for source art before you ask for a generation budget; the guided loop is **"Mask from an image you already have"** below. **If no source art exists and the graphic must be custom, you still don't have to spend** — hand the director a prompt for a **free** image generator (meta.ai / free ChatGPT / a Hugging Face Space) and cut the returned sheet into stickers locally: **"Free manual image-gen"** below.
|
|
2686
2785
|
|
|
@@ -2725,6 +2824,20 @@ Out comes `./stickers/sticker-01-red-barn.png`, `sticker-02-tractor.png`, … ea
|
|
|
2725
2824
|
|
|
2726
2825
|
Override whenever you know better: `--key-color "#FF00FF"` or `--preset magenta` pins the plate (and turns the auto-pick off); `--no-auto-key` forces plain green. If a pack's palette collides with *every* standard plate — a rainbow icon set — split it across two sheets on two different plates rather than fighting `--tolerance`. And when a sticker is a *green* subject, never let the user generate it on green: the hand-off brief (`vidfarm handoff image`) already picks the plate for them and carries the matching `--key-color` into the follow-up command.
|
|
2727
2826
|
|
|
2827
|
+
**Then make the ART key-safe too — CLOSED, SOLIDLY FILLED shapes.** Picking the right plate is only half of surviving the key. The other half is the failure you actually see in the wild: you ask for "sticker icons on a green plate" and the model draws **outline art** — a colored stroke with the shape's *interior left as bare plate*. On the sheet it looks perfect. After the key, the interior is gone with the background, and every sticker composites as a **rim floating around a see-through hole** (an apple that's just an apple-shaped outline, a clock with no face). Same result from three cousins: a fill that's a *near-shade* of the plate (the keyer works on tolerance, not exact match), a **translucent/glassy/glowing** material that lets the plate through, and a **soft glow or drop shadow** that fades into the plate at the edges.
|
|
2828
|
+
|
|
2829
|
+
None of that is fixable after generation — you cannot key back pixels that were never drawn. So it belongs in the **prompt**, every time:
|
|
2830
|
+
|
|
2831
|
+
> Every object must be a CLOSED, SOLIDLY FILLED shape — outlines and strokes must enclose an opaque fill of a different color. No outline-only, hollow, or line-art objects; never leave a shape's interior as bare background. No part of any object — fill, outline, highlight, gradient, shading or detail — may be `<PLATE HEX>` or any near-shade, tint or tone of it. No transparent, translucent, glassy, glowing or ghosted materials; every pixel of every object is fully opaque. No soft glows, blurs or drop shadows fading into the background. Keep the whole palette in strong contrast to `<PLATE HEX>`. The background must be visible ONLY around the outside of the objects, never showing through inside them.
|
|
2832
|
+
|
|
2833
|
+
`cutout --generate`, `sticker-pack --generate`, `vidfarm handoff image`, and the `create-overlay` REST primitive all append that clause automatically with the chosen plate hex substituted in — **you only have to write it yourself when you're prompting a generator directly** (a free web tool by hand, or your own `POST /api/v1/primitives/images/generate` call). Do write it. It costs one paragraph and it is the difference between a usable pack and a re-generation.
|
|
2834
|
+
|
|
2835
|
+
**And it's checked after the key.** `sticker-pack` and `cutout` measure, per item, the transparent area **fully enclosed by that item's own art** and report it as `hole_pct` (plus `holes`, and `hollow: true` at ≥20%) in `--json`, in `stickers.json`, and as a `⚠ N% hollow` flag on the console line. A ring, donut, picture frame or letter "O" reads the same way, so it **warns, never blocks** — the judgement is yours. When items you expected to be solid come back flagged:
|
|
2836
|
+
|
|
2837
|
+
- **Re-generate the sheet** with the fill clause above spelled out. This is the fix ~90% of the time.
|
|
2838
|
+
- If the fill was merely *close* to the plate rather than absent, a lower `--tolerance` can rescue the sheet you already have. Nothing rescues a genuinely empty interior.
|
|
2839
|
+
- Last resort for one stubborn item: `vidfarm mask ./sheet.png --crop x,y,w,h` — ONNX matting doesn't care what color the fill is, so it lifts art the chroma key can't.
|
|
2840
|
+
|
|
2728
2841
|
**Generation is the failure point, not the cutting.** The sheet prompt is auto-appended with the important instruction — *every item fully separated by clear plate-colored background, nothing touching or overlapping, wide margins, no text, no shadows, one consistent style* — because **touching items segment as one sticker**. If a pack comes back merged, re-run the generation asking for more spacing before you fight the `--gap` knob.
|
|
2729
2842
|
|
|
2730
2843
|
### Stickers are not necessarily small — and they usually move
|
|
@@ -2841,15 +2954,15 @@ Everything here is **local and free** — crop, matte/key, and trim are all `ffm
|
|
|
2841
2954
|
|
|
2842
2955
|
1. **List the cast.** Name every illustration the explainer needs, in the house style (flat vector, 2–3 flat colors, no baked-in text).
|
|
2843
2956
|
2. **Hand the director this prompt**, filled in:
|
|
2844
|
-
> A flat vector illustration sheet on a solid pure green background (#00FF00), no gradient, no shadow, no text, no labels. Arranged in a 3×2 grid with generous even spacing, each object fully separated and not touching any other object or the image edge: (1) `<object one>`, (2) `<object two>`, (3) `<object three>`, … Simple flat shapes, 2–3 flat colors, minimal detail, consistent style and line weight across all objects, front-facing, centered in its own cell. Square image, high resolution.
|
|
2957
|
+
> A flat vector illustration sheet on a solid pure green background (#00FF00), no gradient, no shadow, no text, no labels. Arranged in a 3×2 grid with generous even spacing, each object fully separated and not touching any other object or the image edge: (1) `<object one>`, (2) `<object two>`, (3) `<object three>`, … Simple flat shapes, 2–3 flat colors, minimal detail, consistent style and line weight across all objects, front-facing, centered in its own cell. **Every object is a closed, solidly filled shape — outlines must enclose an opaque fill of a different color; no outline-only or hollow objects, and no object's interior left as bare green. Nothing on any object may be green or any near-shade of green; no transparent, translucent, glassy or glowing materials; no soft glows or drop shadows fading into the background.** Square image, high resolution.
|
|
2845
2958
|
|
|
2846
|
-
**No text in the image, ever** — rendered text never keys or scales cleanly; add copy as a caption or HTML layer instead.
|
|
2959
|
+
**No text in the image, ever** — rendered text never keys or scales cleanly; add copy as a caption or HTML layer instead. **And don't drop the bolded fill sentence** — the green gets deleted, so anything green *on* the art gets deleted with it; hollow outline art is the single most common way a hand-generated sheet comes back broken. (`vidfarm handoff image --theme … --items …` mints this whole brief with the plate and the fill clause already filled in — prefer it over typing the prompt yourself.)
|
|
2847
2960
|
3. **Bring the sheet in:** `vidfarm put-file ./sheet.png` (or drag into **My Files** in the web app).
|
|
2848
2961
|
4. **Split it into stickers — one command, all free:**
|
|
2849
2962
|
```
|
|
2850
2963
|
vidfarm sticker-pack ./sheet.png --items "<object one>,<object two>,…" --out-dir ./stickers
|
|
2851
2964
|
```
|
|
2852
|
-
It keys the flat green, finds each object in the alpha channel, and writes one trimmed transparent sticker per object (named from `--items`) plus `stickers.json`. Run it with `--dry-run` first to check the boxes. Two objects merged into one? The generator drew them too close — lower `--gap`, or grab that pair by hand:
|
|
2965
|
+
It keys the flat green, finds each object in the alpha channel, and writes one trimmed transparent sticker per object (named from `--items`) plus `stickers.json`. Run it with `--dry-run` first to check the boxes. Any item flagged **`⚠ N% hollow`** came back as an outline around a transparent hole — unless it's genuinely a ring/frame, re-ask the director for that sheet with the fill sentence, or lift that one item with `vidfarm mask ./sheet.png --crop x,y,w,h` (ONNX matting doesn't care what the fill color is). Two objects merged into one? The generator drew them too close — lower `--gap`, or grab that pair by hand:
|
|
2853
2966
|
```
|
|
2854
2967
|
vidfarm mask ./sheet.png --crop 4%,4%,29%,45% --flat "#00FF00" --out prop-a.png
|
|
2855
2968
|
```
|
package/SKILL.md
CHANGED
|
@@ -41,7 +41,7 @@ The last two are production *methods*, not just budgets. **rich-ai**: generate t
|
|
|
41
41
|
|
|
42
42
|
**Even in `minimize`, you can still get custom images — for free, manually.** When stock and masking existing art won't cover a bespoke graphic, don't say "not possible" and don't quietly spend: ask the user once whether to **default to free manual image generation**, then write the prompt and have them run it in <https://meta.ai>, free-tier ChatGPT, or a free Hugging Face image Space, and hand the PNG back (`vidfarm put-file`, or drag into My Files). Ask for **one image containing every graphic you need**, gridded on a **flat chroma plate** with no text (`#00FF00` green by default — but pick a different plate when the art itself is green, or the key will punch holes through it) — one round trip instead of many, which is the token-efficient move. Then split it locally at $0 in one command: `vidfarm sticker-pack ./sheet.png --items "a,b,c" --out-dir ./stickers` keys the green out, finds each item in the sheet automatically, and writes a snug transparent sticker per item. Prompt template + full loop in the director skill.
|
|
43
43
|
|
|
44
|
-
**"Make me a sticker pack" = one greenscreen sheet, then masked apart.** A pack is a *set* that has to share one art style, so never generate the items one by one — generate a single image holding all of them on a flat `#00FF00` plate, then split it: `vidfarm sticker-pack --generate "<theme>" --items "a,b,c,d"` (one billed image job for the whole set, then free local keying + automatic alpha segmentation + per-item trim + a `stickers.json` manifest). **The plate must be a color the art doesn't use** — a green pack (leaves, frogs, plants, money) on a green plate keys holes through the art. The commands pick the plate off the subject when generating (green → magenta → blue → black → white, and they tell you when they move) and **detect the plate from an existing sheet's corners**, so a red or purple sheet from a web tool just works; pin it with `--key-color`/`--preset`, or `--no-auto-key` for plain green. Stickers are **not necessarily small** — a full-frame landscape or backdrop is as valid an item as an icon — and they're normally **animated on the canvas** (`vidfarm place` + `vidfarm keyframes` presets: pop-in, float, shake, grow, slide-in, drift). A sticker whose own art moves is a **transparent WebM** (`vidfarm remove-greenscreen <video>`), and for GIF-only surfaces both paths can emit **transparent GIFs** (`sticker-pack --output-format gif`, `remove-greenscreen --gif`, 1-bit alpha). Full recipe in the director skill.
|
|
44
|
+
**"Make me a sticker pack" = one greenscreen sheet, then masked apart.** A pack is a *set* that has to share one art style, so never generate the items one by one — generate a single image holding all of them on a flat `#00FF00` plate, then split it: `vidfarm sticker-pack --generate "<theme>" --items "a,b,c,d"` (one billed image job for the whole set, then free local keying + automatic alpha segmentation + per-item trim + a `stickers.json` manifest). **The plate must be a color the art doesn't use** — a green pack (leaves, frogs, plants, money) on a green plate keys holes through the art. The commands pick the plate off the subject when generating (green → magenta → blue → black → white, and they tell you when they move) and **detect the plate from an existing sheet's corners**, so a red or purple sheet from a web tool just works; pin it with `--key-color`/`--preset`, or `--no-auto-key` for plain green. **And the art itself must be closed and solidly filled** — outline-only/hollow art (or a near-plate fill, or anything translucent/glowing) has its interior keyed away and composites as a rim around a see-through hole. The generation prompts say so for you; write it in by hand when you prompt a generator directly, and check the `⚠ N% hollow` / `hole_pct` flag each command reports after keying. Stickers are **not necessarily small** — a full-frame landscape or backdrop is as valid an item as an icon — and they're normally **animated on the canvas** (`vidfarm place` + `vidfarm keyframes` presets: pop-in, float, shake, grow, slide-in, drift). A sticker whose own art moves is a **transparent WebM** (`vidfarm remove-greenscreen <video>`), and for GIF-only surfaces both paths can emit **transparent GIFs** (`sticker-pack --output-format gif`, `remove-greenscreen --gif`, 1-bit alpha). Full recipe in the director skill.
|
|
45
45
|
|
|
46
46
|
## Ask the second question too — interactive or autonomous?
|
|
47
47
|
|
|
@@ -101,6 +101,7 @@ For composition *authoring* craft (motion, keyframes, scene design), Vidfarm shi
|
|
|
101
101
|
3. "Change / re-theme this video" → `references/editor-workflows.md`
|
|
102
102
|
4. "Find footage / use our assets" → `references/assets-and-sourcing.md`
|
|
103
103
|
4b. **"Download this video/audio from <a website URL>"** → Vidfarm fetches it for you on a **paid plan**: `POST /api/v1/primitives/videos/download` (or `/audio/download`); devcli `vidfarm download-video <url>` / `vidfarm download-audio <url>`. Works on YouTube, TikTok, Instagram, X, and other supported posts; returns a durable Vidfarm file (photo/carousel posts → an ordered slideshow). **Free plan gets a 402 — don't call it. Tell the user (or, with browser automation, do it yourself) to open the URL in Chrome and download it from the page, then `vidfarm put-file ./the-file.mp4` to bring it in for $0.** Never answer "I can't download that." Details: `references/assets-and-sourcing.md`.
|
|
104
|
+
4b-ii. **"Tweet to TikTok" / "Reddit to TikTok" / "make a video out of this thread / subreddit / account"** → `vidfarm recycle <source>` (devcli), or `POST /api/v1/primitives/social/recycle` with `{ tracer, payload: { source_url, max_records } }`. It **decomposes** a Reddit thread (post + comments), a subreddit (its threads), an X thread (tweet + replies), or an X profile (their posts) into raw JSON — text, comment tree, media URLs, author avatar, engagement stats — and hands it back **unranked and unsummarized** so you pick the hook, the punchline comment, the stat. Auto-paginates via `nextCursor`; `max_records` is the spend ceiling. Brokers the reddit-lead-gen / x-lead-gen OfficeX apps and waits out their async job for you. **Paid plan.** Details: `references/assets-and-sourcing.md`.
|
|
104
105
|
4c. **"Create an avatar"** (spokesperson / presenter / host / UGC creator / talking head) → always a **talking-head VIDEO with spoken audio**, generated on an exact-key-color **greenscreen** plate and keyed off it in the same job → a **transparent presenter** you layer over any background. `vidfarm avatar "<who they are>" --say "<their line>" [--ref headshot.png]` / `POST /api/v1/primitives/videos/create-avatar`. Details: `references/primitives.md` → "Primitive: talking_avatar".
|
|
105
106
|
5. "Script / batch / render loop" → `references/automation-and-local-dev.md`
|
|
106
107
|
6. "I need TTS / music / captions / background removal" → `references/primitives.md`
|
package/dist/src/cli.js
CHANGED
|
@@ -22,7 +22,7 @@ import { renderCompositionStills } from "./devcli/stills.js";
|
|
|
22
22
|
import { extractCompositionFacts, formatQaReport, qaCompositionHtml } from "./devcli/qa-check.js";
|
|
23
23
|
import { discoverRegime, formatRegimeReport, listBuiltinRegimes, loadAndEvaluateRegime, mergeRegimeIntoReport, parseRegime, resolveRegimePath } from "./devcli/qa-regime.js";
|
|
24
24
|
import { removeGreenscreenLocal, localGreenscreenAvailable, defaultGreenscreenOutPath, GREENSCREEN_PRESETS, trimTransparentBorders, cropImageRegion } from "./devcli/greenscreen-local.js";
|
|
25
|
-
import { segmentAlphaComponents, encodeTransparentGif, encodeTransparentAnimatedGif, pickPlateColor, detectPlateColor } from "./devcli/sticker-pack.js";
|
|
25
|
+
import { segmentAlphaComponents, encodeTransparentGif, encodeTransparentAnimatedGif, pickPlateColor, detectPlateColor, keySafeArtInstruction, analyzeKeyedArt, HOLE_WARN_PCT } from "./devcli/sticker-pack.js";
|
|
26
26
|
import { runDoctorCommand } from "./devcli/doctor.js";
|
|
27
27
|
import { findFreePort } from "./devcli/port-utils.js";
|
|
28
28
|
import { scanLocalServers } from "./devcli/process-scan.js";
|
|
@@ -351,7 +351,10 @@ Generate AI media and drop it on the timeline (for local coding agents):
|
|
|
351
351
|
transparent file (+ stickers.json). One image job for
|
|
352
352
|
the set = one art style, ~1/N the cost of N cutouts.
|
|
353
353
|
Items may be any size, from an icon to a full-frame
|
|
354
|
-
landscape.
|
|
354
|
+
landscape. Prompts for key-safe art (solid fills, no
|
|
355
|
+
outline-only/hollow shapes, nothing in a near-plate
|
|
356
|
+
shade) and flags any item that came out hollow.
|
|
357
|
+
Local, free, ffmpeg-only. Image-only.
|
|
355
358
|
(aliases: stickers, sticker-sheet)
|
|
356
359
|
--generate "<theme>" AI-generate the sheet first (ONE billed image job for
|
|
357
360
|
the WHOLE pack) on a spaced grid over a chroma plate
|
|
@@ -808,6 +811,19 @@ Marketplace (paid, cloud-only — the bazaar never renders locally):
|
|
|
808
811
|
--content-type <a,b> With --search: filter the /raws branch by shot-KIND tag
|
|
809
812
|
(talking_head,b_roll,product_shot,screen_recording,demo,
|
|
810
813
|
reaction,interview,establishing,lifestyle,text_graphic)
|
|
814
|
+
recycle <source> PAID. RECYCLE a Reddit or X/Twitter source into reusable JSON —
|
|
815
|
+
"tweet to tiktok" / "reddit to tiktok". Accepts a Reddit thread URL
|
|
816
|
+
(post + comments), a subreddit (r/name or URL -> its threads), an X
|
|
817
|
+
thread URL (tweet + replies), or an X profile (@handle or URL -> their
|
|
818
|
+
posts). Returns items[] with text, author + avatar, stats, permalinks
|
|
819
|
+
and every image/video media URL - unranked and unsummarized, so YOU
|
|
820
|
+
pick what to remix. Brokered through the reddit-lead-gen / x-lead-gen
|
|
821
|
+
OfficeX apps, billed on the credits they consume.
|
|
822
|
+
Flags: --max-records N (spend ceiling, default 100) --cursor <token>
|
|
823
|
+
--query <text> --filter hot|new|top --mode <mode> --network <net>
|
|
824
|
+
--no-comments --no-profile --out ./recycled.json --no-wait
|
|
825
|
+
Aliases: recycle-social, tweet-to-video, reddit-to-video
|
|
826
|
+
-> POST /api/v1/primitives/social/recycle
|
|
811
827
|
get-file <id> [dest] Resolve a My Files id to its URL and download it
|
|
812
828
|
--print Print text contents (md/txt/csv/json) instead of saving
|
|
813
829
|
annotate-file <id|name> Set metadata notes on one My Files entry → PATCH /api/v1/user/me/attachments/:id
|
|
@@ -1179,6 +1195,13 @@ async function main() {
|
|
|
1179
1195
|
case "download-audio":
|
|
1180
1196
|
await runDownloadPostAudioCommand(rest);
|
|
1181
1197
|
return;
|
|
1198
|
+
// "tweet to tiktok" / "reddit to tiktok": decompose a social source to JSON.
|
|
1199
|
+
case "recycle-social":
|
|
1200
|
+
case "tweet-to-video":
|
|
1201
|
+
case "reddit-to-video":
|
|
1202
|
+
case "recycle":
|
|
1203
|
+
await runRecycleCommand(rest);
|
|
1204
|
+
return;
|
|
1182
1205
|
case "files":
|
|
1183
1206
|
await runFilesCommand(rest);
|
|
1184
1207
|
return;
|
|
@@ -5303,9 +5326,12 @@ function cutoutPlateInstruction(keyColorHex) {
|
|
|
5303
5326
|
: /^#?ff00ff$/i.test(keyColorHex) ? "solid magenta"
|
|
5304
5327
|
: `a solid flat ${keyColorHex}`;
|
|
5305
5328
|
return (`Isolated subject on a completely flat, evenly-lit ${named} (${keyColorHex}) background — ` +
|
|
5306
|
-
`like a green-screen plate.
|
|
5329
|
+
`like a green-screen plate. ` +
|
|
5307
5330
|
`Center the subject with generous empty margin on all sides, no drop shadow or reflection cast on the ` +
|
|
5308
|
-
`background, crisp clean edges, sticker/cutout style, single subject, no text
|
|
5331
|
+
`background, crisp clean edges, sticker/cutout style, single subject, no text. ` +
|
|
5332
|
+
// The plate color is only half the battle — art drawn as a hollow outline
|
|
5333
|
+
// (or in a near-plate shade) gets its middle keyed away too.
|
|
5334
|
+
keySafeArtInstruction(keyColorHex));
|
|
5309
5335
|
}
|
|
5310
5336
|
async function runCutoutCommand(argv) {
|
|
5311
5337
|
const parsed = parseArgs({
|
|
@@ -5482,6 +5508,16 @@ async function runCutoutCommand(argv) {
|
|
|
5482
5508
|
const shrinkPct = trim.origWidth && trim.origHeight && trim.width && trim.height
|
|
5483
5509
|
? Math.round((1 - (trim.width * trim.height) / (trim.origWidth * trim.origHeight)) * 100)
|
|
5484
5510
|
: null;
|
|
5511
|
+
// Did the key eat the ART as well as the plate? Outline-only art (interior
|
|
5512
|
+
// left as bare plate) or a near-plate fill comes back as a rim around a
|
|
5513
|
+
// see-through hole — invisible until it composites over a real background.
|
|
5514
|
+
const art = await analyzeKeyedArt(trim.outputPath);
|
|
5515
|
+
const hollowNote = art?.hollow
|
|
5516
|
+
? `${art.hole_pct}% of this cutout is transparent HOLES inside the art. If it's meant to be a ring/frame/donut, ignore this. ` +
|
|
5517
|
+
`Otherwise the key ate the fill: the art was drawn as an outline with a bare ${keyColor} interior, or filled in a near-${keyColor} shade. ` +
|
|
5518
|
+
`Fix it in the PROMPT — ask for "a closed, solidly filled shape, no outline-only or hollow art, nothing in ${keyColor} or any near-shade of it, ` +
|
|
5519
|
+
`fully opaque, no translucency or glow" — and re-generate. (--tolerance lower can rescue a near-shade fill; nothing rescues an empty one.)`
|
|
5520
|
+
: null;
|
|
5485
5521
|
if (ctx.json) {
|
|
5486
5522
|
printJson({
|
|
5487
5523
|
ok: true,
|
|
@@ -5495,13 +5531,18 @@ async function runCutoutCommand(argv) {
|
|
|
5495
5531
|
trimmed: trim.trimmed,
|
|
5496
5532
|
area_reduced_pct: shrinkPct,
|
|
5497
5533
|
bytes: safeSize(trim.outputPath),
|
|
5498
|
-
key_color: keyColor
|
|
5534
|
+
key_color: keyColor,
|
|
5535
|
+
hole_pct: art?.hole_pct ?? null,
|
|
5536
|
+
hollow: art?.hollow ?? false,
|
|
5537
|
+
hollow_note: hollowNote
|
|
5499
5538
|
});
|
|
5500
5539
|
}
|
|
5501
5540
|
else {
|
|
5502
5541
|
console.log(`${GREEN}Cutout ready:${RESET} ${trim.outputPath} ${DIM}(${trim.width}×${trim.height}, ${formatBytes(safeSize(trim.outputPath))}${trim.trimmed && shrinkPct !== null ? `, −${shrinkPct}% area` : ""})${RESET}`);
|
|
5503
5542
|
if (!trim.trimmed && !noTrim)
|
|
5504
5543
|
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}`);
|
|
5544
|
+
if (hollowNote)
|
|
5545
|
+
console.log(`${YELLOW}Hollow:${RESET} ${DIM}${hollowNote}${RESET}`);
|
|
5505
5546
|
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}`);
|
|
5506
5547
|
}
|
|
5507
5548
|
}
|
|
@@ -5722,8 +5763,9 @@ function stickerSheetInstruction(keyColorHex, count, items) {
|
|
|
5722
5763
|
`${named} (${keyColorHex}) background. ${list} ` +
|
|
5723
5764
|
`CRITICAL: every item must be fully separated from the others by a clear margin of plain ${keyColorHex} background — ` +
|
|
5724
5765
|
`nothing touching, overlapping, or connected. Keep a wide ${keyColorHex} margin around the edges of the sheet too. ` +
|
|
5725
|
-
`No
|
|
5726
|
-
`no frames or dividing lines between items. One consistent art style across all items, crisp clean edges, sticker/cutout style
|
|
5766
|
+
`No drop shadows, no reflections, no text, no labels, ` +
|
|
5767
|
+
`no frames or dividing lines between items. One consistent art style across all items, crisp clean edges, sticker/cutout style. ` +
|
|
5768
|
+
keySafeArtInstruction(keyColorHex));
|
|
5727
5769
|
}
|
|
5728
5770
|
async function runStickerPackCommand(argv) {
|
|
5729
5771
|
const parsed = parseArgs({
|
|
@@ -5930,16 +5972,37 @@ async function runStickerPackCommand(argv) {
|
|
|
5930
5972
|
if (!seg.components.length) {
|
|
5931
5973
|
throw new Error("No items found in the sheet. Either the plate didn't key (check --preset/--key-color, raise --tolerance) or every item was filtered as speckle (lower --min-area).");
|
|
5932
5974
|
}
|
|
5975
|
+
// ---- 3b. Hollow-sticker check -------------------------------------------
|
|
5976
|
+
// The other half of "did the key work": items that came back as a rim around
|
|
5977
|
+
// a transparent hole, because the art was drawn as an outline (interior left
|
|
5978
|
+
// as bare plate) or filled in a near-plate shade. It looks fine on the sheet
|
|
5979
|
+
// and only shows up once the sticker composites over a real background — so
|
|
5980
|
+
// say it here, loudly, with the fix. A ring/donut/frame reads the same way,
|
|
5981
|
+
// which is why this warns and never blocks.
|
|
5982
|
+
let hollowNote = null;
|
|
5983
|
+
if (seg.hollow.length) {
|
|
5984
|
+
const named = seg.hollow.map((i) => `${String(i).padStart(2, "0")}${itemNames[i - 1] ? ` (${itemNames[i - 1]})` : ""} — ${seg.components.find((c) => c.index === i)?.hole_pct}% see-through`);
|
|
5985
|
+
hollowNote =
|
|
5986
|
+
`${seg.hollow.length} of ${seg.components.length} sticker(s) came out mostly hollow: ${named.join(", ")}. ` +
|
|
5987
|
+
`If those are meant to be rings/frames/donuts, ignore this. Otherwise the key ate their FILL: the art was drawn ` +
|
|
5988
|
+
`as an outline with a bare ${keyColor} interior, or filled in a near-${keyColor} shade. Fix it in the PROMPT, not the keyer — ` +
|
|
5989
|
+
`ask for "closed, solidly filled shapes, no outline-only or hollow objects, nothing on the art in ${keyColor} or any ` +
|
|
5990
|
+
`near-shade of it, fully opaque, no translucency or glow" — then re-generate the sheet. ` +
|
|
5991
|
+
`(Lowering --tolerance can rescue a near-shade fill from an existing sheet, but not a genuinely empty one.)`;
|
|
5992
|
+
if (!ctx.json)
|
|
5993
|
+
console.log(`${YELLOW}Hollow:${RESET} ${DIM}${hollowNote}${RESET}`);
|
|
5994
|
+
}
|
|
5933
5995
|
if (parsed.values["dry-run"]) {
|
|
5934
5996
|
// Report the boxes without writing stickers — for eyeballing segmentation
|
|
5935
5997
|
// before spending disk, and for hand-fixing a merged item with `mask --crop`.
|
|
5936
5998
|
if (ctx.json) {
|
|
5937
|
-
printJson({ ok: true, target: "local", dry_run: true, sheet: `${seg.sourceWidth}x${seg.sourceHeight}`, found: seg.components.length, skipped_specks: seg.rejected, key_color: keyColor, key_color_auto: plateAuto, items: seg.components });
|
|
5999
|
+
printJson({ ok: true, target: "local", dry_run: true, sheet: `${seg.sourceWidth}x${seg.sourceHeight}`, found: seg.components.length, skipped_specks: seg.rejected, key_color: keyColor, key_color_auto: plateAuto, hollow: seg.hollow, hollow_note: hollowNote, items: seg.components });
|
|
5938
6000
|
}
|
|
5939
6001
|
else {
|
|
5940
6002
|
console.log(`${GREEN}Found ${seg.components.length} item${seg.components.length === 1 ? "" : "s"}${RESET} ${DIM}on the ${seg.sourceWidth}×${seg.sourceHeight} sheet${seg.rejected ? `, ${seg.rejected} speck(s) skipped` : ""} (dry run — nothing written):${RESET}`);
|
|
5941
6003
|
for (const c of seg.components) {
|
|
5942
|
-
|
|
6004
|
+
const hollowFlag = c.hole_pct >= HOLE_WARN_PCT ? ` ${YELLOW}⚠ ${c.hole_pct}% hollow${RESET}` : "";
|
|
6005
|
+
console.log(` ${DIM}${String(c.index).padStart(2, "0")} crop ${c.x},${c.y},${c.width},${c.height} (${c.width}×${c.height}, ${c.area_pct}% of sheet)${RESET}${hollowFlag}`);
|
|
5943
6006
|
}
|
|
5944
6007
|
console.log(`${DIM}Merged two items into one box? Raise the gap between them in the prompt, lower --gap, or grab that one by hand: vidfarm mask <sheet> --crop x,y,w,h --flat "${keyColor}".${RESET}`);
|
|
5945
6008
|
}
|
|
@@ -5973,10 +6036,14 @@ async function runStickerPackCommand(argv) {
|
|
|
5973
6036
|
height: trim.height,
|
|
5974
6037
|
sheet_crop: { x: c.x, y: c.y, width: c.width, height: c.height },
|
|
5975
6038
|
area_pct: c.area_pct,
|
|
6039
|
+
holes: c.holes,
|
|
6040
|
+
hole_pct: c.hole_pct,
|
|
6041
|
+
hollow: c.hole_pct >= HOLE_WARN_PCT,
|
|
5976
6042
|
bytes: safeSize(finalPath)
|
|
5977
6043
|
});
|
|
5978
6044
|
if (!ctx.json) {
|
|
5979
|
-
|
|
6045
|
+
const hollowFlag = c.hole_pct >= HOLE_WARN_PCT ? ` ${YELLOW}⚠ ${c.hole_pct}% hollow${RESET}` : "";
|
|
6046
|
+
console.log(` ${GREEN}✓${RESET} ${path.relative(process.cwd(), finalPath)} ${DIM}(${trim.width}×${trim.height}, ${formatBytes(safeSize(finalPath))})${RESET}${hollowFlag}`);
|
|
5980
6047
|
}
|
|
5981
6048
|
}
|
|
5982
6049
|
// A manifest so the next step (place/keyframes, or an agent picking props by
|
|
@@ -5986,6 +6053,8 @@ async function runStickerPackCommand(argv) {
|
|
|
5986
6053
|
generated_from: generatePrompt ? "generate" : sourceArg,
|
|
5987
6054
|
key_color: keyColor,
|
|
5988
6055
|
key_color_auto: plateAuto,
|
|
6056
|
+
hollow: seg.hollow,
|
|
6057
|
+
hollow_note: hollowNote,
|
|
5989
6058
|
sheet_width: seg.sourceWidth,
|
|
5990
6059
|
sheet_height: seg.sourceHeight,
|
|
5991
6060
|
format: wantGif ? "gif" : stillExt,
|
|
@@ -5993,7 +6062,7 @@ async function runStickerPackCommand(argv) {
|
|
|
5993
6062
|
stickers: written
|
|
5994
6063
|
}, null, 2)}\n`);
|
|
5995
6064
|
if (ctx.json) {
|
|
5996
|
-
printJson({ ok: true, target: "local", out_dir: outDir, manifest: manifestPath, count: written.length, skipped_specks: seg.rejected, key_color: keyColor, key_color_auto: plateAuto, key_color_note: plateNote, stickers: written });
|
|
6065
|
+
printJson({ ok: true, target: "local", out_dir: outDir, manifest: manifestPath, count: written.length, skipped_specks: seg.rejected, key_color: keyColor, key_color_auto: plateAuto, key_color_note: plateNote, hollow: seg.hollow, hollow_note: hollowNote, stickers: written });
|
|
5997
6066
|
}
|
|
5998
6067
|
else {
|
|
5999
6068
|
console.log(`${GREEN}Sticker pack ready:${RESET} ${written.length} transparent sticker${written.length === 1 ? "" : "s"} in ${outDir} ${DIM}(manifest: stickers.json${seg.rejected ? `, ${seg.rejected} speck(s) skipped` : ""})${RESET}`);
|
|
@@ -8254,6 +8323,118 @@ async function runDownloadPostCommand(argv) {
|
|
|
8254
8323
|
const final = await pollPrimitiveJob(ctx, jobId);
|
|
8255
8324
|
emitPrimitiveJobResult(ctx, final, "download-post");
|
|
8256
8325
|
}
|
|
8326
|
+
async function runRecycleCommand(argv) {
|
|
8327
|
+
const parsed = parseArgs({
|
|
8328
|
+
args: argv,
|
|
8329
|
+
allowPositionals: true,
|
|
8330
|
+
options: {
|
|
8331
|
+
...commonOptions(),
|
|
8332
|
+
"max-records": { type: "string" },
|
|
8333
|
+
cursor: { type: "string" },
|
|
8334
|
+
query: { type: "string" },
|
|
8335
|
+
filter: { type: "string" },
|
|
8336
|
+
sort: { type: "string" },
|
|
8337
|
+
mode: { type: "string" },
|
|
8338
|
+
network: { type: "string" },
|
|
8339
|
+
"no-comments": { type: "boolean", default: false },
|
|
8340
|
+
"no-profile": { type: "boolean", default: false },
|
|
8341
|
+
out: { type: "string" },
|
|
8342
|
+
"no-wait": { type: "boolean", default: false },
|
|
8343
|
+
tracer: { type: "string" }
|
|
8344
|
+
}
|
|
8345
|
+
});
|
|
8346
|
+
const source = parsed.positionals[0];
|
|
8347
|
+
if (!source) {
|
|
8348
|
+
throw new Error("recycle requires a Reddit or X/Twitter source — a thread URL, a subreddit (r/name), a profile URL, or @handle.");
|
|
8349
|
+
}
|
|
8350
|
+
const maxRecords = parsed.values["max-records"] ? Number(parsed.values["max-records"]) : undefined;
|
|
8351
|
+
if (maxRecords !== undefined && (!Number.isInteger(maxRecords) || maxRecords < 1 || maxRecords > 1000)) {
|
|
8352
|
+
throw new Error("recycle --max-records must be an integer between 1 and 1000.");
|
|
8353
|
+
}
|
|
8354
|
+
const ctx = commonContext(parsed.values);
|
|
8355
|
+
guardBilled(ctx, {
|
|
8356
|
+
label: "recycle a Reddit/X source into JSON (paid plan — brokered via the OfficeX lead-gen apps)",
|
|
8357
|
+
estimate: maxRecords && maxRecords > 100
|
|
8358
|
+
? "several cents (max-records drives upstream pagination)"
|
|
8359
|
+
: "~$0.002-$0.02 per pull",
|
|
8360
|
+
freeAlternative: "open the thread in a browser and copy the text you want by hand"
|
|
8361
|
+
});
|
|
8362
|
+
// A bare handle/subreddit is ambiguous without a network; a URL is not.
|
|
8363
|
+
const looksLikeUrl = /^https?:\/\//i.test(source) || source.includes(".com/");
|
|
8364
|
+
const payload = looksLikeUrl ? { source_url: source } : { handle: source };
|
|
8365
|
+
if (!looksLikeUrl && parsed.values.network)
|
|
8366
|
+
payload.network = String(parsed.values.network);
|
|
8367
|
+
if (parsed.values.mode)
|
|
8368
|
+
payload.mode = String(parsed.values.mode);
|
|
8369
|
+
if (maxRecords !== undefined)
|
|
8370
|
+
payload.max_records = maxRecords;
|
|
8371
|
+
if (parsed.values.cursor)
|
|
8372
|
+
payload.cursor = String(parsed.values.cursor);
|
|
8373
|
+
if (parsed.values.query)
|
|
8374
|
+
payload.query = String(parsed.values.query);
|
|
8375
|
+
if (parsed.values.filter)
|
|
8376
|
+
payload.filter = String(parsed.values.filter);
|
|
8377
|
+
if (parsed.values.sort)
|
|
8378
|
+
payload.sort = String(parsed.values.sort);
|
|
8379
|
+
if (parsed.values["no-comments"])
|
|
8380
|
+
payload.include_comments = false;
|
|
8381
|
+
if (parsed.values["no-profile"])
|
|
8382
|
+
payload.include_profile = false;
|
|
8383
|
+
const tracer = String(parsed.values.tracer ?? `devcli-recycle-${Date.now().toString(36)}`);
|
|
8384
|
+
const submit = await apiRequest({
|
|
8385
|
+
method: "POST",
|
|
8386
|
+
host: ctx.host,
|
|
8387
|
+
path: "/api/v1/primitives/social/recycle",
|
|
8388
|
+
auth: ctx.auth,
|
|
8389
|
+
body: { tracer, payload }
|
|
8390
|
+
});
|
|
8391
|
+
assertApiOk(submit, "recycle");
|
|
8392
|
+
const jobId = submit.json?.job_id;
|
|
8393
|
+
if (!jobId || parsed.values["no-wait"]) {
|
|
8394
|
+
emitResult(submit, ctx.json);
|
|
8395
|
+
return;
|
|
8396
|
+
}
|
|
8397
|
+
if (!ctx.json) {
|
|
8398
|
+
console.log(`${DIM}Recycling ${source} (${jobId})… the upstream lead-gen job is polled server-side.${RESET}`);
|
|
8399
|
+
}
|
|
8400
|
+
const final = await pollPrimitiveJob(ctx, jobId);
|
|
8401
|
+
// --out writes the FULL normalized payload to disk; the job result inlines
|
|
8402
|
+
// only a prefix when the item list is large.
|
|
8403
|
+
const outPath = parsed.values.out ? String(parsed.values.out) : null;
|
|
8404
|
+
if (outPath) {
|
|
8405
|
+
const result = (final?.result ?? {});
|
|
8406
|
+
const recycledUrl = result.recycled?.url
|
|
8407
|
+
?? result.primary_file_url;
|
|
8408
|
+
if (typeof recycledUrl === "string" && recycledUrl) {
|
|
8409
|
+
const res = await fetch(recycledUrl);
|
|
8410
|
+
if (res.ok) {
|
|
8411
|
+
mkdirSync(path.dirname(path.resolve(outPath)), { recursive: true });
|
|
8412
|
+
writeFileSync(path.resolve(outPath), await res.text());
|
|
8413
|
+
if (!ctx.json)
|
|
8414
|
+
console.log(`${DIM}Wrote full recycled JSON → ${outPath}${RESET}`);
|
|
8415
|
+
}
|
|
8416
|
+
else if (!ctx.json) {
|
|
8417
|
+
console.warn(`[vidfarm] could not fetch recycled.json (${res.status}) — the job result still has it.`);
|
|
8418
|
+
}
|
|
8419
|
+
}
|
|
8420
|
+
}
|
|
8421
|
+
if (!ctx.json) {
|
|
8422
|
+
const result = (final?.result ?? {});
|
|
8423
|
+
const counts = (result.counts ?? {});
|
|
8424
|
+
console.log(`${DIM}mode=${String(result.mode ?? "?")} items=${String(counts.items ?? "?")} `
|
|
8425
|
+
+ `(posts=${String(counts.posts ?? "?")} comments=${String(counts.comments ?? "?")}) `
|
|
8426
|
+
+ `media=${String(counts.media ?? "?")} credits=${String(counts.credits_charged ?? "?")}${RESET}`);
|
|
8427
|
+
for (const job of result.upstreamJobs ?? []) {
|
|
8428
|
+
if (typeof job.view_url === "string" && job.view_url) {
|
|
8429
|
+
console.log(`${DIM} browse ${String(job.endpoint)} in a spreadsheet: ${job.view_url}${RESET}`);
|
|
8430
|
+
}
|
|
8431
|
+
}
|
|
8432
|
+
if (result.nextCursor) {
|
|
8433
|
+
console.log(`${DIM} more available — re-run with --cursor ${String(result.nextCursor).slice(0, 24)}…${RESET}`);
|
|
8434
|
+
}
|
|
8435
|
+
}
|
|
8436
|
+
emitPrimitiveJobResult(ctx, final, "recycle");
|
|
8437
|
+
}
|
|
8257
8438
|
async function runDownloadPostAudioCommand(argv) {
|
|
8258
8439
|
const parsed = parseArgs({
|
|
8259
8440
|
args: argv,
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
// Both builders return plain text meant to be shown verbatim to the user, plus a
|
|
23
23
|
// structured form for `--json` so an agent can render it its own way. No network,
|
|
24
24
|
// no backend imports — this module is pure string assembly.
|
|
25
|
-
import { pickPlateColor } from "./sticker-pack.js";
|
|
25
|
+
import { pickPlateColor, keySafeArtInstruction } from "./sticker-pack.js";
|
|
26
26
|
const DEFAULT_STYLE = "simple flat vector illustration, minimal detail, 2-3 flat colors, no shadows, no text";
|
|
27
27
|
/** Pick a sensible grid for N items (roughly square, wider than tall). */
|
|
28
28
|
function gridFor(count) {
|
|
@@ -67,18 +67,22 @@ export function buildImageHandoff(input) {
|
|
|
67
67
|
`CRITICAL: every object must be fully separated from the others by a clear margin of plain ${keyColor} background —`,
|
|
68
68
|
`nothing touching, overlapping or connected, and nothing touching the image edge.`,
|
|
69
69
|
`One consistent art style, line weight and palette across all objects. Front-facing, centered in its own cell.`,
|
|
70
|
+
// Load-bearing: the user is about to spend their own time on this sheet,
|
|
71
|
+
// and hollow/outline-only art comes back as rims around holes.
|
|
72
|
+
keySafeArtInstruction(keyColor),
|
|
70
73
|
`Square image, high resolution.`
|
|
71
74
|
].join(" ")
|
|
72
75
|
: [
|
|
73
76
|
`${input.theme} — ${style} — isolated on a solid pure ${keyColor} background`,
|
|
74
77
|
`(flat, evenly lit, no gradient, no shadow cast on the background, no text).`,
|
|
75
78
|
`Center the subject with generous empty margin on all sides, crisp clean edges, single subject.`,
|
|
79
|
+
keySafeArtInstruction(keyColor),
|
|
76
80
|
`Square image, high resolution.`
|
|
77
81
|
].join(" ");
|
|
78
82
|
const steps = pack
|
|
79
83
|
? [
|
|
80
84
|
"Open a FREE image generator you're already signed into (list below).",
|
|
81
|
-
"Paste the prompt below and generate.
|
|
85
|
+
"Paste the prompt below and generate. Two checks before you send it back: (a) if any two objects are touching, re-generate asking for wider spacing — touching objects get cut out as ONE sticker; (b) if any object is a hollow outline with the background showing through its middle, re-generate asking for solid fills — that interior gets deleted with the background and the sticker ends up as a rim around a hole.",
|
|
82
86
|
"Download the image (PNG preferred) and tell me the file path — or drop it in this project folder.",
|
|
83
87
|
`I'll split it into individual transparent stickers locally for $0: \`vidfarm sticker-pack <sheet> --out-dir ${outDir}\`.`
|
|
84
88
|
]
|
|
@@ -132,6 +132,42 @@ export async function detectPlateColor(sourcePath, opts = {}) {
|
|
|
132
132
|
const hex = `#${avg.map((v) => v.toString(16).padStart(2, "0")).join("").toUpperCase()}`;
|
|
133
133
|
return { hex, rgb: avg };
|
|
134
134
|
}
|
|
135
|
+
// ── Key-safe ART instruction ─────────────────────────────────────────────────
|
|
136
|
+
// Picking a plate the art doesn't use (above) is only HALF of surviving a chroma
|
|
137
|
+
// key. The other half is what the art is made of, and it's the failure we see in
|
|
138
|
+
// the wild: an image model hears "sticker on a green plate" and draws OUTLINE
|
|
139
|
+
// art — a colored stroke with the shape's interior left as bare background. On
|
|
140
|
+
// screen that looks fine. After the key, the interior is gone, and the sticker
|
|
141
|
+
// composites as a rim floating around a see-through hole.
|
|
142
|
+
//
|
|
143
|
+
// The same failure arrives three other ways: a fill that's a near-shade of the
|
|
144
|
+
// plate (keyed by tolerance, not by exact match), a translucent/glassy material
|
|
145
|
+
// that lets the plate through, and a soft glow/drop-shadow that fades INTO the
|
|
146
|
+
// plate at the edges.
|
|
147
|
+
//
|
|
148
|
+
// All four are prompt-preventable, so every generation path that mints art
|
|
149
|
+
// destined for a key (cutout, sticker-pack, the hand-off brief the user pastes
|
|
150
|
+
// into a free web tool) appends this clause. Detection after the fact is the net
|
|
151
|
+
// (`detectEnclosedHoles` below) — this is the mechanism.
|
|
152
|
+
/**
|
|
153
|
+
* The "your art has to survive the key" clause, worded for an image model.
|
|
154
|
+
* Append to any prompt whose output will be chroma-keyed on `keyColorHex`.
|
|
155
|
+
*/
|
|
156
|
+
export function keySafeArtInstruction(keyColorHex) {
|
|
157
|
+
const hex = keyColorHex.toUpperCase();
|
|
158
|
+
return (`KEY-SAFE ARTWORK (the ${hex} background gets deleted, so anything ${hex} on the art is deleted too): ` +
|
|
159
|
+
`every object must be a CLOSED, SOLIDLY FILLED shape — outlines and strokes must enclose an opaque fill of a ` +
|
|
160
|
+
`different color. NO outline-only / hollow / line-art objects, and never leave a shape's interior as bare ` +
|
|
161
|
+
`background. No part of any object — fill, outline, highlight, gradient, glow, shading or detail — may be ${hex} ` +
|
|
162
|
+
`or any near-shade, tint or tone of ${hex}. No transparent, translucent, glassy, glowing, misty or ghosted ` +
|
|
163
|
+
`materials; every pixel of every object is fully opaque. No soft glows, blurs or drop shadows fading into the ` +
|
|
164
|
+
`background. Keep the whole palette in strong contrast to ${hex}. The background must be visible ONLY around the ` +
|
|
165
|
+
`outside of the objects, never showing through inside them.`);
|
|
166
|
+
}
|
|
167
|
+
/** At/above this `hole_pct` a sticker is worth warning about: past ~a fifth of
|
|
168
|
+
* its own box, "the key ate the fill" is far more likely than "the artist drew
|
|
169
|
+
* a ring". Tuned to stay quiet on letter counters, handles and small gaps. */
|
|
170
|
+
export const HOLE_WARN_PCT = 20;
|
|
135
171
|
/** Read a still's alpha plane as raw 8-bit luma at a given size (bundle-safe:
|
|
136
172
|
* ffmpeg's `alphaextract` writes alpha as luma; rawvideo skips any decoding on
|
|
137
173
|
* our side). Returns exactly width*height bytes. */
|
|
@@ -199,6 +235,93 @@ function dilate(mask, w, h, radius) {
|
|
|
199
235
|
}
|
|
200
236
|
return out;
|
|
201
237
|
}
|
|
238
|
+
/**
|
|
239
|
+
* Find transparent islands that are fully SURROUNDED by opaque art — the exact
|
|
240
|
+
* signature of the "hollow sticker" bug: art drawn as an outline (or filled in a
|
|
241
|
+
* near-plate shade) has its middle deleted by the key, and composites as a rim
|
|
242
|
+
* around a see-through hole.
|
|
243
|
+
*
|
|
244
|
+
* Mechanically it's the complement of a background flood-fill: every transparent
|
|
245
|
+
* pixel reachable from the mask's border is the plate doing its job; every
|
|
246
|
+
* transparent pixel that is NOT reachable is a hole punched inside something.
|
|
247
|
+
* 4-connected on purpose — an 8-connected fill leaks through a 1px diagonal
|
|
248
|
+
* seam in antialiased line art and would under-report every real hole.
|
|
249
|
+
*
|
|
250
|
+
* Note this cannot distinguish a bug from a deliberate ring/donut/picture-frame,
|
|
251
|
+
* so callers WARN on the result, never reject it.
|
|
252
|
+
*/
|
|
253
|
+
export function detectEnclosedHoles(mask, w, h, opts = {}) {
|
|
254
|
+
const reachable = new Uint8Array(w * h);
|
|
255
|
+
const queue = new Int32Array(w * h);
|
|
256
|
+
let head = 0;
|
|
257
|
+
let tail = 0;
|
|
258
|
+
const push = (p) => {
|
|
259
|
+
if (!mask[p] && !reachable[p]) {
|
|
260
|
+
reachable[p] = 1;
|
|
261
|
+
queue[tail++] = p;
|
|
262
|
+
}
|
|
263
|
+
};
|
|
264
|
+
for (let x = 0; x < w; x++) {
|
|
265
|
+
push(x);
|
|
266
|
+
push((h - 1) * w + x);
|
|
267
|
+
}
|
|
268
|
+
for (let y = 0; y < h; y++) {
|
|
269
|
+
push(y * w);
|
|
270
|
+
push(y * w + (w - 1));
|
|
271
|
+
}
|
|
272
|
+
while (head < tail) {
|
|
273
|
+
const p = queue[head++];
|
|
274
|
+
const py = (p / w) | 0;
|
|
275
|
+
const px = p - py * w;
|
|
276
|
+
if (px > 0)
|
|
277
|
+
push(p - 1);
|
|
278
|
+
if (px < w - 1)
|
|
279
|
+
push(p + 1);
|
|
280
|
+
if (py > 0)
|
|
281
|
+
push(p - w);
|
|
282
|
+
if (py < h - 1)
|
|
283
|
+
push(p + w);
|
|
284
|
+
}
|
|
285
|
+
// Anything transparent and unreached is enclosed — group it into islands.
|
|
286
|
+
const minAreaPx = Math.max(1, Math.round(opts.minAreaPx ?? 4));
|
|
287
|
+
const seen = new Uint8Array(w * h);
|
|
288
|
+
const holes = [];
|
|
289
|
+
const stack = new Int32Array(w * h);
|
|
290
|
+
for (let start = 0; start < mask.length; start++) {
|
|
291
|
+
if (mask[start] || reachable[start] || seen[start])
|
|
292
|
+
continue;
|
|
293
|
+
let top = 0;
|
|
294
|
+
stack[top++] = start;
|
|
295
|
+
seen[start] = 1;
|
|
296
|
+
const hole = { minX: w, minY: h, maxX: -1, maxY: -1, area: 0 };
|
|
297
|
+
while (top > 0) {
|
|
298
|
+
const p = stack[--top];
|
|
299
|
+
const py = (p / w) | 0;
|
|
300
|
+
const px = p - py * w;
|
|
301
|
+
hole.area++;
|
|
302
|
+
if (px < hole.minX)
|
|
303
|
+
hole.minX = px;
|
|
304
|
+
if (px > hole.maxX)
|
|
305
|
+
hole.maxX = px;
|
|
306
|
+
if (py < hole.minY)
|
|
307
|
+
hole.minY = py;
|
|
308
|
+
if (py > hole.maxY)
|
|
309
|
+
hole.maxY = py;
|
|
310
|
+
const neighbors = [px > 0 ? p - 1 : -1, px < w - 1 ? p + 1 : -1, py > 0 ? p - w : -1, py < h - 1 ? p + w : -1];
|
|
311
|
+
for (const q of neighbors) {
|
|
312
|
+
if (q < 0 || mask[q] || reachable[q] || seen[q])
|
|
313
|
+
continue;
|
|
314
|
+
seen[q] = 1;
|
|
315
|
+
stack[top++] = q;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
// Sub-threshold islands are antialiasing dropouts and despill speckle, not
|
|
319
|
+
// a missing fill — a real hollow interior is orders of magnitude bigger.
|
|
320
|
+
if (hole.area >= minAreaPx)
|
|
321
|
+
holes.push(hole);
|
|
322
|
+
}
|
|
323
|
+
return holes;
|
|
324
|
+
}
|
|
202
325
|
/**
|
|
203
326
|
* Find every item on a keyed plate by segmenting its alpha channel into
|
|
204
327
|
* connected islands of opaque pixels — the automatic replacement for measuring
|
|
@@ -312,22 +435,93 @@ export async function segmentAlphaComponents(input) {
|
|
|
312
435
|
// ---- Map sample-space boxes back to source pixels --------------------------
|
|
313
436
|
// One sample pixel of slack on each side covers the downscale's rounding, so a
|
|
314
437
|
// subject's outermost antialiased edge never gets clipped off.
|
|
438
|
+
// ---- Attribute enclosed holes to the item that surrounds them --------------
|
|
439
|
+
// A hole lives strictly inside the art that encloses it, so its center falls
|
|
440
|
+
// in that item's box. Boxes can nest (a small icon inside a big backdrop), so
|
|
441
|
+
// the SMALLEST containing box wins — the nearest enclosing art is the owner.
|
|
442
|
+
const holes = detectEnclosedHoles(mask, sw, sh, { minAreaPx: Math.max(6, Math.round(total * 0.00005)) });
|
|
443
|
+
const holeArea = new Array(ordered.length).fill(0);
|
|
444
|
+
const holeCount = new Array(ordered.length).fill(0);
|
|
445
|
+
for (const hole of holes) {
|
|
446
|
+
const cx = (hole.minX + hole.maxX) / 2;
|
|
447
|
+
const cy = (hole.minY + hole.maxY) / 2;
|
|
448
|
+
let owner = -1;
|
|
449
|
+
let ownerArea = Infinity;
|
|
450
|
+
for (let i = 0; i < ordered.length; i++) {
|
|
451
|
+
const b = ordered[i];
|
|
452
|
+
if (cx < b.minX || cx > b.maxX || cy < b.minY || cy > b.maxY)
|
|
453
|
+
continue;
|
|
454
|
+
const boxArea = (b.maxX - b.minX + 1) * (b.maxY - b.minY + 1);
|
|
455
|
+
if (boxArea < ownerArea) {
|
|
456
|
+
owner = i;
|
|
457
|
+
ownerArea = boxArea;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
if (owner >= 0) {
|
|
461
|
+
holeArea[owner] += hole.area;
|
|
462
|
+
holeCount[owner]++;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
315
465
|
const inv = 1 / scale;
|
|
316
466
|
const components = ordered.map((b, i) => {
|
|
317
467
|
const x0 = Math.max(0, Math.floor((b.minX - 1) * inv));
|
|
318
468
|
const y0 = Math.max(0, Math.floor((b.minY - 1) * inv));
|
|
319
469
|
const x1 = Math.min(dims.width, Math.ceil((b.maxX + 2) * inv));
|
|
320
470
|
const y1 = Math.min(dims.height, Math.ceil((b.maxY + 2) * inv));
|
|
471
|
+
// Measured against the item's own silhouette (fill + holes), not the plate:
|
|
472
|
+
// "a fifth of THIS sticker is missing" is the question that matters.
|
|
473
|
+
const silhouette = b.area + holeArea[i];
|
|
321
474
|
return {
|
|
322
475
|
index: i + 1,
|
|
323
476
|
x: x0,
|
|
324
477
|
y: y0,
|
|
325
478
|
width: Math.max(1, x1 - x0),
|
|
326
479
|
height: Math.max(1, y1 - y0),
|
|
327
|
-
area_pct: Math.round((b.area / total) * 1000) / 10
|
|
480
|
+
area_pct: Math.round((b.area / total) * 1000) / 10,
|
|
481
|
+
holes: holeCount[i],
|
|
482
|
+
hole_pct: silhouette > 0 ? Math.round((holeArea[i] / silhouette) * 1000) / 10 : 0
|
|
328
483
|
};
|
|
329
484
|
});
|
|
330
|
-
|
|
485
|
+
const hollow = components.filter((c) => c.hole_pct >= HOLE_WARN_PCT).map((c) => c.index);
|
|
486
|
+
return { components, sourceWidth: dims.width, sourceHeight: dims.height, sampleWidth: sw, sampleHeight: sh, rejected, hollow };
|
|
487
|
+
}
|
|
488
|
+
/**
|
|
489
|
+
* The single-subject version of the hollow-sticker check: does THIS keyed still
|
|
490
|
+
* have transparency punched through the middle of its art? Used by `cutout`,
|
|
491
|
+
* which has one subject and so needs no segmentation — just the same
|
|
492
|
+
* background-flood-fill complement over the whole alpha plane.
|
|
493
|
+
*/
|
|
494
|
+
export async function analyzeKeyedArt(sourcePath) {
|
|
495
|
+
if (!existsSync(sourcePath))
|
|
496
|
+
return null;
|
|
497
|
+
const dims = await probeImageDimensions(sourcePath);
|
|
498
|
+
if (!dims)
|
|
499
|
+
return null;
|
|
500
|
+
const longSide = Math.max(dims.width, dims.height);
|
|
501
|
+
const scale = longSide > 640 ? 640 / longSide : 1;
|
|
502
|
+
const sw = Math.max(1, Math.round(dims.width * scale));
|
|
503
|
+
const sh = Math.max(1, Math.round(dims.height * scale));
|
|
504
|
+
let alpha;
|
|
505
|
+
try {
|
|
506
|
+
alpha = await readAlphaPlane(sourcePath, sw, sh);
|
|
507
|
+
}
|
|
508
|
+
catch {
|
|
509
|
+
return null; // no alpha channel to read → nothing to say
|
|
510
|
+
}
|
|
511
|
+
const mask = new Uint8Array(sw * sh);
|
|
512
|
+
let opaque = 0;
|
|
513
|
+
for (let i = 0; i < mask.length; i++)
|
|
514
|
+
if (alpha[i] > 8) {
|
|
515
|
+
mask[i] = 1;
|
|
516
|
+
opaque++;
|
|
517
|
+
}
|
|
518
|
+
if (!opaque)
|
|
519
|
+
return null;
|
|
520
|
+
const total = sw * sh;
|
|
521
|
+
const holes = detectEnclosedHoles(mask, sw, sh, { minAreaPx: Math.max(6, Math.round(total * 0.00005)) });
|
|
522
|
+
const holeArea = holes.reduce((sum, h) => sum + h.area, 0);
|
|
523
|
+
const hole_pct = Math.round((holeArea / (opaque + holeArea)) * 1000) / 10;
|
|
524
|
+
return { holes: holes.length, hole_pct, hollow: hole_pct >= HOLE_WARN_PCT };
|
|
331
525
|
}
|
|
332
526
|
/**
|
|
333
527
|
* Re-encode a transparent still as a transparent GIF — the format a lot of
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@officexapp/vidfarm-devcli",
|
|
3
|
-
"version": "0.21.
|
|
3
|
+
"version": "0.21.31",
|
|
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": {
|
|
@@ -95,6 +95,8 @@
|
|
|
95
95
|
"test:clips": "node --import tsx --test test/clip-curation.test.ts",
|
|
96
96
|
"test:qa": "node --import tsx --test test/qa-check.test.ts",
|
|
97
97
|
"test:studio-brand": "node --import tsx --test test/studio-brand.test.ts",
|
|
98
|
+
"test:stickers": "node --import tsx --test test/sticker-pack.test.ts",
|
|
99
|
+
"test:social-recycle": "node --import tsx --test test/social-recycle.test.ts",
|
|
98
100
|
"check:skills": "node scripts/build-director-skill-rollup.mjs --check && node scripts/check-skill-routes.mjs",
|
|
99
101
|
"benchmark:editor-chat": "node --import tsx scripts/benchmark-editor-chat-harness.mjs",
|
|
100
102
|
"cdk:deploy:prod-serverless": "npm run build && dotenv -e .env.production -- npx aws-cdk deploy --app 'node dist/infra/cdk/bin/vidfarm-prod.js'",
|