@officexapp/vidfarm-devcli 0.21.34 → 0.21.36

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.
Files changed (28) hide show
  1. package/.agents/skills/editor-capabilities/SKILL.md +14 -3
  2. package/.agents/skills/vidfarm/SKILL.md +66 -33
  3. package/.agents/skills/vidfarm/harnesses/README.md +112 -0
  4. package/.agents/skills/vidfarm/{regimes/explainer.QA_REGIME.md → harnesses/explainer.HARNESS.md} +3 -2
  5. package/.agents/skills/vidfarm/{regimes/hooks.QA_REGIME.md → harnesses/hooks.HARNESS.md} +3 -3
  6. package/.agents/skills/vidfarm/{regimes/product-demo.QA_REGIME.md → harnesses/product-demo.HARNESS.md} +1 -1
  7. package/.agents/skills/vidfarm/{regimes/short-form.QA_REGIME.md → harnesses/short-form.HARNESS.md} +39 -10
  8. package/.agents/skills/vidfarm/{regimes/ugc-testimonial.QA_REGIME.md → harnesses/ugc-testimonial.HARNESS.md} +3 -3
  9. package/.agents/skills/vidfarm/recipes/{bulk-scripting-with-a-regime.md → bulk-scripting-with-a-harness.md} +20 -12
  10. package/.agents/skills/vidfarm/recipes/cutout-graphics-for-explainers.md +43 -13
  11. package/.agents/skills/vidfarm/recipes/local-edit-render-approve.md +1 -1
  12. package/.agents/skills/vidfarm/references/automation-and-local-dev.md +77 -26
  13. package/.agents/skills/vidfarm/references/editor-workflows.md +18 -5
  14. package/.agents/skills/vidfarm/references/hooks-and-virality.md +65 -7
  15. package/.agents/skills/vidfarm/references/reviewing-renders.md +2 -1
  16. package/.agents/skills/vidfarm-media/SKILL.md +2 -2
  17. package/.agents/skills/vidfarm-media/references/tts.md +26 -4
  18. package/SKILL.director.md +292 -98
  19. package/SKILL.md +33 -15
  20. package/dist/src/cli.js +1200 -141
  21. package/dist/src/devcli/handoff.js +54 -33
  22. package/dist/src/devcli/{qa-regime.js → harness.js} +132 -55
  23. package/dist/src/devcli/plate-key.js +698 -0
  24. package/dist/src/devcli/qa-check.js +209 -4
  25. package/dist/src/devcli/skill-docs.js +136 -0
  26. package/dist/src/devcli/sticker-pack.js +48 -0
  27. package/package.json +6 -4
  28. package/.agents/skills/vidfarm/regimes/README.md +0 -79
@@ -41,38 +41,62 @@ Send a stable `tracer` on export so retries are traceable and filterable in job
41
41
  | | **One-time video** | **Bulk / scripting mode** |
42
42
  |---|---|---|
43
43
  | The deliverable | One MP4 you both look at | A loop that produces N videos nobody watches frame-by-frame |
44
- | Quality control | Your eyes on the render | **A `QA_REGIME.md`** — the batch's written standard |
44
+ | Quality control | Your eyes on the render | **A `HARNESS.md`** — the batch's written standard |
45
45
  | What you optimize | This video | The *variant axis* (one thing changes; everything else is held) |
46
46
  | Cost posture | Per-video decisions are fine | Per-video AI spend × N — reuse assets, prefer clip pools |
47
47
 
48
- A director who says "make me a video about X" usually wants the first. A director who says "I need to post daily" / "make 20 variants" / "test hooks" wants the second and often doesn't know it has a name. **Offer the upgrade explicitly:** *"Want this as one video, or should we set it up as a repeatable batch? Batches get a QA_REGIME.md so variant #37 is as good as #1."* Don't silently build a one-off when they asked for volume, and don't drag someone into a scripting harness when they wanted one clip.
48
+ A director who says "make me a video about X" usually wants the first. A director who says "I need to post daily" / "make 20 variants" / "test hooks" wants the second and often doesn't know it has a name. **Offer the upgrade explicitly:** *"Want this as one video, or should we set it up as a repeatable batch? Batches get a HARNESS.md so variant #37 is as good as #1."* Don't silently build a one-off when they asked for volume, and don't drag someone into a scripting harness when they wanted one clip.
49
49
 
50
- ### `QA_REGIME.md` — the quality contract for a batch
50
+ ### `HARNESS.md` — the reusable AI harness for a format
51
51
 
52
- `vidfarm qa`'s built-in rules are **universal** (no HTML slop, the font regime, the thumbnail frame) — the same for everyone, so they live in code. A regime is the opposite: it's what makes **this** director's **this** format good — their audience, hook shape, banned vocabulary, pacing, compliance line. It can't be hard-coded, so it lives next to the work as Markdown they own and version.
52
+ `vidfarm qa`'s built-in rules are **universal** (no HTML slop, the font regime, the thumbnail frame) — the same for everyone, so they live in code. A harness is the opposite: it's what makes **this** director's **this** format good — their audience, hook shape, banned vocabulary, pacing, compliance line, and the DNA of the template it came from. It can't be hard-coded, so it lives next to the work as Markdown they own and version.
53
53
 
54
- **It exists because bulk output loses its human reviewer.** One video gets eyes on every frame; fifty generated in a loop do not. The regime is what the loop grades against.
54
+ **It exists because bulk output loses its human reviewer.** One video gets eyes on every frame; fifty generated in a loop do not. The harness is what the loop grades against.
55
+
56
+ **Three director phrasings, one artifact:**
57
+
58
+ | They say | You run |
59
+ |---|---|
60
+ | "create me a harness" | `vidfarm harness init <base> --out ./work/HARNESS.md`, then edit it with them |
61
+ | "update the harness for this format" | open the file, add the rule **with its reason**, re-run `vidfarm qa` |
62
+ | "give me the harness for this template_id" | `vidfarm harness derive <templateId\|forkId>` — the **decomposition**, as a harness |
55
63
 
56
64
  ```bash
57
- vidfarm regime list # the bundled starting points
58
- vidfarm regime init short-form --out ./work/QA_REGIME.md # copy, then EDIT it
59
- vidfarm qa ./work # auto-picks up ./work/QA_REGIME.md
60
- vidfarm qa ./work --regime hooks --regime ./brand/HOUSE.md # built-in + your own file — they STACK
65
+ vidfarm harness list # the bundled starting points
66
+ vidfarm harness init short-form --out ./work/HARNESS.md # copy, then EDIT it
67
+ vidfarm harness derive <forkId> --out ./work/HARNESS.md # a decomposed template → a harness
68
+ vidfarm harness show ./work/HARNESS.md --dna visual # ONE strand, not the whole doc
69
+ vidfarm qa ./work # auto-picks up ./work/HARNESS.md
70
+ vidfarm qa ./work --harness hooks --harness ./brand/HOUSE.md # built-in + your own file — they STACK
61
71
  ```
62
72
 
63
- Bundled bases (`vidfarm regime list`, files under `.agents/skills/vidfarm/regimes/`): **`short-form`** (the default the four charges hook/loop/payoff/bait + the standalone rule), **`hooks`** (hook-variant batches: chunk-1 legibility, the unguessable test, the anti-patterns that only show up at volume), **`ugc-testimonial`**, **`explainer`**, **`product-demo`**. Each is a *starting point to edit*, never a house style to conform to — the parts that matter most are the parts the director adds. A regime can also be any file anywhere: `--regime ./campaigns/q3/RULES.md` is fully supported, and `VIDFARM_QA_REGIME=./work/QA_REGIME.md` sets a default for a whole run.
73
+ **A harness mirrors the template JSON's DNA vocabulary.** Every `## DNA` heading is indexed under the same key the decompose pass uses, so a derived harness and a hand-written one read the same:
74
+
75
+ | Strand | What lives there | Decompose source |
76
+ |---|---|---|
77
+ | **Viral DNA** | hook, retention mechanic, payoff, core emotion, contrast | `video-context.json` → `viral_dna` |
78
+ | **Visual DNA** | cut rhythm, energy curve, caption style/placement, b-roll, transitions | `editor-harness.json` → `pacing` / `typography` / `broll` |
79
+ | **Structural DNA** | the beats, their roles, which are load-bearing | `editor-harness.json` → `scenes`, `scene-annotations.json` |
80
+ | **Audio DNA** | voiceover, bed, SFX, comedic timing, intonation | `editor-harness.json` → `audio` / `emotional` |
81
+ | **Build DNA** | which paintbrush per beat, the free-tier path | `replication-harness.json` |
64
82
 
