@officexapp/vidfarm-devcli 0.21.27 → 0.21.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/editor-capabilities/SKILL.md +52 -0
- package/.agents/skills/vidfarm/SKILL.md +58 -5
- package/.agents/skills/vidfarm/recipes/bulk-scripting-with-a-regime.md +65 -0
- package/.agents/skills/vidfarm/recipes/cutout-graphics-for-explainers.md +78 -7
- package/.agents/skills/vidfarm/recipes/local-edit-render-approve.md +4 -3
- package/.agents/skills/vidfarm/recipes/retheme-template.md +1 -1
- package/.agents/skills/vidfarm/references/assets-and-sourcing.md +3 -3
- package/.agents/skills/vidfarm/references/automation-and-local-dev.md +92 -1
- package/.agents/skills/vidfarm/references/editor-workflows.md +135 -6
- package/.agents/skills/vidfarm/references/hooks-and-virality.md +237 -0
- package/.agents/skills/vidfarm/references/onboarding.md +5 -5
- package/.agents/skills/vidfarm/references/primitives.md +5 -1
- package/.agents/skills/vidfarm/regimes/README.md +77 -0
- package/.agents/skills/vidfarm/regimes/explainer.QA_REGIME.md +82 -0
- package/.agents/skills/vidfarm/regimes/hooks.QA_REGIME.md +117 -0
- package/.agents/skills/vidfarm/regimes/product-demo.QA_REGIME.md +92 -0
- package/.agents/skills/vidfarm/regimes/short-form.QA_REGIME.md +163 -0
- package/.agents/skills/vidfarm/regimes/ugc-testimonial.QA_REGIME.md +82 -0
- package/SKILL.director.md +685 -32
- package/SKILL.md +22 -3
- package/demo/dist/app.js +103 -103
- package/dist/src/cli.js +987 -11
- package/dist/src/devcli/handoff.js +162 -0
- package/dist/src/devcli/interaction-mode.js +154 -0
- package/dist/src/devcli/qa-check.js +593 -0
- package/dist/src/devcli/qa-regime.js +396 -0
- package/dist/src/devcli/sticker-pack.js +396 -0
- package/dist/src/devcli/storyboard.js +243 -0
- package/package.json +8 -1
- package/public/serve-shells/tools-image.html +378 -265
- package/public/serve-shells/tools-video.html +760 -167
|
@@ -36,6 +36,50 @@ Send a stable `tracer` on export so retries are traceable and filterable in job
|
|
|
36
36
|
|
|
37
37
|
**Scripting mode** is the recommended posture for repeatable template automation. Use it when a director wants to take a template they like, agree on a base fork, and then drive bulk or one-off edits entirely through REST or `vidfarm api` from a script, Lambda, or local machine.
|
|
38
38
|
|
|
39
|
+
**Ask which mode you're in, once, before you build.** These are different jobs and the answer changes everything downstream:
|
|
40
|
+
|
|
41
|
+
| | **One-time video** | **Bulk / scripting mode** |
|
|
42
|
+
|---|---|---|
|
|
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 |
|
|
45
|
+
| What you optimize | This video | The *variant axis* (one thing changes; everything else is held) |
|
|
46
|
+
| Cost posture | Per-video decisions are fine | Per-video AI spend × N — reuse assets, prefer clip pools |
|
|
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.
|
|
49
|
+
|
|
50
|
+
### `QA_REGIME.md` — the quality contract for a batch
|
|
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.
|
|
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.
|
|
55
|
+
|
|
56
|
+
```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
|
|
61
|
+
```
|
|
62
|
+
|
|
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.
|
|
64
|
+
|
|
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.
|
|
66
|
+
|
|
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.
|
|
68
|
+
|
|
69
|
+
### The bulk loop, with the regime in it
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
vidfarm regime init hooks --out ./work/QA_REGIME.md # once, then edit for this account
|
|
73
|
+
for VARIANT in "${VARIANTS[@]}"; do
|
|
74
|
+
vidfarm set-text ./work --layer hook --text "$VARIANT"
|
|
75
|
+
vidfarm qa ./work --json > "qa/$SLUG.json" # regime auto-discovered from ./work
|
|
76
|
+
jq -e '.ok' "qa/$SLUG.json" >/dev/null || continue # YOUR gate, in YOUR script
|
|
77
|
+
vidfarm render "$FORK_ID" --dir ./work --out "renders/$SLUG.mp4"
|
|
78
|
+
done
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
`vidfarm qa` still exits 0 on findings — the gate above is the *script's* choice, made explicit with `jq`, not a behavior change in the tool. Keep it that way: an agent that can't ship a deliberately weird variant will quietly stop trying weird variants.
|
|
82
|
+
|
|
39
83
|
This section is for a **desktop/local coding agent**, not the web copilot. A local Codex/Claude agent may use its shell and filesystem to write JavaScript/TypeScript/Python/shell scripts, fetch every API page, join and score catalog/library data, calculate statistics, emit CSV/JSON/Markdown reports, manipulate composition DOM files, and run iterative render/inspection loops. The web copilot cannot inherit those abilities from this document: it may only call its declared tools and bounded REST routes. If web chat prepares work for this flow, consume its My Files handoff document as input; do not claim the web chat itself executed the script.
|
|
40
84
|
|
|
41
85
|
Contract:
|
|
@@ -169,9 +213,10 @@ The licensed harness also carries the **generative build workflow** guidance (ch
|
|
|
169
213
|
| `vidfarm generate <image\|video> --prompt "…"` | `POST /api/v1/primitives/{images,videos}/generate` (polls job) | generate AI media → finished URL |
|
|
170
214
|
| `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) |
|
|
171
215
|
| `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 |
|
|
172
|
-
| `vidfarm remove-greenscreen <image\|video> [--preset green\|blue\|white\|black\|digital-green\|magenta] [--key-color #00FF00] [--tolerance 0.3] [--local] [--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. Aliases: `greenscreen`, `remove-background-greenscreen`. |
|
|
216
|
+
| `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`. |
|
|
173
217
|
| `vidfarm cutout <image\|url> [--generate "<prompt>"] [--preset green] [--pad <px>] [--alpha-threshold <n>] [--no-trim] [--output-format png\|webp] [--out <f>]` | **local, free, ffmpeg-only** (no job) — key + `alphaextract`/`cropdetect` trim | **The transparent explainer-STICKER maker.** Keys out the flat plate **and then shrinks the canvas to the cutout's true min width/height** (a 1024² mostly-empty plate → a snug sticker whose pixel size IS the subject) so you can scale/position it precisely. `--generate` AI-generates the graphic first on a matching chroma plate (that step is the billed image primitive), then keys+trims in one shot; without it, keys+trims a file/url you already have. **IMAGE-only** (a moving subject has no single bounding box — key a clip with `remove-greenscreen`). Prefer this over `create-overlay` locally: same idea, but free and auto-trimmed. `--pad` keeps transparent breathing room; `--json` reports final `width`/`height`/`area_reduced_pct`. Alias: `sticker`. See recipe `cutout-graphics-for-explainers.md`. |
|
|
174
218
|
| `vidfarm mask <image\|url> [--crop x,y,w,h] [--flat <hex>] [--pad <px>] [--alpha-threshold <n>] [--no-trim] [--output-format png\|webp] [--keep-region <f>] [--out <f>]` | **local, free** (no job) — ffmpeg crop + ONNX matting (or ffmpeg chroma-key) + `cropdetect` trim | **Lift an illustration OUT of an image you already have** (infographic / poster / marketing graphic / brand sheet / screenshot) → snug transparent PNG, the same reusable explainer sticker `cutout` makes but with **$0 and zero AI generation** — the cost-saving move whenever source art exists. `--crop x,y,w,h` (pixels **or** %) isolates ONE element from a multi-illustration source before masking (re-run with different rects to grab each). Background removed by **local ONNX matting** (any/busy background) by default, or **`--flat <hexcolor>`** chroma-keys a solid fill for crisper edges (an infographic's cream/white paper); then trims to the subject's true min width/height. **IMAGE-only** (matte a clip with `remove-background`). Aliases: `isolate`, `extract`. See recipe `cutout-graphics-for-explainers.md` → "Mask from an image you already have". |
|
|
219
|
+
| `vidfarm sticker-pack [sheet\|url] [--generate "<theme>"] [--items "a,b,c"] [--count <n>] [--dry-run] [--gap <pct>] [--min-area <pct>] [--output-format png\|webp\|gif] [--out-dir <d>]` | **local, free, ffmpeg-only** (no job; only `--generate` bills, ONCE for the whole set) — key + alpha-channel segmentation + per-item trim | **The STICKER-PACK maker — the answer whenever a director asks for "a sticker pack" / prop set / icon set.** A pack is ONE greenscreen sheet holding every item, keyed once and then masked apart: 1/N the cost of N `cutout` calls, and the only way a cast stays on-style. Finds each item **automatically** by segmenting the keyed sheet's alpha into connected islands — no hand-measured `--crop` rects — and writes one snug transparent file per item (named from `--items`, reading order) plus a `stickers.json` manifest. `--dry-run` prints the detected boxes first; `--gap` merges (lower) or splits (raise) items that came out joined/broken; items have **no maximum size** — a full-frame landscape/backdrop is as valid a sticker as a 3% icon. **Plate color is chosen for you:** when generating it reads the subject and moves the plate off any hue the art uses (green → magenta → blue → black → white — a pack of leaves/frogs/money on GREEN would key holes through the art), and when splitting an existing sheet it DETECTS the plate from the sheet's four corners, so a red/purple sheet handed back from a web tool just works. Pin it with `--key-color`/`--preset`, or `--no-auto-key` for plain green. `--output-format gif` emits 1-bit-alpha GIFs for GIF-only surfaces. IMAGE-only. Aliases: `stickers`, `sticker-sheet`. See recipe `cutout-graphics-for-explainers.md` → "A sticker pack". |
|
|
175
220
|
| `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 |
|
|
176
221
|
| `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) |
|
|
177
222
|
| `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 |
|
|
@@ -199,6 +244,10 @@ The licensed harness also carries the **generative build workflow** guidance (ch
|
|
|
199
244
|
| `vidfarm login <email>` / `vidfarm verify <email> <code>` | `POST /api/v1/user/request-otp` · `verify-otp` | get an API key |
|
|
200
245
|
| `vidfarm whoami` | `GET /api/v1/user/me` | who am I (also prints the active cost mode) |
|
|
201
246
|
| `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 |
|
|
247
|
+
| `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`. |
|
|
248
|
+
| `vidfarm handoff image --theme "<what>" [--items "a,b,c"] [--style …] [--single]` | local (pure text) | **The interactive-mode IMAGE brief.** Prints the exact prompt, the numbered steps, the free tools (meta.ai / ChatGPT / Gemini / HF Spaces) and the follow-up command. Defaults to a **sticker pack**: ONE sheet holding every item on a chroma plate → `vidfarm sticker-pack` splits it for $0. Picks a plate the art won't collide with (green art → magenta plate) and carries that `--key-color` into the follow-up. `--single` for one subject. |
|
|
249
|
+
| `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. |
|
|
250
|
+
| `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`. |
|
|
202
251
|
| `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` |
|
|
203
252
|
| `vidfarm provider-keys` / `vidfarm add-provider-key <p> <secret>` | `GET`·`POST /api/v1/user/me/provider-keys` | manage AI keys |
|
|
204
253
|
| `vidfarm upload <file> [--folder <path>]` | presign → S3 PUT → finalize (`.../temporary-files/presign` + `.../temporary-files`) | upload → durable URL (ephemeral, 30-day TTL; prefer `--folder temp` for scratch). Goes direct to S3, so large files (up to **200 MB**) bypass the ~6 MB Lambda body limit |
|
|
@@ -219,6 +268,8 @@ The licensed harness also carries the **generative build workflow** guidance (ch
|
|
|
219
268
|
| `vidfarm raws preset list\|run\|save` / `raws export <ids…> --to <dir>` | (local library) | saved queries; copy raw MP4s out |
|
|
220
269
|
| `vidfarm lint <dir\|composition.html>` | (local static validation) | pre-publish composition check: timing, overlaps, preset names, media src |
|
|
221
270
|
| `vidfarm stills <dir> [--at 0,2.5,…]` | (local in-process render of PNG frames) | visually verify an edit without a full render |
|
|
271
|
+
| `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 |
|
|
272
|
+
| `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 |
|
|
222
273
|
| `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) |
|
|
223
274
|
| `vidfarm skills list\|add <name>\|update` | `GET /skill-pack/index.json` · `/skill-pack/:name/*` | install/refresh skill packs (see "Skill packs — import on demand") |
|
|
224
275
|
| `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 |
|
|
@@ -232,6 +283,46 @@ The licensed harness also carries the **generative build workflow** guidance (ch
|
|
|
232
283
|
|
|
233
284
|
**Approving a locally rendered MP4 (the URL-first rule + the durability rule).** The approve route (`POST /api/v1/approved/posts`) and every media-taking route accept a `url`, never raw file bytes — so you never "upload to approve" in one shot. An approved post is a **permanent** share page, so the media must live in **durable My Files**, not the 30-day temp store (a temp-hosted video would 404 the share page after 30 days). The correct sequence for a big local file is: (1) `POST /api/v1/user/me/attachments/presign` with `{ file_name, content_type, size_bytes }` → (2) PUT the raw bytes to the returned presigned S3 URL → (3) `POST /api/v1/user/me/attachments` (finalize) → use the returned durable `viewUrl` as the approve media `url`. `vidfarm approve --video ./final.mp4` does all of this automatically (durable by default; up to **200 MB**). Never POST a large file as multipart to `.../attachments/upload` against the cloud host: that path proxies through Lambda and caps near 6 MB (it exists only as a fallback for local-storage `vidfarm serve` boxes). Only use the temp-store route (`.../temporary-files/*`, or `vidfarm approve --temp`) for a **throwaway** preview you don't mind losing in 30 days.
|
|
234
285
|
|
|
286
|
+
## `vidfarm qa` — the social-native QA pass (devcli-only, heavily recommended)
|
|
287
|
+
|
|
288
|
+
```bash
|
|
289
|
+
vidfarm qa ./work # human-readable findings + verdict
|
|
290
|
+
vidfarm qa ./work --json # machine-readable: rule / severity / where / fix
|
|
291
|
+
vidfarm qa ./work --strict # ALSO exit 1 on slop (only if you want a CI gate)
|
|
292
|
+
vidfarm qa ./work --regime hooks # + grade against a QA_REGIME.md (repeatable; also
|
|
293
|
+
# auto-discovers ./work/QA_REGIME.md)
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
**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.
|
|
297
|
+
|
|
298
|
+
It also judges **one frame on its own terms: t=0**, because that frame becomes the thumbnail every feed and share sheet freezes on (see `references/editor-workflows.md`, "The first frame is the thumbnail"). Pair it with `vidfarm stills ./work --at 0` — QA finds the structural cause, the still shows you the actual poster.
|
|
299
|
+
|
|
300
|
+
**It is feedback, not a gate.** Default exit code is **0** even when it finds slop; nothing in the render or publish path calls it; it never runs automatically. `--strict` exists only if you deliberately want a CI failure. A finding you disagree with is fine to ignore and say so — it is a lint, not a verdict on the work.
|
|
301
|
+
|
|
302
|
+
**It is a BLOCKLIST, not an allowlist.** It names specific known-bad web patterns. Everything it doesn't name is legal, so a weird, ugly, hand-made, or wildly stylized composition passes untouched. It will never push your videos toward one house style — if it fires on a genuine creative choice, that's a bug in the rule, not in your video.
|
|
303
|
+
|
|
304
|
+
What it flags:
|
|
305
|
+
|
|
306
|
+
| Rule | Severity | What trips it |
|
|
307
|
+
|---|---|---|
|
|
308
|
+
| `cta-button` | error | Action copy ("Sign Up for a Free Trial →") **inside** a filled/gradient rounded capsule. Bare CTA copy in a caption is fine — "BUY NOW" is real social copy |
|
|
309
|
+
| `badge-chip-row` | error | 2+ sibling small rounded filled tags — the "✓ No Credit Card Needed · ✓ 30-Min Trial" strip |
|
|
310
|
+
| `card-panel` / `glass-card` | error | A rounded box with a border/shadow/`backdrop-filter` wrapping 2+ elements. A tight legibility **band** (radius ≤8px, one text run, no border/shadow) stays legal |
|
|
311
|
+
| `gradient-text` | error | `background-clip:text` gradient headline fills |
|
|
312
|
+
| `clickable-element` | error/warn | `<button>`, `<form>`, `<input>`; `<a href>` warns |
|
|
313
|
+
| `web-framework-classes` | error/warn | Bootstrap/Tailwind class tokens (`btn`, `badge`, `card`, `hero`, `col-*`, `rounded-full`, `shadow-lg`, `backdrop-blur`, `bg-gradient-to-*`) or a linked CSS framework. A `<script>` CDN for GSAP/anime.js is fine |
|
|
314
|
+
| `page-structure` / `bullet-list` | error/warn | `<nav>`/`<header>`/`<footer>`/`<table>`; a `<ul>` with visible bullet markers |
|
|
315
|
+
| `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) |
|
|
316
|
+
| `font-size` / `font-weight` | error/warn | `font-size:0` (invisible) is an error; sub-2.6%-of-canvas-width text and weight <600 warn |
|
|
317
|
+
| `caption-safe-zone` | warn | Text outside the 8%–85% band on a **portrait** canvas (landscape/square are exempt) |
|
|
318
|
+
| `thumbnail-blank-open` | error | Nothing on screen at **t=0** — the opening clip starts late, so the poster frame is black |
|
|
319
|
+
| `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 |
|
|
320
|
+
| `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 |
|
|
321
|
+
|
|
322
|
+
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.
|
|
323
|
+
|
|
324
|
+
**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.
|
|
325
|
+
|
|
235
326
|
## Cost mode — the devcli's money-saving guardrail
|
|
236
327
|
|
|
237
328
|
The four modes, quoted as **cost per finished video**. The first two are spend postures; the last two are also *production methods* — `rich-ai` buys reusable greenscreen raws and animates them in HTML, `pure-videogen` generates every frame:
|
|
@@ -370,13 +370,79 @@ A clip/image whose **native aspect differs from the canvas** — a 16:9 landscap
|
|
|
370
370
|
- **`object_fit:"cover"`** (default) fills + crops — right for most social footage **when paired with a subject-aware `object_position`**.
|
|
371
371
|
- **`object_position`** aims WHERE the cover-crop lands: a keyword (`left`, `right`, `top`, `bottom`, `"top left"`, …) or a **percentage pair** (`"30% 50%"` = 30% from the left, 50% down). Landscape clip with the subject on the left → `"left"`/`"25% 50%"`; tall screenshot with key text up top → `"top"`.
|
|
372
372
|
- **`object_fit:"contain"`** shows the WHOLE media with black letterbox/pillarbox bars — use ONLY when nothing may be cropped (an infographic, a whole screenshot, a chart, a logo); a 16:9 source full-canvas on a 9:16 frame reads unfinished in `contain`.
|
|
373
|
-
- **Blurred
|
|
373
|
+
- **Blurred plate** — the default answer for landscape footage on a vertical canvas. Full section below; reach for it instead of `contain` whenever the whole frame matters.
|
|
374
374
|
- Never `object_fit:"fill"` (stretch/distort) unless intentional. You usually know a clip's aspect from how you sourced it (a hunted raw's `--aspect`, a generated clip's `--aspect-ratio`); if you truly need exact dims, `vidfarm probe` / `GET /videos/probe`. When you REPLACE a full-canvas scene, keep it full canvas + `cover` + a focal `object_position` rather than shrinking; when you GENERATE, request the canvas aspect so no crop is needed.
|
|
375
375
|
|
|
376
376
|
Two surfaces:
|
|
377
377
|
- **Editor web/serve copilot** — `editor_action` `action_type=set_layer_media` with `object_fit` and `object_position` (both also seedable on `add_layer`/`generate_layer`). `editor_context.layers[]` reports each clip's current `object_fit`/`object_position` so you can read before re-cropping.
|
|
378
378
|
- **Desktop agents (devcli)** — `vidfarm place ./work --src <url> --object-fit cover --object-position "25% 50%"` (also on `--replace <layer_key>`).
|
|
379
379
|
|
|
380
|
+
### The blurred plate — landscape footage, fullscreen, on a vertical canvas
|
|
381
|
+
|
|
382
|
+
**This is the house treatment for real filmed footage in a fullscreen explainer.** The director hands you 16:9 (or 4:3, or 960×540) footage and wants a 9:16 video where the picture reads *fullscreen* — edge to edge, no bars, no dead space — but where nothing important gets cropped away. Neither default fit gets you there: `cover` throws away the left and right thirds of a wide shot (the subject ends up half out of frame), and `contain` leaves hard black letterbox bars that read as "unfinished export" the instant it hits a feed.
|
|
383
|
+
|
|
384
|
+
The fix is the **blurred plate**: the *same* clip twice, one behind the other.
|
|
385
|
+
|
|
386
|
+
1. **Backdrop** — a duplicate of the clip, full canvas, `cover`, so it's blown up ~3.5× and fills the whole 1080×1920. Heavy **gaussian blur** (`sigma ≈ 60` at 1080 wide), plus a **fade to dark** (brightness ≈ −0.14, saturation ≈ 0.9) so it recedes and never competes with type. Because it's the same frame at the same timecode, the colors, motion, and lighting are continuous with the sharp copy — the eye reads one deep image, not a picture pasted on wallpaper.
|
|
387
|
+
2. **Hero band** — the sharp copy, centered, at (or slightly above) native aspect. Optionally **zoom it ~1.2–1.4×** past canvas width so the sharp band is taller and the blur does less work; the small side-crop that costs you is almost always cheaper than the vertical dead space it buys back.
|
|
388
|
+
3. **Feathered seam** — fade the hero band's top and bottom edges out over ~100px instead of butting them against the backdrop. **This is the step that makes or breaks it.** A hard edge is just a letterbox with a fancy background; a feathered edge makes the sharp band read as the in-focus plane of a single continuous shot, like shallow depth of field.
|
|
389
|
+
|
|
390
|
+
The result: a fullscreen vertical frame, no bars, the entire landscape composition intact, and generous dark space at top and bottom for a header bar and captions. Reference render: `drafts/real-filming-demos/videos/fruitpoint-explainer/renders/video.mp4`.
|
|
391
|
+
|
|
392
|
+
**Bake it once with ffmpeg (recommended).** One 1080×1920 file per clip, produced locally and free, that then behaves like any ordinary full-canvas layer — no dual-track bookkeeping, no double video decode at render, and it works identically in the Trackpad Editor, `vidfarm serve`, and cloud render. Keep the baked plates beside the originals (e.g. `assets/full/` next to `assets/clips/`) so you can re-bake with different numbers later.
|
|
393
|
+
|
|
394
|
+
```bash
|
|
395
|
+
# landscape clip → 1080x1920 blurred plate. Tune: sigma (blur), brightness (fade),
|
|
396
|
+
# 1404 (= 1080 * 1.3 hero zoom; use 1080 for native), 110 (feather height in px).
|
|
397
|
+
ffmpeg -y -i clip.mp4 -filter_complex "\
|
|
398
|
+
[0:v]split=2[bg][fg];\
|
|
399
|
+
[bg]scale=1080:1920:force_original_aspect_ratio=increase,crop=1080:1920,gblur=sigma=60,eq=brightness=-0.14:saturation=0.90[bgb];\
|
|
400
|
+
[fg]scale=1404:-2,crop=1080:in_h:(in_w-1080)/2:0,format=rgba,geq=r='r(X,Y)':g='g(X,Y)':b='b(X,Y)':a='255*min(1,min(Y,H-1-Y)/110)'[fgs];\
|
|
401
|
+
[bgb][fgs]overlay=(W-w)/2:(H-h)/2:format=auto,format=yuv420p[v]" \
|
|
402
|
+
-map "[v]" -map 0:a? -c:v libx264 -crf 18 -preset veryfast -c:a copy clip-plate.mp4
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
For a **still image** the same shape applies — drop `split`/`overlay` timing concerns and swap `-i clip.mp4` for the image with `-loop 1 -t <seconds>`, or just do it in HTML (below), since a still costs nothing to duplicate.
|
|
406
|
+
|
|
407
|
+
For **16:9 canvas fed portrait footage** (a phone clip on a wide frame), it's the identical trick rotated: blurred `cover` backdrop, sharp `contain`-width hero band, feathered left/right edges — swap the feather expression to `min(X,W-1-X)`.
|
|
408
|
+
|
|
409
|
+
**Or do it in the composition HTML** when you're hand-authoring and want the sharp band to animate independently (a slow push-in on the hero while the backdrop sits still):
|
|
410
|
+
|
|
411
|
+
```html
|
|
412
|
+
<div class="plate">
|
|
413
|
+
<video class="clip backdrop" src="assets/clip.mp4" muted playsinline data-start="0" data-duration="4"></video>
|
|
414
|
+
<div class="plate-scrim"></div>
|
|
415
|
+
<video class="clip hero" src="assets/clip.mp4" muted playsinline data-start="0" data-duration="4"></video>
|
|
416
|
+
</div>
|
|
417
|
+
<style>
|
|
418
|
+
.plate { position: absolute; inset: 0; overflow: hidden; }
|
|
419
|
+
.backdrop { position: absolute; inset: 0; width: 100%; height: 100%; object-fit: cover;
|
|
420
|
+
filter: blur(60px) brightness(0.86) saturate(0.9); transform: scale(1.1); }
|
|
421
|
+
.plate-scrim { position: absolute; inset: 0; background: rgba(8,14,6,.18); }
|
|
422
|
+
.hero { position: absolute; left: 50%; top: 50%; transform: translate(-50%,-50%) scale(1.3);
|
|
423
|
+
width: 100%; height: auto; object-fit: contain;
|
|
424
|
+
-webkit-mask-image: linear-gradient(to bottom, transparent 0, #000 100px,
|
|
425
|
+
#000 calc(100% - 100px), transparent 100%);
|
|
426
|
+
mask-image: linear-gradient(to bottom, transparent 0, #000 100px,
|
|
427
|
+
#000 calc(100% - 100px), transparent 100%); }
|
|
428
|
+
</style>
|
|
429
|
+
```
|
|
430
|
+
|
|
431
|
+
Both `<video>` elements carry `class="clip"` with the **same `src`, `data-start`, and `data-duration`**, so framework-owned playback keeps them frame-locked and the render stays seek-safe. `transform: scale(1.1)` on the backdrop hides the blur's soft outer edge (a gaussian blur samples past the element bounds and leaves a pale rim otherwise). Mute the backdrop copy — never both.
|
|
432
|
+
|
|
433
|
+
**Numbers that work, and what to move:**
|
|
434
|
+
|
|
435
|
+
| Knob | Default | Move it when |
|
|
436
|
+
|---|---|---|
|
|
437
|
+
| Blur `sigma` / `blur()` | `60` (at 1080 wide) | Under ~40 the backdrop stays legible and fights the hero; over ~90 it flattens to mush. Scale proportionally on other canvas widths. |
|
|
438
|
+
| Backdrop fade | `brightness −0.14`, `saturation 0.90` | Push darker (−0.20) only if captions sit **on** the blur; a scrim under the type is usually the better fix. Too dark reads as a black bar with texture — the exact thing you were avoiding. |
|
|
439
|
+
| Hero zoom | `1.3×` | `1.0` when the framing is tight and the sides carry the subject; up to `1.4` for loose wide shots where more picture beats more edge. |
|
|
440
|
+
| Feather | `~100px` | Bigger (150px) on soft/organic footage; smaller (60px) on graphic footage with strong horizontals that a long fade turns to smear. |
|
|
441
|
+
|
|
442
|
+
**Do not** vignette the backdrop, don't hard-edge the seam, don't run the backdrop unblurred-but-dark (you get a visible duplicate ghost), and don't reach for a solid color or a gradient behind the band — a flat backdrop is what makes a repurposed landscape clip look repurposed.
|
|
443
|
+
|
|
444
|
+
**Editor-surface caveat:** layer-level blur is not an `editor_action` property, so the two-layer version can't be assembled from the editor alone — **bake the plate with ffmpeg and drop it in as one full-canvas `cover` layer**. That's the path for `set_layer_media` / `vidfarm place` work, and it's why the pre-bake is the recommended default rather than the HTML variant.
|
|
445
|
+
|
|
380
446
|
### The opening frame is the post's thumbnail
|
|
381
447
|
|
|
382
448
|
**The composition's first frame (t=0) is the still that represents the whole video before anyone presses play** — it's the poster on the approved-post share page, the `/discover` card, the autoplay-off feed preview, and the file/scrubber thumbnail. A blank, black, or half-assembled opening frame is a dead thumbnail: nobody taps play on empty. Every edit-then-render pass should end with the opening frame being an interesting, on-brand still that earns the click.
|
|
@@ -405,16 +471,75 @@ Three surfaces:
|
|
|
405
471
|
- **Editor UI (human)** — the Inspector's **Animate (Ken Burns)** dropdown on any selected image.
|
|
406
472
|
- **Desktop agents (devcli)** — `vidfarm place ./work --src <img> --kind image --ken-burns zoom-in [--ken-burns-intensity 0.3]`, or fused with generation: `vidfarm generate image --prompt "…" --place ./work --ken-burns pan-left`.
|
|
407
473
|
|
|
408
|
-
###
|
|
474
|
+
### The first frame is the thumbnail — compose t=0 on purpose
|
|
475
|
+
|
|
476
|
+
**Frame 0 is one frame of ~30 in the first second, and it does more work than the other 29 combined.** Every feed card, share link, embed, DM preview, and paused player freezes on it, and platforms use it as the default poster unless a cover is picked by hand. So it is seen by everyone who scrolls past — including the majority who never press play. Treat it as a still ad for the video, not as "the moment before the video starts."
|
|
477
|
+
|
|
478
|
+
**The failure mode is silent.** A timeline that reads perfectly in the editor (where you scrub to an interesting moment) can open on black, because:
|
|
409
479
|
|
|
410
|
-
|
|
480
|
+
- the opening clip starts at `0.4s` instead of `0`, leaving a black gap at the head;
|
|
481
|
+
- the first clip carries a `fade-black` / `fade-white` / `flash` **entrance** — the intro fades *up from* a flat frame, so the poster is a solid black or white rectangle;
|
|
482
|
+
- the hook line is timed to the first spoken word (`start: 0.6`), so the poster frame is footage with no promise on it;
|
|
483
|
+
- a decomposed fork inherited the source's slate/countdown/blank head.
|
|
484
|
+
|
|
485
|
+
**Rules for every production:**
|
|
486
|
+
|
|
487
|
+
- **Something real is on screen at `start:0`.** No gap, no black leader. `vidfarm retime <dir> --layer <key> --start 0` pulls the opening clip to the head.
|
|
488
|
+
- **No entrance transition on the FIRST clip.** `vidfarm transitions set <dir> --layer <key> --in none`. Junction transitions between later clips are unaffected — this is only about the opening. (`vidfarm transitions apply --intro fade-black` deliberately opens from black; only use it when the piece is not being posted to a feed.)
|
|
489
|
+
- **The hook words are already up at t=0** unless you're deliberately opening on a clean face/product shot. The strongest short-form thumbnails state the promise in text.
|
|
490
|
+
- **Nothing is mid-animation.** A zoom/slide/blur entrance means the poster catches the frame offset, scaled, or blurred. Author the opening as its resting state and animate *after* frame 0.
|
|
491
|
+
- **Verify the actual pixels, never the intent:** `vidfarm stills ./work --at 0` renders exactly that frame to a PNG — look at it before you render. `vidfarm qa` flags the structural causes (`thumbnail-blank-open`, `thumbnail-fade-in`, `thumbnail-no-hook-text`), but the still is the ground truth.
|
|
492
|
+
|
|
493
|
+
Web copilot: same standard, applied by hand — check the opening layer's `start`, clear any entrance transition on it, and start the hook caption at 0.
|
|
494
|
+
|
|
495
|
+
### Social-native visual standard — no HTML slop, ever
|
|
496
|
+
|
|
497
|
+
Compositions are authored in HTML, so the single most common way an AI-edited video betrays itself is by **looking like a web page**. A landing-page button, a row of feature chips, a frosted card with a gradient headline — these are everywhere on the web and **nowhere in a real social post**. A director scrolling TikTok/Reels/Shorts never sees them, so the moment one appears the video reads as machine-made and the retention dies. HTML and hyperframes are the right tool; **Bootstrap/Tailwind furniture is not.**
|
|
498
|
+
|
|
499
|
+
**The one test:** *would this element exist if the video were shot on a phone and captioned in CapCut?* If it only makes sense inside a browser — if its whole job is to look **clickable** — cut it. **Nothing in a video is clickable.**
|
|
500
|
+
|
|
501
|
+
**BANNED — never author, and strip on sight when a fork or a paste brings one in:**
|
|
502
|
+
|
|
503
|
+
- **CTA / "button" shapes.** Any filled capsule or rounded rect containing action copy — "Sign Up for a Free Trial →", "Learn More", "Get Started", "Book a Call" — especially gradient-filled with a glow or drop shadow. A social CTA is *spoken*, or a plain caption line, or an arrow pointing at the real UI. Not a button.
|
|
504
|
+
- **Badge / chip / pill rows.** The "✓ ID-Verified Tutors · ✓ No Credit Card Needed · ✓ 30-Min Trial" strip of small rounded tags. Nothing on TikTok is a `<span class="badge">`. Say the three benefits as three timed caption lines instead — one at a time, on the footage.
|
|
505
|
+
- **Cards, panels, containers.** A rounded-rect box (dark, bordered, shadowed, or `backdrop-filter` frosted) holding a heading plus a subheading/URL. Text belongs **on the footage**, not inside a floating panel with padding around it.
|
|
506
|
+
- **Gradient text fills, neon border glows, elevation shadows, glassmorphism, hover-implying strokes.**
|
|
507
|
+
- **Web page furniture of any kind:** navbars, hero sections, feature grids, two-column layouts, `<ul>` bullet lists with disc markers, tables, alerts, progress bars as decoration, logo-in-a-circle avatars, "as seen in" strips.
|
|
508
|
+
- **Corporate web type:** Inter, Roboto, system-ui, Arial, Helvetica, Georgia, Times — at web weights (400–600) and web sizes (16–24px). Instantly reads as "a screenshot of a website."
|
|
509
|
+
|
|
510
|
+
**Greppable smell test.** If you are typing `class="btn…"`, `badge`, `chip`, `tag`, `card`, `panel`, `container`, `row`/`col-`, `hero`, `cta`, `rounded-full`, `shadow-lg`, `backdrop-blur`, `bg-gradient-to-r`, `border border-…` — **stop.** You are building a web page, not a video. Rewrite as timed text on footage.
|
|
511
|
+
|
|
512
|
+
**ALLOWED and encouraged — these ARE social-native:**
|
|
513
|
+
|
|
514
|
+
- **Arrows** (drawn, animated, hand-style), circles/scribbles/underline strokes highlighting part of the frame, hand-drawn marks, checkmarks *as glyphs inside a caption line* (not as chips).
|
|
515
|
+
- **Italic / oblique** for emphasis, ALL-CAPS, a single word in a pop color, mixed weights across a stacked line.
|
|
516
|
+
- **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.
|
|
517
|
+
- **Full-bleed footage** with text sitting directly on it.
|
|
518
|
+
|
|
519
|
+
**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`").
|
|
520
|
+
|
|
521
|
+
### TikTok-native caption standard (position + font + background) — always adhere
|
|
522
|
+
|
|
523
|
+
> 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.
|
|
524
|
+
|
|
525
|
+
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:
|
|
411
526
|
|
|
412
527
|
- **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).
|
|
413
|
-
- **Font → the composition regime.** Use the bundled display fonts only — **Montserrat** (bold default, weight
|
|
528
|
+
- **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
|
+
- **Background → one of exactly four valid treatments.** Any text you place uses one of these and nothing else:
|
|
414
530
|
|
|
415
|
-
|
|
531
|
+
| # | Treatment | How to set it | When |
|
|
532
|
+
|---|---|---|---|
|
|
533
|
+
| 1 | **Bare text + heavy outline/stroke** (black stroke on white or a bright fill) | `background_style:"outline"` | The default TikTok look |
|
|
534
|
+
| 2 | **Bare text + soft drop shadow only**, no box | `background_style:"plain"` | Clean/cinematic over busy footage |
|
|
535
|
+
| 3 | **Highlight pill behind the ACTIVE word only** | `set_captions caption_style:"spotlight"` / `"karaoke"` (+ `caption_highlight_color`) | Hormozi/CapCut word-by-word. **The only legitimate "pill" in a video** — it tracks the spoken word, so it isn't a badge |
|
|
536
|
+
| 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 |
|
|
416
537
|
|
|
417
|
-
|
|
538
|
+
Treatment 4 is a **band, not a card**: it hugs the glyphs with minimal padding, corner radius ≤ ~8px, **no border, no drop shadow, no gradient, no blur**, and it wraps *one* text run — never a heading + subheading + URL stacked inside one rounded box. The moment it grows padding, a stroke, or a second element inside it, it has become a web card. Fix it.
|
|
539
|
+
|
|
540
|
+
**A common trap: decomposed templates mirror the source's caption placement**, so a forked meme can arrive with its caption pinned at `top:0` in a non-regime font — and a re-theme prompt ("make this for my tutoring service") is exactly where an agent starts inventing landing-page CTAs and benefit chips because the *subject* is a SaaS product. **Fix to the standard, don't inherit it, and don't import the website's design language into the video.** When placing text yourself (`set_captions`, `set_layer_text`, `set_layer_style`, `add_layer`, devcli `place`/`captions`), set `y` / `font_family` / `font_weight` / `background_style` to the standard from the start.
|
|
541
|
+
|
|
542
|
+
> 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.
|
|
418
543
|
|
|
419
544
|
### Animated captions — word-by-word caption styles (TikTok/CapCut)
|
|
420
545
|
|
|
@@ -470,6 +595,10 @@ Beyond the Ken Burns / transition / animated-caption presets, the copilot can ha
|
|
|
470
595
|
|
|
471
596
|
**Make the sticker (`vidfarm cutout`).** Local, free, ffmpeg-only, image-only. `vidfarm cutout --generate "<subject>"` AI-generates on a matching plate then keys+trims (only the generation is billed); `vidfarm cutout <file|url>` keys+trims a graphic you already have for $0. Because the output is trimmed tight, its on-frame **width % maps directly to how big the subject reads** — no invisible padding skewing the layout. Knobs: `--preset`/`--key-color` (match the plate), `--pad <px>` (transparent breathing room), `--alpha-threshold <0..255>` (trim faint haloed edges), `--output-format webp`, `--no-trim`. A *moving* subject has no single bounding box → key it as a clip with `vidfarm remove-greenscreen <video>` (→ transparent WebM) instead.
|
|
472
597
|
|
|
598
|
+
**Make a whole PACK at once (`vidfarm sticker-pack`).** When the scene needs a *set* — props, icons, a cast, plus any backdrops — don't run `cutout` N times. Generate ONE greenscreen sheet holding every item and split it locally: `vidfarm sticker-pack --generate "<theme>" --items "a,b,c" --out-dir ./stickers` (one billed image job for the set), or `vidfarm sticker-pack ./sheet.png` for $0 on a sheet you already have. It keys the plate once, **segments the sheet's alpha channel to find each item automatically** (no hand-measured crop rects), trims each to its true box, and writes `stickers.json`. `--dry-run` to inspect the boxes; `--gap` to fix items that merged (lower) or split (raise). **Item size is unbounded** — a full-frame landscape/skyline/backdrop is as legitimate a sticker as a small icon, and belongs in the same sheet so it shares the cast's style. Recipe: `recipes/cutout-graphics-for-explainers.md` → "A sticker pack".
|
|
599
|
+
|
|
600
|
+
**Sticker formats.** Still sticker → **PNG** (or `--output-format webp` for smaller files). Sticker whose own artwork moves → **transparent WebM** via `vidfarm remove-greenscreen <video>` (ProRes `.mov` is the local fallback; `--cloud` always returns WebM). Sticker destined for a **GIF-only surface** (chat, forum, Notion, a shareable pack) → `--output-format gif` on `sticker-pack`, or `vidfarm remove-greenscreen <video> --gif` for an animated one; GIF alpha is 1-bit, so edges harden and soft shadows vanish — never the choice for compositing on a timeline.
|
|
601
|
+
|
|
473
602
|
**Place it.** Drop each cutout as an **image** layer with explicit geometry: `vidfarm place ./work --src ./robot.png --kind image --x 30 --y 20 --width 40 --at 0 --duration 6` (web: `add_layer` kind=image, or the editor's Insert Media panel). Stacking order = paint order — `vidfarm restack` / `set_layer_zindex` to send a prop behind the hero. Reuse the same PNG across scenes; a cutout is generated once and reused.
|
|
474
603
|
|
|
475
604
|
**Animate it — the explainer move vocabulary.** One layer at a time; all preview AND render identically. Devcli `vidfarm keyframes <dir> --layer <k> --preset <name>`; web `set_layer_keyframes`. Sticker-tuned presets:
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
## The hook & virality harness — apply to EVERY video
|
|
2
|
+
|
|
3
|
+
Most agent-made videos don't fail on polish. They fail on **structure**: no hook worth stopping for, no reason to stay, a payoff that never lands, and no ask at the end. Polish is what an agent is naturally good at, so it over-invests there and ships a beautiful video nobody watches.
|
|
4
|
+
|
|
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
|
+
|
|
7
|
+
The checkable form of this document is the bundled `hooks` regime (`vidfarm regime show hooks`); this file is the craft behind it.
|
|
8
|
+
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## The build order (do it in this order, it is the harness)
|
|
12
|
+
|
|
13
|
+
The reason agent videos come out structureless is that the timeline is the fun part, so it gets built first and the words get retrofitted. Invert it:
|
|
14
|
+
|
|
15
|
+
1. **Write the hook line as TEXT, before touching the timeline.** One line. Show it to the director. If it doesn't survive being read aloud with no visuals, no clip will save it.
|
|
16
|
+
2. **Name the open loop and where it closes.** One sentence: *"opens at 0:08 — 'the third one told me why the first two ghosted' — closes at 0:34."* If you can't state the timestamp it closes at, there is no loop.
|
|
17
|
+
3. **Name the payoff.** What is on screen at that moment, and why does it satisfy the promise?
|
|
18
|
+
4. **Write the bait.** The final-beat ask, in the video and in the post caption.
|
|
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).
|
|
21
|
+
|
|
22
|
+
Steps 1–4 are cheap, reversible, and where the entire outcome is decided. Steps 5–6 are where agents want to start.
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Charge 1 — THE HOOK (first 3 seconds)
|
|
27
|
+
|
|
28
|
+
### The three gates
|
|
29
|
+
|
|
30
|
+
A hook clears three gates **in order**, and they fail in order too — each one silently, so the view count never tells you which one killed you:
|
|
31
|
+
|
|
32
|
+
| # | Gate | Question | Where it lives | Failure looks like |
|
|
33
|
+
|---|---|---|---|---|
|
|
34
|
+
| 1 | **Legibility** | Can a stranger *understand* this? | Caption chunk 1 | Never leaves the test batch |
|
|
35
|
+
| 2 | **Tension** | Is there something they don't know? | The withheld final clause | Views and likes, no clicks |
|
|
36
|
+
| 3 | **Relevance** | Is this person a buyer? | Chunks 2–3 and the body | Clicks and opt-ins, no sales |
|
|
37
|
+
|
|
38
|
+
**The order is the whole insight.** Everyone writes for gate 3 first, because relevance is what marketing training optimizes for. But the platform's first test batch is a few hundred essentially random viewers, of whom the buyer is 1–2%. **A hook that only works on a perfectly-targeted viewer never gets a perfectly-targeted viewer** — ~98% of the batch swipes without comprehending, watch time craters, and the video never escapes to reach the people who'd have understood it.
|
|
39
|
+
|
|
40
|
+
### The unit is caption chunk 1, not "the first 3 seconds"
|
|
41
|
+
|
|
42
|
+
Captions render in phrase chunks of ~2–6 words, and the viewer **reads chunk one before hearing any audio**. The real hook unit is **words 1–5 on screen**. This is why the hook text layer starts at 0 and why `hook_words_max` is a machine check.
|
|
43
|
+
|
|
44
|
+
> **Chunk 1 must be legible to everyone and relevant to few.**
|
|
45
|
+
|
|
46
|
+
Three constraints:
|
|
47
|
+
|
|
48
|
+
1. **Complete clause — subject plus verb.** Not a noun phrase, not a bare number, not a label. This one rule fixes most failures, because a clause forces context in.
|
|
49
|
+
2. **No jargon.** A domain term in chunk 1 kills the cold start even inside a valid clause. Jargon belongs in chunk 2+, where it *qualifies* instead of excluding.
|
|
50
|
+
3. **The cold-viewer test.** Would a random person with no interest in the category understand what situation is being described? Not *would they care* — *would they understand*.
|
|
51
|
+
|
|
52
|
+
### Situations, not labels — the cheapest predictor there is
|
|
53
|
+
|
|
54
|
+
The three constraints are checks you run *after* writing. This one tells you whether a hook will survive *before* you write it:
|
|
55
|
+
|
|
56
|
+
> **Situations are cold-legible by construction. Labels never are.**
|
|
57
|
+
|
|
58
|
+
A **situation** is a thing that happened, with a subject and an outcome: *someone deleted my message · she refused my money · they stopped replying · tracking said delivered · his lot is full.* A stranger parses all of it instantly, because humans are built to parse events.
|
|
59
|
+
|
|
60
|
+
A **label** is a concept with a name: *anonymity · distribution · platform risk · seeding · onboarding.* It carries meaning only for someone who already holds the category — exactly the person the cold-start batch does not contain.
|
|
61
|
+
|
|
62
|
+
Across four avatars and 100 graded hooks, **every hook cut for illegibility was built on a label**, and the one cluster that lost nothing was built entirely on situations. If your opening names a concept rather than an event, assume it fails until proven otherwise.
|
|
63
|
+
|
|
64
|
+
| Fragment (fails) | Complete clause (passes) |
|
|
65
|
+
|---|---|
|
|
66
|
+
| `SIX STARTS, ZERO FINISHES` | `I'VE QUIT SIX BUSINESSES` |
|
|
67
|
+
| `FOURTH APP` | `I SHIPPED MY FOURTH APP` |
|
|
68
|
+
| `ANONYMITY` | `I'VE NEVER SHOWN MY FACE` |
|
|
69
|
+
| `$50,000 IN SALES` | `THEY WANTED $50,000 FIRST` |
|
|
70
|
+
| `A CARTOON FROG` | `A CARTOON FROG OUTSELLS ME` |
|
|
71
|
+
| `TWO HOURS A WEEK` | `THIS TAKES TWO HOURS A WEEK` |
|
|
72
|
+
|
|
73
|
+
The tension survives every fix — it just arrives inside a sentence a stranger can parse. Often the legible version is *stronger*, because a stranger can feel it where the fragment only rewarded someone already in the situation.
|
|
74
|
+
|
|
75
|
+
### Banned openers
|
|
76
|
+
|
|
77
|
+
*So I was thinking · There's a reason · Here's the thing · A lot of people · If you're a… · Let me tell you* — any throat-clearing. Also banned as an opening beat: a logo, a title card, a fade from black, a beat of silence, and any sentence whose subject arrives in the second half. **Context is beat 2**, and it goes after the hook, never before it.
|
|
78
|
+
|
|
79
|
+
### Stack three hooks simultaneously
|
|
80
|
+
|
|
81
|
+
Most videos fire only one:
|
|
82
|
+
|
|
83
|
+
1. **Verbal** — the spoken line.
|
|
84
|
+
2. **Text** — a caption already on screen at frame 0, readable before a word is said.
|
|
85
|
+
3. **Visual** — motion, an unusual angle, a face already mid-sentence, hands already working.
|
|
86
|
+
|
|
87
|
+
**Muted autoplay is the default viewing condition, so the text hook does more work than the verbal one.** If the video only works with sound on, it doesn't work.
|
|
88
|
+
|
|
89
|
+
⚠️ **Frame 0 is the hook AND the thumbnail** — the one frame more people see than watch the video. No black open, no fade-up, subject in frame, caption legible. Full rules in `references/editor-workflows.md` ("The first frame is the thumbnail").
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## Charge 2 — THE CURIOSITY LOOP (retention)
|
|
94
|
+
|
|
95
|
+
**The charge most videos skip entirely**, and the one that separates 200 views from 30,000. The algorithm is measuring whether people stay, and nothing holds attention like an unresolved question.
|
|
96
|
+
|
|
97
|
+
### Identification vs curiosity — know which you're writing
|
|
98
|
+
|
|
99
|
+
| Type | Triggers | Earns |
|
|
100
|
+
|---|---|---|
|
|
101
|
+
| **Identification** | "that's me" · "finally someone said it" | a like, a save, a follow |
|
|
102
|
+
| **Curiosity** | "wait, what?" · "why?" · "I need to know if that's true" | **a click** |
|
|
103
|
+
|
|
104
|
+
Both are legitimate and they are not interchangeable. If the video's job is a click into a funnel, **the loop matters more than the resonance.**
|
|
105
|
+
|
|
106
|
+
### The mechanical fix
|
|
107
|
+
|
|
108
|
+
Most weak hooks state the insight **and its resolution** in one line, so there's nothing left to find out. **Move the payoff out of the line and into the video body** — same insight, same specificity, the last clause just stops answering itself.
|
|
109
|
+
|
|
110
|
+
| Closes (dead) | Opens (loop) |
|
|
111
|
+
|---|---|
|
|
112
|
+
| "I paid three creators. I ended up teaching all three how to hook." | "I paid three creators. The third one told me why the first two ghosted." |
|
|
113
|
+
| "Every customer I have, I'm renting." | "I asked my lead company one question. Their answer told me I don't own a single customer." |
|
|
114
|
+
| "Launch day: three upvotes, two of them mine." | "Fourth launch, three upvotes. The problem was never the product." |
|
|
115
|
+
| "Nobody quits a project that's getting attention." | "Nobody quits a project that's getting attention. So your problem was never discipline." |
|
|
116
|
+
|
|
117
|
+
> **Tension in the first chunk. Withheld answer in the last chunk.**
|
|
118
|
+
|
|
119
|
+
The two constraints pull against each other — the loop withholds at the end, legibility demands the opening earn a read on its own. Solve both. If the line can't carry both, rewrite the line; don't drop one.
|
|
120
|
+
|
|
121
|
+
### ⚠️ The answer must be unguessable
|
|
122
|
+
|
|
123
|
+
A structurally valid loop whose answer the viewer can supply themselves **is not a loop**. The withhold happens on the page; the resolution happens in their head before their thumb moves.
|
|
124
|
+
|
|
125
|
+
This is **the most dangerous failure in this document**, because it passes every mechanical check — complete clause, no jargon, payoff withheld at the end, cold-legible. It looks correct in review and dies in the field. The example that caught it:
|
|
126
|
+
|
|
127
|
+
> *"'TikTok's for kids' is exactly what the Yellow Pages guys said about Google. Ask what happened to them next."*
|
|
128
|
+
|
|
129
|
+
Formally a loop. Useless — everyone already knows what happened to the Yellow Pages. The answer closes instantly and for free.
|
|
130
|
+
|
|
131
|
+
**Test: can the viewer answer your withheld question without watching?** If yes, two ways out:
|
|
132
|
+
|
|
133
|
+
1. **Withhold something only you know** — the other side of a transaction, what the gatekeeper actually checks, what the person who ignored them said. That material is unknowable without watching, and it qualifies naturally.
|
|
134
|
+
2. **Send them to check rather than telling them** — replace the withheld fact with an instruction to go find out: *"You think it's all teenagers on there. Go and look at who's still watching at eleven at night."* The tension is an unresolved *belief*, which they can't close from the sofa.
|
|
135
|
+
|
|
136
|
+
### Loop rules
|
|
137
|
+
|
|
138
|
+
- **Open it by 0:10.** After that you're re-hooking someone who already decided to leave.
|
|
139
|
+
- **Say the loop on screen**, not just out loud — caption it, or run it as a visible timer/counter.
|
|
140
|
+
- **One loop.** Two open questions dilute both.
|
|
141
|
+
- **It opens and closes inside THIS video.** Withholding for forty seconds is retention; deferring the answer to another upload is a broken video — you don't control feed order, and a breakout video is seen alone. "Part two" is not a loop, it's an unpaid debt.
|
|
142
|
+
- **Never bait-and-switch the payoff.** If you promised a number, give the real number.
|
|
143
|
+
- **Don't force it.** When a line lands in one beat and withholding would break it, keep it and *label it an identification hook* — tagged, those become the control group that tells you whether loops actually beat identification for this audience.
|
|
144
|
+
|
|
145
|
+
**Mechanics, ranked:** the withheld number (strongest — a real number is a promise you can keep, and an on-screen timer sustains the loop visually for the whole video) · the withheld part ("one part of this nobody explains, and it decides whether it works for you") · the numbered list ("three reasons, the third is the only one that matters" — tells the viewer exactly how long to stay; deliver all three) · the unresolved visual (a half-cut timeline, an export bar, a finished video not yet played — works with zero words, survives muting) · the contradiction/concession ("X pays better, and I'm still going to explain this" — the concession *is* the loop: *so why bother?*).
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
## Charge 3 — THE PAYOFF (tap the desire)
|
|
150
|
+
|
|
151
|
+
The moment the video pays what the hook promised, landing on **what they actually want** — not on what you're selling.
|
|
152
|
+
|
|
153
|
+
- **Show it, don't summarize it.** The result plays, full frame, ≥5 uninterrupted seconds. The real number appears on screen. Description is not payoff.
|
|
154
|
+
- **The payoff is not the CTA.** No product pitch in this slot. Pitching where the payoff belongs is the most common way a good video converts nothing.
|
|
155
|
+
- **Land it before the last beat.** Payoff → bait → close. A payoff in the final second gets cut off by the loop-around.
|
|
156
|
+
- **Go past the functional layer.** Surface desire ("how do I do this?") gets a mechanism; functional gets a demo; **the emotional layer is usually where the real payoff is** — the honest limit named out loud, the concession that says you're not selling them. For a scam-fatigued or pitch-fatigued audience, *refusing to oversell is the payoff*.
|
|
157
|
+
- **Identity is the deepest layer:** "that's a real skill I now have." When a video can end there, it does.
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## Charge 4 — THE BAIT (comments are distribution)
|
|
162
|
+
|
|
163
|
+
A video with replies gets shown again; a video with none dies at its first audience regardless of watch time. **Bait is a deliberate beat, not something you hope for.**
|
|
164
|
+
|
|
165
|
+
**One ask, in the final beat, and also in the post caption** — many people reply from the caption without finishing. **Pay off first, then ask**; never make the bait the price of the payoff.
|
|
166
|
+
|
|
167
|
+
**Works:**
|
|
168
|
+
|
|
169
|
+
- ⭐ **The confession invite** — *"Tell me which one you already quit. I've quit two."* Low-stakes, high-volume, and it makes the comment section self-reinforcing.
|
|
170
|
+
- ⭐ **The guess** — *"Guess how long it took me. I say it at the end."* Pairs perfectly with a withheld-number loop: one device drives retention *and* comments.
|
|
171
|
+
- **The named omission** — *"I left one thing out on purpose. Ask me."* Converts what you couldn't fit into the reason to reply.
|
|
172
|
+
- **The soft disagree** — a true concession people want to argue with. Not ragebait if you actually believe it.
|
|
173
|
+
- **The specific question** — concrete always beats "thoughts?".
|
|
174
|
+
|
|
175
|
+
**⚠️ Bait is the highest-compliance-risk beat in the video.** For money/health/appearance topics the obvious bait is the illegal one:
|
|
176
|
+
|
|
177
|
+
- ❌ *"Comment MONEY and I'll send you the link"* — the most scam-coded line available, and a DM funnel.
|
|
178
|
+
- ❌ *"How much do you want to make a month?"* — invites an earnings-claim thread you now own.
|
|
179
|
+
- ❌ *"Follow for part two"* — breaks the standalone rule.
|
|
180
|
+
- ❌ Fake controversy, ragebait, or a deliberate error to farm corrections. Works once, costs the positioning permanently.
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
## Gate 3 in depth — relevance and funnel alignment
|
|
185
|
+
|
|
186
|
+
- **Offer adjacency.** The hook should be the **"before" of the offer's "after."** A hook that fully solves the problem, or creates a desire the offer doesn't resolve, is *worse* than a weak hook: a weak hook underperforms; this one fills the funnel with people who will never buy. Symptom — great opt-in rate, lead→paid under ~5% — and by the time it's visible you've scaled it.
|
|
187
|
+
- **Deliverability.** Aggression isn't the problem; undeliverability is. Bold, curiosity-driven, outcome-led hooks are what work — but a promise the offer can't keep buys front-end conversion with churn. **Point the aggression at what you can demonstrate.** A large promise you cash every time beats a larger one you can't.
|
|
188
|
+
- **Guarantees are proof, not headline.** Leading with the guarantee attracts guarantee-shoppers who churn at the boundary, and draws enforcement on cold organic.
|
|
189
|
+
- **Sell the entry promise, not the offer.** The hook's job is to match **what the next screen actually says**. If the funnel opens on an eligibility question, sell the eligibility curiosity. A hook that pitches the end offer desyncs the sequence.
|
|
190
|
+
- **Move the whole awareness ladder together.** Hook = problem-aware · body = bridge to the solution · landing page/quiz = solution-aware · proof = product-aware. Changing the hook's awareness stage without moving the page it points at is the most common self-inflicted conversion loss — the drop-off reads as "bad hook" when the failure was the handoff. Unaware-stage hooks are a trap on short-form: that structure needs seven beats and you have one second.
|
|
191
|
+
- **TAM comes from identity, not awareness stage.** Widening the stage adds viewers, not buyers. Widening the *identity* — from a narrow role to the broader self-image it sits inside — multiplies the audience while keeping the desire intact.
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
## Anti-patterns (they show up hardest at volume)
|
|
196
|
+
|
|
197
|
+
- **Never point a generator at your grader.** A model writing hooks scored by a model converges on whatever the rubric measures, not on what works — in practice it discovered that appending a proper noun moved "specificity," and produced 21 variants of one sentence with the city and trade swapped. Scores climbed every round; nothing got better. **The rubric is a check on a hook you already believe in, never a ranking function.** Use it to catch defects, not to pick winners. This applies directly to you: do not generate 50 hooks and then rank them with the same reasoning that wrote them.
|
|
198
|
+
- **A different noun is not a different hook.** If two variants could be filmed from one script by changing one word, you have one hook — and a batch of twenty of those teaches nothing. The dominant failure when writing for a category that spans verticals.
|
|
199
|
+
- **Specificity is observed detail, not generated nouns.** `Boston window cleaner` is a generated token; `comment posted 9:14, video finished 9:55` is an observation. One sounds invented, the other sounds witnessed — which is the entire point of the principle.
|
|
200
|
+
- **Obscurity is not qualification.** A hard-to-parse opening doesn't filter for serious buyers, it filters for nobody, because the video never reaches anyone. Qualification is the body's job.
|
|
201
|
+
- **Founder-confession drift.** "I paid… / my ads… / I spent…" is relatable but makes the video about you. A loop makes it about information the viewer wants. Bias toward the latter.
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
205
|
+
## Measure and diagnose by charge
|
|
206
|
+
|
|
207
|
+
**The cluster is the unit, not the hook.** A single hook at low view counts tells you nothing — variance dominates. A cluster is a bet on one buyer insight and one emotional lever; that's what you kill or scale. One attribution slug per cluster (use the render `tracer`), one cluster per account — mixing them stops the algorithm finding the audience.
|
|
208
|
+
|
|
209
|
+
**Judge on the metric that pays rent.** In increasing order of truth: views → completions → profile visits → **funnel/quiz completions per post** → **revenue per 1,000 views**. Comments and views are distribution signals, not leads.
|
|
210
|
+
|
|
211
|
+
| What the numbers say | Weak charge | Fix |
|
|
212
|
+
|---|---|---|
|
|
213
|
+
| Barely any views | 🪝 Hook | Rewrite the first 3s. New opening line, new frame 0 |
|
|
214
|
+
| Views, mass exit at 3–8s | 🔄 Loop | No open question. Plant one by 0:10 |
|
|
215
|
+
| Watched to the end, no reaction | 😍 Payoff | Landed on features, not desire — go to the emotional layer |
|
|
216
|
+
| Good retention, no comments | 🎣 Bait | No ask, or the ask was "thoughts?" |
|
|
217
|
+
| Comments, but hostile | 🎣 Bait | Ragebait or an over-claim |
|
|
218
|
+
| Good clicks, no sales | Relevance | Offer adjacency — wrong audience |
|
|
219
|
+
|
|
220
|
+
**Vary one charge at a time.** Same body, new hook. Same hook, new bait. A batch where everything changed teaches you nothing. **Then play the power law:** ~2% of videos produce ~90% of results — when one hits, make ten more of exactly it. Iterate on winners; never fix losers.
|
|
221
|
+
|
|
222
|
+
---
|
|
223
|
+
|
|
224
|
+
## Running this harness in Vidfarm
|
|
225
|
+
|
|
226
|
+
| Step | Web copilot | Desktop / devcli |
|
|
227
|
+
|---|---|---|
|
|
228
|
+
| Generate hook candidates | `brainstorm/hooks` (the consultation chain — see `references/onboarding.md`) | same, via `vidfarm api` |
|
|
229
|
+
| 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
|
+
| Place the hook at frame 0 | `add_layer` / `set_captions` with `start:0` | `vidfarm set-text ./work --layer hook --text "…"` |
|
|
231
|
+
| 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` |
|
|
234
|
+
|
|
235
|
+
**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
|
+
|
|
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).
|