@officexapp/vidfarm-devcli 0.21.31 → 0.21.33

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.
@@ -105,11 +105,12 @@ Full rationale + the devcli twins (`vidfarm stills --at 0`, `vidfarm qa`) in `vi
105
105
 
106
106
  You author into HTML, which makes it dangerously easy to build a **web page instead of a video**. This is the #1 way an AI-edited composition betrays itself. Apply on every text/graphic you place — and strip it when a decomposed fork or a pasted brand asset brings one in.
107
107
 
108
- **The test:** *would this element exist if the video were shot on a phone and captioned in CapCut?* If its whole job is to look **clickable**, cut it. Nothing in a video is clickable.
108
+ **The test — the native-editor test:** *could you have made this element with the tools inside TikTok's own editor?* Its text tool gives you a font, a color, a stroke/outline, a soft shadow, a tight text box, alignment, opacity, rotation, and animation presets plus stickers, emoji, drawn marks, and clips. It gives you **no padded capsule, no border, no gradient fill, no blur panel, no card**. If you reached past that toolset, you are decorating like a web designer and the frame will read as machine-made. Second half of the test: if the element's whole job is to look **clickable**, cut it. Nothing in a video is clickable.
109
109
 
110
110
  **BANNED (never `add_layer` / `replace_composition_html` these):**
111
111
  - **CTA buttons** — a filled/gradient capsule with action copy ("Sign Up for a Free Trial →", "Get Started", "Book a Call"), glow or drop shadow. A social CTA is *spoken* or a plain caption line.
112
- - **Badge / chip / pill rows** — "✓ ID-Verified · ✓ No Credit Card Needed · ✓ 30-Min Trial". Say them as three *timed caption lines* on the footage instead.
112
+ - **Badges / chips / pillsa row of them, and equally a SINGLE one.** The strip "✓ ID-Verified · ✓ No Credit Card Needed · ✓ 30-Min Trial" is obvious; the common miss is one lonely capsule around a stat or label — `( 10 hrs / week )`, `( STEP 2 )`, `( EP.01 )`, `( +40% )`. Alone ≠ native: a rounded, padded, filled tag on static text is a web badge. **The only legitimate pill is the active-word `spotlight`/`karaoke` highlight**, because it moves with the spoken word. Emphasize a stat the way the editor would — bigger, heavier, ALL-CAPS, an accent color, a drawn circle, or its own beat — and say benefits as *timed caption lines* on the footage, one at a time.
113
+ - **Rule of thumb:** on anything holding words, `border-radius` > ~8px **plus** a background fill **plus** padding = a badge. Drop the fill, or drop the radius until the band hugs the glyphs.
113
114
  - **Cards / panels** — a bordered, shadowed, or `backdrop-filter`-frosted rounded box holding a headline + subheading/URL. Text goes ON the footage, not in a floating panel.
114
115
  - **Gradient text fills, neon border glows, elevation shadows, glassmorphism**, navbars, hero sections, feature grids, `<ul>` bullet lists, tables, "as seen in" strips.
115
116
  - **Web-default type** — Inter/Roboto/system-ui/Arial/Helvetica at weight 400–600 and 16–24px.
@@ -121,7 +122,7 @@ You author into HTML, which makes it dangerously easy to build a **web page inst
121
122
  **Font + background regime (every caption/title, via `set_captions` / `set_layer_style` / `add_layer`):**
122
123
  - **Font:** Montserrat (default) or TikTok Sans / Abel / Source Code Pro / Yesteryear — a family the composition actually imports, or it silently falls back to the slop sans. Weight **700–900**. `font_size` in px of the render canvas: **~36–64px** on a 1080-wide frame; never <28, never 0 (invisible). ~2 lines, ~5 words per line; `line_height` 0.95–1.15.
123
124
  - **Position:** inside the **8%–85%** vertical safe zone (phone UI clips the edges) and clear of the right ~12% action rail — a centered box at `x:10 width:80` is safe. Lower-third ≈ `y:70`; a "POV:" top line ≈ `y:8`, never `y:0`.
124
- - **Background — exactly one of four:** `background_style:"outline"` (stroke, the default look) · `"plain"` (bare + soft shadow) · an **active-word highlight pill** via `set_captions caption_style:"spotlight"|"karaoke"` (the *only* legitimate pill — it tracks the spoken word) · `"highlight-solid"`/`"highlight-translucent"` as a band that **hugs** the text (radius ≤~8px, no border, no shadow, no gradient, no blur, one text run — never a heading+subheading+URL stacked inside it). Anything else is a web card.
125
+ - **Background — exactly one of four:** `background_style:"outline"` (stroke, the default look) · `"plain"` (bare + soft shadow) · an **active-word highlight pill** via `set_captions caption_style:"spotlight"|"karaoke"` (the *only* legitimate pill anywhere in the frame — it tracks the spoken word; a static label never gets one) · `"highlight-solid"`/`"highlight-translucent"` as a band that **hugs** the text (radius ≤~8px, no border, no shadow, no gradient, no blur, one text run — never a heading+subheading+URL stacked inside it). Anything else is a web card.
125
126
 
126
127
  **There is no QA tool for you.** The devcli ships `vidfarm qa <dir>` — a free local blocklist pass over exactly the rules above — but it is **devcli-only with no REST twin**, so in the web editor you enforce this by reading your own output. When you hand a heavy job off to a local coding agent, tell them to run `vidfarm qa ./work` before rendering.
127
128
 
@@ -269,13 +269,13 @@ You may be running as the **in-web AI chat** (the /editor copilot, the chat dock
269
269
 
270
270
  - **Determine the surface before claiming capabilities.** The web chat has only its declared tools and REST routes. It cannot execute arbitrary JavaScript/Python, open a shell, create a local repository script, or use the user's filesystem. Never tell a web-chat user that you ran code or wrote a script unless a dedicated declared tool actually did so. A desktop coding agent has a real shell and filesystem and MAY write/run scripts, perform arbitrary local computations over paginated API results, create reports/CSVs/JSON, edit composition files, and orchestrate long devcli workflows within the user's authorization.
271
271
  - **The web AI chat can do all three paintbrushes** — clip raws, author HTML/hyperframe motion, and generate AI media — and it drives edits directly on the live timeline. Keep small-to-medium jobs here: text/caption swaps, a scene or two replaced, single generations, captions, approve/schedule. Just do them.
272
- - **No HTML slop — a video is not a web page.** Compositions are authored in HTML, so the #1 tell of an AI-edited video is landing-page furniture: gradient CTA "buttons" ("Sign Up for a Free Trial →"), rows of benefit chips/badges ("✓ No Credit Card Needed"), frosted/bordered cards holding a gradient headline + a URL, feature grids, bullet lists, web-default type (Inter/Roboto/Arial at weight 400-600). None of that exists in a real TikTok/Reel — **nothing in a video is clickable**. If you are typing `btn` / `badge` / `chip` / `card` / `rounded-full` / `shadow-lg` / `backdrop-blur` / `bg-gradient-to-r`, stop and rewrite it as timed text on the footage. Arrows, scribble/underline marks, italics, ALL-CAPS, single-word color pops, emoji, transparent cut-out stickers, and mock social UI (iMessage bubbles, comment cards) are all *fine* — they're native to the platform.
272
+ - **No HTML slop — a video is not a web page.** Compositions are authored in HTML, so the #1 tell of an AI-edited video is landing-page furniture: gradient CTA "buttons" ("Sign Up for a Free Trial →"), rows of benefit chips/badges ("✓ No Credit Card Needed"), frosted/bordered cards holding a gradient headline + a URL, feature grids, bullet lists, web-default type (Inter/Roboto/Arial at weight 400-600). None of that exists in a real TikTok/Reel — **nothing in a video is clickable**. **The test is the native-editor test: could you have made this element with the tools inside TikTok's own editor?** That toolset is font / color / stroke / shadow / tight text box / alignment / rotation / animation presets, plus stickers, emoji, drawn marks and clips — it has no padded capsule, no border, no gradient fill, no blur panel, no card. If you reached past it, cut it. That includes **a single lonely pill around a stat or label** — `( 10 hrs / week )`, `( STEP 2 )`, `( EP.01 )`: being alone doesn't make a badge native, and the only legitimate capsule in a video is the active-word `spotlight`/`karaoke` highlight, which moves with the spoken word. Emphasize a stat the way the editor would instead: bigger, heavier, ALL-CAPS, an accent color, a hand-drawn circle, or its own beat. If you are typing `btn` / `badge` / `chip` / `card` / `rounded-full` / `shadow-lg` / `backdrop-blur` / `bg-gradient-to-r` — or a `border-radius` over ~8px on anything filled that holds words — stop and rewrite it as timed text on the footage. Arrows, scribble/underline marks, italics, ALL-CAPS, single-word color pops, emoji, transparent cut-out stickers, and mock social UI (iMessage bubbles, comment cards) are all *fine* — they're native to the platform.
273
273
  - **Every video gets the four charges — hook, loop, payoff, bait — and you write them BEFORE you touch the timeline.** This is the largest quality delta in the product and it costs nothing: most agent-made videos fail on structure, not polish, because the timeline is the fun part so it gets built first and the words get retrofitted. Invert it: (1) write the **hook line as text** — a complete clause, subject + verb, no jargon, naming a **situation** ("I've quit six businesses"), never a label ("anonymity") — and put it on screen at `start:0`; (2) name the **curiosity loop** and the timestamp it closes at, *inside this video* (if you can't state the timestamp, there is no loop, and the withheld answer must be one the viewer **can't guess**); (3) name the **payoff** — shown, not summarized, landing before the final beat; (4) write the **bait** — one ask in the final beat and in the post caption. Then build. Chunk 1 is read before any audio (muted autoplay is the default), so the text hook does more work than the spoken one. Full harness — the three gates, banned openers, loop mechanics, compliance, and diagnosis-by-charge — in `references/hooks-and-virality.md`; the checkable form is `vidfarm regime show hooks`.
274
274
  - **The first frame IS the thumbnail — compose it on purpose.** Frame 0 is a single frame of ~30 in the first second, but it's the poster every feed, share link, and paused player freezes on, so **more people see that one frame than watch the video**. It must never be black, empty, mid-fade, or caught mid-animation: put a real visual at `start:0`, have the hook words already on screen at t=0, and never hang a `fade-black`/`fade-white`/`flash` *entrance* on the **first** clip (junction transitions between later clips are fine — this rule is only about the opening). Verify it, don't assume: devcli `vidfarm stills ./work --at 0` renders that exact frame, and `vidfarm qa` flags a blank or fading open.
275
275
  - **Ask early: one-time video, or bulk?** "Make me a video about X" and "I need to post daily / give me 20 hook variants" are different jobs, and directors often don't know the second one has a name. Ask once, up front: *"One video, or should we set this up as a repeatable batch?"* Bulk = **scripting mode** (a pinned base fork + a loop that varies ONE thing per variant; a public-raws shelf is the cheapest source of the N), and every batch gets a **`QA_REGIME.md`** — because a loop of fifty videos has no human looking at every frame, and the regime is what replaces those eyes. Don't silently ship a one-off when they asked for volume, or drag someone into a harness when they wanted one clip.
276
276
  - **`QA_REGIME.md` is the director's own quality contract, and it's a first-class artifact.** `vidfarm qa`'s built-ins are universal (slop, fonts, the thumbnail frame); a regime is what makes *this* format good — audience, hook shape, banned vocabulary, pacing, compliance line. It lives next to the work, they own it, it stacks: `vidfarm regime init short-form --out ./work/QA_REGIME.md` (bundled bases: `short-form`, `hooks`, `ugc-testimonial`, `explainer`, `product-demo` — each a starting point to **edit**, never a house style), then `vidfarm qa ./work --regime hooks --regime ./brand/HOUSE.md`, and any user file anywhere is valid. Its `checks:` front matter is machine-settled; its `- [ ]` checklist comes back as **review items you answer honestly in your report** — never claim a video passed the half the CLI can't judge. When a batch teaches you something, **write it back into the regime**: that's the artifact that compounds. Details in `references/automation-and-local-dev.md` ("Scripting mode"), format in `regimes/README.md`.
277
277
  - **On devcli, QA every video you produce: `vidfarm qa ./work`.** Free, instant, local-only — it blocklists exactly the slop above plus first-frame/thumbnail and font-regime/safe-zone drift, and prints a concrete fix per finding. **Feedback, not a gate**: it exits 0 even on findings, never runs automatically, and is a blocklist (unusual/stylized compositions pass untouched), so there's no reason not to run it before every publish. `--json` for scripted batches, `--strict` only if you want a CI failure. **Web-chat copilot: this command does not exist for you** (devcli-only, no REST twin) — apply the standard by hand, and when handing a heavy job to a local coding agent, tell them to run `vidfarm qa`.
278
- - **Every production adheres to the TikTok-native caption standard.** On-screen text lives inside the readable safe zone (**~8%–85%** of a 9:16 frame — never pinned to the top/bottom edges the phone UI clips), uses the composition's bold font regime (**Montserrat** default / TikTok Sans, weight **700–900**, ~36–64px on a 1080-wide frame), and uses **exactly one of four valid backgrounds**: outline/stroke (`background_style:"outline"`, the default), plain + shadow (`"plain"`), an active-word highlight pill (`set_captions` `spotlight`/`karaoke` — the only legitimate pill), or a tight-hugging solid band (`"highlight-solid"`, radius ≤8px, no border/shadow/gradient/blur). Decomposed forks often inherit the source's edge-pinned caption in an off-regime font — fix it, don't inherit it. Local devcli renders auto-normalize position + font family only (never the slop), so author it correctly. Full rules in `references/editor-workflows.md` ("Social-native visual standard" + "TikTok-native caption standard").
278
+ - **Every production adheres to the TikTok-native caption standard.** On-screen text lives inside the readable safe zone (**~8%–85%** of a 9:16 frame — never pinned to the top/bottom edges the phone UI clips), uses the composition's bold font regime (**Montserrat** default / TikTok Sans, weight **700–900**, ~36–64px on a 1080-wide frame), and uses **exactly one of four valid backgrounds**: outline/stroke (`background_style:"outline"`, the default), plain + shadow (`"plain"`), an active-word highlight pill (`set_captions` `spotlight`/`karaoke` — the only legitimate pill *in the whole frame*, static labels included), or a tight-hugging solid band (`"highlight-solid"`, radius ≤8px, no border/shadow/gradient/blur). Decomposed forks often inherit the source's edge-pinned caption in an off-regime font — fix it, don't inherit it. Local devcli renders auto-normalize position + font family only (never the slop), so author it correctly. Full rules in `references/editor-workflows.md` ("Social-native visual standard" + "TikTok-native caption standard").
279
279
  - **Where the web chat struggles: complex, long, multi-step transformations.** A full multi-scene re-theme, an iterative render-critique-iterate loop, heavy scripted or batch work, or anything needing a real filesystem and many sequential tool calls will hit context limits, turn/timeout ceilings, and the web editor's constraints (CSS/declarative motion only — JS animation adapters are stripped on save). Don't grind a big transformation one layer at a time in a chat turn and stall.
280
280
  - **Practical workaround — hand the heavy job to local devcli.** When a task is genuinely large or long-running, **proactively recommend the director run it locally with an AI coding agent** (Claude Code / OpenAI Codex / any capable agent): `vidfarm pull <forkId>` writes the composition + the `.harness/` grounding bundle to disk, the agent edits with the full devcli verb set and JS animation adapters, renders free with `vidfarm serve`, and `vidfarm publish` pushes it back. This is the **best-quality (B) harness's** natural home (adversarial grading with a coding agent). Frame it as "this is a big rebuild — you'll get a better, faster result running it locally with a coding agent; here's how," not as a dead end.
281
281
  - **Offer a handoff, do not impersonate the desktop agent.** When web chat reaches that boundary, offer to save a Markdown handoff in My Files containing the objective, selected template/fork IDs, asset paths, grounding, constraints, completed work, and suggested devcli commands. Create it only after the user agrees. The desktop agent should read that document, pull the referenced fork, and then use its actual code/shell capabilities.
@@ -319,6 +319,7 @@ Choose the narrowest path that satisfies the request.
319
319
  4. If the task is “find footage” or “use our existing assets,” read `references/assets-and-sourcing.md`.
320
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
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*.
322
323
  5. If the task is scripted, local, CI-driven, or `vidfarm serve`-based, read `references/automation-and-local-dev.md`.
323
324
  6. If the task explicitly asks for a primitive or needs specialized generation/transcription work, read `references/primitives.md`.
324
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`.
@@ -52,6 +52,18 @@ done
52
52
 
