@officexapp/vidfarm-devcli 0.21.52 → 0.21.53

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
@@ -188,7 +188,7 @@ Cost mode answers *how much money may I spend*. It does not answer *how much of
188
188
  **How aggressively to run phase 2 depends on the cost mode.** This is the single biggest lever on both quality and spend:
189
189
 
190
190
  - **`minimize` — the collection phase is the whole job. Budget most of your effort here.** You have no generation to fall back on, so the pool has to be good, and it can be: work the ladder in order and stop at the first rung that answers the beat.
191
- 1. `vidfarm public-raws --categories` then `--category <shelf>` — pre-tagged, free, already cut to short-form. Check this first, every time.
191
+ 1. `vidfarm public-raws --categories` then `--category <shelf>` — pre-tagged, free, already cut to short-form. Check this first, every time. Need the **same face in several shots**? Read the `actor_<uuid>` token off a card's summary, then `vidfarm public-raws --query actor_<uuid>` returns that person's other clips (`references/assets-and-sourcing.md` → *Same face across clips*).
192
192
  2. `vidfarm media search "<meaning>" --type <bgm|sfx|image|vector|video>` (keyless via openverse/iconify) and `vidfarm iconscout "<meaning>" --free` for icons, stickers, illustrations and props.
193
193
  3. `vidfarm raws search "<meaning>"` — what the director already owns from previous videos.
194
194
  4. **Browser control** — `vidfarm browse videos "<the shot>"` drives the user's own Chrome for $0 (`references/browser-harness.md`). This is the free substitute for the paid `video-search` / `image-search` / `news-search` / `download-video` primitives. **Set it up rather than declaring the shot unavailable.**
