@officexapp/vidfarm-devcli 0.21.34 → 0.21.35

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 (25) hide show
  1. package/.agents/skills/editor-capabilities/SKILL.md +13 -2
  2. package/.agents/skills/vidfarm/SKILL.md +59 -30
  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} +36 -7
  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 +1 -1
  11. package/.agents/skills/vidfarm/recipes/local-edit-render-approve.md +1 -1
  12. package/.agents/skills/vidfarm/references/automation-and-local-dev.md +73 -22
  13. package/.agents/skills/vidfarm/references/editor-workflows.md +18 -5
  14. package/.agents/skills/vidfarm/references/hooks-and-virality.md +62 -5
  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 +236 -77
  19. package/SKILL.md +32 -14
  20. package/dist/src/cli.js +772 -89
  21. package/dist/src/devcli/{qa-regime.js → harness.js} +132 -55
  22. package/dist/src/devcli/qa-check.js +209 -4
  23. package/dist/src/devcli/skill-docs.js +136 -0
  24. package/package.json +4 -3
  25. package/.agents/skills/vidfarm/regimes/README.md +0 -79
@@ -1,12 +1,12 @@
1
- ## Recipe: Bulk Video Generation (Scripting Mode) with a QA_REGIME.md
1
+ ## Recipe: Bulk Video Generation (Scripting Mode) with a HARNESS.md
2
2
 
3
3
  Use this when the director wants **volume** — daily posting, hook tests, one video per clip in a pool, N variants of a template. Ask first if you're not sure: *"One video, or should we set this up as a repeatable batch?"* If they want volume, this is the shape.
4
4
 
5
- The thing that makes bulk work is not the loop — loops are easy. It's that **nobody is going to watch variant #37 as carefully as variant #1**, so the standard has to be written down before the loop runs. That's the `QA_REGIME.md`.
5
+ The thing that makes bulk work is not the loop — loops are easy. It's that **nobody is going to watch variant #37 as carefully as variant #1**, so the standard has to be written down before the loop runs. That's the `HARNESS.md`.
6
6
 
7
7
  ### 0. Read the craft harness first
8
8
 
9
- `references/hooks-and-virality.md` — the four charges (hook / loop / payoff / bait), the three gates, and the anti-patterns that only bite at volume. Two of them decide whether this batch is worth running at all: **a different noun is not a different hook** (twenty variants of one sentence with the nouns swapped is one video), and **never point a generator at your grader** (a model writing hooks scored by the same model converges on the rubric, not on what works — scores climb, nothing improves). The regime catches defects; it does not rank winners.
9
+ `references/hooks-and-virality.md` — the four charges (hook / loop / payoff / bait), the three gates, and the anti-patterns that only bite at volume. Two of them decide whether this batch is worth running at all: **a different noun is not a different hook** (twenty variants of one sentence with the nouns swapped is one video), and **never point a generator at your grader** (a model writing hooks scored by the same model converges on the rubric, not on what works — scores climb, nothing improves). The harness catches defects; it does not rank winners.
10
10
 
11
11
  ### 1. Agree the variant axis — before any code
12
12
 
@@ -20,14 +20,22 @@ vidfarm pull <forkId> --dir ./work # one canonical base fork per batch
20
20
 
21
21
  Read `./work/.harness/agent-guide.md` first, as always.
22
22
 
23
- ### 3. Install and EDIT the regime
23
+ ### 3. Install and EDIT the harness
24
+
25
+ Two ways in, depending on where the format came from:
24
26
 
25
27
  ```bash
26
- vidfarm regime list # short-form | hooks | ugc-testimonial | explainer | product-demo
27
- vidfarm regime init hooks --out ./work/QA_REGIME.md
28
+ # (a) From a bundled base when the format is one you're defining
29
+ vidfarm harness list # short-form | hooks | ugc-testimonial | explainer | product-demo
30
+ vidfarm harness init hooks --out ./work/HARNESS.md
31
+
32
+ # (b) From the template you're batching — when the format is one you're REPLICATING
33
+ vidfarm harness derive <forkId> --out ./work/HARNESS.md # the decomposition, as a harness
28
34
  ```
29
35
 