53
53
  `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.
54
54
 
55
+ ### 5b. Deduplicate the renders (ask first)
56
+
57
+ 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.
58
+
59
+ ```bash
60
+ for MP4 in renders/*.mp4; do
61
+ vidfarm dedupe "$MP4" --variants "$SLOTS" --seed "$(basename "$MP4" .mp4)" --out-dir ./posts
62
+ done
63
+ ```
64
+
65
+ 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.
66
+
55
67
  ### 6. Answer the review items — don't skip this
56
68
 
57
69
  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.
@@ -7,9 +7,10 @@ Use this when a coding agent is doing the work locally or the user wants a repro
7
7
  3. Read `./work/.harness/agent-guide.md` and `./work/.harness/context.json` before editing.
8
8
  4. Make deterministic edits to `composition.html` and optionally `composition.json`.
9
9
  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.
10
- 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.
10
+ 6. **QA before you render: `vidfarm qa ./work`.** Free, instant, devcli-only. It blocklists HTML slop (CTA buttons, benefit chip rows, a lone pill around a static stat/label, 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.
11
11
  7. Render with `vidfarm render <forkId> --dir ./work --wait`.
12
- 8. Approve the finished MP4 with `vidfarm approve --video <url|./final.mp4> --caption "..."`. This prints the shareable `share_url`.
12
+ 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*.
13
+ 9. Approve the finished MP4 with `vidfarm approve --video <url|./final.mp4> --caption "..."`. This prints the shareable `share_url`.
13
14
 
14
15
  **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.
15
16
 
@@ -78,6 +78,17 @@ for VARIANT in "${VARIANTS[@]}"; do
78
78
  done
79
79
  ```
80
80
 
81
+ **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:
82
+
83
+ ```bash
84
+ # after the loop: one distinct copy per posting slot, free and offline
85
+ for MP4 in renders/*.mp4; do
86
+ vidfarm dedupe "$MP4" --variants "$SLOTS" --seed "$(basename "$MP4" .mp4)" --out-dir ./posts
87
+ done
88
+ ```
89
+
90
+ 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*.
91
+
81
92
  `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.
82
93
 
83
94
  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.
@@ -214,6 +225,7 @@ The licensed harness also carries the **generative build workflow** guidance (ch
214
225
  | `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
226
  | `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
227
  | `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`. |
228
+ | `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`. |
217
229
  | `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
230
  | `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
231
  | `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". |
@@ -308,6 +320,7 @@ What it flags:
308
320
  |---|---|---|
309
321
  | `cta-button` | error | Action copy ("Sign Up for a Free Trial →") **inside** a filled/gradient rounded capsule. Bare CTA copy in a caption is fine — "BUY NOW" is real social copy |
310
322
  | `badge-chip-row` | error | 2+ sibling small rounded filled tags — the "✓ No Credit Card Needed · ✓ 30-Min Trial" strip |
323
+ | `static-pill` | error | ONE filled, padded, ≥20px-radius capsule around static text — a stat/label badge like "10 hrs / week", "STEP 2", "EP.01". Skips active-word `spotlight`/`karaoke` highlights (the only legitimate pill) and mock social UI (chat bubbles, comment cards — mark yours `data-vf-mock-ui` if the heuristic misses it) |
311
324
  | `card-panel` / `glass-card` | error | A rounded box with a border/shadow/`backdrop-filter` wrapping 2+ elements. A tight legibility **band** (radius ≤8px, one text run, no border/shadow) stays legal |
312
325
  | `gradient-text` | error | `background-clip:text` gradient headline fills |
313
326
  | `clickable-element` | error/warn | `<button>`, `<form>`, `<input>`; `<a href>` warns |
@@ -337,7 +350,7 @@ The four modes, quoted as **cost per finished video**. The first two are spend p
337
350
 
338
351
  **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.
339
352
 
340
- `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.
353
+ `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.
341
354
 
342
355
  - **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.
343
356
  - **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”).
@@ -137,6 +137,25 @@ Every publish creates an immutable version snapshot at `versions/<N>/composition
137
137
 
138
138
  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.
139
139
 
140
+ ## Deduplicate before you publish (ASK FIRST)
141
+
142
+ 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.
143
+
144
+ **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.
145
+
146
+ ```bash
147
+ # free, offline, no wallet, no re-render — the default path
148
+ vidfarm dedupe ./out/final.mp4 # one distinct copy
149
+ vidfarm dedupe ./out/final.mp4 --variants 3 --out-dir ./out/posts
150
+ ```
151
+
152
+ - 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.
153
+ - `--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.
154
+ - `--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.
155
+ - Cloud equivalent: `POST /api/v1/primitives/media/dedupe` — same transforms, billed. See `references/primitives.md` → *Primitive: media_dedupe*.
156
+
157
+ Dedupe the **finished MP4**, then approve/schedule each variant separately. Do not dedupe the composition and re-render.
158
+
140
159
  ## Approve a finished post
141
160
 
142
161
  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.
@@ -158,21 +177,59 @@ devcli: `vidfarm approve --video <mp4-url> --caption "..."` prints the `share_ur
158
177
 
159
178
  ## Schedule a post
160
179
 
161
- Schedule an approved post to a connected destination channel (FlockPoster social account or email) at one ISO timestamp:
180
+ A **destination** is one connected channel. There are exactly two kinds, and they are billed and owned differently:
181
+
182
+ - **`email`** — a verified email address on the vidfarm account. Delivered by vidfarm itself. Your vidfarm API key is the only credential involved.
183
+ - **`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.
184
+
185
+ 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.
186
+
187
+ **List destinations first** (or just send an address — see below):
188
+
189
+ ```
190
+ GET /api/v1/user/me/channels
191
+ → { "channels": [ { "destination_type": "email", "destination_id": "cus_…:default-email",
192
+ "handle": "operator@example.com", "status": "verified", "schedulable": true,
193
+ "accepts": ["cus_…:default-email", "operator@example.com", "operator"] } ],
194
+ "flockposter_connected": false, "flockposter_error": null }
195
+ ```
196
+
197
+ Then schedule:
162
198
 
163
199
  ```
164
200
  POST /api/v1/approved/posts/:postId/schedules
165
201
  Content-Type: application/json
166
202
 
167
- { "destination_type": "flockposter" | "email", "destination_id": "<channel or email>",
203
+ { "destination_type": "flockposter" | "email", "destination_id": "<see below>",
168
204
  "scheduled_at": "2026-07-10T14:00:00Z", "timezone": "America/New_York", "additional_notes": "optional" }
169
205
  ```
170
206
 
171
- Minimum 10-minute lead time. Response (`201`) is the schedule record. Browse existing schedules with `GET /api/v1/approved/posts/:postId/schedules`.
207
+ **`destination_id` accepts whatever you have** it is resolved server-side, so you do not need to look up an id:
208
+
209
+ | For `email` | For `flockposter` |
210
+ |---|---|
211
+ | the channel id (`cus_…:default-email`, or `email:<id>`) | the integration id |
212
+ | the address (`operator@example.com`) | the handle (`@brandname` or `brandname`) |
213
+ | the local part (`operator`) | the channel title |
214
+ | the channel title | the platform (`tiktok`) when exactly one is connected |
215
+
216
+ 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.
217
+
218
+ Minimum 10-minute lead time. Response (`201`) is the schedule record.
219
+
220
+ **Managing a schedule** (all authed with the same vidfarm API key — an agent has full control):
221
+
222
+ - `GET /api/v1/approved/posts/:postId/schedules` — browse this post's schedules
223
+ - `PATCH /api/v1/approved/posts/:postId/schedules/:scheduleId` — reschedule; same body as POST. Cancels the queued send and re-queues it.
224
+ - `DELETE /api/v1/approved/posts/:postId/schedules/:scheduleId` — cancel the queued send
225
+
226
+ 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.
227
+
228
+ Schedules created outside vidfarm (`managed_by: "external"`) are read-only here and return `409`; change those in FlockPoster.
172
229
 
173
- devcli: `vidfarm schedule <postId> --at <iso> --to <destinationId> [--type flockposter|email]`, and `vidfarm schedules <postId>` to browse.
230
+ devcli: `vidfarm channels` lists destinations, `vidfarm schedule <postId> --at <iso> --to <destination> [--type flockposter|email]` schedules, `vidfarm schedules <postId>` browses.
174
231
 
175
- 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`).
232
+ 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`).
176
233
 
177
234
  ## Marketplace — order videos from specialist agents
178
235
 
@@ -496,12 +496,12 @@ Web copilot: same standard, applied by hand — check the opening layer's `start
496
496
 
497
497
  Compositions are authored in HTML, so the single most common way an AI-edited video betrays itself is by **looking like a web page**. A landing-page button, a row of feature chips, a frosted card with a gradient headline — these are everywhere on the web and **nowhere in a real social post**. A director scrolling TikTok/Reels/Shorts never sees them, so the moment one appears the video reads as machine-made and the retention dies. HTML and hyperframes are the right tool; **Bootstrap/Tailwind furniture is not.**
498
498
 
499
- **The one test:** *would this element exist if the video were shot on a phone and captioned in CapCut?* If it only makes sense inside a browser if its whole job is to look **clickable** cut it. **Nothing in a video is clickable.**
499
+ **The one test — the native-editor test:** *could you have made this element with the tools inside TikTok's (or CapCut's) own editor?* That editor's text tool gives you exactly this: a font, a color, a stroke/outline, a soft shadow, a tight text-box band, alignment, opacity, rotation, and animation presets plus stickers, emoji, drawn marks, and clips. It does **not** give you a padded capsule, a border, a gradient fill, a blur panel, or a card. If you had to reach past that toolset, you are decorating like a web designer, and the frame will read as machine-made no matter how good the copy is. The second half of the same test: if the element's whole job is to look **clickable**, cut it **nothing in a video is clickable.**
500
500
 
501
501
  **BANNED — never author, and strip on sight when a fork or a paste brings one in:**
502
502
 
503
503
  - **CTA / "button" shapes.** Any filled capsule or rounded rect containing action copy — "Sign Up for a Free Trial →", "Learn More", "Get Started", "Book a Call" — especially gradient-filled with a glow or drop shadow. A social CTA is *spoken*, or a plain caption line, or an arrow pointing at the real UI. Not a button.
504
- - **Badge / chip / pill rows.** The "✓ ID-Verified Tutors · ✓ No Credit Card Needed · ✓ 30-Min Trial" strip of small rounded tags. Nothing on TikTok is a `<span class="badge">`. Say the three benefits as three timed caption lines instead one at a time, on the footage.
504
+ - **Badges, chips, pills including a SINGLE one.** The "✓ ID-Verified Tutors · ✓ No Credit Card Needed · ✓ 30-Min Trial" strip is the obvious case, but the far more common one is **one lonely capsule holding a stat or a label**: `( 10 hrs / week )`, `( STEP 2 )`, `( EP.01 )`, `( BEGINNER )`, `( +40% )`. Being alone does not make it native — a rounded, padded, filled tag around static text is a `<span class="badge">` wearing a different hat, and it is one of the loudest web tells in the whole frame. **The only legitimate pill in a video is the active-word highlight** (`spotlight`/`karaoke`), because it tracks the spoken word and moves. Static text gets `outline`, `plain`, or a tight band that hugs the glyphs (radius ≤ ~8px). If a stat deserves emphasis, give it emphasis the *editor* can give: bigger, heavier, ALL-CAPS, an accent color, a hand-drawn circle or underline around it, its own beat on screen.
505
505
  - **Cards, panels, containers.** A rounded-rect box (dark, bordered, shadowed, or `backdrop-filter` frosted) holding a heading plus a subheading/URL. Text belongs **on the footage**, not inside a floating panel with padding around it.
506
506
  - **Gradient text fills, neon border glows, elevation shadows, glassmorphism, hover-implying strokes.**
507
507
  - **Web page furniture of any kind:** navbars, hero sections, feature grids, two-column layouts, `<ul>` bullet lists with disc markers, tables, alerts, progress bars as decoration, logo-in-a-circle avatars, "as seen in" strips.
@@ -509,6 +509,8 @@ Compositions are authored in HTML, so the single most common way an AI-edited vi
509
509
 
510
510
  **Greppable smell test.** If you are typing `class="btn…"`, `badge`, `chip`, `tag`, `card`, `panel`, `container`, `row`/`col-`, `hero`, `cta`, `rounded-full`, `shadow-lg`, `backdrop-blur`, `bg-gradient-to-r`, `border border-…` — **stop.** You are building a web page, not a video. Rewrite as timed text on footage.
511
511
 
512
+ **The capsule rule of thumb.** On any element that holds words: `border-radius` over ~8px **combined with** a background fill and padding = a badge. Either take the fill away (bare text + outline/shadow) or take the radius and padding down until the band hugs the glyphs. There is no third option for static text.
513
+
512
514
  **ALLOWED and encouraged — these ARE social-native:**
513
515
 
514
516
  - **Arrows** (drawn, animated, hand-style), circles/scribbles/underline strokes highlighting part of the frame, hand-drawn marks, checkmarks *as glyphs inside a caption line* (not as chips).
@@ -535,7 +537,7 @@ Short-form is watched on a phone, and the phone's UI eats the frame's edges. **N
535
537
  | 3 | **Highlight pill behind the ACTIVE word only** | `set_captions caption_style:"spotlight"` / `"karaoke"` (+ `caption_highlight_color`) | Hormozi/CapCut word-by-word. **The only legitimate "pill" in a video** — it tracks the spoken word, so it isn't a badge |
536
538
  | 4 | **Solid band that tightly hugs the text lines** (CapCut "text box") | `background_style:"highlight-solid"` (or `"highlight-translucent"`) + a `background` color | Guaranteed legibility over noisy footage |
537
539
 
538
- Treatment 4 is a **band, not a card**: it hugs the glyphs with minimal padding, corner radius ≤ ~8px, **no border, no drop shadow, no gradient, no blur**, and it wraps *one* text run — never a heading + subheading + URL stacked inside one rounded box. The moment it grows padding, a stroke, or a second element inside it, it has become a web card. Fix it.
540
+ Treatment 4 is a **band, not a card**: it hugs the glyphs with minimal padding, corner radius ≤ ~8px, **no border, no drop shadow, no gradient, no blur**, and it wraps *one* text run — never a heading + subheading + URL stacked inside one rounded box. The moment it grows padding, a stroke, or a second element inside it, it has become a web card. Fix it. And the moment its radius goes fully round, it has become a **badge** — treatment 3 is the *only* capsule allowed, and only because it tracks the spoken word. A static "10 hrs / week" in a rounded pill is web furniture; the same words in treatment 1 or 2, bigger and heavier, are a beat.
539
541
 
540
542
  **A common trap: decomposed templates mirror the source's caption placement**, so a forked meme can arrive with its caption pinned at `top:0` in a non-regime font — and a re-theme prompt ("make this for my tutoring service") is exactly where an agent starts inventing landing-page CTAs and benefit chips because the *subject* is a SaaS product. **Fix to the standard, don't inherit it, and don't import the website's design language into the video.** When placing text yourself (`set_captions`, `set_layer_text`, `set_layer_style`, `add_layer`, devcli `place`/`captions`), set `y` / `font_family` / `font_weight` / `background_style` to the standard from the start.
541
543
 
@@ -237,38 +237,83 @@ curl -X POST "$VIDFARM_BASE/api/v1/primitives/videos/remove-captions" \
237
237
 
238
238
  ## Primitive: media_dedupe
239
239
 
240
- 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.
240
+ **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.
241
+
242
+ **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.
241
243
 
242
244
  - `POST /api/v1/primitives/media/dedupe`
243
245
  - Body: `{ "tracer": "...", "payload": { ...fields... }, "webhook_url"?: "..." }`
244
246
  - 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.
245
- - Response: standard primitive job. Poll to completion, then read `primary_file_url` (also `video.file_url` for MP4 or `image.file_url` for stills)
246
- - Billing: metered as a Vidfarm render (via `hyperframes_lambda` on prod cloud render; free on a local serve box)
247
+ - 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.
248
+ - 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.
249
+
250
+ ### Presets
251
+
252
+ `preset` picks a calibrated transformation set. Default `standard`.
253
+
254
+ | preset | skew | zoom | rotate | speed | saturation | when |
255
+ | --- | --- | --- | --- | --- | --- | --- |
256
+ | `none` | — | — | — | — | — | no-op; re-encode only |
257
+ | `light` | 1% | 2% | 0.75° | +1% | +2% | lightly reused footage, or tight framing you can't crop |
258
+ | `standard` | 2% | 3% | 2° | +2% | +4% | **the default** — the house standard |
259
+ | `strong` | 3.5% | 6% | 3° | +5% | +8% | an Nth re-post, or an account that already ran this clip |
260
+ | `legacy` | — | 4% | 3° | +5% | +5% | the pre-ffmpeg composition-renderer defaults |
261
+
262
+ `light`/`standard`/`strong` also move contrast, brightness, hue and grain. Any individual knob in `effects` overrides the preset.
247
263
 
248
- Payload fields:
264
+ ### Minting N distinct copies
265
+
266
+ `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.
267
+
268
+ ### Payload fields
249
269
 
250
270
  - `source_media_url` (required, URL) — the image or video to transform
251
271
  - `media_type` (`"image" | "video"`, optional) — auto-detected from URL extension if omitted (`.mp4/.mov/.webm/.m4v` → video, else image)
252
- - `effects` (optional object). All fields optional; defaults camouflage lightly:
253
- - `zoom` (default `1.04`) — scale factor
254
- - `tilt` (default `3`) — degrees of 3D X-axis tilt (perspective 1000px)
255
- - `rotate` (default `3`) — degrees of 2D rotation
256
- - `saturation` (default `1.05`)
257
- - `speed` (default `1.05`, video only) playback rate, also compresses output duration accordingly
258
- - `horizontal_flip` (default `false`)
259
- - `contrast` (default `1.05`)
260
- - `brightness` (default `1.05`)
261
- - `hue_rotate` (default `0`) degrees
262
- - `blur` (default `0`)pixels
263
- - `tint_color` (default `"#FF8C00"`), `tint_opacity` (default `0.08`) subtle color overlay
264
- - `width` (default `1080`), `height` (default `1920`) output canvas
265
- - `duration_ms` — output duration for video; if omitted, `fallback_duration_ms` (default `5000`) is used
266
- - `object_fit` (`"cover" | "contain" | "fill" | "none" | "scale-down"`, default `"cover"`)
267
- - `background_color` (default `"#000000"`) — visible when `object_fit` leaves letterboxing
268
- - `muted` (default `false`), `volume` (default `1`) — audio pass-through on video
272
+ - `preset` (`"none" | "light" | "standard" | "strong" | "legacy"`, default `"standard"`)
273
+ - `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.
274
+ - `variant` (int ≥ 1, default `1`), `seed` (string, optional), `jitter` (bool, optional defaults on for `variant > 1`)
275
+ - `strip_metadata` (default `true`) — drop creation time / encoder / source handler. Several platforms compare that **before** they compare pixels.
276
+ - `effects` (optional object). Every field optional; each one overrides the preset:
277
+ - `zoom` — scale factor, centre-cropped back (`1.03` = 3% punch-in)
278
+ - `skew` — horizontal shear as a **percent of frame width** (ffmpeg engine only)
279
+ - `rotate` degrees of 2D rotation
280
+ - `tilt` degrees of 3D X-axis tilt on the composition engine; folded into the shear budget on ffmpeg
281
+ - `speed` playback multiplier, video only; changes duration **and** pitch-preserved audio tempo
282
+ - `saturation`, `contrast`, `brightness` — multipliers around `1`
283
+ - `hue_rotate` — degrees
284
+ - `noise` film grain `0..100` (ffmpeg engine only). Cheap, invisible, moves a lot of hash.
285
+ - `blur` — gaussian sigma in px. Usually `0` blur is the one knob viewers notice.
286
+ - `volume` audio gain multiplier
287
+ - `horizontal_flip` (default `false`) — the strongest single knob, but it visibly reverses on-screen text. Opt in deliberately.
288
+ - `tint_color` (default `"#FF8C00"`), `tint_opacity` (default `0.08`) — flat color wash; set opacity `0` to skip
289
+ - `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.
290
+ - `crf` — base x264 quality; jittered ±1 per variant so the coded bitstream differs too
269
291
  - `output_format` (`"png" | "jpeg" | "webp"`, default `"png"`) — image mode only; video mode always outputs MP4
292
+ - Composition-engine only: `duration_ms` / `fallback_duration_ms` (default `5000`), `object_fit`, `object_position`, `background_color`, `muted`
293
+
294
+ **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`.
295
+
296
+ Video example — three copies of one render, one per account:
297
+
298
+ ```bash
299
+ for V in 1 2 3; do
300
+ curl -X POST "$VIDFARM_BASE/api/v1/primitives/media/dedupe" \
301
+ -H "vidfarm-api-key: $VIDFARM_API_KEY" \
302
+ -H "content-type: application/json" \
303
+ -d "{
304
+ \"tracer\": \"dedupe-launch-reel-v$V\",
305
+ \"payload\": {
306
+ \"source_media_url\": \"https://cdn.example.com/reel.mp4\",
307
+ \"media_type\": \"video\",
308
+ \"preset\": \"standard\",
309
+ \"variant\": $V,
310
+ \"seed\": \"launch-reel\"
311
+ }
312
+ }"
313
+ done
314
+ ```
270
315
 
271
- Video example (camouflage a reused clip):
316
+ Custom knobs (keep the framing, lean on color and timing instead):
272
317
 
273
318
  ```bash
274
319
  curl -X POST "$VIDFARM_BASE/api/v1/primitives/media/dedupe" \
@@ -279,15 +324,7 @@ curl -X POST "$VIDFARM_BASE/api/v1/primitives/media/dedupe" \
279
324
  "payload": {
280
325
  "source_media_url": "https://cdn.example.com/reel.mp4",
281
326
  "media_type": "video",
282
- "duration_ms": 8500,
283
- "effects": {
284
- "zoom": 1.05,
285
- "tilt": 2,
286
- "rotate": -2,
287
- "speed": 1.03,
288
- "hue_rotate": 4,
289
- "horizontal_flip": true
290
- },
327
+ "effects": { "rotate": 0, "skew": 1.5, "zoom": 1.02, "speed": 1.03, "hue_rotate": 6, "noise": 2 },
291
328
  "tint_color": "#00A3FF",
292
329
  "tint_opacity": 0.06
293
330
  }
@@ -305,12 +342,18 @@ curl -X POST "$VIDFARM_BASE/api/v1/primitives/media/dedupe" \
305
342
  "payload": {
306
343
  "source_media_url": "https://cdn.example.com/photo.jpg",
307
344
  "media_type": "image",
308
- "effects": { "zoom": 1.06, "rotate": -1, "hue_rotate": 8 },
345
+ "preset": "light",
309
346
  "output_format": "webp"
310
347
  }
311
348
  }'
312
349
  ```
313
350
 
351
+ Local equivalent (free, offline, identical transforms — prefer this):
352
+
353
+ ```bash
354
+ vidfarm dedupe ./out/final.mp4 --variants 3 --out-dir ./out/posts
355
+ ```
356
+
314
357
  ## Primitive: music (text → music)
315
358
 
316
359
  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.