@@ -382,7 +382,7 @@ You may be running as the **in-web AI chat** (the /editor copilot, the chat dock
382
382
  **Two halves, and only one is machine-checkable.** The `checks:` front matter is settled deterministically by `vidfarm qa` (duration, aspect, `hook_words_max`, `forbid_text`, …); every `- [ ]` line comes back as a **review item you answer honestly in your report** — never claim a video passed the half the CLI can't judge. Harnesses stack and auto-discover: `vidfarm qa ./work` picks up `./work/HARNESS.md`, `--harness hooks --harness ./brand/HOUSE.md` adds more, and any file of theirs anywhere is valid. Format and strand table: `harnesses/README.md`; scripting-mode detail: `references/automation-and-local-dev.md`. *(Formerly `QA_REGIME.md` — same file, and `vidfarm regime …` still works as an alias.)*
383
383
  - **A video is judged as a SEQUENCE, so review it as one.** Agents build scene by scene and each scene passes in isolation while the video drifts — inconsistent margins, three type sizes, an accent colour that wanders, beats that are all the same length, a jarring join. Tile a dozen stills into one contact sheet (`vidfarm stills ./work --sheet`) and read it as an image before you call anything done, fix drift by defining the system rather than patching the odd scene out, and remember that **your own confident "verified, looks good" is the single least reliable signal in this workflow** — it was wrong on every video of a 32-video batch. Method: `references/reviewing-renders.md`.
384
384
  - **On devcli there's an OPTIONAL checker: `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). **Skipping it is fine — watching the render is the review that actually counts, and a clean `qa` is not one.** When you do run it, it allows **one** fix round by default: the first pass names the slop, one fix clears it, and a second round is nearly always taste rather than a defect. The human owns that number — `--max-revisions <n>` raises it, `0` disables it; ask rather than raising it yourself. `--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`.
385
- - **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) and, inside that band, is **placed in the emptiest part of the frame** rather than dumped on the default lower third (read a still first — `vidfarm stills ./work --at <t>`; words over open sky or a blank wall beat words over the subject, and usually need no plate at all). Long narration is **paged into 3–5-word kinetic cues** (`vidfarm captions generate --style word-pop`), never one static wall of text. It 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").
385
+ - **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) and, inside that band, is **placed in the emptiest part of the frame** rather than dumped on the default lower third (read a still first — `vidfarm stills ./work --at <t>`; words over open sky or a blank wall beat words over the subject, and usually need no plate at all). Long narration is **paged into 3–5-word kinetic cues** (`vidfarm captions generate --style word-pop`), never one static wall of text. It uses the composition's bold font regime **exactly five allowed families and no others**: TikTok Sans (safe default), Montserrat (bold default), Abel (condensed), Source Code Pro (mono, 700 only), Yesteryear (script accent line only) — at weight **700–900**, ~36–64px on a 1080-wide frame. Any other family (Inter / Roboto / Arial / system-ui / Georgia / a client brand font) is not imported and silently falls back to a web-default sans at render. **The regime on one page: <https://vidfarm.cc/fonts>** — all five rendered as real captions, the four legal backgrounds, and copy-paste commands. It 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").
386
386
  - **Use the REAL track — a popular/copyrighted song in the render is expected here, not a problem.** Do not refuse it, do not silently substitute a generic royalty-free bed, and do not stop to warn about it. **Why it is legitimate:** the render is a **review artifact**, and at post time the user re-attaches the *same* song from the platform's own in-app music library (TikTok / Reels / Shorts), which is licensed through the platform's agreements with the labels. The music is therefore cleared on the surface where viewers actually hear it. **Why it is required:** a trending song IS the edit — cut points, pacing, the drop, the meme association. A render with the wrong bed, or no bed, reads as a different and much weaker video, so a review over a substitute bed grades a video nobody will ever post. Silent or generic-bed review is the bigger defect. **How to do it:** get the exact track (`vidfarm download-audio <post-url>`, the user's own file, or the audio off the source post the format is copying), and keep it on **its own `<audio>` layer** at its own `data-volume` — never baked into the footage or mixed into the voice stem. One separate track means the user can mute or swap it in one action when they upload, which is exactly what the platform flow expects. Say which track you used in the handoff. **The one limit:** this is for review renders and platform posting where the platform holds the license. If the user asks to sell, syndicate, or run the render as a **paid ad** with the track baked in, say once that ad placements are not covered by the in-app music license, and offer `vidfarm music "<same vibe, same BPM>"` as the swap for that cut. Then do what they decide.
387
387
  - **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.
388
388
  - **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.
@@ -875,7 +875,7 @@ curl -s -X POST -H "vidfarm-api-key: $VIDFARM_API_KEY" -H "content-type: applica
875
875
  curl -s -X POST -H "x-api-key: $DP" "https://dollarplatoon.com/api/gigs/$GIG/rollups"
876
876
  ```
877
877
 
878
- `paid_out_at` on the proof — not `approved` — is the field that means the money moved. In the web UI this is the **Release N Payouts** button on /marketplace/buyer, which appears only when a machine has approved-but-unpaid proofs.
878
+ `paid_out_at` on the proof — not `approved` — is the field that means the money moved. In the web UI this is the **Release N Payouts** button on /marketplace/inbound/mailbox, which appears only when a machine has approved-but-unpaid proofs.
879
879
 
880
880
  **4 — Add a new task.** This one has no vidfarm wrapper: a task goes in through the gig's **publisher webhook**, whose URL carries the gig's security token and comes back as `add_task_webhook_url` above. No key header — the token in the URL is the auth.
881
881
 
@@ -908,7 +908,7 @@ curl -s -X POST -H "x-api-key: $DP" -H "content-type: application/json" \
908
908
  -d '{"max_uses":null,"label":"vidfarm"}' "https://dollarplatoon.com/api/gigs/$GIG/invites"
909
909
  ```
910
910
 
911
- If you ring the bell directly against Dollar Platoon (`POST /feeds/$FEED/notifications`), resolve that invite FIRST and send it as `destination_url`. The feed registry's `invite_url` is tokened too and works as a fallback. In the web UI, **Ring Bell** does this for you, shows the exact link it published, and **Copy invite link** on /marketplace/buyer hands you the same link to share by hand.
911
+ If you ring the bell directly against Dollar Platoon (`POST /feeds/$FEED/notifications`), resolve that invite FIRST and send it as `destination_url`. The feed registry's `invite_url` is tokened too and works as a fallback. In the web UI, **Ring Bell** does this for you, shows the exact link it published, and **Copy invite link** on /marketplace/inbound/swipe hands you the same link to share by hand.
912
912
 
913
913
  Ring it when something actually **changed** — a new batch, a new price, a format you now want. An empty bell teaches agents to stop opening yours.
914
914
 
@@ -1071,23 +1071,36 @@ For any replication, offer the director **two harnesses** and recommend (A) unle
1071
1071
 
1072
1072
  ### Writing a meme recaption: aim at a pain or a win
1073
1073
 
1074
+ > ## → Building a meme video from scratch? Use the harness.
1075
+ > **`https://vidfarm.cc/experimental/meme-recaption.md`** is the full standalone format: casting a
1076
+ > clip off the public-raws shelf for the caption's **verb** and its **intensity**, keying a
1077
+ > MemeScreens raw onto a background world, caption placement, naming the offer, rights tiers, and a
1078
+ > render-level QA gate that exits non-zero. It runs at $0 and it supersedes this section for any
1079
+ > from-scratch build.
1080
+ >
1081
+ > **This section is the short version**, for when you already have the clip on a timeline and are
1082
+ > only rewriting the line (`set_captions` / `set_layer_text`). The writing craft below is the same
1083
+ > craft the harness uses — the harness just adds everything around it.
1084
+
1074
1085
  Recaptioning is the cheapest brush, but the *line* is the whole product — the clip is just the punchline delivery system. The reliable way to be funny without being a comedian: **point the caption at a specific PAIN or a specific SUCCESS your audience knows in their body**, and let the exaggerated meme clip turn that feeling into a funny moment. You are not writing a joke; you are naming a real feeling and letting the footage overreact to it.
1075
1086
 
1076
- - **Pain** — the annoying, tedious, humiliating, or absurd moment in the audience's day. The meme clip supplies the reaction they wish they could have. *"me opening the bank app after a weekend of 'small' purchases"*.
1077
- - **Success** — the small win, the relief, the flex, the moment something finally worked. The clip supplies the celebration. *"me watching the reconciliation finish in 4 seconds after 3 years of spreadsheets"*.
1087
+ - **Pain** — the annoying, tedious, humiliating, or absurd moment in the audience's day. The meme clip supplies the reaction they wish they could have. *"me opening the bank app after a weekend of 'small' purchases"*. Ends on the problem; does not resolve.
1088
+ - **Success** — the small win, the relief, the flex, the moment something finally worked. The clip supplies the celebration. *"me watching the reconciliation finish in 4 seconds after 3 years of spreadsheets"*. Resolves by design — the relief IS the joke.
1089
+
1090
+ **Decide the pole before you write.** It changes whether the caption may resolve, and it is the most useful thing to A/B: run both across the same clip and see whether the market wants to be *seen* or wants *relief*.
1078
1091
 
1079
1092
  How to work it:
1080
1093
  1. **Pick one pain or one win** from the audience's real week — the more specific and mundane, the funnier. Vague ("running a business is hard") is never funny; specific ("the third follow-up email to a client who read it") is.
1081
- 2. **Match the intensity of the clip to the size of the feeling.** The comedy is the mismatch: a tiny pain + a wildly over-the-top reaction clip, or a small win + a stadium-scale celebration. Choose the pain/win that the existing clip's energy already fits, rather than fighting the footage.
1082
- 3. **Keep the meme's grammar** — `me when…` / `POV: you…` / `my clients when…` / `us after…`. Swap the subject to whoever owns the feeling (the customer, the founder, the team), keep the frame.
1083
- 4. **Never name the product in the line.** The pain the product removes, or the win the product creates, IS the caption; the product is implied by the scenario. A viewer should want to tag a friend, not click "skip ad". Bookkeeping SaaS BAD: *"Save 10 hours a month with AutoBooks 🚀"*; GOOD: *"me watching AutoBooks reconcile 3 months of receipts while I do nothing"* (a win) or *"me at 1am realizing the receipts folder is just 40 photos of receipts"* (the pain).
1084
- 5. **The line must make sense to a stranger to the offer — the cold-viewer test.** Before you ship a recaption, read it as a person who works in the niche but has NEVER heard of the product, the brand, the feature names, or the category jargon. If they cannot get the joke in one read, the caption failed, no matter how clever it is to the team. Most of a good recaption is niche **experience** (the day, the client, the tool everyone already uses, the shared annoyance); the offer gets a small implied mention at most, and often none.
1094
+ 2. **Match the intensity of the clip to the size of the feeling.** The comedy is the mismatch: a tiny pain + a wildly over-the-top reaction clip, or a small win + a stadium-scale celebration. Choose the pain/win that the existing clip's energy already fits, rather than fighting the footage — the clip is the part you cannot edit.
1095
+ 3. **Keep the meme's grammar** — `me when…` / `my clients when…` / `us after…` / `<thing> watching me…`. Swap the subject to whoever owns the feeling (the customer, the founder, the team), keep the frame.
1096
+ 4. **Naming the product in the line is fine as a subject, not as the answer.** On a **pain** caption the name may stand in the setup as a witness (*"AutoBooks watching me photograph a 41st receipt"*) but never in the last clause. On a **win** caption it belongs in the last clause, because the relief is the joke (*"me watching AutoBooks reconcile 3 months of receipts while I do nothing"*). What is never allowed on either pole is a **CTA** (*"Save 10 hours a month with AutoBooks 🚀"*) or a **feature** (*"after the auto-reconcile v2 sync clears"*). If naming it makes the line clumsy, leave it out of the sentence and put the offer in a small dimmed handle under the caption instead — both tiers, and the checks that enforce them, are in the harness.
1097
+ 5. **The line must make sense to a stranger to the offer — the cold-viewer test.** Read it as a person who works in the niche but has NEVER heard of the product, the brand, the feature names, or the category jargon. If they cannot get the joke in one read, the caption failed, no matter how clever it is to the team.
1085
1098
  - **Write about the niche, not the feature.** A meme about a product feature needs product context to land, so it lands only on people who already bought. A meme about the niche's lived experience lands on everybody in the niche — which is the whole point of a meme ad.
1086
1099
  - BAD (needs product context): *"me after the auto-reconcile v2 sync finally clears"* — "auto-reconcile v2" means nothing to a cold viewer. GOOD: *"me at 1am matching bank lines to receipts by hand"*.
1087
1100
  - **Test:** cover the brand and the feature names. If the caption still reads as a true, funny moment from the audience's week, keep it. If it turns into nonsense, rewrite it around the feeling instead of the feature.
1088
1101
  - **No inside jokes, no invented vocabulary, no setups that only the founder's demo explains.** Slang the niche already uses is fine; slang only the product uses is not.
1089
1102
  6. **One short punchy line**, matching the original's brevity, tone, and comedic timing. If the original was two stacked lines (setup / payoff), keep two — pain on top, reaction beneath.
1090
- 7. **Batch it.** One meme clip + a list of ten pains and ten wins is ten videos. Enumerate the audience's pains and wins once, then recaption the same clip (or a small set of clips) across the whole list — this is the highest-output, lowest-cost loop in Vidfarm.
1103
+ 7. **Batch it.** One meme clip + a list of ten pains and ten wins is twenty videos. Enumerate the audience's pains and wins ONCE, then recaption the same clip (or a small set of clips) across the whole list — this is the highest-output, lowest-cost loop in Vidfarm. The list is the asset; the clips are interchangeable.
1091
1104
 
1092
1105
  If the user insists on explicit ad copy in the recaption, say once that it flattens the joke, then give them the pain/win version alongside what they asked for.
1093
1106
 
@@ -1632,7 +1645,18 @@ Short-form is watched on a phone, and the phone's UI eats the frame's edges. **N
1632
1645
  - *Judgement call, inside that band:* **put the words where the picture isn't.** `y≈70%` is the `captions generate` default because most footage puts its subject mid-frame — it is a default, not a law. Before you place text, **look at an actual frame** (`vidfarm stills ./work --at <t>`, free) and find the region with the least going on: open sky above a dashboard, a blank wall behind a talking head, an out-of-focus background, an empty tabletop. If nothing else in the video is competing for attention there — no subject, no motion, no product, no second text layer — that is where the caption belongs, even if it means **high-centre at y≈10–25%** instead of a lower third. A caption dropped over the busiest third of the frame (hands on a steering wheel, a face, the product) fights the shot and forces you to armour it with a plate; the same words parked in the sky are legible with no plate at all.
1633
1646
  - *When you're only rescuing an inherited caption* off a dead-zone edge, preserve its top-vs-bottom anchoring and just pull it inside the band — don't recentre a template you haven't re-read. When **you** are the one placing the text, place it deliberately.
1634
1647
  - **Size → scaled to the line, not maxed out.** Sizes are PIXELS of a 1080-wide frame: **~36–64px** reads well; below ~28px is unreadable on a phone and **0 is invisible**. Above ~64px is a *hook-word* size — one to three words, on purpose. The failure this catches: a full sentence set at display size runs edge-to-edge, wraps to three lines, and eats a third of the frame, so it has to be armoured with a full-width plate and there is nowhere left to put it. **If a line reaches the frame edges, the fix is a smaller size (or fewer words per cue), not a wider box.** Keep captions to ~2 lines / ~5 words per line; `line_height` 0.95–1.15 for stacked display lines.
1635
- - **Font → the composition regime.** Use the bundled display fonts only**Montserrat** (bold default, weight **700–900**), **TikTok Sans**, Abel, Source Code Pro, Yesteryear. Don't request a font the composition doesn't import (it silently falls back to a web-default sans, which is exactly the slop look).
1648
+ - **Font → the composition regime. Five families, no others.** The whole regime on one pageeach family rendered as a real caption, the four legal backgrounds, copy-paste `set-style` commands: **<https://vidfarm.cc/fonts>** (specimen image alone: `https://vidfarm.cc/assets/tiktok-caption-fonts.png`). Read it once before you style anything, and hand the link to a human director who is picking a look.
1649
+
1650
+ | Family | Weights it really has | Use it for |
1651
+ |---|---|---|
1652
+ | **TikTok Sans** | 400 / 600 / 700 / 800 / 900 | The native TikTok caption look. **The safe default when unsure.** |
1653
+ | **Montserrat** | 600 / 700 / 800 / 900 | Geometric bold display. Hooks, hard statements, the Hormozi caption. **The bold default.** |
1654
+ | **Abel** | 400 only | Condensed headline / newsletter vibe. A long line that must stay on one row. |
1655
+ | **Source Code Pro** | 700 only | Code / terminal beats only. Never a whole video. |
1656
+ | **Yesteryear** | 400 only | Cursive script. **One accent line** (a quote) — never a caption track; it is unreadable at cue size. |
1657
+ | ~~Georgia~~ | — | **Decompose-only. Never author in it.** The decompose vision pass may report `Georgia` off a source video's serif (it is the 6th value in `ALLOWED_FONTS`, `src/services/hyperframes.ts`), but the composition does not import it and `normalizeTikTokCaptionLayout` coerces it to Montserrat on every local render. Rebuild an editorial look in Abel or Montserrat instead. |
1658
+
1659
+ These five are exactly what the composition `@import`s from Google Fonts, and exactly what `CAPTION_FONT_REGIME` (`src/devcli/composition-edit.ts`) keeps. **Anything else is not imported**: `Inter`, `Roboto`, `Arial`, `Helvetica`, `system-ui`, a client's brand font — the render silently falls back to a web-default sans, which is exactly the slop look. Matching a client brand font is fine for a *wordmark image*; it is never fine for the caption layer. Asking for a weight the family does not ship (Abel 900, Yesteryear 700) fakes it with a synthetic bold and looks smeared — pick a family that has the weight instead. `vidfarm qa-check` flags off-regime families (`font-regime` rule).
1636
1660
  - **Background → one of exactly four valid treatments.** Any text you place uses one of these and nothing else:
1637
1661
 
1638
1662
  | # | Treatment | How to set it | When |
@@ -2334,6 +2358,29 @@ What each shelf is for, in practice: **Scroll Stoppers** are half-second pattern
2334
2358
 
2335
2359
  A bookmarked public raw lands in the director's own `/raws/public/*` folder, after which it behaves like any other library raw (`set_layer_media` / `vidfarm set-media` to drop it on the timeline).
2336
2360
 
2361
+ ### Same face across clips — the `actor_<uuid>` tag
2362
+
2363
+ A shelf is dozens of clips of a much smaller number of creators. When a video needs **the same person in more than one shot** — a hook, a reaction cutaway and a payoff that must read as one spokesperson — the shelf alone can't tell you which clips share a face. The `actor_<uuid>` tag can.
2364
+
2365
+ Every tagged public raw carries one actor id in its `summary` (and in its tags), so the id is **readable off any card you like** and searchable as a plain query token:
2366
+
2367
+ ```bash
2368
+ vidfarm public-raws --category ugc-reaction --limit 60 # 1. browse the shelf, pick a face
2369
+ # 2. read actor_<uuid> off that card's summary
2370
+ vidfarm public-raws --query actor_5d9f0c31-… # 3. every other clip of that same person
2371
+ ```
2372
+
2373
+ REST twin: `GET /api/v1/public-raws?q=actor_<uuid>`. It is an ordinary keyword search, so it composes: `?category=ugc-reaction&q=actor_<uuid>`.
2374
+
2375
+ Rules that matter:
2376
+
2377
+ - **The id is opaque and permanent — never guess or invent one.** Read it off a card; a made-up id matches nothing.
2378
+ - **One id per person, not per clip.** A card with no `actor_` token is simply not tagged yet (older raws, or a raw with no person on camera) — treat "no tag" as unknown, not as "a different person".
2379
+ - **Use it for continuity, not casting from scratch.** Pick the face first (browse the shelf, judge the frame), then pull that face's other takes.
2380
+ - Same trick when a director asks *"more of her"* / *"the guy from the last video"* — the actor id off the raw already in the composition is the answer.
2381
+
2382
+ The `ugc-reaction` shelf is tagged: 177 raws across 53 people, and **almost every clip on it has siblings** — 49 of those 53 actors carry 2+ clips, several carry 5-7. Assume a face you like has more takes, and check.
2383
+
2337
2384
  ## Raws (long-form → short-form raws)
2338
2385
 
2339
2386
  Mine a **long-form** video (podcast, stream VOD, webinar, any YouTube/TikTok/IG/X URL, or an upload) into a library of tagged, searchable **raws**. **Don't have a source URL yet? `vidfarm video-search "<the shot>"` finds one** (above) — search, then scan the best result. This is the `/raws` surface — browse it at `https://vidfarm.cc/library/raws` (the Library page's "Approved / Raws" tabs).