30
- Then **edit it with the director**. The bundled file is a starting point; the parts that matter are the ones they add who the viewer is, their banned vocabulary, the compliance line, the pacing this account actually uses. A regime nobody edited isn't about their videos. Existing regime somewhere else on disk? Just point at it: `--regime ./brand/HOUSE_RULES.md`. They stack.
36
+ (b) is what a director means by *"give me the harness for this template_id"*: the decompose pass already extracted the template's viral / visual / structural / audio / build DNA, and `derive` folds those strands into the same editable Markdown a bundled base produces. If the fork was never decomposed, run `vidfarm decompose` first.
37
+
38
+ Either way, **edit it with the director**. The generated file is a starting point; the parts that matter are the ones they add — who the viewer is, their banned vocabulary, the compliance line, the pacing this account actually uses. A harness nobody edited isn't about their videos. A *derived* harness has the extra failure mode of sounding authoritative: it was written by a model that watched one video, so its "unknown" lines and its confident-but-wrong lines both need a human pass. Existing harness somewhere else on disk? Just point at it: `--harness ./brand/HOUSE_RULES.md`. They stack.
31
39
 
32
40
  ### 4. Source the N cheaply
33
41
 
@@ -38,13 +46,13 @@ vidfarm public-raws --category scroll-stoppers --limit 20 --json > pool.json
38
46
 
39
47
  A curated shelf is a pre-tagged, free, already-hosted clip pool — the cheapest way to get N distinct variants without N downloads or N generation calls.
40
48
 
41
- ### 5. Loop: edit → QA against the regime → render
49
+ ### 5. Loop: edit → QA against the harness → render
42
50
 
43
51
  ```bash
44
52
  for VARIANT in "${VARIANTS[@]}"; do
45
53
  SLUG="$(echo "$VARIANT" | tr ' ' '-' | cut -c1-40)"
46
54
  vidfarm set-text ./work --layer hook --text "$VARIANT"
47
- vidfarm qa ./work --json > "qa/$SLUG.json" # ./work/QA_REGIME.md auto-discovered
55
+ vidfarm qa ./work --json > "qa/$SLUG.json" # ./work/HARNESS.md auto-discovered
48
56
  jq -e '.ok' "qa/$SLUG.json" >/dev/null || { echo "skipped $SLUG"; continue; }
49
57
  vidfarm render "$FORK_ID" --dir ./work --out "renders/$SLUG.mp4" --tracer "batch-$SLUG"
50
58
  done
@@ -81,11 +89,11 @@ Read the sheets. In a batch you're looking for two different things: **per-video
81
89
 
82
90
  ### 6. Answer the review items — don't skip this
83
91
 
84
- The regime's `- [ ]` checklist comes back on every run because the CLI *can't* settle it. Machine checks catch a 13-word hook or a black first frame; only you can answer "is this variant genuinely different from its siblings?" or "can the viewer guess the withheld answer?" **Report both halves honestly**: what the machine checked, and what you judged. A batch report claiming a clean pass on the judgment half is worse than no report.
92
+ The harness's `- [ ]` checklist comes back on every run because the CLI *can't* settle it. Machine checks catch a 13-word hook or a black first frame; only you can answer "is this variant genuinely different from its siblings?" or "can the viewer guess the withheld answer?" **Report both halves honestly**: what the machine checked, and what you judged. A batch report claiming a clean pass on the judgment half is worse than no report.
85
93
 
86
- ### 7. Feed what you learn back into the regime
94
+ ### 7. Feed what you learn back into the harness
87
95
 
88
- When the director says "the label-framed hooks all died" or "anything over 30s tanked", write it into `QA_REGIME.md` as a rule or a checklist line — with the reason attached, so the next agent doesn't argue it away. The compositions are disposable; **the regime is the artifact that compounds across batches.**
96
+ When the director says "the label-framed hooks all died" or "anything over 30s tanked", write it into `HARNESS.md` as a rule or a checklist line — with the reason attached, so the next agent doesn't argue it away. The compositions are disposable; **the harness is the artifact that compounds across batches.**
89
97
 
90
98
  ### Cost note
91
99
 
@@ -16,7 +16,7 @@ The mechanical trio — **generate on a chroma plate → key it out → trim to
16
16
  ```
17
17
  One accent color for the active word, everything else near-black. No outline/stroke, no drop shadow, no pill — those exist to survive busy footage and just add noise on white.
18
18
 
19
- **Those hexes are the answer for a white stage, not the answer.** They are one instance of a general rule: **caption color, active-word color and plate are chosen by MEASURING the background behind the caption band, never by taste or habit.** On a near-black stage the same flags ship a bright plate the design never asked for and an active word nobody can read. The measurement procedure and the three treatments live in `regimes/short-form.QA_REGIME.md` → "Caption styling is measured off the background" — read it before you copy the line above onto anything that isn't white.
19
+ **Those hexes are the answer for a white stage, not the answer.** They are one instance of a general rule: **caption color, active-word color and plate are chosen by MEASURING the background behind the caption band, never by taste or habit.** On a near-black stage the same flags ship a bright plate the design never asked for and an active word nobody can read. The measurement procedure and the three treatments live in `harnesses/short-form.HARNESS.md` → "Caption styling is measured off the background" — read it before you copy the line above onto anything that isn't white.
20
20
  - **Female TTS narration.** Default to a warm, friendly **female** voice and say which one you picked: local-first `vidfarm tts "<script>" --voice coral` (OpenAI — `nova` if the script wants more energy, `sage` for calmer), `--voice Kore` or `Leda` on Gemini, or `vidfarm voices` → `vidfarm tts --cloud --voice <voice_id>` on ElevenLabs. Tell the director they can swap it in one flag.