65
- **The format is two halves, and the split is deliberate:** a front-matter `checks:` block the CLI settles deterministically (duration, aspect, `hook_words_max`, `forbid_text`, `first_frame_text`, … full key list in `regimes/README.md`), and every `- [ ]` checkbox in the body, which comes back as a **review item for you to answer**. "Is the withheld answer one the viewer can't supply themselves?" is a judgment call; a linter claiming to settle it would be lying. **Answer the review items honestly in your report** — the CLI prints them precisely because it can't.
83
+ `harness derive` writes what the decompose pass actually recorded and marks the rest `unknown` — it never invents a strand to look complete. Treat its output as a **first draft**: the model watched the video, it didn't talk to the customer.
66
84
 
67
- **Build on it.** When you learn something from a batch ("the label-framed hooks all died"), write it into the regime as a new rule or checklist line. That is the artifact that compounds across runs; the composition files don't.
85
+ > Don't confuse `HARNESS.md` with the `.harness/` directory `vidfarm pull` writes. That directory is machine-generated context (`context.json`, `agent-guide.md`), regenerated on every pull never hand-edit it. `HARNESS.md` is the one the director owns.
68
86
 
69
- ### The bulk loop, with the regime in it
87
+ Bundled bases (`vidfarm harness list`, files under `.agents/skills/vidfarm/harnesses/`): **`short-form`** (the default — the four charges hook/loop/payoff/bait + the standalone rule), **`hooks`** (hook-variant batches: chunk-1 legibility, the unguessable test, the anti-patterns that only show up at volume), **`ugc-testimonial`**, **`explainer`**, **`product-demo`**. Each is a *starting point to edit*, never a house style to conform to — the parts that matter most are the parts the director adds. A harness can also be any file anywhere: `--harness ./campaigns/q3/RULES.md` is fully supported, and `VIDFARM_HARNESS=./work/HARNESS.md` sets a default for a whole run.
88
+
89
+ **The format is two halves, and the split is deliberate:** a front-matter `checks:` block the CLI settles deterministically (duration, aspect, `hook_words_max`, `forbid_text`, `first_frame_text`, … — full key list in `harnesses/README.md`), and every `- [ ]` checkbox in the body, which comes back as a **review item for you to answer**. "Is the withheld answer one the viewer can't supply themselves?" is a judgment call; a linter claiming to settle it would be lying. **Answer the review items honestly in your report** — the CLI prints them precisely because it can't.
90
+
91
+ **Build on it.** When you learn something from a batch ("the label-framed hooks all died"), write it into the harness as a new rule or checklist line. That is the artifact that compounds across runs; the composition files don't.
92
+
93
+ ### The bulk loop, with the harness in it
70
94
 
71
95
  ```bash
72
- vidfarm regime init hooks --out ./work/QA_REGIME.md # once, then edit for this account
96
+ vidfarm harness init hooks --out ./work/HARNESS.md # once, then edit for this account
73
97
  for VARIANT in "${VARIANTS[@]}"; do
74
98
  vidfarm set-text ./work --layer hook --text "$VARIANT"
75
- vidfarm qa ./work --json > "qa/$SLUG.json" # regime auto-discovered from ./work
99
+ vidfarm qa ./work --json > "qa/$SLUG.json" # harness auto-discovered from ./work
76
100
  jq -e '.ok' "qa/$SLUG.json" >/dev/null || continue # YOUR gate, in YOUR script
77
101
  vidfarm render "$FORK_ID" --dir ./work --out "renders/$SLUG.mp4"
78
102
  done
@@ -224,14 +248,14 @@ The licensed harness also carries the **generative build workflow** guidance (ch
224
248
  | `vidfarm generate <image\|video> --prompt "…"` | `POST /api/v1/primitives/{images,videos}/generate` (polls job) | generate AI media → finished URL |
225
249
  | `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) |