@@ -2495,6 +2542,15 @@ On `/tools/image` these are one-tap preset chips under the prompt box; in chat o
2495
2542
  - **move** — `POST /api/v1/user/me/directory/move` `{ path, to, file_id? }` · `vidfarm directory move <path> <to-folder> [--file-id <id>]`. Relocates a FILE into `to` (with `file_id`) or nests a whole FOLDER under `to`. **Same root only** (`/files`·`/temp`·`/raws`·`/approved`); metadata-only, so S3 objects are untouched.
2496
2543
  - **copy** — `POST /api/v1/user/me/directory/copy` `{ path, to?, file_id?, new_name? }` · `vidfarm directory copy <path> [<to-folder>] [--file-id <id>] [--as <name>]`. Duplicates a file/folder sharing the same underlying S3 object (cheap, no re-upload). Same root only, `/files`·`/temp`·`/raws` (not `/approved` — a ready post is a single publishable unit; not `/projects` — read-only). Omit `to` to duplicate in place.
2497
2544
 
2545
+ **Vector notes — what makes a file findable by MEANING.** Every file in `/files` and `/raws`, and every **folder** in any annotatable root, carries one plain-text *vector note*: what the thing actually is. The note is embedded, so `directory/search` finds it from any phrasing — `IMG_4821.mp4` is worth nothing to a search, "founder holding the product in a kitchen, warm daylight" is worth everything. Folders are searched the same way, so a well-noted folder is itself a hit.
2546
+
2547
+ - **read** — `GET /api/v1/user/me/directory/note?path=<path>[&id=<file_id>]` → `{ note, can_edit }`.
2548
+ - **write** — `PUT /api/v1/user/me/directory/note` `{ path, id?, note }` → `{ note, embedded }`. Omit `id` to annotate the FOLDER at `path`; pass the file's backend id to annotate a file. An empty `note` clears it. `embedded: false` means it saved but only keyword search will find it — the account has no AI key for embeddings.
2549
+ - **through a share link** — same two routes at `/api/v1/share/:token/directory/note`. `read` links never write. **`upload` links may FILL an empty note but never overwrite one**, so a contributor says what they dropped without rewriting anyone else's meaning. `edit` links and the owner may always write.
2550
+ - **automatic** — on upload, when the account has its own AI key saved (Gemini / OpenAI / OpenRouter), one vision call on the file's poster frame writes the note for you. Skipped for files over **100 MB** and when there is no key — then a human types it. The model runs on the customer's own key; only the Lambda compute is wallet-billed, at 2x.
2551
+
2552
+ In the explorer each row shows a note badge: `✎ note` (missing) or `✓ note` (written). Click it to read or write the note.
2553
+
2498
2554
  **Sharing a folder publicly (crowdsourcing + agent-readable libraries).** Any folder in **any** root can carry share tokens — one folder, as many links as you want, each with its own mode:
2499
2555
 
2500
2556
  - `read` — browse **and vector-search** the folder. Nothing else.
