@officexapp/vidfarm-devcli 0.21.30 → 0.21.32

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/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,8 @@ 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`.
322
+ 4d. If the task is **“post this again / to several accounts / on another platform”**, or you are about to publish or bulk-produce at all — that is **deduplication**. Run `vidfarm dedupe <mp4> [--variants N]` on the **exported file** (free, local ffmpeg, no re-render), then approve/schedule each variant. **Ask the operator whether they want deduplicated copies, and how many, BEFORE the render/bulk run** — deciding after means paying for a second render. Details in `references/core-workflows.md` → *Deduplicate before you publish* and `references/primitives.md` → *Primitive: media_dedupe*.
320
323
  5. If the task is scripted, local, CI-driven, or `vidfarm serve`-based, read `references/automation-and-local-dev.md`.
321
324
  6. If the task explicitly asks for a primitive or needs specialized generation/transcription work, read `references/primitives.md`.
322
325
  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`.
@@ -487,6 +490,25 @@ Every publish creates an immutable version snapshot at `versions/<N>/composition
487
490
 
488
491
  The Web UI **Render** button and devcli render both use this same endpoint. The fast `202` response includes the deterministic `expectedOutputPublicUrl` so a caller can store or pass along the final public S3 URL before the render has completed, then poll by `renderId` until `status` settles.
489
492
 
493
+ ## Deduplicate before you publish (ASK FIRST)
494
+
495
+ Social platforms fingerprint every upload. If a render is going out **more than once** — to several accounts, to a second platform, or again in a few weeks — the later copies get suppressed as duplicate/reused content unless each one carries a distinct fingerprint.
496
+
497
+ **Before you render for publication, and before any bulk run, ask the operator: "Do you want deduplicated copies for posting? How many?"** Ask *then*, not after — dedupe runs on the exported MP4, so the order is **render once → dedupe N times**. Getting the answer up front is what stops you paying for a second render later.
498
+
499
+ ```bash
500
+ # free, offline, no wallet, no re-render — the default path
501
+ vidfarm dedupe ./out/final.mp4 # one distinct copy
502
+ vidfarm dedupe ./out/final.mp4 --variants 3 --out-dir ./out/posts
503
+ ```
504
+
505
+ - Default preset `standard` = skew 2%, zoom 3%, rotate 2°, speed +2%, saturation +4%, plus contrast/brightness/hue/grain, a container-metadata strip and a per-variant CRF walk. Invisible to a viewer.
506
+ - `--variants N` mints N copies that differ from the original **and from each other** — one per account/slot. Two accounts posting the same variant defeats the point.
507
+ - `--preset light|standard|strong` for how hard to push; `--rotate 0` if the forced corner-hiding crop (~6.7% on a tall frame at 2°) matters more than fingerprint distance.
508
+ - Cloud equivalent: `POST /api/v1/primitives/media/dedupe` — same transforms, billed. See `references/primitives.md` → *Primitive: media_dedupe*.
509
+
510
+ Dedupe the **finished MP4**, then approve/schedule each variant separately. Do not dedupe the composition and re-render.
511
+
490
512
  ## Approve a finished post
491
513
 
492
514
  A render produces a bare MP4 URL. **Approving** wraps that MP4 (plus caption, title, pinned comment, and any carousel slides) into a shareable preview page — the phone-mockup page a human opens to review and copy the post.
@@ -508,21 +530,59 @@ devcli: `vidfarm approve --video <mp4-url> --caption "..."` prints the `share_ur
508
530
 
509
531
  ## Schedule a post
510
532
 
511
- Schedule an approved post to a connected destination channel (FlockPoster social account or email) at one ISO timestamp:
533
+ A **destination** is one connected channel. There are exactly two kinds, and they are billed and owned differently:
534
+
535
+ - **`email`** — a verified email address on the vidfarm account. Delivered by vidfarm itself. Your vidfarm API key is the only credential involved.
536
+ - **`flockposter`** — a social account (TikTok/IG/X/…) connected through FlockPoster, a separate product. Needs the customer's FlockPoster key saved in vidfarm Settings → Channels. Vidfarm brokers the call; FlockPoster does the posting.
537
+
538
+ Every account always has at least one email destination: its own signup address, pre-verified, created automatically. So `destination_type: "email"` works on a fresh account with no setup at all.
539
+
540
+ **List destinations first** (or just send an address — see below):
541
+
542
+ ```
543
+ GET /api/v1/user/me/channels
544
+ → { "channels": [ { "destination_type": "email", "destination_id": "cus_…:default-email",
545
+ "handle": "operator@example.com", "status": "verified", "schedulable": true,
546
+ "accepts": ["cus_…:default-email", "operator@example.com", "operator"] } ],
547
+ "flockposter_connected": false, "flockposter_error": null }
548
+ ```
549
+
550
+ Then schedule:
512
551
 
513
552
  ```
514
553
  POST /api/v1/approved/posts/:postId/schedules
515
554
  Content-Type: application/json
516
555
 
517
- { "destination_type": "flockposter" | "email", "destination_id": "<channel or email>",
556
+ { "destination_type": "flockposter" | "email", "destination_id": "<see below>",
518
557
  "scheduled_at": "2026-07-10T14:00:00Z", "timezone": "America/New_York", "additional_notes": "optional" }
519
558
  ```