21
21
  - **Clean and simple wins.** One idea on screen at a time. Two or three cutouts per beat, not eight. Generous white space, one accent color, one font. When in doubt, remove an element rather than add one.
22
22
 
@@ -7,7 +7,7 @@ Use this when a coding agent is doing the work locally or the user wants a repro
7
7
  3. Read `./work/.harness/agent-guide.md` and `./work/.harness/context.json` before editing.
8
8
  4. Make deterministic edits to `composition.html` and optionally `composition.json`.
9
9
  5. Validate with `vidfarm lint` or `vidfarm stills` when useful. **Always look at `vidfarm stills ./work --at 0`** — that frame becomes the thumbnail, so it must not be black, empty, or mid-fade.
10
- 6. **QA before you render: `vidfarm qa ./work`.** Free, instant, devcli-only. It blocklists HTML slop (CTA buttons, benefit chip rows, a lone pill around a static stat/label, frosted cards, gradient text, web-page classes/fonts), checks the caption font regime + safe zone, and flags a blank/fading first frame (the thumbnail). Feedback only — exit 0 even on findings, never automatic — but it catches the #1 tell of an agent-made video, so run it on every production. Fix what's real, ignore what's a deliberate style call, then render.
10
+ 6. **QA before you render: `vidfarm qa ./work`.** Free, instant, devcli-only. It blocklists HTML slop (CTA buttons, benefit chip rows, a lone pill around a static stat/label, frosted cards, gradient text, web-page classes/fonts), checks the caption font regime + safe zone, flags oversized captions and static walls of text, and flags a blank/fading first frame (the thumbnail). It cannot see pixels, so *where in the frame* the caption sits is still on you — which is why every run ends with a **`▶ NOW WATCH THE VIDEO`** block: render, `vidfarm stills ./work --sheet`, open the contact sheet, and judge each caption against its actual picture. Do that before you report the video as done. Feedback only — exit 0 even on findings, never automatic — but it catches the #1 tell of an agent-made video, so run it on every production. Fix what's real, ignore what's a deliberate style call, then render.
11
11
  7. Render with `vidfarm render <forkId> --dir ./work --wait`.
12
12
  7b. **Review the render as a whole before you approve — this is the step that most changes quality.** `vidfarm qa` and `lint` are static checks on the DOM; neither can see the video. Tile ~12 stills into one contact sheet and read it as an image — `vidfarm stills ./work --sheet` does both in one command (add `--at 0,2,4,…` to pick the timestamps): consistent margins, one type scale, one accent colour, deliberate pacing, no jarring join, no dead band under top-anchored content, end card settled ≥2s before the last frame. Compare frames from two different scenes — a frozen render (overlay pass without `-loop 1`, assets outside the composition root) passes duration, frame-count and audio-hash checks while every frame is identical. Check the mix by measurement, not by ear. Full method + the six most common defects: `references/reviewing-renders.md`.
13
13
  8. **Ask about deduplication before you approve** — "is this going out more than once (several accounts, another platform, a re-post later)?" If yes, run `vidfarm dedupe ./final.mp4 [--variants N]` on the **exported** MP4 (free, local ffmpeg, no re-render) and approve each variant separately. Asking here rather than after publication is what avoids paying for a second render. See `references/core-workflows.md` → *Deduplicate before you publish*.
@@ -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
@@ -231,7 +255,7 @@ The licensed harness also carries the **generative build workflow** guidance (ch
231
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. **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". |
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 |
@@ -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)
@@ -229,9 +286,9 @@ A video with replies gets shown again; a video with none dies at its first audie
229
286
  | 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
287
  | Place the hook at frame 0 | `add_layer` / `set_captions` with `start:0` | `vidfarm set-text ./work --layer hook --text "…"` |
231
288
  | 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` |
289
+ | Grade the structure | by hand, against this file | `vidfarm qa ./work --harness hooks` |
290
+ | Bulk hook test | hand off to a local agent | `recipes/bulk-scripting-with-a-harness.md` |
234
291
 
235
292
  **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
293
 
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).
294
+ **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