226
250
  | `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 |
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`. |
251
+ | `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. **`--smart`** (IMAGE + local only) swaps the flat chromakey for the connectivity keyer `cutout`/`sticker-pack` use, so plate-colored detail inside the subject and hollow/outline art survive. Aliases: `greenscreen`, `remove-background-greenscreen`. |
228
252
  | `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`. |
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`. |
253
+ | `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. Keys by CONNECTIVITY by default (`--key-mode smart`): the plate is flood-filled in from the frame edge, so plate-colored detail inside the subject and hollow/outline art survive, and the plate is un-mixed out of each edge pixel individually instead of by a global `despill`. `--key-mode flat` is the old plain chromakey. `--pad` keeps transparent breathing room; `--json` reports final `width`/`height`/`area_reduced_pct`, `key_mode`, `protected_px` (plate-colored pixels kept inside the art), plus `hole_pct`/`hollow` (under the smart keyer an enclosed hole is usually real art, so it warns only). Alias: `sticker`. See recipe `cutout-graphics-for-explainers.md`. |
230
254
  | `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". |
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". |
255
+ | `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. **The key is CONNECTIVITY-based (`--key-mode smart`, the default):** the plate is flood-filled inward from the sheet's edge and only background that REACHES the edge is deleted, so plate-colored detail inside an item, and an outline shape whose interior is bare plate, both survive; edge pixels get the plate un-mixed out of them individually (real alpha math, better than a global `despill`). `--key-mode flat` is the old plain chromakey the simple fallback and the way to match a cloud render exactly. **ONE PLATE PER STICKER (`--sheet-mode zoned`):** generates a grid of solid color PANELS, one item per panel, each panel's plate picked against that item (green frog on magenta beside a pink flower on green), keyed panel-by-panel with the color read back off that panel's own corners — item art may then use any palette including other panels' colors, and names map to PANELS instead of reading order (`panel`/`plate` land in `stickers.json`). `--sheet-mode auto` (default) zones a generation of 2+ named items; `--sheet-mode flat` forces the classic one-color sheet for a weaker model, and if a model ignores the grid anyway the keyer detects it (a panel with no plate, or one whose item filled it corner to corner) and **re-keys the sheet as one plate automatically**. Read a zoned sheet you already have with `--zones auto` (default) or `--zones 3x2`. **`--refine`** re-cuts each located item from the UN-keyed sheet with the local ONNX matting model (free, ~1–2s each) for painterly/soft/furry/glassy art, falling back per item to the keyed cut when the matte comes back empty or fully opaque. **Plate color is still chosen for you** when generating flat (green magenta blue black white) and detected off an existing sheet. Per-item `holes`/`hole_pct`/`hollow` are still reported (console `⚠ N% hollow` at ≥20%, `--json`, `stickers.json`) under the smart keyer a flag usually means real art (ring/frame/donut), so it **warns, never blocks**; lift a stubborn 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". |
232
256
  | `vidfarm tts "…" [--style "…"] [--voice <v>] [--out <file>]` | (LOCAL-FIRST: your own OPENAI/GEMINI/OPENROUTER_API_KEY → audio file on disk; `--cloud` = `POST /api/v1/primitives/audio/speech` + poll, ElevenLabs on the platform key by default, `--own-key` for yours) | text → narration audio; `--cloud --voice <voice_id>` picks an ElevenLabs voice |
233
257
  | `vidfarm music "<prompt>" [--length <sec>] [--out <f>] [--own-key]` | `POST /api/v1/primitives/music/generate` (polls job) | prompt → music track (ElevenLabs; platform key + wallet by default, `--own-key` for yours) |
234
- | `vidfarm voices [--own-key] [--limit N]` | `GET /api/v1/primitives/audio/voices` | list ElevenLabs voices (voice_id/name/labels) for `tts --voice`; default a voice + tell the user they can choose |
258
+ | `vidfarm voices [--sample] [--search "…"] [--free\|--all] [--own-key] [--limit N]` | `GET /api/v1/primitives/audio/voices` | **Browse AND sample narration voices.** Default roster = the premium ElevenLabs catalog reached through **vidfarm's own ElevenLabs connection** — the user needs no ElevenLabs account, API key, or subscription; narration is billed as vidfarm wallet credits (pennies each). `--free` = the $0 local Kokoro roster (`--all` = both). `--sample` writes listenable clips to `./voice-samples` (`--sample-count`, `--sample-out`, `--sample-text`) and is **free on both tiers** — premium samples are ElevenLabs' own preview clips, free samples render locally — so it's safe in `minimize`. `--search` filters by name/labels/description. **In interactive mode play the samples and let the USER pick**; autonomous = default a voice and still say they can choose. `--own-key` lists the customer's own ElevenLabs account instead. |
235
259
  | `vidfarm stt <file\|url> [--out <base>] [--no-diarize]` (alias: `transcribe`) | (LOCAL-FIRST: local ffmpeg demux + your own key; `--cloud` = `POST /api/v1/primitives/audio/transcribe` + poll, ElevenLabs Scribe on the platform key by default, `--own-key` for yours) | video/audio → transcript in BOTH formats: simple subtitles (txt + SRT) and multi-speaker segments (json) |
236
260
  | `vidfarm place <dir> --src <url\|file> [--at\|--replace]` | (edits local composition.html; local files → serve disk store or temp upload) | drop media (URL **or local file**) into a gap / over a scene |
237
261
  | `vidfarm captions generate <dir> [--style <preset>] [--audio <f>\|--srt <f>\|--text "…"]` | (LOCAL-FIRST: STT on your own key — OpenAI = real word timestamps — then edits local composition.html) | transcribe narration → animated word-by-word caption cues |
@@ -257,7 +281,7 @@ The licensed harness also carries the **generative build workflow** guidance (ch
257
281
  | `vidfarm whoami` | `GET /api/v1/user/me` | who am I (also prints the active cost mode) |
258
282
  | `vidfarm cost-mode [minimize\|hybrid\|rich-ai\|pure-videogen]` | (local `~/.vidfarm/cost-mode.json`) | show/set the money-saving preference every billed command respects; no arg = show + explain each; `--clear` forgets it |
259
283
  | `vidfarm mode [interactive\|autonomous]` | local (`~/.vidfarm/interaction-mode.json`) | **The SECOND preference axis — ask it alongside cost mode.** Is the human in the loop? `interactive` = at checkpoints you hand them a copy-paste brief (an image prompt for a FREE web generator; a keyword list for sourcing clips) and they hand files back — usually BETTER output, and in `minimize` the only way to get custom art at all. `autonomous` = you finish solo within the budget. Independent of cost mode: every cost mode runs either way. No arg prints the pair + the explainer to relay; `--clear` forgets; per-run `--interactive`/`--autonomous`. Aliases: `interaction-mode`, `interaction`. |
260
- | `vidfarm handoff image --theme "<what>" [--items "a,b,c"] [--style …] [--single]` | local (pure text) | **The interactive-mode IMAGE brief.** Prints the exact prompt, the numbered steps, the free tools (meta.ai / ChatGPT / Gemini / HF Spaces) and the follow-up command. Defaults to a **sticker pack**: ONE sheet holding every item on a chroma plate → `vidfarm sticker-pack` splits it for $0. Picks a plate the art won't collide with (green art → magenta plate), spells out the **key-safe fill rules** (closed solidly-filled shapes, no outline-only/hollow art, nothing in a near-plate shade, fully opaque) so the sheet doesn't come back as rims around holes, and carries that `--key-color` into the follow-up. `--single` for one subject. |
284
+ | `vidfarm handoff image --theme "<what>" [--items "a,b,c"] [--style …] [--single]` | local (pure text) | **The interactive-mode IMAGE brief.** Prints the exact prompt, the numbered steps, the free tools (meta.ai / ChatGPT / Gemini / HF Spaces) and the follow-up command. Defaults to a **sticker pack**: ONE sheet holding every item on a chroma plate → `vidfarm sticker-pack` splits it for $0. Picks a plate the art won't collide with (green art → magenta plate), spells out what the local keyer actually needs (a crisp silhouette in a different color from the plate, sealed shapes, clear gaps between items hollow art and plate-colored detail INSIDE a shape are fine now), and carries that `--key-color` into the follow-up. `--single` for one subject. `--zoned` asks instead for a color-block sheet (one panel colour per item) and hands back a `--zones RxC` follow-up — worth it when the pack's own colors fight one plate, but leave it off for a free consumer tool that may not follow a grid. |
261
285
  | `vidfarm handoff raws --keywords "a,b" [--platforms tiktok,youtube] [--count N] [--purpose "…"]` | local (pure text) | **The interactive-mode CLIP-SOURCING brief** — the bottom rung of the sourcing ladder (browser control → `clipper`/`raws scan --cloud` → public raws → the human). Prints what to search, how to download (a Google *search* for a downloader, never a link that rots), and the import command for when the folder is ready. |
262
286
  | `vidfarm storyboard [dir] [--init] [--frames "Title\|scene,…"] [--json]` | local (`STORYBOARD.md` / `SCRIPT.md`) | **The plan pass, and a core part of the composition format.** Scaffolds/reads the project's `STORYBOARD.md`: ordered frames with `duration`/`status`/`src`/`scene`/`voiceover`. The Vidfarm editor renders this file in its **Storyboard** view (contact sheet + per-frame comments + `outline → built → animated` progress), so it's the cheapest place to get a director's approval before building. Not to be confused with `vidfarm sequence` (which GENERATES storyboard images for the pure-videogen pipeline). Alias: `plan`. |
263
287
  | `vidfarm wallet [--job <id>\|--tracer <t>] [--limit <n>]` | `GET /api/v1/user/me/wallet` | cost log: balance + lifetime spend + recent charges. `--job <renderJobId>` prints **what that one video cost** (sums its charges); `--tracer <t>` sums a tracer. Cloud-only; readable on the free plan too (shows $0.00). Aliases: `spend`, `costs` |
@@ -281,10 +305,11 @@ The licensed harness also carries the **generative build workflow** guidance (ch
281
305
  | `vidfarm raws preset list\|run\|save` / `raws export <ids…> --to <dir>` | (local library) | saved queries; copy raw MP4s out |
282
306
  | `vidfarm lint <dir\|composition.html>` | (local static validation) | pre-publish composition check: timing, overlaps, preset names, media src |
283
307
  | `vidfarm stills <dir> [--at 0,2.5,…] [--sheet]` | (local in-process render of PNG frames) | visually verify an edit without a full render. **`--sheet` also tiles them into one contact sheet** (`<out>/contact-sheet.png`, `--sheet-out`/`--sheet-width` to tune) — the whole-video review pass: read it as ONE image and sequence-level drift (uneven margins, three type sizes, a wandering accent colour, N identical beats, a jarring join) becomes obvious where per-scene checks never see it |
284
- | `vidfarm qa <dir\|composition.html> [--regime <name\|path>…] [--json] [--strict]` | (local static QA — **devcli-only**, no cloud/REST twin) | **social-native QA: HTML slop + first frame + font regime. Run it on EVERY video you produce.** `--regime` grades against a QA_REGIME.md too (stackable). Free, instant, feedback-only |
285
- | `vidfarm regime list\|show <name\|path>\|init <name> [--out <path>]` | (local — **devcli-only**) | QA_REGIME.md: the director's own per-style quality contract. `init` copies a bundled base to edit the pattern for bulk/scripting mode |
308
+ | `vidfarm qa <dir\|composition.html> [--harness <name\|path>…] [--json] [--strict]` | (local static QA — **devcli-only**, no cloud/REST twin) | **social-native QA: HTML slop + first frame + font regime. Run it on EVERY video you produce.** `--harness` grades against a HARNESS.md too (stackable). Free, instant, feedback-only |
309
+ | `vidfarm harness list\|show <ref> [--dna <strand>]\|init <name> [--out <path>]\|derive <forkId\|dir>\|check <dir>` | (local — **devcli-only**) | **HARNESS.md: the reusable AI harness for one format or template.** `init` copies a bundled base to edit; `derive` turns a decomposed template's DNA into one ("give me the harness for this template_id"); `check` is `vidfarm qa` under the harness noun |
286
310
  | `vidfarm doctor` | (local environment triage) | check ffmpeg/node/keys/agent CLI/poisoned env + list local serve/preview processes before debugging anything else; `--kill-orphans` reaps dead servers squatting ports (fixes the "Waiting for preview server…" hang) |
287
311
  | `vidfarm skills list\|add <name>\|update` | `GET /skill-pack/index.json` · `/skill-pack/:name/*` | install/refresh skill packs (see "Skill packs — import on demand") |
312
+ | `vidfarm skill ls\|show <path>\|search "<term>"\|path` | (local — **offline, no account**) | **Read this pack straight off disk.** A full copy ships inside the devcli tarball and is pinned to the installed version. `search` greps all 22 files at once — the cheapest way to find one paragraph without loading a whole reference |
288
313
  | `vidfarm tts "…" --engine local` / `vidfarm stt <file> --engine whisper` | (keyless LOCAL engines: Kokoro-82M TTS, whisper.cpp STT) | narration + word-timestamp transcripts with zero keys and zero accounts |
289
314
  | `vidfarm remove-background <video\|image>` | (local ONNX matting — free) | transparent-subject media for occlusion captions/cutouts (arbitrary/messy background; for a FLAT solid background use `remove-background-greenscreen`) |
290
315
  | `vidfarm capture <url>` | (local headless-Chrome capture) | website screenshots/assets for website-to-video flows |
@@ -302,8 +327,8 @@ The licensed harness also carries the **generative build workflow** guidance (ch
302
327
  vidfarm qa ./work # human-readable findings + verdict
303
328
  vidfarm qa ./work --json # machine-readable: rule / severity / where / fix
304
329
  vidfarm qa ./work --strict # ALSO exit 1 on slop (only if you want a CI gate)
305
- vidfarm qa ./work --regime hooks # + grade against a QA_REGIME.md (repeatable; also
306
- # auto-discovers ./work/QA_REGIME.md)
330
+ vidfarm qa ./work --harness hooks # + grade against a HARNESS.md (repeatable; also
331
+ # auto-discovers ./work/HARNESS.md)
307
332
  ```
308
333
 
309
334
  **Run this on every video you produce.** It is free, instant (pure DOM, no ffmpeg/Chrome/network), and it is the only automated check for the thing that most often ruins an agent-made video: **HTML slop**. Compositions are authored in HTML, so an agent's web-page instincts leak straight onto the frame as landing-page furniture that appears on every website and in **zero** real TikToks.
@@ -329,15 +354,22 @@ What it flags:
329
354
  | `font-regime` | error/warn | A text layer in a website body font (Inter/Roboto/Arial/system-ui → **error**) or any family outside the imported regime (Montserrat, TikTok Sans, Abel, Source Code Pro, Yesteryear → warn, it silently falls back at render) |
330
355
  | `font-size` / `font-weight` | error/warn | `font-size:0` (invisible) is an error; sub-2.6%-of-canvas-width text and weight <600 warn |
331
356
  | `caption-safe-zone` | warn | Text outside the 8%–85% band on a **portrait** canvas (landscape/square are exempt) |
357
+ | `caption-oversize` | warn | Display-size type (>7.5% of canvas width) on a line of **5+ words** — it runs edge-to-edge, wraps, covers the frame, and forces a full-width plate. Both signals required, so a giant 2-word hook card passes |
358
+ | `wall-of-text` | warn | One **static** text layer carrying 14+ words — a paragraph, not a caption. Page it into 3–5-word kinetic cues (`captions generate --style word-pop`). Layers already part of an animated caption run are exempt |
359
+ | `dead-air` | warn | A gap of **≥2.5s between cues** with nothing on screen to read (needs 3+ cues, so a two-card title sequence is exempt). Dead screen time is a free exit — cut it and `ripple` the hole closed |
360
+ | `dead-tail` | warn | The video keeps running **>1.5s after the last word** — an outro, an end card, or an untrimmed clip. End on the bait |
361
+ | `slow-scene` | warn | One clip >6s **and** >2.5× the median clip length — judged against the video's OWN rhythm, so a deliberately slow piece or a single-take talking head passes |
332
362
  | `thumbnail-blank-open` | error | Nothing on screen at **t=0** — the opening clip starts late, so the poster frame is black |
333
363
  | `thumbnail-fade-in` | error/warn | An **entrance** transition on the FIRST clip: `fade-black`/`fade-white`/`flash`/`smoke` → **error** (frame 0 is a flat solid); any other preset → warn (frame 0 caught mid-move). Junction transitions on later clips are never flagged |
334
364
  | `thumbnail-no-hook-text` | warn | The composition has text, but none of it is up at t=0 — the poster carries no hook words. Ignorable when you're deliberately opening on a clean face/product shot |
335
365
 
336
366
  Every finding carries a concrete `fix` line — the answer is always "say it as timed text on the footage", never just "delete it". Fold `--json` into scripted batch runs to QA N variants at once.
337
367
 
338
- **`vidfarm qa` is a static DOM checkit cannot see the rendered video.** It never looks at pixels, motion, spacing, colour drift, pacing, or the joins between scenes, so a clean `qa` run says nothing about whether the video reads as one coherent piece. That judgment is a separate, mandatory pass: tile stills into a contact sheet, read it as an image, and check balance/spacing/type/colour/rhythm across the whole sequence. It also can't catch a **frozen render** (every frame identical while duration, frame count and audio hash all pass), which is why you compare frames from two different scenes. Full method: `references/reviewing-renders.md`.
368
+ **Every run ends by telling you to go watch the video — that instruction is part of the output, not a footnote.** `vidfarm qa` closes with a `▶ NOW WATCH THE VIDEO this check never did` block (and a `watch_the_video: { required: true, why, steps[] }` object in `--json`), printed on **clean** runs too, because a green tick on DOM attributes is the single easiest thing to mistake for a reviewed video. The steps are dir-aware and paste-ready: render, `stills --sheet` *open the contact sheet*, read it as one sequence, judge each caption against its picture, compare two different scenes (a frozen render passes every mechanical check), measure the audio with `volumedetect`, and report what you measured separately from what you judged. **Do them.** Reporting "QA passed" to a director without opening a frame is not a review, and the tool now says so to your face.
369
+
370
+ **`vidfarm qa` is a static DOM check — it cannot see the rendered video.** In particular it can tell you a caption is *too big* or *outside the safe zone*, but never whether it sits in the **empty** part of the frame — that needs pixels, so it stays your job: `vidfarm stills <dir> --at <t>`, look, then place (see `references/editor-workflows.md` → "TikTok-native caption standard"). It never looks at pixels, motion, spacing, colour drift, pacing, or the joins between scenes, so a clean `qa` run says nothing about whether the video reads as one coherent piece. That judgment is a separate, mandatory pass: tile stills into a contact sheet, read it as an image, and check balance/spacing/type/colour/rhythm across the whole sequence. It also can't catch a **frozen render** (every frame identical while duration, frame count and audio hash all pass), which is why you compare frames from two different scenes. Full method: `references/reviewing-renders.md`.
339
371
 
340
- **The two halves, and why the tool only claims one.** Everything above is universal and mechanical. The half that decides whether a *particular* video is any good — is the hook legible cold, does the loop close, is this variant genuinely different from its siblings — is the director's, and it lives in a **`QA_REGIME.md`** (see "Scripting mode" above). Pass one with `--regime <name|path>` (repeatable, and a `QA_REGIME.md` sitting next to the composition is picked up automatically): its `checks:` front matter is settled deterministically alongside the built-ins, and its `- [ ]` checklist comes back as **review items you must answer yourself**. `vidfarm qa` deliberately never fakes a verdict on those — a "PASS" it couldn't have earned is worse than no check at all.
372
+ **The two halves, and why the tool only claims one.** Everything above is universal and mechanical. The half that decides whether a *particular* video is any good — is the hook legible cold, does the loop close, is this variant genuinely different from its siblings — is the director's, and it lives in a **`HARNESS.md`** (see "Scripting mode" above). Pass one with `--harness <name|path>` (repeatable, and a `HARNESS.md` sitting next to the composition is picked up automatically): its `checks:` front matter is settled deterministically alongside the built-ins, and its `- [ ]` checklist comes back as **review items you must answer yourself**. `vidfarm qa` deliberately never fakes a verdict on those — a "PASS" it couldn't have earned is worse than no check at all.
341
373
 
342
374
  ## Cost mode — the devcli's money-saving guardrail
343
375
 
@@ -446,6 +478,25 @@ The customer-facing walkthrough (the "VidFarm Walkthrough Tutorial" course) is p
446
478
 
447
479
  Both are public and read-only (no auth). Prefer these to guessing steps — quote the real chapter and link the reader to its `url`. Chapters cover onboarding/setup, the operating funnel (angles/hooks/awareness), each guided edit demo (recaption, product tease, remix-with-raws, actor replacement, animate-static-book, drama series, product promo, motion explainers), sourcing/clipping raws, the wallet, cancellation/refunds, and the developer devcli/scripting/free-mode chapters.
448
480
 
481
+ ## The director pack ships inside the devcli — read it offline
482
+
483
+ Installing `@officexapp/vidfarm-devcli` puts a **complete copy of this pack on disk**, pinned to that CLI version. You never have to be online, logged in, or in a project with `.agents/skills/` to read it:
484
+
485
+ ```bash
486
+ vidfarm skill ls # every file, with sizes
487
+ vidfarm skill show primitives # shorthand resolves to references/primitives.md
488
+ vidfarm skill show harnesses/README.md # or an exact path
489
+ vidfarm skill search "greenscreen" # grep all of it — find the paragraph, then open that file
490
+ vidfarm skill path # where the bundled copy lives
491
+ ```
492
+
493
+ **Prefer `skill search` over opening a big reference.** `editor-workflows.md` is ~650 lines and `automation-and-local-dev.md` ~520; a grep that returns `references/primitives.md:214` costs almost nothing and tells you exactly which file to load.
494
+
495
+ Two things this does NOT mean:
496
+
497
+ - **Pinned, not live.** The bundled copy matches the installed CLI — which is the pairing that actually works, since a newer skill against an older binary is the usual cause of *"the skill says to do X but the command 404s"*. For the host's latest, `vidfarm skills add vidfarm` (installs into a project) or `vidfarm skill --print --remote`. When they disagree, update **both halves together**: <https://vidfarm.cc/update.md>.
498
+ - **Documentation, not entitlement.** Reading about a paid primitive offline does not make it run offline. The **free-local** half genuinely needs nothing — clip hunting, hyperframes, `vidfarm serve` render, `vidfarm qa`, harnesses, `vidfarm dedupe`, Kokoro TTS, whisper STT. The **paid-cloud** half still needs `vidfarm login` and a network call: AI image/video/voice generation, hosted render, `recycle`, `download-video`, marketplace, and the hosted file directory. Tell the director which half a plan lands in *before* you build it.
499
+
449
500
  ## Skill packs — import on demand (HyperFrames-grade authoring power)
450
501
 
451
502
  This skill stays lean on purpose. Deep authoring craft lives in **skill packs** — Vidfarm's whitelabel of the open-source `hyperframes` skill suite (same engine as `vidfarm hf` / `vidfarm render`, Vidfarm-branded) plus Vidfarm's own media pack — vendored on the Vidfarm host and installed only when a task needs them. Never install skills from upstream vendor orgs or third-party registries; the vidfarm mirror is the source (`vidfarm skills add <name>` fetches `GET /skill-pack/:name/*` with hash verification into `.agents/skills/` + a `.claude/skills/` link, pinned in `skills-lock.json`; `vidfarm skills list` shows what is available/installed; `vidfarm skills update` refreshes pins).
@@ -518,16 +518,20 @@ Compositions are authored in HTML, so the single most common way an AI-edited vi
518
518
  - **Emoji inline in text** (sparingly), **sticker/cut-out overlays** on transparent PNG (`create-overlay`), mock social UI when the format calls for it (iMessage bubbles, a TikTok comment card, a fake DM, a countdown/progress bar) — these are native artifacts of the platform, not web furniture.
519
519
  - **Full-bleed footage** with text sitting directly on it.
520
520
 
521
- **On devcli there's a checker: `vidfarm qa <dir|composition.html>`.** Free, instant, local-only — a blocklist pass for everything above plus the font regime and safe zone, with a concrete fix per finding. **Run it on every video you produce.** It is feedback, not a gate (exit 0 even on findings, never runs automatically, `--strict` only if you want a CI failure) and a blocklist, not an allowlist (stylized/hand-made compositions pass untouched — it will not homogenize your videos). No cloud/REST twin: the web copilot enforces this standard by hand. Details in `references/automation-and-local-dev.md` ("`vidfarm qa`").
521
+ **On devcli there's a checker: `vidfarm qa <dir|composition.html>`.** Free, instant, local-only — a blocklist pass for everything above plus the font regime and safe zone, with a concrete fix per finding. **Run it on every video you produce.** It is feedback, not a gate (exit 0 even on findings, never runs automatically, `--strict` only if you want a CI failure) and a blocklist, not an allowlist (stylized/hand-made compositions pass untouched — it will not homogenize your videos). Every run — including a clean one — ends with a **`▶ NOW WATCH THE VIDEO`** block, because the check never rendered or saw the video and a green tick is not a review; do those steps before you tell anyone the video is done. No cloud/REST twin: the web copilot enforces this standard by hand. Details in `references/automation-and-local-dev.md` ("`vidfarm qa`").
522
522
 
523
523
  ### TikTok-native caption standard (position + font + background) — always adhere
524
524
 
525
525
  > Captions are also the *delivery system* for three of the four charges: the hook is read before any audio, the loop has to stay on screen, and the payoff number needs its own card. What the words should SAY is in `references/hooks-and-virality.md`; this section is how they must LOOK.
526
526
 
527
- Short-form is watched on a phone, and the phone's UI eats the frame's edges. **Never pin on-screen text to the extreme top or bottom** — the top ~8% sits under the status bar / "Following · For You" tabs and the bottom ~15% under the username, caption text, music marquee, and action rail. Text there is literally clipped and reads as amateur. Three rules, applied to **every** caption/title/overlay you place or inherit:
527
+ Short-form is watched on a phone, and the phone's UI eats the frame's edges. **Never pin on-screen text to the extreme top or bottom** — the top ~8% sits under the status bar / "Following · For You" tabs and the bottom ~15% under the username, caption text, music marquee, and action rail. Text there is literally clipped and reads as amateur. Four rules, applied to **every** caption/title/overlay you place or inherit:
528
528
 
529
- - **Position → readable safe zone.** Keep the text box's vertical extent inside **~8%–85%** of canvas height (9:16). A lower-third subtitle lives around **y≈70%** (the `captions generate` default); a "me when…" / "POV:" meme line sits near the **top of the safe zone (y≈8%), not y:0**. Preserve the original top-vs-bottom anchoring — just pull it off the dead-zone edge, don't recenter everything. Also keep wide captions clear of the **right ~12%** action rail (a centered box at `x:10 width:80` is safe).
530
- - **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). Sizes are in PIXELS of a 1080-wide frame: **~36–64px** reads well, never below ~28px and **never 0** (invisible). Keep captions to ~2 lines / ~5 words per line; `line_height` 0.95–1.15 for stacked display lines.
529
+ - **Position → the safe zone first, then the EMPTIEST part of the frame.** Two constraints, in that order.
530
+ - *Hard constraint:* the text box's vertical extent stays inside **~8%–85%** of canvas height (9:16), and wide captions stay clear of the **right ~12%** action rail (a centered box at `x:10 width:80` is safe).
531
+ - *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.
532
+ - *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.
533
+ - **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.
534
+ - **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).
531
535
  - **Background → one of exactly four valid treatments.** Any text you place uses one of these and nothing else:
532
536
 
533
537
  | # | Treatment | How to set it | When |
@@ -537,10 +541,19 @@ Short-form is watched on a phone, and the phone's UI eats the frame's edges. **N
537
541
  | 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 |
538
542
  | 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 |
539
543
 
540
- **Pick between them by MEASURING the background behind the caption band, not by habit.** Dark-and-calm behind the band (luma < ~70, variation < ~42) light type, **no plate** (treatment 1/2 a plate there is a bright slab the design never asked for); bright-and-calm (luma > ~160) dark type, no plate; busy / mid-tone / moving colour → treatment 4, because nothing else stays readable. The active-word colour has to follow the same call a deep red that reads on a white plate is unreadable on near-black. **One treatment for the whole video**; styling that flips every few seconds reads as a bug. Procedure, thresholds and how to measure the *composited* value (not the source file): `regimes/short-form.QA_REGIME.md` "Caption styling is MEASURED off the background".
544
+ **Move the text before you armour it.** The plate is the *last* resort, not the default: if the band you picked is busy, first try moving the caption into the calm/empty region the position rule points at a caption over open sky needs no background at all, and "no plate" is the cleaner, more native look every time you can afford it. Only when the whole frame is busy (or the text has to sit on the subject for meaning) do you reach for treatment 4.
545
+
546
+ **Then pick between them by MEASURING the background behind the caption band, not by habit.** Dark-and-calm behind the band (luma < ~70, variation < ~42) → light type, **no plate** (treatment 1/2 — a plate there is a bright slab the design never asked for); bright-and-calm (luma > ~160) → dark type, no plate; busy / mid-tone / moving colour → treatment 4, because nothing else stays readable. The active-word colour has to follow the same call — a deep red that reads on a white plate is unreadable on near-black. **One treatment for the whole video**; styling that flips every few seconds reads as a bug. Procedure, thresholds and how to measure the *composited* value (not the source file): `harnesses/short-form.HARNESS.md` → "Caption styling is MEASURED off the background".
541
547
 
542
548
  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.
543
549
 
550
+ **Long narration → kinetic cues, never a wall of text.** A caption layer is a *page*, not a transcript. The moment a single static text run carries more than ~10–12 words — or sits on screen longer than ~4 seconds while the voice keeps going — it stops being a caption and becomes a paragraph the viewer has to read while also watching the video. Nobody does both; they scroll. Page it instead:
551
+
552
+ - **Transcribe and let the tool page it:** `vidfarm captions generate ./work --style word-pop` (or `spotlight` / `karaoke`) splits narration into ~3–5-word cues with real word-level timings, so one short phrase is on screen at a time and the active word tracks the voice. Web copilot twin: the `/primitives/audio/captions` job → `set_captions` (see "Animated captions" below). `--max-words-per-cue` tightens it further.
553
+ - **The cue count is the readability dial.** Short cues that change with the speech read as *momentum*; one long block reads as homework. Kinetic word-by-word also lets the type be **smaller** (the eye is led to the moving word instead of having to scan a wall), which frees up frame space and usually removes the need for a plate.
554
+ - **Static text is for the beats that deserve their own moment** — a hook line, a payoff number, a title card. Those are short by nature. Anything spoken should be a caption run, not a static block.
555
+ - **Exception: verbatim UGC/testimonial captions** stay one plain line at a time (see `harnesses/ugc-testimonial.HARNESS.md`) — the kinetic VFX look is the "made by a marketing team" tell there. Paging still applies; the animation preset doesn't.
556
+
544
557
  **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.
545
558
 
546
559
  > Local devcli renders enforce part of this automatically: `renderCompositionLocally` runs `normalizeTikTokCaptionLayout` (src/devcli/composition-edit.ts) on every production, clamping caption/text layers into the 8%–85% safe zone and coercing off-regime primary fonts to Montserrat. It only fixes position and font family — it will happily render your Bootstrap card. Get it right in the composition so the editor preview, the local render, and any cloud render match.
@@ -4,7 +4,7 @@ Most agent-made videos don't fail on polish. They fail on **structure**: no hook
4
4
 
5
5
  This is the harness that fixes it. It is not a style — it's the load-bearing anatomy of anything that travels on TikTok/Reels/Shorts, distilled from grading hundreds of hooks against real funnels. **Run it on one-off videos and on batches alike.** It costs no credits, adds no render time, and it is the single largest quality delta available in this product.
6
6
 
7
- The checkable form of this document is the bundled `hooks` regime (`vidfarm regime show hooks`); this file is the craft behind it.
7
+ The checkable form of this document is the bundled `hooks` harness (`vidfarm harness show hooks`); this file is the craft behind it.
8
8
 
9
9
  ---
10
10
 
@@ -17,10 +17,67 @@ The reason agent videos come out structureless is that the timeline is the fun p
17
17
  3. **Name the payoff.** What is on screen at that moment, and why does it satisfy the promise?
18
18
  4. **Write the bait.** The final-beat ask, in the video and in the post caption.
19
19
  5. **Only now build the timeline** — and place the hook text at `start:0` so it's on screen at frame 0 (which is also the thumbnail).
20
- 6. **Verify the frame and the structure:** `vidfarm stills ./work --at 0` (look at the actual poster) and `vidfarm qa ./work --regime hooks` (machine checks + the judgment checklist).
20
+ 6. **Verify the frame and the structure:** `vidfarm stills ./work --at 0` (look at the actual poster) and `vidfarm qa ./work --harness hooks` (machine checks + the judgment checklist).
21
21
 
22
22
  Steps 1–4 are cheap, reversible, and where the entire outcome is decided. Steps 5–6 are where agents want to start.
23
23
 
24
+ 7. **Cut it.** Nothing ships at its first length — see the next section. Assume your first assembly is 30–50% too long and go find the seconds.
25
+
26
+ ---
27
+
28
+ ## Density — every second must earn its place, and most don't
29
+
30
+ **A viewer's thumb is a hard time limit that resets every second.** They are not "watching your video"; they are re-deciding to stay, ~24 times a second, against an infinite feed of alternatives. A second that carries nothing is not neutral — it is a free exit. This is why the same script cut to 22s outperforms its own 41s version with better footage: fewer exit ramps.
31
+
32
+ Agents are structurally bad at this. A model writes a video the way it writes prose — with connective tissue, restatement, a wind-up before the point, a tidy conclusion — and every one of those habits is a hole in the retention curve. **You must cut against your own instinct, and you must cut more than feels right.**
33
+
34
+ ### The deletion test — the only test that matters
35
+
36
+ For every beat, ask: **delete it. Does the video still make sense, and does the payoff still land?** If yes, it stays deleted. Not "trimmed" — deleted. Run this on every scene, every sentence, and every caption before you render, and be honest: the beat you're defending because it took work to make is exactly the one this test exists to kill.
37
+
38
+ Second filter for whatever survives: **which of the four charges does this beat serve — hook, loop, payoff, or bait?** A beat that serves none is fluff wearing a costume. "It gives context" is not a charge. "It looks nice" is not a charge.
39
+
40
+ ### Cut on sight — the standard fluff, in the order it usually appears
41
+
42
+ - **Any intro.** Logo sting, title card, brand animation, "welcome back", a beat of black. The video starts at the claim. Frame 0 is the hook (and the thumbnail).
43
+ - **The wind-up before the point.** "So I wanted to talk about…", "Here's the thing…", "Let me explain…", "In this video I'm going to show you…". Delete the sentence; the next one was the real opening.
44
+ - **Context before the claim.** Context is beat 2 at the earliest, and usually one clause, not a scene.
45
+ - **Restatement.** Saying the same thing a second way "so it's clear." It was clear. If it wasn't, fix the first version.
46
+ - **Dead air in the narration.** Breaths, "um", and any inter-sentence gap over ~0.35s. This alone routinely takes 15–20% off a TTS or talking-head cut.
47
+ - **Real-time process.** Nobody watches the upload bar. Speed-ramp it, jump-cut it, or show the before and the after and skip the middle.
48
+ - **Establishing shots.** They know what an office/kitchen/laptop looks like. Open inside the action.
49
+ - **Reading what's already on screen.** Voice and text should split the work, not duplicate it (the same rule as captions-vs-display-text).
50
+ - **The tail.** "Thanks for watching", a logo card, an end screen, or footage that keeps rolling after the last word. The bait is the last beat; then it **ends**, hard, on the frame that loops best.
51
+ - **Filler motion.** A slow pan or Ken Burns that exists because the clip was too short for its slot. Shorten the slot instead.
52
+
53
+ ### Density is not speed, and this is where over-correcting ruins videos
54
+
55
+ Cutting fluff means **removing beats that carry nothing**, never rushing the beats that carry everything. Three things are load-bearing and must keep their seconds:
56
+
57
+ - **The comedic beat.** The held pause before a punchline IS the joke. Cutting it saves 0.6s and costs the video.
58
+ - **The payoff.** It plays, full frame, uninterrupted — ≥5s if that's what it takes. Summarising the payoff to save time is the most expensive cut available.
59
+ - **A caption's readability.** A cue nobody can finish reading is worse than no cue. If tightening the edit makes text unreadable, cut *words*, not the time they're on screen.
60
+
61
+ The target is **information per second**, not seconds. A dense 45s video beats a hollow 20s one; both lose to the same 45s cut to 30s with nothing lost.
62
+
63
+ ### Length is an output, not a plan
64
+
65
+ Don't decide "make it 60 seconds" and then fill 60 seconds — filling is where every one of the fluff patterns above comes from. Build the four charges, cut to the deletion test, and **the length is whatever's left.** If the payoff lands at 0:25, the video ends around 0:27. A brief that dictates a duration is a brief that ordered fluff.
66
+
67
+ ### How to actually cut it, in Vidfarm
68
+
69
+ | Move | devcli | Web copilot |
70
+ |---|---|---|
71
+ | Find the dead air | `vidfarm qa ./work` (flags gaps ≥2.5s with nothing on screen, and a tail that keeps rolling after the last word) + read the word timings from `vidfarm captions generate` / `stt` | read `video_context`'s timestamped segments and look for the gaps between them |
72
+ | Trim one clip's edge | `vidfarm trim ./work --layer <k> --edge start --to-time <sec>` | `editor_action trim_layer` |
73
+ | Close the hole you just made | `vidfarm ripple ./work --at <sec> --delta -<sec>` (negative = close time, shifts everything downstream) | `editor_action ripple_edit` |
74
+ | Drop a whole beat | `vidfarm retime`/`remove` the layers, then `ripple` the gap closed | `remove_layer` + `ripple_edit` |
75
+ | Re-time captions after cutting | re-run `vidfarm captions generate` against the new audio — never hand-shift cues | the `/primitives/audio/captions` job → `set_captions` |
76
+
77
+ **Always ripple the gap closed.** A cut that leaves a hole is not a cut; it converts fluff into dead air, which is worse — the viewer now stares at a frozen frame instead of a boring one.
78
+
79
+ **Cheap habit that pays every time:** shave the first ~0.5–1s off every sourced clip and the last ~0.5s. People start recording before the action and stop after it, so a montage of raws is carrying a second of nothing per clip by default.
80
+
24
81
  ---
25
82
 
26
83
  ## Charge 1 — THE HOOK (first 3 seconds)
@@ -171,10 +228,11 @@ A video with replies gets shown again; a video with none dies at its first audie
171
228
  - **The named omission** — *"I left one thing out on purpose. Ask me."* Converts what you couldn't fit into the reason to reply.
172
229
  - **The soft disagree** — a true concession people want to argue with. Not ragebait if you actually believe it.
173
230
  - **The specific question** — concrete always beats "thoughts?".
231
+ - **The keyword comment** — *"Comment CLIPPER and I'll send you the breakdown."* Standard, allowed, and it works: the keyword is the reply the algorithm counts, and the word itself doubles as a filter. Keep the keyword topical (name the thing, not the money), promise something you actually hand over, and say what they get. What makes this beat go wrong is the *promise*, not the mechanic.
174
232
 
175
- **⚠️ Bait is the highest-compliance-risk beat in the video.** For money/health/appearance topics the obvious bait is the illegal one:
233
+ **⚠️ Bait is the highest-compliance-risk beat in the video.** For money/health/appearance topics the risk is what you promise in exchange for the reply:
176
234
 
177
- - ❌ *"Comment MONEY and I'll send you the link"* — the most scam-coded line available, and a DM funnel.
235
+ - ❌ *"Comment MONEY and I'll show you how to make $10k/mo"* — the keyword is fine; the earnings claim attached to it is what's scam-coded and enforceable. Same line with a topical keyword and a deliverable ("Comment CLIPPER for the template") is fine.
178
236
  - ❌ *"How much do you want to make a month?"* — invites an earnings-claim thread you now own.
179
237
  - ❌ *"Follow for part two"* — breaks the standalone rule.
180
238
  - ❌ Fake controversy, ragebait, or a deliberate error to farm corrections. Works once, costs the positioning permanently.
@@ -229,9 +287,9 @@ A video with replies gets shown again; a video with none dies at its first audie
229
287
  | Check what the source template's hook actually was | `editor_context` → `viral_dna.hook` / `retention` / `payoff` / `emotional_punch` | `.harness/context.json`, `video-context.json` |
230
288
  | Place the hook at frame 0 | `add_layer` / `set_captions` with `start:0` | `vidfarm set-text ./work --layer hook --text "…"` |
231
289
  | Look at the poster frame | ask the user to scrub to 0 | `vidfarm stills ./work --at 0` |
232
- | Grade the structure | by hand, against this file | `vidfarm qa ./work --regime hooks` |
233
- | Bulk hook test | hand off to a local agent | `recipes/bulk-scripting-with-a-regime.md` |
290
+ | Grade the structure | by hand, against this file | `vidfarm qa ./work --harness hooks` |
291
+ | Bulk hook test | hand off to a local agent | `recipes/bulk-scripting-with-a-harness.md` |
234
292
 
235
293
  **Re-theming a decomposed template?** `viral_dna` already names the source's hook, retention device, and payoff — that structure is *why the template worked*. Rebuild each charge for the new subject; don't drop the loop because the new topic feels self-explanatory. Flattening a template's loop into a product statement is the single most common way a re-theme kills a format.
236
294
 
237
- **The checkable version of everything above:** `vidfarm regime show hooks` — the twelve-item pre-flight checklist is the part you answer honestly on every video, and two items carry most of the weight: *situation, not label* (predicts cold-start survival before you write a word) and *unguessable* (the only item a hook can fail while passing every other one, which is why it ships).
295
+ **The checkable version of everything above:** `vidfarm harness show hooks` — the twelve-item pre-flight checklist is the part you answer honestly on every video, and two items carry most of the weight: *situation, not label* (predicts cold-start survival before you write a word) and *unguessable* (the only item a hook can fail while passing every other one, which is why it ships).
@@ -37,6 +37,7 @@ ffmpeg -y -pattern_type glob -i "qa/f*.png" \
37
37
  Then answer these, out loud, in your report:
38
38
 
39
39
  - **Balance.** Is weight distributed across the frame, or is every scene top-anchored with an empty band underneath? Does the composition use the canvas, or does it use the top third of the canvas and leave the rest as dead area? A sheet of twelve frames makes a recurring dead zone obvious; one frame at a time never will.
40
+ - **Fluff, named out loud.** Which beats would you cut? Answer with specific timestamps, not "it's tight". Every tile has to justify its seconds: a frame that repeats the previous one, a scene the video would survive losing, an intro, a tail after the last word, a hold that's just waiting. **Assume 30–50% of the first assembly can go** and name what you'd remove — "nothing to cut" on a first pass is almost always a review that didn't look. Then cut it and `ripple` the hole closed (craft: `references/hooks-and-virality.md` → "Density"; the mechanical half is `vidfarm qa`'s `dead-air` / `dead-tail` / `slow-scene`).
40
41
  - **Spacing and breathing room.** Are margins consistent scene to scene? Does one beat have generous air and the next one crowd the safe zone? Uneven padding across scenes is the single loudest "assembled by a machine" tell, and it's invisible while you're inside any one scene.
41
42
  - **Typographic continuity.** One type system, or three? Headline sizes should belong to a small set (two, maybe three), not be individually chosen per scene. Same for weight, case, and colour. If scene 2's headline is 64px and scene 5's is 41px for no dramatic reason, that's drift, not design.
42
43
  - **Colour and style coherence.** One accent colour, one background treatment, one illustration style. Assets generated or sourced at different moments drift — a flat-vector sticker next to a photographic cutout next to a gradient panel reads as three videos spliced together.
@@ -134,6 +135,6 @@ ffmpeg -i final.mp4 -ss 3.1 -t 1.4 -af "volumedetect" -f null - # a span where
134
135
  When you hand back a render, say what you **measured** and what you **judged**, separately:
135
136
 
136
137
  - Machine-settled: `vidfarm qa ./work` findings, `vidfarm lint`, durations, peak dBFS, frame-difference checks.
137
- - Human-judgment: the holistic pass above — balance, spacing, type continuity, colour coherence, pacing, joins — plus the regime's `- [ ]` review items.
138
+ - Human-judgment: the holistic pass above — balance, spacing, type continuity, colour coherence, pacing, joins — plus the harness's `- [ ]` review items.
138
139
 
139
140
  **Never report a clean pass on the half you didn't actually look at.** A confident "verified, looks good" over an unreviewed video is worse than no review, because it spends the director's trust on nothing — and per the base rate at the top of this file, it is usually wrong.
@@ -73,8 +73,8 @@ openrouter key; music always needs ElevenLabs (own key or platform).
73
73
  | Need | Command / route | Notes |
74
74
  | --- | --- | --- |
75
75
  | **Music** (bed, beat, jingle, song, score) | `vidfarm music "upbeat lo-fi beat" --length 30` · `POST /api/v1/primitives/music/generate` | ElevenLabs. `use_wallet_credits` default true (platform key + wallet); `--own-key` = your ElevenLabs key. `music_length_ms` ≤ 300000 (5 min). Place as its own `<audio>` layer ~0.1–0.2 under narration. |
76
- | **Narration** (default) | `vidfarm tts "…" --cloud` · `POST /api/v1/primitives/audio/speech` | Default = ElevenLabs on the platform key (wallet-billed). Pick a voice with `--voice <voice_id>` (browse below). `--own-key` for your ElevenLabs/BYOK key. Local-first `vidfarm tts` (no `--cloud`) still runs on your env openai/gemini key. |
77
- | **List voices** | `vidfarm voices` · `GET /api/v1/primitives/audio/voices` | ElevenLabs voice catalog (voice_id, name, labels, preview_url). Default = platform account; `--own-key` / `?use_wallet_credits=false` = the customer's ElevenLabs account. **Default a sensible voice AND tell the user they can pick from many.** |
76
+ | **Narration** (default) | `vidfarm tts "…" --cloud` · `POST /api/v1/primitives/audio/speech` | Premium ElevenLabs **through vidfarm's own ElevenLabs connection** the user needs NO ElevenLabs account or API key; it's billed as vidfarm wallet credits. Pick a voice with `--voice <voice_id>` (browse below). `--own-key` for your own ElevenLabs/BYOK key. Local-first `vidfarm tts` (no `--cloud`) still runs on your env openai/gemini key. |
77
+ | **Browse + SAMPLE voices** | `vidfarm voices [--sample] [--search "…"] [--free\|--all]` · `GET /api/v1/primitives/audio/voices` | The premium catalog reached over vidfarm's connection — **no ElevenLabs signup needed, wallet credits only** plus `--free` for the $0 local Kokoro roster. `--sample` writes listenable clips to `./voice-samples`, **free on both tiers** (preview CDN clips + local renders), so it's safe in `minimize`. **Interactive mode: play samples and let the USER choose. Autonomous: default a sensible voice and still say they can pick from many.** |
78
78
  | Narration, zero keys / cost-saving | `vidfarm tts "…" --out narration.wav` (free local by default in `minimize`/`hybrid`) · or `npx hyperframes tts "…" -v af_heart --json` | Kokoro-82M, local, WAV + duration in JSON. Fixed voice presets, no `--style`. |
79
79
  | **Transcript + SRT** | `vidfarm stt <file\|url> --cloud` · `POST /api/v1/primitives/audio/transcribe` | Default = ElevenLabs Scribe (native diarization + real word timestamps), wallet-billed. `--own-key`/BYOK: gemini labels speakers, openai/whisper-1 gives real word timings. |
80
80
  | Reword existing narration in the (approximate) original voice | `POST /api/v1/primitives/audio/regenerate-speech` | Listens, profiles the speaker (needs a Gemini key), rewords, regenerates with the closest preset voice + matched style. Approximation, never a clone. Details: `references/tts.md` |
@@ -32,13 +32,35 @@ vidfarm music "chill lo-fi hip hop beat with jazzy piano" --length 30 --json
32
32
  ## Voices — `vidfarm voices`
33
33
 
34
34
  ```bash
35
- vidfarm voices # platform-account voices (voice_id, name, labels, preview_url)
36
- vidfarm voices --own-key # the customer's own ElevenLabs account voices
35
+ vidfarm voices # premium ElevenLabs catalog, via VIDFARM'S OWN connection
36
+ vidfarm voices --sample # download 6 preview clips to ./voice-samples — FREE
37
+ vidfarm voices --search "british narrator" --limit 10
38
+ vidfarm voices --free --sample # the $0 local Kokoro voices, rendered locally — also FREE
39
+ vidfarm voices --all # both rosters
40
+ vidfarm voices --own-key # the customer's own ElevenLabs account voices
37
41
  ```
38
42
 
43
+ **The premium voices do NOT require an ElevenLabs account.** This is the single most under-told
44
+ thing in the whole audio surface: vidfarm holds its own ElevenLabs connection, so any user —
45
+ free-tier, no API key, no ElevenLabs subscription — can narrate with the full premium catalog and
46
+ simply pay **vidfarm wallet credits** (pennies per narration). Say that out loud when you offer
47
+ voices; don't let the user think "premium voice" means "go sign up for ElevenLabs first".
48
+ `--own-key` (`?use_wallet_credits=false`) is the opt-out for users who already have a key and would
49
+ rather bill their own account.
50
+
51
+ **Sampling is free on both tiers**, so it is safe even in cost mode `minimize`: premium samples are
52
+ ElevenLabs' own static preview clips (a CDN download, not a synthesis call) and free samples render
53
+ on the local engine. `--sample-count N`, `--sample-out <dir>`, `--sample-text "<line>"` tune it.
54
+ Free samples need the local Kokoro deps installed; if they're missing the command says so and the
55
+ premium samples still work.
56
+
39
57
  - `GET /api/v1/primitives/audio/voices` (`?use_wallet_credits=false` for the user's own key).
40
- - **Default a sensible voice and TELL the user they can pick from many** surface a few names +
41
- the returned `voice_library_url`, then pass the chosen `voice_id` to `tts --voice <voice_id>`.
58
+ - **In interactive mode, the user picks the voice by ear, not from a list of names.** Sample a
59
+ handful, hand over the files, let them choose, then pass their `voice_id` to `tts --voice`. In
60
+ autonomous mode default a sensible voice and still tell them they can pick from many (surface a
61
+ few names + the returned `voice_library_url`).
62
+ - `vidfarm tts` prints the same reminder on stderr whenever narration is about to run with no
63
+ `--voice` and the mode is interactive (or was never set).
42
64
 
43
65
  ## Preflight
44
66