520
559
 
521
- Minimum 10-minute lead time. Response (`201`) is the schedule record. Browse existing schedules with `GET /api/v1/approved/posts/:postId/schedules`.
560
+ **`destination_id` accepts whatever you have** it is resolved server-side, so you do not need to look up an id:
561
+
562
+ | For `email` | For `flockposter` |
563
+ |---|---|
564
+ | the channel id (`cus_…:default-email`, or `email:<id>`) | the integration id |
565
+ | the address (`operator@example.com`) | the handle (`@brandname` or `brandname`) |
566
+ | the local part (`operator`) | the channel title |
567
+ | the channel title | the platform (`tiktok`) when exactly one is connected |
568
+
569
+ Matching is case-insensitive. When nothing matches, the `400` names what was searched for and lists the channels that do exist — read it rather than guessing another id.
522
570
 
523
- devcli: `vidfarm schedule <postId> --at <iso> --to <destinationId> [--type flockposter|email]`, and `vidfarm schedules <postId>` to browse.
571
+ Minimum 10-minute lead time. Response (`201`) is the schedule record.
524
572
 
525
- Deeper FlockPoster work (channel management, direct posting/analytics outside vidfarm's schedule wrapper) is FlockPoster's own API grab its skill first: `npx skills add https://github.com/OfficeXApp/flockposter-skill` (mirrored as `vidfarm skills add flockposter`).
573
+ **Managing a schedule** (all authed with the same vidfarm API keyan agent has full control):
574
+
575
+ - `GET /api/v1/approved/posts/:postId/schedules` — browse this post's schedules
576
+ - `PATCH /api/v1/approved/posts/:postId/schedules/:scheduleId` — reschedule; same body as POST. Cancels the queued send and re-queues it.
577
+ - `DELETE /api/v1/approved/posts/:postId/schedules/:scheduleId` — cancel the queued send
578
+
579
+ Cancel/reschedule reaches through to the provider (Resend for email, FlockPoster for social), so a `400` here means the send was **not** stopped — the email or post is still queued. Never report a failed cancel as cancelled.
580
+
581
+ Schedules created outside vidfarm (`managed_by: "external"`) are read-only here and return `409`; change those in FlockPoster.
582
+
583
+ devcli: `vidfarm channels` lists destinations, `vidfarm schedule <postId> --at <iso> --to <destination> [--type flockposter|email]` schedules, `vidfarm schedules <postId>` browses.
584
+
585
+ Deeper FlockPoster work (connecting accounts, channel management, direct posting/analytics outside vidfarm's schedule wrapper) is FlockPoster's own API, not vidfarm's — grab its skill first: `npx skills add https://github.com/OfficeXApp/flockposter-skill` (mirrored as `vidfarm skills add flockposter`).
526
586
 
527
587
  ## Marketplace — order videos from specialist agents
528
588
 
@@ -1247,7 +1307,7 @@ Beyond the Ken Burns / transition / animated-caption presets, the copilot can ha
1247
1307
 
1248
1308
  **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
1309
 
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".
1310
+ **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
1311
 
1252
1312
  **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
1313
 
@@ -1542,6 +1602,23 @@ Both are async primitive jobs — poll `GET /api/v1/primitives/jobs/:jobId` unti
1542
1602
 
1543
1603
  **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
1604
 
1605
+ ## Recycle a Reddit or X source into content ("tweet to TikTok", "Reddit to TikTok")
1606
+
1607
+ 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:
1608
+
1609
+ | Paste this | You get back |
1610
+ | --- | --- |
1611
+ | `reddit.com/r/<sub>/comments/<id>/...` | the post + its whole comment tree |
1612
+ | `reddit.com/r/<sub>` or `r/<sub>` | that subreddit's threads |
1613
+ | `x.com/<user>/status/<id>` | the tweet + self-thread + replies |
1614
+ | `x.com/<user>` or `@<user>` | that account's posts, pinned first |
1615
+
1616
+ 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.
1617
+
1618
+ 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*.
1619
+
1620
+ 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.
1621
+
1545
1622
  ## Public raws catalog — browse BY CATEGORY (check here first)
1546
1623
 
1547
1624
  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:
@@ -1772,6 +1849,17 @@ for VARIANT in "${VARIANTS[@]}"; do
1772
1849
  done
1773
1850
  ```
1774
1851
 
1852
+ **Before you start a bulk run, ask the operator whether the output should be deduplicated, and for how many posting slots.** A bulk run's whole point is volume across accounts/platforms, which is exactly the shape platforms flag as duplicate content. Dedupe is a post-render ffmpeg pass, so asking up front is what keeps it at *render once → dedupe N* instead of a second render per slot:
1853
+
1854
+ ```bash
1855
+ # after the loop: one distinct copy per posting slot, free and offline
1856
+ for MP4 in renders/*.mp4; do
1857
+ vidfarm dedupe "$MP4" --variants "$SLOTS" --seed "$(basename "$MP4" .mp4)" --out-dir ./posts
1858
+ done
1859
+ ```
1860
+
1861
+ Reuse one `--seed` per source so a batch is reproducible, and post each variant to a **different** account — two accounts posting the same variant defeats the point. See `references/core-workflows.md` → *Deduplicate before you publish*.
1862
+
1775
1863
  `vidfarm qa` still exits 0 on findings — the gate above is the *script's* choice, made explicit with `jq`, not a behavior change in the tool. Keep it that way: an agent that can't ship a deliberately weird variant will quietly stop trying weird variants.
1776
1864
 
1777
1865
  This section is for a **desktop/local coding agent**, not the web copilot. A local Codex/Claude agent may use its shell and filesystem to write JavaScript/TypeScript/Python/shell scripts, fetch every API page, join and score catalog/library data, calculate statistics, emit CSV/JSON/Markdown reports, manipulate composition DOM files, and run iterative render/inspection loops. The web copilot cannot inherit those abilities from this document: it may only call its declared tools and bounded REST routes. If web chat prepares work for this flow, consume its My Files handoff document as input; do not claim the web chat itself executed the script.
@@ -1908,9 +1996,10 @@ The licensed harness also carries the **generative build workflow** guidance (ch
1908
1996
  | `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
1997
  | `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
1998
  | `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`. Alias: `sticker`. See recipe `cutout-graphics-for-explainers.md`. |
1999
+ | `vidfarm dedupe <video\|image\|url> [--preset light\|standard\|strong] [--variants N] [--seed <s>] [--zoom/--rotate/--skew/--speed/--saturation/--hue/--noise/--flip] [--local\|--cloud] [--out <f>\|--out-dir <d>]` | **local, free, ffmpeg-only** by default (no job); `--cloud` = `POST /api/v1/primitives/media/dedupe` (polls job) | **DEDUPLICATION the publish-safety pass.** Makes a finished render read as a NEW upload to a platform's duplicate-content detector, invisibly to a viewer. Default preset `standard` = skew 2%, zoom 3%, rotate 2°, speed +2%, saturation +4%, plus contrast/brightness/hue/grain, a container-metadata strip and a per-variant CRF walk. **Runs on the EXPORTED file never re-render for this.** `--variants N` mints N copies that differ from the original AND from each other (jittered magnitudes, alternating signs), one per account/posting slot; `--seed` makes a batch reproducible. A rotate forces a bigger centre-crop to hide the black corners (~6.7% on a tall frame at 2°) and says so — pass `--rotate 0` when framing matters more. `--flip` is the strongest single knob but visibly reverses on-screen text. **Ask the operator whether they want this BEFORE publishing or bulk-producing.** Aliases: `dedup`, `deduplicate`, `uniquify`. |
2000
+ | `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
2001
  | `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". |
2002
+ | `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
2003
  | `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
2004
  | `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
2005
  | `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 +2028,7 @@ The licensed harness also carries the **generative build workflow** guidance (ch
1939
2028
  | `vidfarm whoami` | `GET /api/v1/user/me` | who am I (also prints the active cost mode) |
1940
2029
  | `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
2030
  | `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. |
2031
+ | `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
2032
  | `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
2033
  | `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
2034
  | `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 +2037,7 @@ The licensed harness also carries the **generative build workflow** guidance (ch
1948
2037
  | `vidfarm download <url> [dest]` | (streams any URL to disk) | download media from a **direct** media URL. Free — no plan, no job |
1949
2038
  | `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
2039
  | `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` |
2040
+ | `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
2041
 
1952
2042
  > **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
2043
  | `vidfarm files [--folder <path>]` | `GET /api/v1/user/me/attachments` | list My Files assets + folders |
@@ -2030,7 +2120,7 @@ The four modes, quoted as **cost per finished video**. The first two are spend p
2030
2120
 
2031
2121
  **All of it bills to the user's own AI provider keys (BYOK)** — the keys saved with `vidfarm add-provider-key <provider> <key>` or at **Settings → Bring your own keys** (<https://vidfarm.cc/settings/developer>). The model providers charge those keys directly; Vidfarm wallet credits only come into play when the user deliberately runs on the platform key instead of their own. So `minimize` isn't "cheap", it's **zero**: nothing reaches a paid key at all.
2032
2122
 
2033
- `vidfarm cost-mode <minimize|hybrid|rich-ai|pure-videogen>` records a single spend preference (in `~/.vidfarm/cost-mode.json`) that every **billed** command honors: `generate`, `music`, `decompose`, `inspiration-decompose`, `create`, `replicate`, `inpaint`, `create-overlay`, `cutout --generate` (only the generation step; a bare `cutout` on an existing graphic is free), and the cloud paths of `render --target cloud`, `tts --cloud`, `stt --cloud`, `remove-greenscreen` (non-`--local`). FREE local engines never gate (`render` local default, `tts --engine local`, `stt --engine whisper`, `remove-greenscreen --local`, `cutout` on a file/url, all the file-editing verbs). `vidfarm media search` (the free stock catalog — pixabay/openverse/iconify music, SFX, images, icons, stock video) is also free and never gates.
2123
+ `vidfarm cost-mode <minimize|hybrid|rich-ai|pure-videogen>` records a single spend preference (in `~/.vidfarm/cost-mode.json`) that every **billed** command honors: `generate`, `music`, `decompose`, `inspiration-decompose`, `create`, `replicate`, `inpaint`, `create-overlay`, `cutout --generate` (only the generation step; a bare `cutout` on an existing graphic is free), and the cloud paths of `render --target cloud`, `tts --cloud`, `stt --cloud`, `remove-greenscreen` (non-`--local`), `dedupe --cloud`. FREE local engines never gate (`render` local default, `tts --engine local`, `stt --engine whisper`, `remove-greenscreen --local`, `dedupe --local`, `cutout` on a file/url, all the file-editing verbs). `vidfarm media search` (the free stock catalog — pixabay/openverse/iconify music, SFX, images, icons, stock video) is also free and never gates.
2034
2124
 
2035
2125
  - **minimize ($0 videos)** — a billed op is **refused** unless you add `--yes`; the error names the free local alternative (which now includes the matching `vidfarm media search` for music/SFX/image/video). Use this to guarantee no surprise AI spend. Before paying to generate music, sound effects, or images, try `vidfarm media search "<meaning>" --type <bgm|sfx|image|vector|icon|video>` first — free royalty-free assets instead of a billed `music`/`generate` call. **Check the keyless sources first — Openverse (CC/CC0 music, SFX, images) and iconify (icons) need no account at all**, so they always work in `minimize`. Photos/vectors/stock-video need a **free Pixabay key** that **may already be saved** — check `vidfarm provider-keys` (or web **Settings → Bring your own keys** / <https://vidfarm.cc/settings/developer>) before assuming a short result means "no key." If absent, save one once: `vidfarm add-provider-key pixabay <key>` (free key from <https://pixabay.com/api/docs/>), the Settings surface, or hand it to the desktop AI agent to run that command.
2036
2126
  - **minimize still gets CUSTOM images — via a free manual generator.** A refused `generate` is not the end of the road. Offer the user the manual loop (ask once, then make it the session default): **you write the prompt → they run it free in <https://meta.ai>, free-tier ChatGPT, or a free Hugging Face image Space (<https://huggingface.co/spaces>) → they hand the PNG back** via `vidfarm put-file ./sheet.png` or web **My Files**. Ask for **one sheet holding every graphic you need**, gridded on a **flat pure-green plate** (`#00FF00`), no text — one round trip instead of N, which saves the user's time and your tokens. Then split it locally for $0: `vidfarm mask ./sheet.png --crop x,y,w,h --flat "#00FF00" --out prop-a.png`, once per element (drop `--flat` and let local ONNX matting handle it if the tool ignored the green background). Full prompt template + loop: recipe `recipes/cutout-graphics-for-explainers.md` (“Free manual image-gen”).
@@ -2225,7 +2315,7 @@ curl -X POST "$VIDFARM_BASE/api/v1/primitives/images/remove-background" \
2225
2315
 
2226
2316
  - `POST /api/v1/primitives/images/create-overlay` (flat alias: `POST /api/v1/primitives/create-media-overlay`)
2227
2317
  - 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.
2318
+ - 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
2319
  - `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
2320
  - The chroma-key params (`tolerance`/`softness`/`despill`) behave exactly as in `image_remove_background_greenscreen`.
2231
2321
  - 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 +2416,85 @@ curl -X POST "$VIDFARM_BASE/api/v1/primitives/audio/download" \
2326
2416
  -d '{"tracer": "demo-download-audio", "payload": {"source_url": "https://www.youtube.com/watch?v=example"}}'
2327
2417
  ```
2328
2418
 
2419
+ ## Primitive: social/recycle
2420
+
2421
+ **"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.
2422
+
2423
+ **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.
2424
+
2425
+ - `POST /api/v1/primitives/social/recycle` (alias: `POST /api/v1/primitives/recycle-social`)
2426
+ - Body: `{ "tracer": "...", "payload": { "source_url": "https://..." }, "webhook_url"?: "..." }`
2427
+
2428
+ ### The four shapes
2429
+
2430
+ | You pass | Mode (auto-detected) | You get |
2431
+ | --- | --- | --- |
2432
+ | `https://reddit.com/r/<sub>/comments/<id>/...` | `reddit_thread` | the post + its comment tree, flattened in reading order |
2433
+ | `https://reddit.com/r/<sub>` (or `r/<sub>`) | `reddit_channel` | that subreddit's threads, paginated |
2434
+ | `https://x.com/<user>/status/<id>` | `twitter_thread` | the tweet + the author's self-thread + its replies, paginated |
2435
+ | `https://x.com/<user>` (or `@<user>`) | `twitter_channel` | that account's tweets (pinned first), paginated |
2436
+
2437
+ 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.
2438
+
2439
+ ### Payload
2440
+
2441
+ | Field | Default | Notes |
2442
+ | --- | --- | --- |
2443
+ | `source_url` | — | Reddit/X URL. Required unless you pass `handle` + `network`. Aliases: `url`, `thread_url`, `profile_url` |
2444
+ | `handle` + `network` | — | `{ "handle": "wallstreetbets", "network": "reddit" }`. Aliases for handle: `username`, `screenname`, `subreddit` |
2445
+ | `mode` | auto | Override the detected shape |
2446
+ | `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** |
2447
+ | `cursor` | — | Resume token from a previous run's `nextCursor` |
2448
+ | `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 |
2449
+ | `query` | — | Restrict a channel pull to matching posts (Reddit search / X `from:<user> <query>`) |
2450
+ | `filter` / `sort` / `time_filter` | — | Reddit listing controls (`hot`, `new`, `top`, …) |
2451
+ | `search_type` | — | X search mode (`Top`, `Latest`, `Photos`, `Videos`) |
2452
+ | `include_profile` | `true` | One extra upstream job for the subreddit/account avatar, banner, bio, follower count |
2453
+ | `include_comments` | `true` | Thread modes only — set `false` to skip the replies leg (and its cost) |
2454
+ | `save_raw` | `true` | Persist the untouched upstream pages as `raw-pages.json` |
2455
+
2456
+ ### Result
2457
+
2458
+ Poll `GET /api/v1/primitives/jobs/:jobId` until `status: "succeeded"`, then read:
2459
+
2460
+ - `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.
2461
+ - `profile` — the subreddit/account itself (avatar, banner, bio, subscriber/follower count).
2462
+ - `mediaUrls[]` — every media URL flattened, ready to drop into layers or feed to `videos/download`.
2463
+ - `nextCursor` — pass it back as `payload.cursor` to page further. `truncated: true` means there IS more.
2464
+ - `counts` — `{ items, posts, comments, media, pages, upstream_jobs, credits_charged }`.
2465
+ - `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.**
2466
+ - `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.
2467
+ - `raw-pages.json` — every untouched upstream page, so you can reach fields this primitive doesn't model.
2468
+
2469
+ - 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.
2470
+ - 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.
2471
+ - 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.
2472
+ - 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.
2473
+
2474
+ ### Turning a recycle into a video
2475
+
2476
+ 1. Recycle the source with a small `max_records` (25–50 is usually plenty for one video).
2477
+ 2. Read `items[]` yourself and pick the beat: the post's hook line, the two funniest comments, the stat everyone reacted to.
2478
+ 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.
2479
+ 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.
2480
+
2481
+ Example:
2482
+
2483
+ ```bash
2484
+ curl -X POST "$VIDFARM_BASE/api/v1/primitives/social/recycle" \
2485
+ -H "vidfarm-api-key: $VIDFARM_API_KEY" \
2486
+ -H "content-type: application/json" \
2487
+ -d '{"tracer": "demo-recycle", "payload": {"source_url": "https://www.reddit.com/r/wallstreetbets/comments/p0esdp/slug/", "max_records": 50}}'
2488
+ ```
2489
+
2490
+ ```bash
2491
+ # An X account's recent posts, no replies, cheapest possible pull
2492
+ curl -X POST "$VIDFARM_BASE/api/v1/primitives/recycle-social" \
2493
+ -H "vidfarm-api-key: $VIDFARM_API_KEY" \
2494
+ -H "content-type: application/json" \
2495
+ -d '{"tracer": "demo-recycle-x", "payload": {"source_url": "https://x.com/elonmusk", "max_records": 40, "include_profile": true}}'
2496
+ ```
2497
+
2329
2498
  ## Primitive: video_remove_captions
2330
2499
 
2331
2500
  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.
@@ -2348,38 +2517,83 @@ curl -X POST "$VIDFARM_BASE/api/v1/primitives/videos/remove-captions" \
2348
2517
 
2349
2518
  ## Primitive: media_dedupe
2350
2519
 
2351
- Apply subtle camouflage transforms (zoom, tilt, rotate, saturation, playback speed, contrast, brightness, hue rotate, blur, horizontal flip, tint overlay) to any source **image or video** URL. Useful when reposting existing media and you want small perceptual differences to reduce duplicate-detection.
2520
+ **Deduplication** make a finished image or video read as a **new upload** to a social platform's duplicate-content detector, while staying invisible to a viewer. Platforms fingerprint every upload; posting the same render twice (across accounts, or again next month) gets the later copy suppressed as duplicate/reused content. This primitive nudges geometry, color, timing and grain by a couple of percent, strips container metadata, and walks the encoder's CRF, so each copy carries a distinct fingerprint.
2521
+
2522
+ **Ask the operator before you publish or bulk-produce.** Dedupe runs on the EXPORTED file, so the correct order is *render once → dedupe N times*, never *render N times*. Deciding up front avoids paying for a second render later.
2352
2523
 
2353
2524
  - `POST /api/v1/primitives/media/dedupe`
2354
2525
  - Body: `{ "tracer": "...", "payload": { ...fields... }, "webhook_url"?: "..." }`
2355
2526
  - Note: webhook delivery is not yet active — `webhook_url` is accepted and persisted on the job but never fired. Poll the job endpoints (`GET /api/v1/primitives/jobs/:jobId`) for completion.
2356
- - Response: standard primitive job. Poll to completion, then read `primary_file_url` (also `video.file_url` for MP4 or `image.file_url` for stills)
2357
- - Billing: metered as a Vidfarm render (via `hyperframes_lambda` on prod cloud render; free on a local serve box)
2527
+ - Response: standard primitive job. Poll to completion, then read `primary_file_url` (also `video.file_url` for MP4 or `image.file_url` for stills). `output.dedupe` carries the resolved effects, the effective zoom, the CRF and a human-readable `notes` list.
2528
+ - Billing: one ffmpeg pass, metered at real compute (much cheaper than a render). Free on a local serve box — and free ANYWHERE via `vidfarm dedupe --local`, which runs the identical filter graph on bundled ffmpeg.
2358
2529
 
2359
- Payload fields:
2530
+ ### Presets
2531
+
2532
+ `preset` picks a calibrated transformation set. Default `standard`.
2533
+
2534
+ | preset | skew | zoom | rotate | speed | saturation | when |
2535
+ | --- | --- | --- | --- | --- | --- | --- |
2536
+ | `none` | — | — | — | — | — | no-op; re-encode only |
2537
+ | `light` | 1% | 2% | 0.75° | +1% | +2% | lightly reused footage, or tight framing you can't crop |
2538
+ | `standard` | 2% | 3% | 2° | +2% | +4% | **the default** — the house standard |
2539
+ | `strong` | 3.5% | 6% | 3° | +5% | +8% | an Nth re-post, or an account that already ran this clip |
2540
+ | `legacy` | — | 4% | 3° | +5% | +5% | the pre-ffmpeg composition-renderer defaults |
2541
+
2542
+ `light`/`standard`/`strong` also move contrast, brightness, hue and grain. Any individual knob in `effects` overrides the preset.
2543
+
2544
+ ### Minting N distinct copies
2545
+
2546
+ `variant` (1-based) is what makes bulk posting work. Variant 1 is the preset as authored; later variants get deterministically jittered magnitudes and **alternating signs** (a sign flip moves a perceptual hash much further than a magnitude nudge), so N copies differ from the original *and from each other*. Reuse one `seed` across the batch, and post each variant to a different account/slot.
2547
+
2548
+ ### Payload fields
2360
2549
 
2361
2550
  - `source_media_url` (required, URL) — the image or video to transform
2362
2551
  - `media_type` (`"image" | "video"`, optional) — auto-detected from URL extension if omitted (`.mp4/.mov/.webm/.m4v` → video, else image)
2363
- - `effects` (optional object). All fields optional; defaults camouflage lightly:
2364
- - `zoom` (default `1.04`) — scale factor
2365
- - `tilt` (default `3`) — degrees of 3D X-axis tilt (perspective 1000px)
2366
- - `rotate` (default `3`) — degrees of 2D rotation
2367
- - `saturation` (default `1.05`)
2368
- - `speed` (default `1.05`, video only) playback rate, also compresses output duration accordingly
2369
- - `horizontal_flip` (default `false`)
2370
- - `contrast` (default `1.05`)
2371
- - `brightness` (default `1.05`)
2372
- - `hue_rotate` (default `0`) degrees
2373
- - `blur` (default `0`)pixels
2374
- - `tint_color` (default `"#FF8C00"`), `tint_opacity` (default `0.08`) subtle color overlay
2375
- - `width` (default `1080`), `height` (default `1920`) output canvas
2376
- - `duration_ms` — output duration for video; if omitted, `fallback_duration_ms` (default `5000`) is used
2377
- - `object_fit` (`"cover" | "contain" | "fill" | "none" | "scale-down"`, default `"cover"`)
2378
- - `background_color` (default `"#000000"`) — visible when `object_fit` leaves letterboxing
2379
- - `muted` (default `false`), `volume` (default `1`) — audio pass-through on video
2552
+ - `preset` (`"none" | "light" | "standard" | "strong" | "legacy"`, default `"standard"`)
2553
+ - `engine` (`"ffmpeg" | "composition"`, default `"ffmpeg"`) — `ffmpeg` is a real pixel/timing transform on the source file (true shear, honest playback speed, grain, metadata strip) and is both cheaper and stronger. `composition` is the legacy HyperFrames-render path, kept only for callers that depend on its exact output.
2554
+ - `variant` (int ≥ 1, default `1`), `seed` (string, optional), `jitter` (bool, optional defaults on for `variant > 1`)
2555
+ - `strip_metadata` (default `true`) — drop creation time / encoder / source handler. Several platforms compare that **before** they compare pixels.
2556
+ - `effects` (optional object). Every field optional; each one overrides the preset:
2557
+ - `zoom` — scale factor, centre-cropped back (`1.03` = 3% punch-in)
2558
+ - `skew` — horizontal shear as a **percent of frame width** (ffmpeg engine only)
2559
+ - `rotate` degrees of 2D rotation
2560
+ - `tilt` degrees of 3D X-axis tilt on the composition engine; folded into the shear budget on ffmpeg
2561
+ - `speed` playback multiplier, video only; changes duration **and** pitch-preserved audio tempo
2562
+ - `saturation`, `contrast`, `brightness` — multipliers around `1`
2563
+ - `hue_rotate` — degrees
2564
+ - `noise` film grain `0..100` (ffmpeg engine only). Cheap, invisible, moves a lot of hash.
2565
+ - `blur` — gaussian sigma in px. Usually `0` blur is the one knob viewers notice.
2566
+ - `volume` audio gain multiplier
2567
+ - `horizontal_flip` (default `false`) — the strongest single knob, but it visibly reverses on-screen text. Opt in deliberately.
2568
+ - `tint_color` (default `"#FF8C00"`), `tint_opacity` (default `0.08`) — flat color wash; set opacity `0` to skip
2569
+ - `width` / `height` — **optional on the ffmpeg engine**; omit to keep the source's own frame size (forcing 1080×1920 onto a 16:9 source would squash it). The composition engine falls back to 1080×1920.
2570
+ - `crf` — base x264 quality; jittered ±1 per variant so the coded bitstream differs too
2380
2571
  - `output_format` (`"png" | "jpeg" | "webp"`, default `"png"`) — image mode only; video mode always outputs MP4
2572
+ - Composition-engine only: `duration_ms` / `fallback_duration_ms` (default `5000`), `object_fit`, `object_position`, `background_color`, `muted`
2573
+
2574
+ **A rotate forces a bigger crop than you asked for.** Black corners have to go somewhere, so the primitive raises `zoom` to the smallest value that covers the rotation and says so in `output.dedupe.notes`. On a tall 1080×1920 frame a 2° rotate costs ~6.7% of the frame. If framing matters more than fingerprint distance, pass `effects.rotate: 0`.
2575
+
2576
+ Video example — three copies of one render, one per account:
2577
+
2578
+ ```bash
2579
+ for V in 1 2 3; do
2580
+ curl -X POST "$VIDFARM_BASE/api/v1/primitives/media/dedupe" \
2581
+ -H "vidfarm-api-key: $VIDFARM_API_KEY" \
2582
+ -H "content-type: application/json" \
2583
+ -d "{
2584
+ \"tracer\": \"dedupe-launch-reel-v$V\",
2585
+ \"payload\": {
2586
+ \"source_media_url\": \"https://cdn.example.com/reel.mp4\",
2587
+ \"media_type\": \"video\",
2588
+ \"preset\": \"standard\",
2589
+ \"variant\": $V,
2590
+ \"seed\": \"launch-reel\"
2591
+ }
2592
+ }"
2593
+ done
2594
+ ```
2381
2595
 
2382
- Video example (camouflage a reused clip):
2596
+ Custom knobs (keep the framing, lean on color and timing instead):
2383
2597
 
2384
2598
  ```bash
2385
2599
  curl -X POST "$VIDFARM_BASE/api/v1/primitives/media/dedupe" \
@@ -2390,15 +2604,7 @@ curl -X POST "$VIDFARM_BASE/api/v1/primitives/media/dedupe" \
2390
2604
  "payload": {
2391
2605
  "source_media_url": "https://cdn.example.com/reel.mp4",
2392
2606
  "media_type": "video",
2393
- "duration_ms": 8500,
2394
- "effects": {
2395
- "zoom": 1.05,
2396
- "tilt": 2,
2397
- "rotate": -2,
2398
- "speed": 1.03,
2399
- "hue_rotate": 4,
2400
- "horizontal_flip": true
2401
- },
2607
+ "effects": { "rotate": 0, "skew": 1.5, "zoom": 1.02, "speed": 1.03, "hue_rotate": 6, "noise": 2 },
2402
2608
  "tint_color": "#00A3FF",
2403
2609
  "tint_opacity": 0.06
2404
2610
  }
@@ -2416,12 +2622,18 @@ curl -X POST "$VIDFARM_BASE/api/v1/primitives/media/dedupe" \
2416
2622
  "payload": {
2417
2623
  "source_media_url": "https://cdn.example.com/photo.jpg",
2418
2624
  "media_type": "image",
2419
- "effects": { "zoom": 1.06, "rotate": -1, "hue_rotate": 8 },
2625
+ "preset": "light",
2420
2626
  "output_format": "webp"
2421
2627
  }
2422
2628
  }'
2423
2629
  ```
2424
2630
 
2631
+ Local equivalent (free, offline, identical transforms — prefer this):
2632
+
2633
+ ```bash
2634
+ vidfarm dedupe ./out/final.mp4 --variants 3 --out-dir ./out/posts
2635
+ ```
2636
+
2425
2637
  ## Primitive: music (text → music)
2426
2638
 
2427
2639
  Generate music (instrumental, songs with lyrics, background beds, jingles, scores) via ElevenLabs. **Default this on freely — music is a core primitive.** `use_wallet_credits` defaults **true**: it runs on vidfarm's platform ElevenLabs key and bills the customer's wallet. Recommend keeping it on; set it false only to save wallet credits or to use the customer's OWN saved ElevenLabs key.
@@ -2588,7 +2800,8 @@ Use this when a coding agent is doing the work locally or the user wants a repro
2588
2800
  5. Validate with `vidfarm lint` or `vidfarm stills` when useful. **Always look at `vidfarm stills ./work --at 0`** — that frame becomes the thumbnail, so it must not be black, empty, or mid-fade.
2589
2801
  6. **QA before you render: `vidfarm qa ./work`.** Free, instant, devcli-only. It blocklists HTML slop (CTA buttons, benefit chip rows, frosted cards, gradient text, web-page classes/fonts), checks the caption font regime + safe zone, and flags a blank/fading first frame (the thumbnail). Feedback only — exit 0 even on findings, never automatic — but it catches the #1 tell of an agent-made video, so run it on every production. Fix what's real, ignore what's a deliberate style call, then render.
2590
2802
  7. Render with `vidfarm render <forkId> --dir ./work --wait`.
2591
- 8. Approve the finished MP4 with `vidfarm approve --video <url|./final.mp4> --caption "..."`. This prints the shareable `share_url`.
2803
+ 8. **Ask about deduplication before you approve** — "is this going out more than once (several accounts, another platform, a re-post later)?" If yes, run `vidfarm dedupe ./final.mp4 [--variants N]` on the **exported** MP4 (free, local ffmpeg, no re-render) and approve each variant separately. Asking here rather than after publication is what avoids paying for a second render. See `references/core-workflows.md` → *Deduplicate before you publish*.
2804
+ 9. Approve the finished MP4 with `vidfarm approve --video <url|./final.mp4> --caption "..."`. This prints the shareable `share_url`.
2592
2805
 
2593
2806
  **Approving a locally rendered file → cloud preview link.** Approve takes media by **URL**, not bytes, and an approved post is **permanent** — so the local MP4 must land in **durable My Files**, not the 30-day temp store (a temp video would 404 the share page after 30 days). The devcli presigns, PUTs the bytes **direct to S3**, finalizes, then approves with that durable URL — so `vidfarm approve --video ./final.mp4` handles files up to **200 MB**, bypasses the ~6 MB Lambda request-body limit, and the share link never breaks. By raw REST: `POST /api/v1/user/me/attachments/presign` → PUT to the returned S3 URL → `POST /api/v1/user/me/attachments` (finalize) → pass the returned `viewUrl` in the approve `media` array. Do not multipart-POST a big file to `.../attachments/upload` against the cloud host (Lambda-bound, ~6 MB cap). Add `vidfarm approve --temp` only when you want a disposable 30-day preview.
2594
2807
 
@@ -2648,6 +2861,18 @@ done
2648
2861
 
2649
2862
  `vidfarm qa` still exits 0 on findings — the `jq -e` line is **your** gate, in your script, made explicit. Keep it that way; a hard gate inside the tool would quietly train the loop to stop trying anything unusual.
2650
2863
 
2864
+ ### 5b. Deduplicate the renders (ask first)
2865
+
2866
+ A bulk run exists to put volume across accounts and platforms — which is exactly the shape a platform's duplicate-content detector flags. **Ask the director up front: "deduplicated copies for posting, and how many slots?"** Ask before the loop, not after: dedupe is a post-render ffmpeg pass, so answering early keeps it at *render once → dedupe N* rather than a second render per slot.
2867
+
2868
+ ```bash
2869
+ for MP4 in renders/*.mp4; do
2870
+ vidfarm dedupe "$MP4" --variants "$SLOTS" --seed "$(basename "$MP4" .mp4)" --out-dir ./posts
2871
+ done
2872
+ ```
2873
+
2874
+ Free, offline, no wallet. Variant 1 is the `standard` preset as authored (skew 2%, zoom 3%, rotate 2°, speed +2%, saturation +4%); later variants get jittered magnitudes and flipped signs, so they differ from the original **and from each other**. One variant per account — two accounts posting the same variant defeats the point. Reuse one `--seed` per source so the batch is reproducible.
2875
+
2651
2876
  ### 6. Answer the review items — don't skip this
2652
2877
 
2653
2878
  The regime's `- [ ]` checklist comes back on every run because the CLI *can't* settle it. Machine checks catch a 13-word hook or a black first frame; only you can answer "is this variant genuinely different from its siblings?" or "can the viewer guess the withheld answer?" **Report both halves honestly**: what the machine checked, and what you judged. A batch report claiming a clean pass on the judgment half is worse than no report.
@@ -2680,7 +2905,7 @@ The mechanical trio — **generate on a chroma plate → key it out → trim to
2680
2905
  - **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
2906
  - **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
2907
 
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"`.
2908
+ **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
2909
 
2685
2910
  **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
2911
 
@@ -2725,6 +2950,20 @@ Out comes `./stickers/sticker-01-red-barn.png`, `sticker-02-tractor.png`, … ea
2725
2950
 
2726
2951
  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
2952
 
2953
+ **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.
2954
+
2955
+ None of that is fixable after generation — you cannot key back pixels that were never drawn. So it belongs in the **prompt**, every time:
2956
+
2957
+ > 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.
2958
+
2959
+ `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.
2960
+
2961
+ **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:
2962
+
2963
+ - **Re-generate the sheet** with the fill clause above spelled out. This is the fix ~90% of the time.
2964
+ - 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.
2965
+ - 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.
2966
+
2728
2967
  **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
2968
 
2730
2969
  ### Stickers are not necessarily small — and they usually move
@@ -2841,15 +3080,15 @@ Everything here is **local and free** — crop, matte/key, and trim are all `ffm
2841
3080
 
2842
3081
  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
3082
  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.
3083
+ > 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
3084
 
2846
- **No text in the image, ever** — rendered text never keys or scales cleanly; add copy as a caption or HTML layer instead.
3085
+ **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
3086
  3. **Bring the sheet in:** `vidfarm put-file ./sheet.png` (or drag into **My Files** in the web app).
2848
3087
  4. **Split it into stickers — one command, all free:**
2849
3088
  ```
2850
3089
  vidfarm sticker-pack ./sheet.png --items "<object one>,<object two>,…" --out-dir ./stickers
2851
3090
  ```
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:
3091
+ 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
3092
  ```
2854
3093
  vidfarm mask ./sheet.png --crop 4%,4%,29%,45% --flat "#00FF00" --out prop-a.png
2855
3094
  ```