@@ -2505,6 +2561,8 @@ Minting or editing a link is **paid** (unlike the rest of the directory routes,
2505
2561
 
2506
2562
  A token is scoped to **exactly one subfolder and everything under it** — never a sibling, never a parent, so a leaked link cannot walk the rest of the drive. The shared base folder itself can never be renamed or deleted through its own token. Owner routes: `GET|POST /api/v1/user/me/directory/shares`, `PATCH|DELETE …/shares/<token>` (`{ path, mode, label }`; `disabled: true` cuts a link off instantly and is reversible). Public routes, no auth: `GET /api/v1/share/:token/directory` (`?path=`), `POST /api/v1/share/:token/directory/search` `{ query, path?, mode? }`, plus `…/directory/folders`, `…/directory/rename`, `…/directory/delete`, `…/attachments/upload`. The human page is `/directory/preview/<token>/<path…>`.
2507
2563
 
2564
+ **Which roots accept a share upload:** `/files`, `/temp` and `/raws`. A `/raws` share is the **contributor drop-box** — the bytes land as a `role: "source"` raw inside the shared folder (video/audio only), exactly as `POST /raws/import` writes one. `/projects` (fork storage) and `/approved` (published renders) stay browse + search.
2565
+
2508
2566
  **Uploading over ~6 MB through a share link:** `…/attachments/upload` is a multipart POST through the API Lambda, whose request body caps at about 6 MB — a bigger file answers **413** before the handler runs. Use the two-step presigned path instead (the share page's own uploader does): `POST /api/v1/share/:token/attachments/presign` `{ file_name, content_type?, size_bytes?, folder_path? }` → `{ transport: "presigned", file_id, storage_key, folder_path, upload: { method, url, headers } }`; PUT the raw bytes to `upload.url` with those headers; then `POST /api/v1/share/:token/attachments/finalize` `{ file_id, file_name, content_type, size_bytes, storage_key, folder_path }` to record the file. A server with no presigned transport answers `transport: "server"` and you post the multipart form. Ceiling is the owner's own: 200 MB.
2509
2567
 
2510
2568
  In the UI: the file explorer's folder kebab → **Share…**, the `⤴ Share` toolbar button, and the folder kebabs on `/library` (**Raws** and **Approved** tabs). `⤴ Manage Shares` lists every token you own.
@@ -2513,14 +2571,20 @@ In the UI: the file explorer's folder kebab → **Share…**, the `⤴ Share` to
2513
2571
 
2514
2572
  ```bash
2515
2573
  vidfarm shared info "$LINK" # folder, mode, what you may do
2516
- vidfarm shared ls "$LINK" [subfolder] # browse
2574
+ vidfarm shared ls "$LINK" [subfolder] --tree # browse (--tree walks every subfolder)
2517
2575
  vidfarm shared search "$LINK" "founder talking head, no captions"
2518
- vidfarm shared get "$LINK" hero.mp4 --out ./work # one file (--all for the folder)
2576
+ vidfarm shared grab "$LINK" "kitchen b-roll, no faces" --limit 3 --out ./assets
2577
+ vidfarm shared note "$LINK" hero.mp4 # what IS this? (--set "…" to write)
2578
+ vidfarm shared get "$LINK" hero.mp4 --out ./work # one file (--all [--tree] for the folder)
2519
2579
  vidfarm shared mkdir "$LINK" task-014-yvette # your own subfolder (upload/edit)
2520
- vidfarm shared put "$LINK" final.mp4 --subfolder task-014-yvette
2580
+ vidfarm shared put "$LINK" final.mp4 --subfolder task-014-yvette --note "Final cut, 9:16"
2521
2581
  ```
2522
2582
 
2523
- `shared put` runs presign PUT finalize, so it is not bound by the ~6 MB multipart ceiling; it falls back to multipart automatically when the server has no presigned transport.
2583
+ **`shared grab` is the command to reach for.** It runs the vector search and downloads the hits in one step, so an agent goes from "I need kitchen b-roll" to files on disk without paging folders. `search` and `grab` match on each item's **vector note**, and folders are hits in their own right — so read notes before you pick (`shared ls --tree` prints them, `✓` = described, `✎` = not).
2584
+
2585
+ **Write a note when you upload** (`shared put --note`, or `shared note <file> --set`). An `upload` link may FILL an empty note but never overwrite one; an `edit` link and the owner may always write. That is what makes your deliverable findable by the client's own agent later.
2586
+
2587
+ `shared put` runs presign → PUT → finalize, so it is not bound by the ~6 MB multipart ceiling; it falls back to multipart automatically when the server has no presigned transport. A `get`/`grab` whose stored view URL points at a host that no longer answers is retried against the share link's own host.
2524
2588
 
2525
2589
  **The crowdsourcing pattern (DollarPlatoon custom-request vending machine).** Two links per campaign, minted once and reused across every task:
2526
2590
 
@@ -3425,11 +3489,13 @@ Read this when a director says **"give me content ideas"**, "what should I post"
3425
3489
 
3426
3490
  | Axis | Bank | Question it answers |
3427
3491
  |---|---|---|
3428
- | **Subject** | the director's own pool of adjacent topics | *What is this one about, in the niche?* (settling the dinner argument · eating what you actually craved · reading a menu before you commit) |
3492
+ | **Subject** | the director's own pool of adjacent topics, each at a **distance** | *What is this one about?* (settling the dinner argument · eating what you actually craved · **and further out**: why the best-reviewed place is rarely the best meal) |
3429
3493
  | **Frame** | 50 content frames | *What is the video the story of?* (`the rise of`, `then vs now`, `one decision that changed everything`) |
3430
3494
  | **Awareness** | 5 stages | *What does the viewer already know?* (unaware → problem-aware → solution-aware → product-aware → most-aware) |
3431
3495
  | **Angle** | 44 problem angles | *From which side do we talk about the problem?* (the hidden cost, the myth, the confession, the honest downside) |
3432
3496
 
3497
+ The subject axis carries a **distance**: how far from the offer the video is allowed to sit. Distance is what produces genuinely different videos rather than differently-shaped ones, and the **turn back to the offer** is what keeps distance from becoming irrelevance — see *Distance* below. Freeze the subject at distance zero and no amount of frames or angles will save the list.
3498
+
3433
3499
  A **frame** is a reusable shape for a video's subject — not a hook line, not a script. You pour the director's topic into it: `the rise of` + `dropshipping supplements` → *"The rise of the supplement dropship store"*. An **angle** decides which face of the problem that video shows; the same frame at two angles is two genuinely different videos. An **awareness stage** decides who it is allowed to be for — the same idea aimed at a stranger and at a buyer needs different first ten seconds and a different ask.
3434
3500
 
3435
3501
  **The grid is where the volume comes from.** 50 frames × 44 angles × 5 stages, over a pool of adjacent subjects, is not a number to take literally, but it is why "I need 30 videos this month" is an easy ask, not a hard one. Vary **two** axes across a set and it never reads as repeats. Vary only one and it does — and the axis most often left frozen is the subject, because the offer line is right there and it is easy to paste.
@@ -3482,6 +3548,72 @@ Now the same four frames are four videos:
3482
3548
 
3483
3549
  **One rule to check the finished list.** Read the titles with the frames covered up. If what is left is the same sentence 20 times, you produced one idea, not 20.
3484
3550
 
3551
+ ### Distance — going further than the niche, safely
3552
+
3553
+ The pool above stays inside the niche, and the ❌ above bans a different business outright. **That ban is correct as long as nothing carries the video home.** Write the turn back to the offer first, and you can go considerably further out — which is where the strongest ideas live, because the viewer did not come to the feed looking for your category.
3554
+
3555
+ > "Why smart people make less money than dumb people… because nobody hands them the lessons — Prism gives you a lifetime of them from the best biographies, five minutes each."
3556
+
3557
+ That is a video about status and income, for a book summary app. It is not off topic, because the last clause is written before the video is.
3558
+
3559
+ **The dial.** Give every subject a distance, and quota the spread — do not ask for "variety", which returns everything at the mode:
3560
+
3561
+ | d | What it is | Share of 12 |
3562
+ |---|---|---|
3563
+ | 1 | Next door. The viewer sees it is adjacent to the category. | 5 |
3564
+ | 2 | The wider human subject. The category is one example inside it. | 5 |
3565
+ | 3 | Far. The video never names the category until the last line. | 2 |
3566
+
3567
+ Far-heavy was tried and rejected. At 2 / 4 / 6 you get a video about IKEA's wordless instructions for a book app, and a viewer needs two steps of explaining before anything sells.
3568
+
3569
+ **The one step rule.** The link from the subject to the offer must be ONE step. If a viewer needs two steps to see how they connect, the subject is too far — take a nearer one. A viewer will not do the work.
3570
+
3571
+ **The turn — the field that makes distance safe.** Every idea carries the line the video says when it stops being about the subject and reaches the offer:
3572
+
3573
+ - It NAMES the client, by name.
3574
+ - It states the concrete thing the viewer GETS.
3575
+ - It follows on in ONE move, with no new argument.
3576
+ - 12 to 25 words, sayable out loud.
3577
+
3578
+ | | |
3579
+ |---|---|
3580
+ | ✅ | "Churchill demanded one page. Prism applies the same discipline to whole books — habits, business, history, five minutes each." |
3581
+ | ❌ | "Nobody gives anything an hour any more." |
3582
+
3583
+ The second names nobody and gives nothing. It is a theme, not a turn. **If you cannot write the turn, drop the subject** — do not soften it, and do not pad the count. That discard rule is also what protects a thin client: some offers have a deep cultural hinterland and some do not, and a short honest pool beats a long strained one.
3584
+
3585
+ This is Gate 3 of the `hooks` harness — offer adjacency — applied one level up, at the subject rather than the hook.
3586
+
3587
+ **The proof type — or the whole list becomes trivia.** Demand concrete detail and every idea arrives as "in 1978, a man…". That is the same flatness in a new costume. Declare what kind of concrete thing carries each subject, and spread across all five:
3588
+
3589
+ | Proof type | What it is |
3590
+ |---|---|
3591
+ | `historical` | a dated event, a person from the past, an old record |
3592
+ | `present_number` | a number true today: a price, a count, a duration, a share |
3593
+ | `ranking` | an order: best to worst, a tier list, a bracket, a league table |
3594
+ | `named_person` | someone the audience can look up, alive now |
3595
+ | `everyday_observation` | something the viewer can check in their own life, no source |
3596
+
3597
+ Use each at least once; use none more than three times.
3598
+
3599
+ **Flag what needs checking.** A distant subject means dates, numbers and named people, and a video that states a false fact is the *client's* problem once it is posted. Mark every idea that carries one, and put the instruction on the brief: *"Confirm this before you film. If you cannot confirm it, film the story version and drop the claim."*
3600
+
3601
+ ### Prompting a model for range
3602
+
3603
+ The clauses below are what actually move the output. Copy the wording, not the intent.
3604
+
3605
+ 1. **Define the unit, and say what it is not.** *"It is not the product. It is not the problem. It is not the product's own category."* A negative definition beats three adjectives.
3606
+ 2. **Make concreteness a required field, not an adjective.** *"Every item must carry `objects`: named people, years, numbers, records, institutions. An item with only abstract nouns is rejected."* A required field is a constraint; an instruction in prose is a suggestion.
3607
+ 3. **Quota the axis you want spread on.** *"5 at distance 1, 5 at distance 2, 2 at distance 3."* Never "spread them out".
3608
+ 4. **Withhold the product until last.** Order the prompt audience → what they already care about → offer. A prompt that opens with "this is a book summary app" returns fifty videos about book summaries whatever the rules underneath say. Free, and it changes the whole answer.
3609
+ 5. **Ban the obvious answer by name.** *"A far title may not use these words: book, summary, audiobook."* The model's first three ideas are always the banned ones. Read the ban list off the offer, per client.
3610
+ 6. **Require the return path, then push distance hard.** The turn, plus the discard rule.
3611
+ 7. **Pass the titles already written, and forbid a repeat.** *"Do not write a video that would be the SAME VIDEO as any of them. A different frame on the same subject is the same video."* The only dedup that works across calls.
3612
+
3613
+ **Three that kill range:** one call per item — N calls are N samples from one mode, so generate a SET in one call · "be creative" — zero constraint, and the model already believes it is · an example with no anti-example — always show the flat version beside the good one.
3614
+
3615
+ **Count the result in code, not in the prompt.** The model proposes; a quota checked afterwards is the only one that holds. Report the misses by name and let the director decide — a counter that silently repairs is a counter nobody trusts.
3616
+
3485
3617
  ### The 50 frames
3486
3618
 
3487
3619
  **Arc & subject frames — what the video is the story of**
@@ -3750,6 +3882,9 @@ The frame also suggests how to build it, which saves a planning round:
3750
3882
  - **Do not treat the frame as the hook.** "The history of X" spoken flat at `start:0` is a banned opener shape — the hook still has to name a situation. `references/hooks-and-virality.md`.
3751
3883
  - **Do not ship a set that sits at one awareness stage.** It is the single most common defect in a month of posts, and neither the frame nor the angle can repair it. Label the stage on every idea so the imbalance is visible before production, not after.
3752
3884
  - **Do not sell at stage 1 or 2.** The ask belongs to the stage. A link in a stage-1 video costs the reach that made the video worth making.
3885
+ - **Do not hand over a distant idea with no turn written.** A video about Napoleon's field library, for a book app, is a great video and a dead one until somebody writes the line that reaches the offer. Write the turn with the idea, in the same breath, or drop the idea.
3886
+ - **Do not let the whole list become dated trivia.** "Be concrete" pushes a model straight into "in 1978, a man…". Fifty history videos is one idea fifty times, exactly like the frozen subject. Spread the proof types.
3887
+ - **Do not state a date, a number or a record without flagging it for checking.** The editor films what the brief says. A wrong figure is the director's problem the moment it posts, not the writer's.
3753
3888
 
3754
3889
  ## Primitive: image_remove_background
3755
3890
 
package/SKILL.md CHANGED
@@ -69,6 +69,7 @@ vidfarm templates search "ugc testimonial" # find a published temp
69
69
  vidfarm fork <templateId> # → forkId
70
70
  vidfarm pull <forkId> --dir ./work # composition.html + .harness/ to disk
71
71
  vidfarm public-raws --category greenscreen --limit 20 # source footage, $0
72
+ vidfarm public-raws --query actor_<uuid> # same person's other clips (id is in each card's summary)
72
73
  # edit ./work/composition.html — parse, edit, re-serialize the DOM; never string-concat
73
74
  vidfarm tts "Your cold emails are landing in spam." --dir ./work # free local voice
74
75
  vidfarm qa ./work # optional free local slop check
@@ -93,6 +94,7 @@ vidfarm publish <forkId> # push edits back to th
93
94
  | "Make a graphic / sticker / illustration" in cost mode `minimize` | free stock → IconScout → `vidfarm mask` → **generate it with YOUR OWN image tool if this subscription includes one** (Antigravity / Gemini CLI, Codex / ChatGPT — Claude Code has none): `vidfarm agent-image "<subject>"`. $0, and no `--yes` needed | `references/agent-included-imagegen.md` |
94
95
  | A free plan / a 402 / cost mode `minimize` on any search or download | `vidfarm browser setup`, then `vidfarm browse videos\|images\|news\|page "<q>"`. Never answer a sourcing ask with "that needs a paid plan." | `references/browser-harness.md` |
95
96
  | "Make a video about what just happened" | two stages: `vidfarm news-search "<topic>" --fresh w` for the STORY, then `video-search` for the VISUALS | `vidfarm.cc/experimental/google-news-to-video.md` |
97
+ | "Recaption this meme" / "make a meme for our product" / "a reaction video" | cast a MemeScreens raw for the caption's **verb** (`vidfarm public-raws --category greenscreen`), key it onto a background world, one static caption. $0 | `vidfarm.cc/experimental/meme-recaption.md` |
96
98
  | "Download this video from `<url>`" | `vidfarm download-video <url>` (paid). Free plan gets a 402 — `vidfarm browse page "<url>"` and save it from their Chrome yourself, else have them download it, then `vidfarm put-file`. Never answer "I can't." | `references/browser-harness.md` |
97
99
  | "Turn this thread / subreddit / profile into a video" | `vidfarm recycle <source>` — returns the raw decomposition, unranked; you pick the hook (paid) | `references/assets-and-sourcing.md` |
98
100
  | "Create an avatar / spokesperson / talking head" | `vidfarm avatar "<who>" --say "<line>"` — a greenscreen talking-head video, keyed in the same job | `references/primitives.md` |
@@ -146,7 +148,7 @@ The standards themselves are not optional. `vidfarm qa <dir>` is — it is a fre
146
148
  - **Write the four charges before the timeline.** Hook: first line is a complete clause naming a situation, on screen at `start:0`. Loop: one open question by 0:10, closing inside this video. Payoff: shown, not summarized. Bait: one ask, final beat. Banned openers — throat-clearing, a logo, a title card, a fade from black.
147
149
  - **No HTML slop.** Test: could you have made this element inside TikTok's own editor? If you reached past that — a padded capsule, border, gradient fill, blur panel, card, CTA "button", benefit chips, feature grid — cut it. Nothing in a video is clickable. Emphasize with size, weight, caps, an accent colour, or a drawn mark.
148
150
  - **No layout templates — judge the whole frame.** The stack is the tell, not the box: headline + subheadline + CTA centred in a well is a landing page even with every border, fill and capsule stripped off. Banned at frame level — a modal/dialog floating over a dimmed or blurred backdrop, a full-frame wash used to stage a floating block, nav/hero/feature-row sections, a blurred website screenshot as the background plate, document margins. Unstack it into time: hook at t=0, support line on the next cut, CTA spoken or a bare caption. Are you arranging words relative to each other, or relative to the picture? Two text runs at once is the ceiling.
149
- - **Captions**: imported display font (Montserrat 700–900 default), ~36–64px on a 1080 frame, inside the 8%–85% safe zone, and within that band placed in the **emptiest part of the frame**. Long narration pages into 3–5-word cues. Exactly one of four backgrounds: `outline`, `plain`, active-word `spotlight`/`karaoke`, or a tight `highlight-solid` band.
151
+ - **Captions**: an imported display font — **only 5 are allowed**: TikTok Sans, Montserrat (700–900 default), Abel, Source Code Pro, Yesteryear; anything else (Inter/Roboto/Arial/Georgia/a brand font) falls back to a web sans at render. The regime on one page, with a rendered specimen of each: <https://vidfarm.cc/fonts>. Size ~36–64px on a 1080 frame, inside the 8%–85% safe zone, and within that band placed in the **emptiest part of the frame**. Long narration pages into 3–5-word cues. Exactly one of four backgrounds: `outline`, `plain`, active-word `spotlight`/`karaoke`, or a tight `highlight-solid` band.
150
152
  - **The first frame is the thumbnail.** A real visual and the hook words at `start:0`. No fade-up, no entrance transition on the first clip. Check with `vidfarm stills <dir> --at 0`.
151
153
  - **Cut ruthlessly.** First assembly is 30–50% too long. Delete every beat that the video survives without, then ripple the hole closed. Length is an output, not a target.
152
154
  - **Review the whole video, never one frame.** Tile ~12 stills with `vidfarm stills <dir> --sheet` and read it as an image — one type scale, one palette, deliberate pacing, clean joins. Verify audio by measurement (~12–15 dB speech-over-bed, peak <0 dBFS). Report what you measured separately from what you judged. Your own "looks good" is the least reliable signal here.
@@ -191,4 +193,6 @@ Otherwise fetch `https://vidfarm.cc/skill-pack/vidfarm/files/<path>`. Load one f
191
193
  | `recipes/*.md` | `find-and-fork-template` · `retheme-template` · `local-edit-render-approve` · `onboard-a-new-director` · `bulk-scripting-with-a-harness` · `cutout-graphics-for-explainers` |
192
194
  | `harnesses/README.md` | anything harness-shaped — start here. Bases: `short-form` · `hooks` · `ugc-testimonial` · `explainer` · `product-demo` · `product-explainer` |
193
195
 
194
- Also served at `vidfarm.cc`: `/experiments.md` (ad testing), `/marketplace.md` (producing a batch with gigworkers), `/update.md` (upgrade runbook), `/experimental` (methods under live testing), `/skill/vidfarm-platform` (architecture), `/skill/hyperframes` (composition-authoring craft — route broad "make me a video" asks here first).
196
+ Also served at `vidfarm.cc`: `/experiments.md` (ad testing), `/marketplace.md` (the marketplace manual — routes you to one of the two side harnesses below), `/marketplace-buyer.md` (**buyer side**: commission videos from the crowd), `/agentic-clipper.md` (**worker side**: "Agentic Clipper" mode — one orchestrator on a long-horizon earning mission, one subagent per task), `/update.md` (upgrade runbook), `/experimental` (format harnesses under live testing — the index a clipper routes tasks against), `/skill/vidfarm-platform` (architecture), `/skill/hyperframes` (composition-authoring craft — route broad "make me a video" asks here first).
197
+
198
+ **The two marketplace harnesses never mix.** A buyer's agent must never load the clipper loop and a clipper must never load the buyer loop. If the user's side is unclear, ask: *"Do you want videos made for you, or do you want to get paid to make videos for other people?"*
package/clipper.md CHANGED
@@ -5,6 +5,15 @@ description: Run an agentic clipper loop — earn side income by having your own
5
5
 
6
6
  # /clipper — the agentic clipper loop
7
7
 
8
+ > **This file is the beginner walkthrough for ONE gig**: phase by phase, with a scaffolded project
9
+ > the user can read six months later. It is the right file when someone hands you a single invite
10
+ > link and asks you to set it up with them.
11
+ >
12
+ > **For the full marketplace mission — one orchestrator working MANY machines over weeks, a subagent
13
+ > per task, format routing, risk-scoring the gigs, and the earnings ledger — read
14
+ > `https://vidfarm.cc/agentic-clipper.md`.** That is what "run in agentic clipper mode" means.
15
+ > This file and that one agree; that one is the wider loop and this one is the first gig inside it.
16
+
8
17
  **You are the user's own AI agent, running on their computer.** Your job is to set up and then
9
18
  run a loop that earns them money:
10
19
 
@@ -266,6 +275,19 @@ This is ordinary Vidfarm director work — **the full method is in
266
275
  <https://vidfarm.cc/skill.md> and the director skill it points to. Follow that, not a shortcut.**
267
276
  What matters for a clipper loop specifically:
268
277
 
278
+ **First, decide what FORMAT the task is, and follow that format's harness.** A product explainer
279
+ built like a meme recaption gets rejected, and so does the reverse. Read the live index at
280
+ <https://vidfarm.cc/experimental> and pick — a product/feature introduction routes to
281
+ `unique-product-explainers.md`, a recaption or reaction routes to `meme-recaption.md`, a timely
282
+ event routes to `google-news-to-video.md`. Nothing fits? Fall back to a CLI base
283
+ (`vidfarm harness list`) — and if no harness fits at all, **freestyle it against the best practices
284
+ in <https://vidfarm.cc/skill.md>.** That is a normal outcome, not a failure: harnesses reproduce a
285
+ *known* format, and a task outside every known format is still one you can deliver well. Never force
286
+ a task into the wrong harness, and never decline one just because no harness matched.
287
+ **If the task payload carries a `format_harness` field, that wins over your own judgement** — the
288
+ buyer chose. Record which harness you used (or `freestyle`), so over a month you learn which formats
289
+ actually get approved for this user.
290
+
269
291
  **Set the cost posture once, at setup:**
270
292
 
271
293
  ```bash
package/dist/src/cli.js CHANGED
@@ -1144,6 +1144,9 @@ Marketplace (paid, cloud-only — the bazaar never renders locally):
1144
1144
  --file-id <id> Move a FILE; omit to move the folder (same root, e.g. /raws/demos → /raws/archive)
1145
1145
  directory copy <path> [<to-folder>] Duplicate a file/folder (shared S3 obj) → POST /api/v1/user/me/directory/copy
1146
1146
  --file-id <id> --as <name> Copy a FILE; omit --file-id to copy the folder; --as renames the copy
1147
+ directory note <path> Read/write the VECTOR NOTE of a file or FOLDER → GET|PUT /api/v1/user/me/directory/note
1148
+ --file-id <id> Annotate a FILE; omit to annotate the FOLDER at <path>
1149
+ --set "<text>" What the thing IS, in plain words — embedded, so search finds it by meaning
1147
1150
  directory save-url <url> Save a durable media URL INTO My Files at a folder → POST /api/v1/user/me/attachments/from-url
1148
1151
  --folder <path> Destination folder under /files (e.g. inpaints, promos)
1149
1152
  --as <name> Name the saved file · --notes <text> vector-embedded notes
@@ -1161,7 +1164,10 @@ Marketplace (paid, cloud-only — the bazaar never renders locally):
1161
1164
  vidfarm gigs proofs --status pending · vidfarm gigs approve PRF_01H…
1162
1165
  (see: vidfarm gigs help)
1163
1166
  shared <sub> <link> USE a link someone shared with you — NO account, NO API key
1164
- info · ls · search · mkdir · put · get e.g. vidfarm shared put <link> clip.mp4 --subfolder batch-01
1167
+ info · ls --tree · search · grab · note · mkdir · put · get
1168
+ grab is the one to reach for: it finds footage by MEANING and downloads it in one step.
1169
+ e.g. vidfarm shared grab <link> "founder talking head, kitchen" --out ./assets
1170
+ vidfarm shared put <link> cut-v2.mp4 --subfolder batch-01 --note "Final cut, 9:16"
1165
1171
  (the gigworker/agent side of directory share; see: vidfarm shared help)
1166
1172
  put-file / get-file / files / annotate-file are the My Files (persistent) set;
1167
1173
  upload is the throwaway temp store for dropping media into a composition.
@@ -2741,7 +2747,7 @@ Rules:
2741
2747
  - When swapping visuals, match both the literal scene DNA and the narrative purpose of the beat.
2742
2748
  - For replacement graphics, screenshots, or still-like scenes, prefer AI image generation plus Ken Burns before paying for AI video unless static_vs_pivot says motion footage is load-bearing.
2743
2749
  - If narration must be customized, default to premium ElevenLabs first, then the user's own ElevenLabs path, then BYOK OpenAI/Gemini/OpenRouter. If captions or scenes were timed to the old VO, retime them to the new narration.
2744
- - NO HTML SLOP. You are editing HTML, but the output is a social video, not a web page. THE TEST IS THE NATIVE-EDITOR TEST: could you have made this element with the tools inside TikTok's own editor? That toolset is a font, a color, a stroke/outline, a soft shadow, a tight text box, alignment, opacity, 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. Never author landing-page furniture: CTA "buttons" (a filled/gradient rounded capsule with action copy like "Sign Up for a Free Trial →"), benefit chip/badge rows ("✓ No Credit Card Needed"), bordered/shadowed/frosted cards holding a headline + URL, gradient text fills, feature grids, bulleted lists, or web-default fonts (Inter/Roboto/Arial/system-ui). AND NOT A SINGLE PILL EITHER: one lonely rounded, padded, filled capsule around a static stat or label — "10 hrs / week", "STEP 2", "EP.01", "+40%" — is a web badge, and being the only one on screen does not make it native. The ONLY legitimate capsule in a video is the active-word spotlight/karaoke caption highlight, because it moves with the spoken word. Emphasize a stat the way the editor would: bigger, heavier, ALL-CAPS, an accent color, a hand-drawn circle or underline, or its own beat on screen. Rule of thumb on anything holding words: border-radius over ~8px PLUS a background fill PLUS padding = a badge; drop the fill or drop the radius until the band hugs the glyphs. None of this appears in a real TikTok, and nothing in a video is clickable — say it as timed text on the footage instead. 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. Captions use an imported family (Montserrat default / TikTok Sans / Abel / Source Code Pro / Yesteryear) at weight 700-900, ~36-64px on a 1080-wide frame, inside the 8%-85% safe zone, with exactly one of four backgrounds: outline, plain, an active-word spotlight/karaoke pill, or a tight-hugging solid band (radius <=8px, no border/shadow/gradient/blur).
2750
+ - NO HTML SLOP. You are editing HTML, but the output is a social video, not a web page. THE TEST IS THE NATIVE-EDITOR TEST: could you have made this element with the tools inside TikTok's own editor? That toolset is a font, a color, a stroke/outline, a soft shadow, a tight text box, alignment, opacity, 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. Never author landing-page furniture: CTA "buttons" (a filled/gradient rounded capsule with action copy like "Sign Up for a Free Trial →"), benefit chip/badge rows ("✓ No Credit Card Needed"), bordered/shadowed/frosted cards holding a headline + URL, gradient text fills, feature grids, bulleted lists, or web-default fonts (Inter/Roboto/Arial/system-ui). AND NOT A SINGLE PILL EITHER: one lonely rounded, padded, filled capsule around a static stat or label — "10 hrs / week", "STEP 2", "EP.01", "+40%" — is a web badge, and being the only one on screen does not make it native. The ONLY legitimate capsule in a video is the active-word spotlight/karaoke caption highlight, because it moves with the spoken word. Emphasize a stat the way the editor would: bigger, heavier, ALL-CAPS, an accent color, a hand-drawn circle or underline, or its own beat on screen. Rule of thumb on anything holding words: border-radius over ~8px PLUS a background fill PLUS padding = a badge; drop the fill or drop the radius until the band hugs the glyphs. None of this appears in a real TikTok, and nothing in a video is clickable — say it as timed text on the footage instead. 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. Captions use one of the FIVE imported families and nothing else (Montserrat default / TikTok Sans / Abel / Source Code Pro / Yesteryear - the full regime, with a rendered specimen of each, is at https://vidfarm.cc/fonts) at weight 700-900, ~36-64px on a 1080-wide frame, inside the 8%-85% safe zone, with exactly one of four backgrounds: outline, plain, an active-word spotlight/karaoke pill, or a tight-hugging solid band (radius <=8px, no border/shadow/gradient/blur).
2745
2751
  - NO LAYOUT TEMPLATES — JUDGE THE WHOLE FRAME, NOT JUST THE ELEMENT. Every rule above judges one element, and a frame can pass element-by-element and still be a web page. The archetype is the MODAL: the backdrop dimmed and blurred out of focus, and floating on top of it a rounded bordered box holding a big headline, a smaller support line, and a fat CTA button. THE STACK IS THE TELL, NOT THE BOX — delete the border, the fill and the capsule, keep headline then subheadline then CTA centred in a well with even margins, and it STILL reads as a landing page, because a viewer recognizes the SHAPE before reading a single word. Banned at frame level: a modal/dialog staged on top of a backdrop that has been dimmed, blurred, greyed or scaled back (nothing in a video pops "above" the video); the hero triplet and its cousins (title + kicker + logo lockup, question + answer + URL); a full-frame dark wash used to stage a floating block (a legibility band on ONE caption is legal, a page-wide wash to stage a panel is not; likewise a blurred backdrop is fine alone — a blurred fill behind a 16:9 clip in a 9:16 frame is a real technique — but blur PLUS dimming is modal staging); nav strip / hero / three-up feature row / testimonial block / footer fine print; a blurred website screenshot used as the background plate (if the backdrop is a web page, the frame is a screen recording of a web page — show the real product UI full-bleed and in focus, or don't show it); a centred content column with even gutters and document margins. THE FIX IS ALWAYS TO UNSTACK IT INTO TIME: the headline is the hook at start:0, the support line lands on the next cut, the CTA is SPOKEN or a bare caption on the final frame. You lose nothing — a viewer reads one line at a time anyway — and you gain the pacing that makes it look shot rather than designed. Self-check before you place any text group: am I arranging words relative to EACH OTHER, or relative to the PICTURE? Relative to each other is a layout, which is web. Two on-screen text runs at once is the ceiling. Verify on real pixels: \`vidfarm stills . --at <t>\` — if the still could be a screenshot of a website, rebuild the beat. \`vidfarm qa\` catches only the mechanical half (layout-template, modal-scrim); the frame-level judgement is yours.
2746
2752
  - STRUCTURE BEFORE POLISH — THE FOUR CHARGES, WRITTEN BEFORE YOU TOUCH THE TIMELINE. 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) HOOK — write the opening line as text first: a complete clause (subject + verb), no jargon, naming a SITUATION ("I've quit six businesses") not a label ("anonymity"); it goes on screen at start:0, because caption chunk 1 is read before any audio and muted autoplay is the default. Banned openings: throat-clearing ("so I was thinking", "here's the thing"), a logo, a title card, a fade from black, context before the claim. (2) LOOP — one open question by 0:10, said ON SCREEN, closing INSIDE this video (state the timestamp it closes at; if you can't, there is no loop), and the withheld answer must be one the viewer CANNOT supply themselves — a formally-correct loop with a guessable answer passes every mechanical check and dies in the field. (3) PAYOFF — shown, not summarized, ≥5 uninterrupted seconds, landing BEFORE the final beat; the payoff is not the CTA. (4) BAIT — one ask in the final beat and in the post caption; a keyword comment ask ("comment CLIPPER and I'll send the breakdown") is standard and allowed, but never "follow for part two", ragebait, or an earnings/health claim traded for the reply. Then build the timeline. Re-theming a decomposed template: viral_dna already names the source's hook/retention/payoff — rebuild each charge for the new subject, never flatten the loop into a product statement. Full craft harness: the vidfarm skill's references/hooks-and-virality.md. Checkable form: \`vidfarm harness show hooks\`.
2747
2753
  - ORIENT THE COLD VIEWER IN THE FIRST 3 SECONDS — THE VIEWER HAS NO CONTEXT AND DID NOT CHOOSE THIS VIDEO. Distinct from the hook: the hook makes them WANT to watch, orientation makes the watching POSSIBLE. A stranger mid-scroll must be able to answer three things by ~3s — what am I looking at (the CATEGORY noun), who is it for, and why is this on my screen (the situation). The failure is not a bad first frame, it is a good video that BEGINS AT BEAT TWO, and the author cannot see it because the author already knows what the thing is. Signatures, each a rebuild not a polish: a pronoun with no referent ("it just works", "this changes everything", "here's how they do it"); starting at step three (the process already running, the dashboard already full); a metaphor whose subject only lands at 6s; insider vocabulary, a product's own feature name, or an ACRONYM in the first line; a detail crop that reads as texture until you know the whole. Instead, the opening beat is BOTH channels at once: an EASY IMAGE (one large subject, already moving, legible at a glance and at thumbnail scale — a relevant die-cut sticker names the category before a word is read) AND an EASY LINE (first spoken sentence one clause, <=12 words, everyday words, concrete noun + verb, no subordinate clause, brand name said once plainly, and the CATEGORY named: "X is a language app that…"). Give the SITUATION, not the label — "the end of the month, and your receipts are in a shoebox" orients, "expense automation" does not. THIS IS NOT AN INTRO AND COSTS NO EXTRA SECONDS: it replaces the wind-up sentence, it never precedes it, and it never licenses a logo, a title card or a fade from black. Test it on the render, not the script: play the first 3 seconds ONLY to somebody with no context and stop — they should say what kind of thing it is and roughly who it is for. "Something about audio" is a fail. Fullest form: \`vidfarm harness show product-explainer\` (Rule 0).
@@ -11104,6 +11110,17 @@ const DIRECTORY_HELP = `vidfarm directory — browse the unified file tree (/fil
11104
11110
  e.g. vidfarm directory copy /raws/demos /raws/archive
11105
11111
  vidfarm directory copy /files/brand/logo.png /files/inbox --file-id att_123 --as logo-copy.png
11106
11112
 
11113
+ directory note <path> Read the VECTOR NOTE of a file or FOLDER → GET /api/v1/user/me/directory/note
11114
+ --file-id <id> Annotate a FILE; omit to annotate the FOLDER at <path>
11115
+ --set "<text>" Write the note ("" clears it). It is embedded, so the
11116
+ thing is findable by MEANING — a file name is not.
11117
+ --json
11118
+ e.g. vidfarm directory note /raws/AboutOffer/BRoll --set "Kitchen b-roll, no faces, 9:16"
11119
+ vidfarm directory note /files/brand/logo.png --file-id att_123
11120
+ Notes live on /files entries, /raws entries and FOLDERS in any writable root.
11121
+ Uploads self-describe when the account has an AI key saved (skipped over 100 MB);
11122
+ with no key the note is yours to type, and search falls back to keyword-only.
11123
+
11107
11124
  directory save-url <url> Save a durable media URL INTO My Files at a folder → POST /api/v1/user/me/attachments/from-url
11108
11125
  --folder <path> Destination folder under /files (e.g. inpaints, promos)
11109
11126
  --as <name> Name the saved file (else derived from the URL)
@@ -11155,6 +11172,10 @@ async function runDirectoryCommand(argv) {
11155
11172
  case "copy":
11156
11173
  case "cp":
11157
11174
  return runDirectoryCopy(rest);
11175
+ case "note":
11176
+ case "notes":
11177
+ case "annotate":
11178
+ return runDirectoryNote(rest);
11158
11179
  case "save-url":
11159
11180
  case "from-url":
11160
11181
  case "import-url":
@@ -11247,6 +11268,55 @@ async function runDirectorySearch(argv) {
11247
11268
  }
11248
11269
  printDirectorySearch(merged, query, spaces.length > 1 ? "both" : spaces[0]);
11249
11270
  }
11271
+ // Read or write the VECTOR NOTE of a file or folder. The note is the plain-text
11272
+ // answer to "what is this?", embedded so search finds it from any phrasing —
11273
+ // which is the difference between a folder of IMG_4821.mp4 and a usable library.
11274
+ async function runDirectoryNote(argv) {
11275
+ const parsed = parseArgs({
11276
+ args: argv,
11277
+ allowPositionals: true,
11278
+ options: { ...commonOptions(), "file-id": { type: "string" }, set: { type: "string" }, note: { type: "string" } }
11279
+ });
11280
+ const ctx = commonContext(parsed.values);
11281
+ const targetPath = parsed.positionals[0];
11282
+ if (!targetPath) {
11283
+ throw new Error('directory note requires a path: vidfarm directory note /raws/BRoll [--file-id <id>] [--set "what this is"]');
11284
+ }
11285
+ const fileId = parsed.values["file-id"];
11286
+ const next = parsed.values.set ?? parsed.values.note;
11287
+ const space = targetSpaces(ctx.target)[0];
11288
+ if (next == null) {
11289
+ const result = await dispatch(ctx, { method: "GET", path: "/api/v1/user/me/directory/note", query: { path: targetPath, id: fileId } }, space);
11290
+ assertApiOk(result, "directory note");
11291
+ if (ctx.json) {
11292
+ printJson(result.json ?? result.text);
11293
+ return;
11294
+ }
11295
+ const note = result.json?.note;
11296
+ console.log(`${BOLD}${targetPath}${RESET}`);
11297
+ console.log(note ? ` ${note}` : ` ${DIM}(no vector note yet)${RESET}`);
11298
+ if (!note)
11299
+ console.log(` ${DIM}Write one: vidfarm directory note ${targetPath}${fileId ? ` --file-id ${fileId}` : ""} --set "what this is"${RESET}`);
11300
+ return;
11301
+ }
11302
+ const result = await dispatch(ctx, {
11303
+ method: "PUT",
11304
+ path: "/api/v1/user/me/directory/note",
11305
+ body: { path: targetPath, ...(fileId ? { id: fileId } : {}), note: next }
11306
+ }, space);
11307
+ assertApiOk(result, "directory note");
11308
+ if (ctx.json) {
11309
+ printJson(result.json ?? result.text);
11310
+ return;
11311
+ }
11312
+ const saved = result.json?.note;
11313
+ console.log(`${GREEN}✓${RESET} ${saved ? "note saved on" : "note cleared on"} ${BOLD}${targetPath}${RESET}`);
11314
+ if (saved) {
11315
+ console.log(result.json?.embedded
11316
+ ? ` ${DIM}searchable by meaning${RESET}`
11317
+ : ` ${DIM}saved, but keyword-search only — add a gemini/openai key to embed it${RESET}`);
11318
+ }
11319
+ }
11250
11320
  async function runDirectoryRename(argv) {
11251
11321
  const parsed = parseArgs({
11252
11322
  args: argv,
@@ -11568,6 +11638,20 @@ function mergeSearch(parts, limit) {
11568
11638
  };
11569
11639
  }
11570
11640
  // Compact "12.3s · 4.2 MB · video/mp4" line for a directory file item.
11641
+ // ── vector notes in list/search output ───────────────────────────────────────
11642
+ // The note is what a human (or an agent) recognises the item by, so it prints
11643
+ // under the name; the mark makes a MISSING note visible as a gap to fill.
11644
+ function noteLine(item) {
11645
+ const note = typeof item?.note === "string" ? item.note.replace(/\s+/g, " ").trim() : "";
11646
+ if (!note)
11647
+ return "";
11648
+ return note.length > 110 ? `${note.slice(0, 109)}…` : note;
11649
+ }
11650
+ function noteMark(item) {
11651
+ if (item?.note === undefined)
11652
+ return ""; // backend carries no note field
11653
+ return item.note ? `${GREEN}✓${RESET} ` : `${DIM}✎${RESET} `;
11654
+ }
11571
11655
  function directoryFileMeta(item) {
11572
11656
  const parts = [];
11573
11657
  if (typeof item?.durationSec === "number" && item.durationSec > 0)
@@ -11598,11 +11682,17 @@ function printDirectoryListing(data, requestedPath, space) {
11598
11682
  console.log(` ${DIM}(empty)${RESET}`);
11599
11683
  }
11600
11684
  for (const f of folders) {
11601
- console.log(` ${DIM}dir ${RESET} ${originBadge(f)}${f?.name ?? ""}/ ${DIM}${f?.path ?? ""}${RESET}`);
11685
+ console.log(` ${DIM}dir ${RESET} ${noteMark(f)}${originBadge(f)}${f?.name ?? ""}/ ${DIM}${f?.path ?? ""}${RESET}`);
11686
+ const note = noteLine(f);
11687
+ if (note)
11688
+ console.log(` ${DIM}${note}${RESET}`);
11602
11689
  }
11603
11690
  for (const f of files) {
11604
11691
  const meta = directoryFileMeta(f);
11605
- console.log(` file ${originBadge(f)}${f?.name ?? ""}${meta ? ` ${DIM}${meta}${RESET}` : ""} ${DIM}${f?.path ?? ""}${RESET}`);
11692
+ console.log(` file ${noteMark(f)}${originBadge(f)}${f?.name ?? ""}${meta ? ` ${DIM}${meta}${RESET}` : ""} ${DIM}${f?.path ?? ""}${RESET}`);
11693
+ const note = noteLine(f);
11694
+ if (note)
11695
+ console.log(` ${DIM}${note}${RESET}`);
11606
11696
  if (f?.viewUrl)
11607
11697
  console.log(` ${FRONTEND}${f.viewUrl}${RESET}`);
11608
11698
  }
@@ -11623,7 +11713,10 @@ function printDirectorySearch(data, query, space) {
11623
11713
  results.forEach((r, i) => {
11624
11714
  const score = typeof r?.score === "number" ? ` ${DIM}score=${r.score.toFixed(3)}${RESET}` : "";
11625
11715
  const kind = r?.kind === "folder" ? "dir " : "file";
11626
- console.log(` ${String(i + 1).padStart(2)}. ${kind} ${originBadge(r)}${r?.path ?? r?.name ?? ""}${score}`);
11716
+ console.log(` ${String(i + 1).padStart(2)}. ${kind} ${noteMark(r)}${originBadge(r)}${r?.path ?? r?.name ?? ""}${score}`);
11717
+ const note = noteLine(r);
11718
+ if (note)
11719
+ console.log(` ${DIM}${note}${RESET}`);
11627
11720
  const meta = directoryFileMeta(r);
11628
11721
  if (meta)
11629
11722
  console.log(` ${DIM}${meta}${RESET}`);