@officexapp/vidfarm-devcli 0.21.33 → 0.21.34
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/vidfarm/SKILL.md +16 -1
- package/.agents/skills/vidfarm/recipes/bulk-scripting-with-a-regime.md +15 -0
- package/.agents/skills/vidfarm/recipes/cutout-graphics-for-explainers.md +46 -2
- package/.agents/skills/vidfarm/recipes/local-edit-render-approve.md +1 -0
- package/.agents/skills/vidfarm/references/automation-and-local-dev.md +12 -1
- package/.agents/skills/vidfarm/references/editor-workflows.md +2 -0
- package/.agents/skills/vidfarm/references/reviewing-renders.md +139 -0
- package/.agents/skills/vidfarm/regimes/README.md +2 -0
- package/.agents/skills/vidfarm/regimes/explainer.QA_REGIME.md +20 -1
- package/.agents/skills/vidfarm/regimes/product-demo.QA_REGIME.md +18 -0
- package/.agents/skills/vidfarm/regimes/short-form.QA_REGIME.md +31 -0
- package/.agents/skills/vidfarm/regimes/ugc-testimonial.QA_REGIME.md +7 -0
- package/SKILL.director.md +232 -4
- package/SKILL.md +1 -0
- package/dist/src/cli.js +27 -2
- package/dist/src/devcli/stills.js +65 -1
- package/package.json +1 -1
|
@@ -213,7 +213,7 @@ Present both harnesses to the director, recommend (A) unless they've asked for p
|
|
|
213
213
|
- **The ART must be CLOSED and SOLIDLY FILLED — this is the other half of surviving the key, and the #1 way stickers come back broken.** Ask an image model for "icons on a green plate" and it will happily draw **outline art**: a colored stroke with the shape's interior left as bare plate. It looks perfect on the sheet, and after the key each sticker is a **rim floating around a see-through hole** (an apple-shaped outline with nothing inside it). Same outcome from a *near-plate* fill (the keyer works on tolerance, not exact match), a translucent/glassy/glowing material, or a soft glow fading into the plate. **You cannot key those pixels back — it has to be in the prompt:** *"every object is a closed, solidly filled shape; outlines must enclose an opaque fill of a different color; no outline-only or hollow art; nothing on the art in the plate color or any near-shade of it; fully opaque, no translucency, glow or drop shadow."* `cutout --generate`, `sticker-pack --generate`, `handoff image` and the `create-overlay` primitive **append that clause for you** with the chosen plate hex — write it yourself only when you prompt a generator directly. After the key, both commands report per-item `hole_pct`/`hollow` (console `⚠ N% hollow`, `--json`, `stickers.json`) — a ring or picture frame reads the same way, so it **warns, never blocks**. Flagged and it shouldn't be? Re-generate with the fill clause; a *near*-plate fill can sometimes be rescued with a lower `--tolerance`; one stubborn item can be lifted with `vidfarm mask --crop …` (ONNX matting ignores fill color).
|
|
214
214
|
- **Transparent GIF is supported, for GIF-only surfaces.** `vidfarm sticker-pack … --output-format gif` (stills) and `vidfarm remove-greenscreen <video> --gif` (animated) emit transparent GIFs. GIF alpha is **1-bit**, so edges go hard — fine for chat/forum/Notion sticker surfaces, worse than PNG/WebP/WebM for compositing on a timeline. Prefer PNG/WebP/WebM unless the destination only eats GIF.
|
|
215
215
|
|
|
216
|
-
**Explainer house style — the defaults to build with unless told otherwise.** **White background / light mode** (plain white stage, no gradients, no dark mode, no photo backdrop), **kinetic word-by-word captions** in dark ink on the light stage (`vidfarm captions generate --style word-pop --color "#111111" --active-color "#7C3AED" --background-style plain` — skip outlines/shadows, they're only needed over busy footage), and **female TTS narration** (`vidfarm tts --voice coral` on OpenAI — `nova` for energy, `sage` for calm; `Kore`/`Leda` on Gemini; any ElevenLabs voice via `vidfarm voices`). **Keep it clean and simple** — one idea on screen at a time, two or three cutouts per beat, one accent color, one font, lots of white space; remove before you add. **Illustrations default to simplicity**: flat vector, simple shapes, minimal detail, 2–3 flat colors, no baked-in text — simple art keys cleanly, trims tight, and stays on-style across the whole cast. State the defaults once so the director can override any of them. Full detail: recipe `recipes/cutout-graphics-for-explainers.md` (“House style — the explainer defaults”).
|
|
216
|
+
**Explainer house style — the defaults to build with unless told otherwise.** **White background / light mode** (plain white stage, no gradients, no dark mode, no photo backdrop), **kinetic word-by-word captions** in dark ink on the light stage (`vidfarm captions generate --style word-pop --color "#111111" --active-color "#7C3AED" --background-style plain` — skip outlines/shadows, they're only needed over busy footage), and **female TTS narration** (`vidfarm tts --voice coral` on OpenAI — `nova` for energy, `sage` for calm; `Kore`/`Leda` on Gemini; any ElevenLabs voice via `vidfarm voices`). **Keep it clean and simple** — one idea on screen at a time, two or three cutouts per beat, one accent color, one font, lots of white space; remove before you add. **Illustrations default to simplicity**: flat vector, simple shapes, minimal detail, 2–3 flat colors, no baked-in text — simple art keys cleanly, trims tight, and stays on-style across the whole cast. State the defaults once so the director can override any of them. Full detail: recipe `recipes/cutout-graphics-for-explainers.md` (“House style — the explainer defaults”). **If the director takes the stage off white**, two things stop being optional: every sticker's **white die-cut rim** has to be stripped (on a dark stage it's a glaring halo and the most obvious bot-made artefact in the frame — recipe → “Stickers on a DARK or photographic stage”), and the caption hexes above stop applying — **caption colour, active-word colour and plate are chosen by measuring the composited background behind the caption band**, one treatment per video (`regimes/short-form.QA_REGIME.md` → “Caption styling is MEASURED off the background”). Related: **on-screen text and captions must not say the same thing at once** — display text carries the argument, captions carry only what the screen doesn't show.
|
|
217
217
|
|
|
218
218
|
**Landscape footage in a fullscreen vertical explainer — use the blurred plate, never bars.** When an explainer is built on **real filmed footage** and the source is 16:9 (or 4:3) on a 9:16 canvas, do not `contain` it (hard black letterbox bars read as an unfinished export) and do not blindly `cover` it (a wide shot loses its left and right thirds). Duplicate the clip: a full-canvas `cover` copy behind, heavily **gaussian-blurred and faded dark**, plus the sharp copy centered as a hero band — optionally zoomed ~1.3× — with its **top and bottom edges feathered** into the blur. Same clip, same timecode, so it reads as one continuous image with a shallow-depth-of-field plane, fullscreen edge to edge, nothing cropped, and clean dark space for the header and captions. Bake it once with ffmpeg into a single 1080×1920 file (free, local) and place it as one ordinary full-canvas layer — layer blur is not an editor property, so the pre-bake is the path that works in the editor, `serve`, and cloud render alike. Copy-paste ffmpeg + HTML recipes, tuning table, and the failure modes: `references/editor-workflows.md` (“The blurred plate — landscape footage, fullscreen, on a vertical canvas”).
|
|
219
219
|
|
|
@@ -251,6 +251,18 @@ The mechanism is deterministic, not luck: rendering is seek-safe, so frame 0 sho
|
|
|
251
251
|
|
|
252
252
|
Full mechanics and editor verbs: `references/editor-workflows.md` (“The opening frame is the post's thumbnail”); poster-state authoring craft: `hyperframes-creative/references/beat-direction.md`.
|
|
253
253
|
|
|
254
|
+
## Judge the WHOLE video, not the parts you built — and never by one frame
|
|
255
|
+
|
|
256
|
+
**Assume your own finished video has a defect you can't see.** That's the observed base rate, not modesty: across a 32-video batch, *every* first-pass video had a real defect that the agent who built it had already reported as "verified, looks good" — dead space under the content, a placeholder that reads as a failed render, contradictory numbers in one frame, a CTA still animating at the last frame.
|
|
257
|
+
|
|
258
|
+
**The cause is how agents build: part by part, each part correct in isolation.** Scene 3 gets authored while scene 3 is the whole world, so every scene passes on its own and the video fails *as a video* — type size jumps between beats, one scene breathes and the next is crammed, the accent colour drifts, a transition lands like a slap because nothing before it moved that fast, one asset is flat vector and the next is photographic. Nobody watches a scene; they watch the sequence. **So before you ship, look at the whole thing at once as a stranger would**, and ask: is it visually balanced (or top-anchored with a dead band below), is the spacing consistent scene to scene, is there ONE type scale / accent colour / illustration style, does the pacing have a deliberate rhythm instead of N identical beats, is anything jarring at the joins, does any frame make two elements compete for the eye, and — the summary question — **does it look like one person made it in one sitting?** When it doesn't, fix the *system* (define the type scale, the margin, the default beat, the palette, then apply it to every scene) rather than patching the one scene that stood out.
|
|
259
|
+
|
|
260
|
+
The mechanical form is a contact sheet, because one image read shows drift that twelve separate checks can't — and it's one command: **`vidfarm stills ./work --sheet`** renders the frames and tiles them into `stills/contact-sheet.png`. **Read the sheet as an image.**
|
|
261
|
+
|
|
262
|
+
**And never verify a video by a single frame.** Frame 0 is the thumbnail so you judge it alone — but a whole class of render bug (an overlay pass missing `-loop 1`, assets outside the composition root so the timeline never runs) freezes *every* frame while duration, frame count and audio hash all still pass, and frame 0 looks perfect. **Always compare two frames from different scenes; they must differ a lot.** Likewise, verify audio by measurement, never "it sounds good" — you can't hear it.
|
|
263
|
+
|
|
264
|
+
Full method — the holistic checklist, the six defects in observed frequency order, the "does it rest there" test, frozen-render verification, audio measurement, and how to brief a revision pass: **`references/reviewing-renders.md`**.
|
|
265
|
+
|
|
254
266
|
## Web AI chat vs. local devcli — know your surface's limits
|
|
255
267
|
|
|
256
268
|
**Desktop users: this is a skill, not an SDK or required runtime.** It gives an AI agent Vidfarm's operating knowledge; it does not proxy requests, add hidden capabilities, or require the agent to use a particular client. Every cloud workflow described here can be performed with ordinary HTTP against the Vidfarm REST API. For convenience and safer composition/file workflows, recommend the `vidfarm` command from `@officexapp/vidfarm-devcli`: it wraps the same REST routes, handles auth, uploads, polling, pagination, local files, and pull/edit/render loops. Use raw REST whenever the user prefers it, the CLI lacks a new route, or the work belongs in an existing service or automation stack.
|
|
@@ -274,6 +286,7 @@ You may be running as the **in-web AI chat** (the /editor copilot, the chat dock
|
|
|
274
286
|
- **The first frame IS the thumbnail — compose it on purpose.** Frame 0 is a single frame of ~30 in the first second, but it's the poster every feed, share link, and paused player freezes on, so **more people see that one frame than watch the video**. It must never be black, empty, mid-fade, or caught mid-animation: put a real visual at `start:0`, have the hook words already on screen at t=0, and never hang a `fade-black`/`fade-white`/`flash` *entrance* on the **first** clip (junction transitions between later clips are fine — this rule is only about the opening). Verify it, don't assume: devcli `vidfarm stills ./work --at 0` renders that exact frame, and `vidfarm qa` flags a blank or fading open.
|
|
275
287
|
- **Ask early: one-time video, or bulk?** "Make me a video about X" and "I need to post daily / give me 20 hook variants" are different jobs, and directors often don't know the second one has a name. Ask once, up front: *"One video, or should we set this up as a repeatable batch?"* Bulk = **scripting mode** (a pinned base fork + a loop that varies ONE thing per variant; a public-raws shelf is the cheapest source of the N), and every batch gets a **`QA_REGIME.md`** — because a loop of fifty videos has no human looking at every frame, and the regime is what replaces those eyes. Don't silently ship a one-off when they asked for volume, or drag someone into a harness when they wanted one clip.
|
|
276
288
|
- **`QA_REGIME.md` is the director's own quality contract, and it's a first-class artifact.** `vidfarm qa`'s built-ins are universal (slop, fonts, the thumbnail frame); a regime is what makes *this* format good — audience, hook shape, banned vocabulary, pacing, compliance line. It lives next to the work, they own it, it stacks: `vidfarm regime init short-form --out ./work/QA_REGIME.md` (bundled bases: `short-form`, `hooks`, `ugc-testimonial`, `explainer`, `product-demo` — each a starting point to **edit**, never a house style), then `vidfarm qa ./work --regime hooks --regime ./brand/HOUSE.md`, and any user file anywhere is valid. Its `checks:` front matter is machine-settled; its `- [ ]` checklist comes back as **review items you answer honestly in your report** — never claim a video passed the half the CLI can't judge. When a batch teaches you something, **write it back into the regime**: that's the artifact that compounds. Details in `references/automation-and-local-dev.md` ("Scripting mode"), format in `regimes/README.md`.
|
|
289
|
+
- **A video is judged as a SEQUENCE, so review it as one.** Agents build scene by scene and each scene passes in isolation while the video drifts — inconsistent margins, three type sizes, an accent colour that wanders, beats that are all the same length, a jarring join. Tile a dozen stills into one contact sheet (`vidfarm stills ./work --sheet`) and read it as an image before you call anything done, fix drift by defining the system rather than patching the odd scene out, and remember that **your own confident "verified, looks good" is the single least reliable signal in this workflow** — it was wrong on every video of a 32-video batch. Method: `references/reviewing-renders.md`.
|
|
277
290
|
- **On devcli, QA every video you produce: `vidfarm qa ./work`.** Free, instant, local-only — it blocklists exactly the slop above plus first-frame/thumbnail and font-regime/safe-zone drift, and prints a concrete fix per finding. **Feedback, not a gate**: it exits 0 even on findings, never runs automatically, and is a blocklist (unusual/stylized compositions pass untouched), so there's no reason not to run it before every publish. `--json` for scripted batches, `--strict` only if you want a CI failure. **Web-chat copilot: this command does not exist for you** (devcli-only, no REST twin) — apply the standard by hand, and when handing a heavy job to a local coding agent, tell them to run `vidfarm qa`.
|
|
278
291
|
- **Every production adheres to the TikTok-native caption standard.** On-screen text lives inside the readable safe zone (**~8%–85%** of a 9:16 frame — never pinned to the top/bottom edges the phone UI clips), uses the composition's bold font regime (**Montserrat** default / TikTok Sans, weight **700–900**, ~36–64px on a 1080-wide frame), and uses **exactly one of four valid backgrounds**: outline/stroke (`background_style:"outline"`, the default), plain + shadow (`"plain"`), an active-word highlight pill (`set_captions` `spotlight`/`karaoke` — the only legitimate pill *in the whole frame*, static labels included), or a tight-hugging solid band (`"highlight-solid"`, radius ≤8px, no border/shadow/gradient/blur). Decomposed forks often inherit the source's edge-pinned caption in an off-regime font — fix it, don't inherit it. Local devcli renders auto-normalize position + font family only (never the slop), so author it correctly. Full rules in `references/editor-workflows.md` ("Social-native visual standard" + "TikTok-native caption standard").
|
|
279
292
|
- **Where the web chat struggles: complex, long, multi-step transformations.** A full multi-scene re-theme, an iterative render-critique-iterate loop, heavy scripted or batch work, or anything needing a real filesystem and many sequential tool calls will hit context limits, turn/timeout ceilings, and the web editor's constraints (CSS/declarative motion only — JS animation adapters are stripped on save). Don't grind a big transformation one layer at a time in a chat turn and stall.
|
|
@@ -292,6 +305,7 @@ Read only the relevant reference file for the current task.
|
|
|
292
305
|
- Getting-started interviews, **consultations**, strategy docs, onboarding flow: `references/onboarding.md`
|
|
293
306
|
- Primitive routes such as TTS, STT, music, overlays, background removal, product placement: `references/primitives.md`
|
|
294
307
|
- **Hooks, retention, and why a video travels — the four charges: `references/hooks-and-virality.md`.** Read it before writing ANY hook, caption script, or re-theme, and before a hook-variant batch. It is the craft harness; the rest of the skill is mechanics.
|
|
308
|
+
- **Reviewing a finished render — the holistic pass, the six common defects, frozen-render and audio verification: `references/reviewing-renders.md`.** Read it before you report a video as done, and before you grade someone else's.
|
|
295
309
|
- Bulk / N-variant production: recipe `recipes/bulk-scripting-with-a-regime.md`; the `QA_REGIME.md` format and the bundled bases: `regimes/README.md` (+ the regime files beside it, readable as-is)
|
|
296
310
|
- Complete REST API map and raw-HTTP conventions: `references/rest-api.md`. Load it only when the user asks for REST, an endpoint/schema, direct HTTP integration, or exhaustive API coverage. For the entire specification, follow its domain links and load every listed reference; do not preload them into ordinary director conversations.
|
|
297
311
|
|
|
@@ -334,6 +348,7 @@ Choose the narrowest path that satisfies the request.
|
|
|
334
348
|
- Submission routes are generally not idempotent. Especially for renders and expensive primitives, check status before retrying.
|
|
335
349
|
- In the web editor, use CSS/declarative motion only. Script-bearing HTML is stripped or rejected there.
|
|
336
350
|
- **Never render or approve without judging frame 0 as a standalone still.** It is the thumbnail everywhere the post appears; an empty/black opening frame ships a dead post. See “The FIRST FRAME is the thumbnail”.
|
|
351
|
+
- **Never judge the VIDEO by one frame, and never report a render as reviewed without the holistic pass.** Compare frames from at least two different scenes (a frozen render passes every other check), read a contact sheet for balance/spacing/style/pacing drift, and state separately what you measured vs. what you judged. See “Judge the WHOLE video”.
|
|
337
352
|
|
|
338
353
|
## Recommended Recipes
|
|
339
354
|
|
|
@@ -64,6 +64,21 @@ done
|
|
|
64
64
|
|
|
65
65
|
Free, offline, no wallet. Variant 1 is the `standard` preset as authored (skew 2%, zoom 3%, rotate 2°, speed +2%, saturation +4%); later variants get jittered magnitudes and flipped signs, so they differ from the original **and from each other**. One variant per account — two accounts posting the same variant defeats the point. Reuse one `--seed` per source so the batch is reproducible.
|
|
66
66
|
|
|
67
|
+
### 5c. Eyeball the renders — the loop cannot do this for you
|
|
68
|
+
|
|
69
|
+
**A batch is exactly where "the agent passed its own broken work" compounds**: nobody is watching variant #37, and `vidfarm qa` is a static DOM check that never sees a rendered pixel. So add one cheap visual pass over the output — a contact sheet per video, read as an image:
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
for MP4 in renders/*.mp4; do
|
|
73
|
+
S="$(basename "$MP4" .mp4)"; mkdir -p "qa/$S"
|
|
74
|
+
for t in 0 3 6 9 12 15; do ffmpeg -y -ss $t -i "$MP4" -frames:v 1 "qa/$S/f$(printf %03d $t).png"; done
|
|
75
|
+
ffmpeg -y -pattern_type glob -i "qa/$S/f*.png" \
|
|
76
|
+
-vf "scale=320:-1,tile=3x2:margin=6:padding=6:color=0x999999" -frames:v 1 "qa/$S-sheet.png"
|
|
77
|
+
done
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Read the sheets. In a batch you're looking for two different things: **per-video** defects (dead regions, a placeholder that reads as a failed render, contradictory numbers, an unlanded CTA, superimposed headlines at a handoff) and **cross-video** drift (variants that no longer look like siblings, or look *too* identical to be N distinct posts). Also compare two frames from different scenes in at least a sample of the renders — a systematic frozen-render bug in the loop will produce N broken files that all pass duration/frame-count checks. Full method: `references/reviewing-renders.md`.
|
|
81
|
+
|
|
67
82
|
### 6. Answer the review items — don't skip this
|
|
68
83
|
|
|
69
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.
|
|
@@ -8,13 +8,15 @@ The mechanical trio — **generate on a chroma plate → key it out → trim to
|
|
|
8
8
|
|
|
9
9
|
**Unless the director asks for something else, build every explainer this way. Don't ask, just do it, and mention the defaults once so they can override.** The whole point of the house style is that explainers read as *clean, bright, and easy* — a busy explainer is a failed explainer.
|
|
10
10
|
|
|
11
|
-
- **White background, light mode.** A plain white (or near-white `#FFFFFF`–`#FAFAFA`) stage. No dark mode, no gradients, no photographic backdrop, no texture. Light mode reads cleaner on every feed, keeps cutout stickers legible, and makes flat-vector art look intentional. Set the composition/scene background to white first, before placing anything.
|
|
11
|
+
- **White background, light mode.** A plain white (or near-white `#FFFFFF`–`#FAFAFA`) stage. No dark mode, no gradients, no photographic backdrop, no texture. Light mode reads cleaner on every feed, keeps cutout stickers legible, and makes flat-vector art look intentional. Set the composition/scene background to white first, before placing anything. **This is the default, not a law** — a director who asks for a dark or photographic stage gets one, but it changes two things mechanically: the stickers need their white die-cut rim stripped, and the caption treatment has to be re-measured. Both are documented in **"Stickers on a DARK or photographic stage"** below.
|
|
12
12
|
- **Kinetic captions.** Narration is always captioned word-by-word (`vidfarm captions generate ./work --style word-pop`). Because the stage is white, **override the preset's dark-canvas colors to dark ink on light**:
|
|
13
13
|
```
|
|
14
14
|
vidfarm captions generate ./work --style word-pop \
|
|
15
15
|
--color "#111111" --active-color "#7C3AED" --background-style plain --max-words 4
|
|
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
|
+
|
|
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.
|
|
18
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.
|
|
19
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.
|
|
20
22
|
|
|
@@ -96,6 +98,48 @@ vidfarm remove-greenscreen ./mascot.mp4 --gif --gif-fps 12 --gif-width 480 # AN
|
|
|
96
98
|
|
|
97
99
|
GIF alpha is **1-bit** — a pixel is fully opaque or fully gone, so antialiased edges go hard and semi-transparent shadows/glows disappear (`--gif-alpha <0..255>` moves where that line falls). That's the format, not the key. **For anything going onto a composition, prefer PNG/WebP (still) or transparent WebM (clip);** reach for GIF only when the destination demands it.
|
|
98
100
|
|
|
101
|
+
### Stickers on a DARK or photographic stage — strip the white die-cut rim
|
|
102
|
+
|
|
103
|
+
The house style above puts stickers on a **white** stage, and on white the thing this section is about is invisible. The moment the stage goes dark, photographic, or coloured, every sticker arrives wearing a **white die-cut rim** — a 4–12px light halo tracing its silhouette — and that halo is the single most obvious "a bot made this" artefact in the frame. The art stops reading as an element in the scene and starts reading as a cutout pasted on top of it.
|
|
104
|
+
|
|
105
|
+
**Why the rim is there:** it's a PRINT convention. Real die-cut vinyl needs a white border so the blade has something to cut along, so sticker art is drawn with one, so generators reproduce it. It has no purpose whatsoever in a video composition. **This is a different failure from the `⚠ N% hollow` flag** `sticker-pack` prints — hollow means outline-only art whose interior got keyed away (fix it in the prompt, see above); the rim is extra art that was drawn on purpose and has to be removed after the key.
|
|
106
|
+
|
|
107
|
+
**The fix:** delete exactly the band of light pixels **connected to the transparent edge**, by morphological reconstruction inward from the boundary. Interior whites — an eyeball's sclera, a screen highlight, a paper label — are enclosed by linework, so they are not connected to the edge and survive untouched. Local, free, `numpy` + `scipy` + `PIL`:
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
def strip_rim(img, light=188):
|
|
111
|
+
a = np.array(img.convert("RGBA")).astype(np.int16)
|
|
112
|
+
solid = a[..., 3] > 128
|
|
113
|
+
is_light = (a[..., :3].mean(axis=2) > light) & solid
|
|
114
|
+
seed = is_light & ndimage.binary_dilation(~solid, iterations=3) # light AND touching transparency
|
|
115
|
+
if seed.any():
|
|
116
|
+
rim = ndimage.binary_propagation(seed, mask=is_light) # flood through light only
|
|
117
|
+
a[..., 3][rim] = 0
|
|
118
|
+
return Image.fromarray(a.astype(np.uint8), "RGBA")
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
**Test LIGHTNESS, not per-channel whiteness — this is the gotcha that makes the whole thing non-obvious.** The rim is **not white**. It is white **contaminated with the chroma plate**, because the plate fringes into it during keying. Measured on a magenta-plate pack, the outermost solid ring averaged **R≈250, G≈205, B≈250** — the green channel is nowhere near white. A per-channel test like `(rgb > 224).all(axis=2)` therefore misses the rim on every sticker whose edge is even slightly anti-aliased: in testing it stripped **1 of 4** stickers, and because the one it did strip looked *different* from its three siblings, the result read as broken art rather than as a bad threshold. `rgb.mean(axis=2) > ~188` catches all four.
|
|
122
|
+
|
|
123
|
+
**Then clean up what stripping leaves behind.** Removing the rim produces two artefacts, and both read to a viewer as "the sticker didn't mask properly":
|
|
124
|
+
|
|
125
|
+
1. **A dotted halo** — surviving specks along the old rim edge. Measured **213** and **164** stray connected components of 3–20px each on two different stickers of the same pack.
|
|
126
|
+
2. **A bright blob** — a large uniform light region the art *enclosed* is no longer visually held in by the rim. Measured at 7,983px (a ring's centre) and 9,582px (a stamp's paper plate).
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
def clean(img, speck=0.008, blob=0.015, light=200):
|
|
130
|
+
# 1. drop connected components smaller than `speck` of the largest
|
|
131
|
+
# 2. for each enclosed light region larger than `blob` of the sticker area:
|
|
132
|
+
# holes = binary_fill_holes(m) & ~m
|
|
133
|
+
# if holes.sum() < m.sum() * 0.02: # solid fill, no detail -> it is background
|
|
134
|
+
# set alpha 0 there
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
**The discriminator is worth remembering on its own: an enclosed light region is only background if it has NO internal detail, and its holes are the giveaway.** An eyeball's sclera is riddled with drawn veins and a pupil → many holes → keep it. A ring's centre is a flat fill → no holes → punch it transparent. Area alone gets this wrong in both directions.
|
|
138
|
+
|
|
139
|
+
**When you recolour a pack to a palette** (a duotone or luminance ramp onto a brand accent), **cap the top of the ramp** — e.g. `accent + 0.66·(white − accent)` — so interior whites land as a light tint instead of glaring pure white against a flat two-colour design. Uncapped, every kept interior white becomes the brightest pixel in the frame, which undoes the point of the ramp.
|
|
140
|
+
|
|
141
|
+
> **Known gap:** none of this is in the CLI. `vidfarm sticker-pack` / `vidfarm cutout` have no `--strip-rim` (or equivalent) flag today, so on a dark stage you run the two passes above yourself as a local post-step. A `--strip-rim` flag on both commands, defaulting off, is the right long-term home for it.
|
|
142
|
+
|
|
99
143
|
### The guided sequence (prompt harness)
|
|
100
144
|
|
|
101
145
|
**Step 0 — Decide the cast of stickers.** With the director, list every element the explainer needs as its own cutout: the hero subject, each labelled prop, each icon/arrow/emoji, any mascot, any full-frame backdrop. Each becomes one transparent PNG. Stickers are reusable — generate once, reuse across scenes. **If the cast is more than two or three items, make it a PACK** (one sheet, split locally — see "A sticker pack" above) rather than N separate `cutout` calls.
|
|
@@ -130,7 +174,7 @@ GIF alpha is **1-bit** — a pixel is fully opaque or fully gone, so antialiased
|
|
|
130
174
|
|
|
131
175
|
**Both are image-only.** A moving subject has no single bounding box — matte a video clip with `vidfarm remove-background <video>` or key a flat backdrop with `vidfarm remove-greenscreen <video>` (→ transparent WebM/mov).
|
|
132
176
|
|
|
133
|
-
**Step 2 — Show the director each cutout, get corrections.** Cutouts are cheap to regenerate. Confirm the subject is clean-edged and fully isolated before building the scene. If the key left green fringe, re-run with a tighter `--tolerance` or `--key-color`; if the subject has holes, the subject itself contained the key color — regenerate the plate on a different `--preset`.
|
|
177
|
+
**Step 2 — Show the director each cutout, get corrections.** Cutouts are cheap to regenerate. Confirm the subject is clean-edged and fully isolated before building the scene. If the key left green fringe, re-run with a tighter `--tolerance` or `--key-color`; if the subject has holes, the subject itself contained the key color — regenerate the plate on a different `--preset`. **If the stage isn't white, strip the white die-cut rim here**, before anything is staged — see "Stickers on a DARK or photographic stage" above.
|
|
134
178
|
|
|
135
179
|
**Step 3 — Stage them on the composition.** Fork/seed a working composition (`vidfarm pull` or `vidfarm serve`), **set the stage to a white light-mode background first** (house style), then drop each cutout as an **image layer**, sized and positioned deliberately:
|
|
136
180
|
```
|
|
@@ -9,6 +9,7 @@ Use this when a coding agent is doing the work locally or the user wants a repro
|
|
|
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
10
|
6. **QA before you render: `vidfarm qa ./work`.** Free, instant, devcli-only. It blocklists HTML slop (CTA buttons, benefit chip rows, a lone pill around a static stat/label, frosted cards, gradient text, web-page classes/fonts), checks the caption font regime + safe zone, and flags a blank/fading first frame (the thumbnail). Feedback only — exit 0 even on findings, never automatic — but it catches the #1 tell of an agent-made video, so run it on every production. Fix what's real, ignore what's a deliberate style call, then render.
|
|
11
11
|
7. Render with `vidfarm render <forkId> --dir ./work --wait`.
|
|
12
|
+
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`.
|
|
12
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*.
|
|
13
14
|
9. Approve the finished MP4 with `vidfarm approve --video <url|./final.mp4> --caption "..."`. This prints the shareable `share_url`.
|
|
14
15
|
|
|
@@ -280,7 +280,7 @@ The licensed harness also carries the **generative build workflow** guidance (ch
|
|
|
280
280
|
| `vidfarm raws search "…"` / `raws match "…"` / `raws list` / `raws sources` | (local library; NL→criteria via local agent or provider key) | search/reuse the raws library |
|
|
281
281
|
| `vidfarm raws preset list\|run\|save` / `raws export <ids…> --to <dir>` | (local library) | saved queries; copy raw MP4s out |
|
|
282
282
|
| `vidfarm lint <dir\|composition.html>` | (local static validation) | pre-publish composition check: timing, overlaps, preset names, media src |
|
|
283
|
-
| `vidfarm stills <dir> [--at 0,2.5,…]` | (local in-process render of PNG frames) | visually verify an edit without a full render |
|
|
283
|
+
| `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
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
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 |
|
|
286
286
|
| `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) |
|
|
@@ -335,6 +335,8 @@ What it flags:
|
|
|
335
335
|
|
|
336
336
|
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
337
|
|
|
338
|
+
**`vidfarm qa` is a static DOM check — it 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`.
|
|
339
|
+
|
|
338
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.
|
|
339
341
|
|
|
340
342
|
## Cost mode — the devcli's money-saving guardrail
|
|
@@ -360,6 +362,15 @@ The four modes, quoted as **cost per finished video**. The first two are spend p
|
|
|
360
362
|
|
|
361
363
|
**Narration defaults to the FREE local voice in minimize AND hybrid.** A bare `vidfarm tts "…"` runs the keyless local Kokoro-82M engine in both of those modes — you no longer have to remember `--engine local`. A run **opts out** of that default by asking for a premium voice (`--style`, `--provider`, `--model`, `--own-key`, or a non-Kokoro `--voice` like `alloy`/`Kore`/an ElevenLabs id), by passing `--cloud`/`--engine byok`, or by being in `rich-ai`/`pure-videogen`. If the local engine isn't installed on the machine (it needs `pip install kokoro-onnx soundfile` + a ~340MB model on first use), the run **falls back** to the user's provider key / cloud instead of failing — it prints the reason on stderr so you can tell the user why the voice changed.
|
|
362
364
|
|
|
365
|
+
**Narration gotchas that ship a correct-looking, wrong-sounding video.** Each of these produces output that passes every structural check:
|
|
366
|
+
|
|
367
|
+
- **Never `adelay` the voiceover to position it.** Whisper's word timings — and therefore every caption generated from them — are relative to the raw `vo.wav`. An `adelay` desyncs every caption in the video while the file still plays perfectly. Use `apad` + `atrim`.
|
|
368
|
+
- **Whisper's default model is English-only and hallucinates fluent English over other languages.** Non-English narration needs `--model large-v3 --language <code>`; without it you get a clean, confident, entirely invented transcript.
|
|
369
|
+
- **`vidfarm tts` reads stdin** — redirect `</dev/null` when calling it inside a shell loop, or the loop consumes its own input.
|
|
370
|
+
- **Check the brand/product name's pronunciation** before you render 20 variants with it. TTS engines mangle proper nouns (Kokoro reads *Genki* as "Jenki"); respell it phonetically in the TTS input and confirm with a whisper round-trip — you're already running whisper for the caption timings.
|
|
371
|
+
- **For a calm, unhurried read, render line by line** and concatenate the takes with measured silences, rather than one continuous pass. A single pass reads rushed however slow the copy is, because the pauses are TTS filler rather than real beats.
|
|
372
|
+
- **Verify the mix by measurement, not by ear** — you can't hear the render. Target **12–15 dB** of speech-over-bed separation measured across the actual word spans, peak below **0 dBFS**. A "separation" figure computed over the music-only tail measures bed-vs-bed and over-reports badly; don't retune against it. See `references/reviewing-renders.md`.
|
|
373
|
+
|
|
363
374
|
Precedence: `--cost-mode <m>` flag → `VIDFARM_COST_MODE` env → the saved `cost-mode` → default (hybrid, flagged as "not set"). When nothing is saved and a billed op runs, the CLI prints a "no preference set — ask the user" nudge instead of silently spending, so the default posture really is *ask before you spend*.
|
|
364
375
|
|
|
365
376
|
**Agent-memory handoff.** After the user picks, offer to remember it across sessions — but the destination depends on the agent, so ask: Claude Code → `CLAUDE.md` (or its memory dir); Codex / OpenCode / most others → `AGENTS.md`; or a note file the user names. `vidfarm cost-mode <choice>` already persists the devcli-side preference; agent memory is the extra step that survives a fresh checkout. In the **web app UI** there is no memory file — ask each time unless the user states a standing preference for the session.
|
|
@@ -537,6 +537,8 @@ Short-form is watched on a phone, and the phone's UI eats the frame's edges. **N
|
|
|
537
537
|
| 3 | **Highlight pill behind the ACTIVE word only** | `set_captions caption_style:"spotlight"` / `"karaoke"` (+ `caption_highlight_color`) | Hormozi/CapCut word-by-word. **The only legitimate "pill" in a video** — it tracks the spoken word, so it isn't a badge |
|
|
538
538
|
| 4 | **Solid band that tightly hugs the text lines** (CapCut "text box") | `background_style:"highlight-solid"` (or `"highlight-translucent"`) + a `background` color | Guaranteed legibility over noisy footage |
|
|
539
539
|
|
|
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".
|
|
541
|
+
|
|
540
542
|
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.
|
|
541
543
|
|
|
542
544
|
**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.
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
## Reviewing a render — look at the whole video, and never trust one frame
|
|
2
|
+
|
|
3
|
+
**Assume your own finished video has a defect you can't see, because you built it.** This is not humility, it's the observed base rate: across a 32-video bespoke batch, **every single first-pass video had a real defect that the agent who built it had already reported as "verified, looks good."** Dead space under the content, a placeholder that reads as a failed render, two contradictory numbers 200px apart, a CTA still animating when the video ends. None of these are subtle. All of them survived a confident self-review.
|
|
4
|
+
|
|
5
|
+
The reason is structural, not sloppiness: **an agent builds a video the way it builds code — part by part, each part correct in isolation.** Scene 3 is written while scene 3 is the whole world. So each scene passes on its own and the video fails as a video: the type jumps two sizes between beats, one scene breathes and the next is crammed to the margins, the accent colour drifts, a transition lands like a slap because nothing before it moved that fast. Nobody watches a scene. They watch the sequence.
|
|
6
|
+
|
|
7
|
+
So the review has two jobs, and they need two different passes:
|
|
8
|
+
|
|
9
|
+
1. **The holistic pass** — does this read as ONE video, made by one person, on purpose?
|
|
10
|
+
2. **The defect pass** — is any individual frame broken in one of the six ways frames are usually broken?
|
|
11
|
+
|
|
12
|
+
Do them in that order. The holistic pass is the one agents skip, and it is the one that separates "technically correct" from "good."
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Pass 1 — the holistic pass: watch it as one object
|
|
17
|
+
|
|
18
|
+
**Before you look for defects, look at the video the way a stranger will: all at once, start to finish, with no memory of how it was built.** You cannot do this from the code, from the storyboard, or from the scene you just edited — you have to look at the actual frames, in order, side by side.
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
# ONE command — renders the stills AND tiles them into ./work/stills/contact-sheet.png.
|
|
22
|
+
# No MP4 render needed; free, local, in-process.
|
|
23
|
+
vidfarm stills ./work --sheet # default timestamps = midpoint of each scene clip (cap 8)
|
|
24
|
+
vidfarm stills ./work --sheet --at 0,2,4,6,8,10,12,14,16,18,20,22 # or pick them yourself
|
|
25
|
+
# then READ ./work/stills/contact-sheet.png as an image
|
|
26
|
+
# --sheet-out <file> relocates it; --sheet-width <px> for bigger tiles (default 320)
|
|
27
|
+
|
|
28
|
+
# already have the MP4? same idea, straight off the file
|
|
29
|
+
for t in 0 2 4 6 8 10 12 14 16 18 20 22; do
|
|
30
|
+
ffmpeg -y -ss $t -i final.mp4 -frames:v 1 "qa/f$(printf %03d $t).png"; done
|
|
31
|
+
ffmpeg -y -pattern_type glob -i "qa/f*.png" \
|
|
32
|
+
-vf "scale=300:-1,tile=4x3:margin=6:padding=6:color=0x999999" -frames:v 1 qa/sheet.png
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
**The tile sheet is the point.** One image read, twelve frames, and the eye picks up drift instantly that no per-scene check can see. Read it as an image — not the filenames, not the HTML that produced it. (The still filenames are zero-padded seconds, so a glob stays in chronological order.)
|
|
36
|
+
|
|
37
|
+
Then answer these, out loud, in your report:
|
|
38
|
+
|
|
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
|
+
- **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
|
+
- **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
|
+
- **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.
|
|
43
|
+
- **Rhythm and pacing.** Do scene durations form a deliberate pattern (a fast open, a longer explanation, a fast close), or is every scene the same length because a loop wrote them? Same-length beats are hypnotic in the bad way. Conversely, one 9-second hold in a video of 2-second cuts stalls it dead.
|
|
44
|
+
- **Nothing jarring at the joins.** Watch each transition specifically. A cut from a dark scene to a white one is a flash in the face; a scale-up entrance immediately after a scale-up exit reads as a stutter; two consecutive scenes whose subjects sit in the same screen position with different content look like a glitch, not a cut. Where a join is harsh, either match the two frames either side of it (colour, position, energy) or make the harshness deliberate and rhythmic.
|
|
45
|
+
- **One idea per moment.** Across the whole sheet, is there any frame where two things compete for the eye — display text over captions saying the same words, a busy background under type, two headlines superimposed at a handoff? At the video level this shows up as a *density* problem: some beats carry three elements and some carry one.
|
|
46
|
+
- **Does it look like one person made it in one sitting?** The summary question. If the honest answer is "it looks assembled," name specifically which scenes don't belong and fix them toward the majority, don't average everything.
|
|
47
|
+
|
|
48
|
+
**When something is off, fix it globally, not locally.** The instinct after spotting drift is to patch the one scene that stands out. Usually the right fix is to define the rule (two headline sizes, one accent, 8% margins, 2.5s default beat) and apply it across every scene — including the ones that already looked fine. A video is a system; patching one node keeps the system inconsistent.
|
|
49
|
+
|
|
50
|
+
**Build order helps too, if you're still building.** Author the shared system first — type scale, palette, margins, motion vocabulary, default beat length — as one thing that every scene reads from, then fill the scenes. Scenes written first and harmonised later almost never fully converge.
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## Pass 2 — the defect pass: what actually goes wrong, in frequency order
|
|
55
|
+
|
|
56
|
+
From the same 32-video batch, ranked by how often it happened. Look for these specifically; they are what your own review misses.
|
|
57
|
+
|
|
58
|
+
1. **Large flat dead regions.** Content top-anchored with an empty band below it. Agents do this constantly and never notice, because during authoring the element is the subject and the emptiness is just "background."
|
|
59
|
+
2. **A placeholder empty state that reads as a missing asset.** A big empty dashed rectangle held for two seconds looks exactly like the render failed. If a scene's job is "an empty inbox," it still has to look designed, not broken.
|
|
60
|
+
3. **Two contradictory numbers in one frame.** Especially on anything data-shaped — a stat in the headline and a different one in the visual beneath it.
|
|
61
|
+
4. **A CTA or end card still building when the video ends.** The final state must be **settled at least 2 seconds before the last frame**, or the loop-around cuts it off and the ask never lands.
|
|
62
|
+
5. **Two headlines superimposed at a scene handoff.** The outgoing scene's text hasn't left when the incoming one arrives. Fix at the timing level: exit at `nextIn − 0.18`, duration `0.24`, ease `power2.out` — a slow-leaving `power2.in` is what causes the overlap in the first place.
|
|
63
|
+
6. **Type colliding with a busy background layer** exactly at the moment it's spoken. Particles, pins, footage detail — legible in the still you checked, unreadable at the second the word lands.
|
|
64
|
+
|
|
65
|
+
**If a frame looks empty, check whether it RESTS there.** Sample at 0.2–0.25s intervals through that transition. A transient near-empty wipe frame is fine and normal; anything holding empty for **>0.5s** is a hole in the video.
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
vidfarm stills ./work --at 6.0,6.2,6.4,6.6,6.8,7.0 # is it a wipe, or a hole?
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## Never verify a video by one frame
|
|
74
|
+
|
|
75
|
+
**This is the failure mode that survives every check you'd think to run.** Whole classes of render bug produce a video where *every frame is identical* — the timeline never ran — while duration, frame count, file size and audio hash all come out exactly right. Frame 0 looks perfect, so a single-frame check passes and you ship a frozen video.
|
|
76
|
+
|
|
77
|
+
Two real causes, both silent:
|
|
78
|
+
|
|
79
|
+
- **A watermark/overlay pass without `-loop 1` on a single-frame PNG input.** The frame-sync collapses the whole video onto one frame. Five videos shipped this way before it was caught.
|
|
80
|
+
- **Assets outside the composition root.** Only `<style>`/`<script>` *inside* the `data-composition-id` root execute, and sibling relative files may not resolve — fonts, images, even the animation library itself. The timeline never starts; frame 0 still renders fine because frame 0 is the static DOM.
|
|
81
|
+
|
|
82
|
+
**The rule that catches both: always compare two frames from different scenes.** They must differ a lot. And when you've applied any pass over an existing video (watermark, overlay, dedupe, re-encode), also compare each output frame against **its own** input frame at the same timestamp — that difference should be tiny. Two checks, opposite directions:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
# consecutive/distant frames must DIFFER (motion preserved)
|
|
86
|
+
vidfarm stills ./work --at 0,4,9,14
|
|
87
|
+
# after an overlay pass: same timestamp, before vs after, must be NEARLY IDENTICAL
|
|
88
|
+
ffmpeg -y -ss 7 -i clean.mp4 -frames:v 1 a.png
|
|
89
|
+
ffmpeg -y -ss 7 -i final.mp4 -frames:v 1 b.png
|
|
90
|
+
ffmpeg -i a.png -i b.png -filter_complex "psnr" -f null - # very high PSNR = only the mark changed
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
This sits directly beside the frame-0 rule and is its necessary counterweight: **frame 0 is the thumbnail, so judge it alone — but never judge the VIDEO by it.** The two rules are checking different things, and an agent that only internalises the first one has a perfect blind spot for a frozen render.
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## Verify audio by measurement, not by ear
|
|
98
|
+
|
|
99
|
+
**You cannot hear the render.** Do not report "the mix sounds good" — you have no way to know it, and it is the claim that most often turns out false. Measure instead.
|
|
100
|
+
|
|
101
|
+
- **Speech-over-bed separation: target 12–15 dB.** Measure the RMS of the mix across the spans where words actually occur, minus the RMS of a bed-only stretch. Word spans come free from the transcription you're already running for captions (`vidfarm stt <file> --engine whisper` → word timings).
|
|
102
|
+
- **Peak below 0 dBFS.** A mix that clips reads as amateur instantly on a phone speaker.
|
|
103
|
+
- **Beware "separation" numbers computed over the music-only tail** — they measure the wrong thing (bed alone vs. bed alone) and over-report by a wide margin. Don't retune a mix based on one.
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
ffmpeg -i final.mp4 -af "volumedetect" -f null - # peak + mean over the whole file
|
|
107
|
+
ffmpeg -i final.mp4 -ss 3.1 -t 1.4 -af "volumedetect" -f null - # a span where a word is spoken
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
**Narration timing gotchas that produce a correct-looking, wrong-sounding video:**
|
|
111
|
+
|
|
112
|
+
- **Never `adelay` the voiceover.** Whisper's word timings — and therefore every caption you generated from them — are relative to the raw `vo.wav`. Delaying the VO desyncs every caption in the video while the file still plays fine. Use `apad` + `atrim` to place it instead.
|
|
113
|
+
- **Scene handoffs can leave ~0.3s of silence.** Extend each clip's audio ~0.35s into the next.
|
|
114
|
+
- **Whisper's default model is English-only and will hallucinate fluent English over another language.** Non-English narration needs `--model large-v3 --language <code>`. The output looks like a clean transcript, so this one ships silently.
|
|
115
|
+
- **`vidfarm tts` reads stdin** — always redirect `</dev/null` when calling it inside a shell loop, or the loop eats its own input.
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## The revision pass — how to fix what review found
|
|
120
|
+
|
|
121
|
+
**Spawn a fresh pass rather than re-litigating with the context that produced the defect.** If you're handing fixes to a subagent (or picking the work back up yourself later), the brief that works:
|
|
122
|
+
|
|
123
|
+
- **State it as N targeted fixes and nothing else.** "The video is good — you are making three specific fixes." Open-ended "improve it" turns a working video into a different, differently-broken video.
|
|
124
|
+
- **Edit the generator, not the generated output.** If a script produced `composition.html`, fix the script. Check first that a generator exists — some compositions are hand-authored.
|
|
125
|
+
- **Back up before overwriting** — keep `<slug>-v1.mp4`. Re-renders are cheap locally; a lost good version isn't.
|
|
126
|
+
- **Keep audio bit-identical unless audio is the defect.** Reuse the existing `vo.wav` / word timings rather than re-recording; a re-record retimes every caption for no reason.
|
|
127
|
+
- **Give the PROBLEM, not just your proposed solution.** Repeatedly, the agent handed a described defect found a better fix than the one specified — using an app's own collapsed UI state instead of a redaction box, a type safe-zone solver instead of a scrim, making a document's *arrival* the spectacle instead of cutting the document. Say what's wrong and at what timestamp; let the fix be found.
|
|
128
|
+
- **Then sweep for the same class of problem** across the rest of the video, and report what else turned up. Defects of a given kind are rarely solitary — they come from a habit.
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## Report both halves honestly
|
|
133
|
+
|
|
134
|
+
When you hand back a render, say what you **measured** and what you **judged**, separately:
|
|
135
|
+
|
|
136
|
+
- 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
|
+
|
|
139
|
+
**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.
|
|
@@ -66,6 +66,8 @@ Good regimes tend to have: a **Part 0** naming the viewer in one line (the thing
|
|
|
66
66
|
|
|
67
67
|
Keep the checklist short enough that answering it honestly is cheaper than skipping it.
|
|
68
68
|
|
|
69
|
+
**Give every regime a "whole-video review" block, and put it last.** The bundled ones all have one. Front-matter `checks:` grade the composition's structure and `vidfarm qa` grades its DOM — neither can see the finished video, and the defects that actually ship are sequence-level: margins that shift scene to scene, three type sizes, an accent colour that wanders, N identically-long beats, a jarring join, a dead band under top-anchored content. Those come from how the video was built (one scene at a time, each correct in isolation), so they are invisible to every per-scene check *and* to the agent that built it — across a 32-video batch, every first-pass video had a real defect its own author had already called "verified, looks good." The review block is what forces the contact-sheet pass that catches them. Method: `references/reviewing-renders.md`.
|
|
70
|
+
|
|
69
71
|
## Built-ins
|
|
70
72
|
|
|
71
73
|
| Name | For |
|
|
@@ -56,7 +56,15 @@ A figure spoken over a busy frame doesn't land. If a number matters, it appears
|
|
|
56
56
|
|
|
57
57
|
Short clauses. One idea per sentence. No subordinate clause stacking. Read it aloud once — if you run out of breath or have to re-read a line, rewrite it. TTS in particular will happily deliver an unreadable sentence at a perfectly even pace, which is how a script defect ships.
|
|
58
58
|
|
|
59
|
-
### Rule 6 —
|
|
59
|
+
### Rule 6 — on-screen text and captions divide the labour, they don't duplicate it
|
|
60
|
+
|
|
61
|
+
This format is the one most exposed to the defect, because it *invents* its visuals: animated text and diagrams end up competing with voiceover captions for the same visual space and the same attention, as if the two layers aren't aware of each other. **Display text carries the argument; captions carry only the parts of the narration the screen does NOT show.** When both render the same words, the viewer reads one line twice in two sizes while hearing it once.
|
|
62
|
+
|
|
63
|
+
The mechanical form: compare each caption phrase against the words on screen in that scene and **drop the caption when overlap is ≥60% of its content words** (words longer than 2 chars, case- and punctuation-normalised). On a reference build that suppressed **10 of 27 tiles**, and the typographic hook and end card came out **entirely caption-free** — correct, not a bug. The survivors also get wider time windows (narrowest tile **0.41s → 0.71s**), so it improves readability too.
|
|
64
|
+
|
|
65
|
+
**Caption styling itself is measured, not hardcoded.** A caption plate copied from another video onto this video's stage is a slab the design never asked for. Measure the composited background behind the caption band and choose light-type-no-plate / dark-type-no-plate / plate accordingly, one treatment for the whole video — the procedure and thresholds are in `short-form.QA_REGIME.md` → "Caption styling is MEASURED off the background".
|
|
66
|
+
|
|
67
|
+
### Rule 7 — production floor
|
|
60
68
|
|
|
61
69
|
Captions verbatim in the font regime and safe zone · frame 0 states the claim (it is the thumbnail, and for this format it's usually pure typography, which makes it the *easiest* format to get a good thumbnail from — no excuse for a black open) · no HTML slop: an explainer's subject matter drags authors toward feature grids, comparison tables, and card layouts, and those are exactly the banned web furniture. A comparison is an animated before/after, not a two-column table.
|
|
62
70
|
|
|
@@ -77,6 +85,17 @@ Reuse across variants: the mechanism scenes are often identical, so build them o
|
|
|
77
85
|
- [ ] Every number that matters gets its own readable moment
|
|
78
86
|
- [ ] No unsourced "studies show" / "experts agree"
|
|
79
87
|
- [ ] The narration was read aloud and survived it
|
|
88
|
+
- [ ] No caption repeats the words already on screen in that scene (≥60% overlap → drop the caption)
|
|
89
|
+
- [ ] Caption colour / active colour / plate were measured off the composited background, and one treatment holds for the whole video
|
|
80
90
|
- [ ] Frame 0 states the claim and works as a standalone thumbnail
|
|
81
91
|
- [ ] No comparison tables, feature grids, or card layouts (`vidfarm qa` clean)
|
|
82
92
|
- [ ] In a batch: this variant makes a genuinely different claim, not a rephrased one
|
|
93
|
+
|
|
94
|
+
**Whole-video review** — on the render, not the plan. This format is the most exposed to drift, because every visual is *invented*: each diagram gets designed while it is the whole world, so twelve individually-fine scenes come out as twelve different design languages.
|
|
95
|
+
- [ ] A contact sheet of ~12 stills was read as an image: consistent margins, one type scale, one accent colour, one illustration/diagram style
|
|
96
|
+
- [ ] Diagram and label conventions are shared across scenes (same arrow, same emphasis, same number treatment) — not reinvented per beat
|
|
97
|
+
- [ ] Pacing is deliberate rather than N identically-long beats, and no join is jarring
|
|
98
|
+
- [ ] No dead band under top-anchored content; no frame rests empty >0.5s; the end card is settled ≥2s before the last frame
|
|
99
|
+
- [ ] No frame carries two contradictory numbers, and no placeholder empty state reads as a failed render
|
|
100
|
+
- [ ] Frames from two different scenes were compared (a frozen render passes duration, frame-count and audio-hash checks)
|
|
101
|
+
- [ ] Audio verified by measurement — ~12–15 dB speech-over-bed, peak <0 dBFS — not by "it sounds fine"
|
|
@@ -90,3 +90,21 @@ Resist the temptation to fan out on visual style instead. Ten themes of one vide
|
|
|
90
90
|
- [ ] Price is either stated plainly or absent entirely
|
|
91
91
|
- [ ] Frame 0 shows the product mid-task and works as a standalone thumbnail
|
|
92
92
|
- [ ] In a batch: this variant opens on a genuinely different pain, not a restyled one
|
|
93
|
+
|
|
94
|
+
**Whole-video review** — on the render, not the plan
|
|
95
|
+
- [ ] A contact sheet of ~12 stills was read as an image: consistent margins, one type scale, one accent colour, one crop/zoom convention on the UI
|
|
96
|
+
- [ ] Pacing is deliberate rather than N identically-long beats, and no join is jarring
|
|
97
|
+
- [ ] No dead band under top-anchored content; no frame rests empty >0.5s; the end card is settled ≥2s before the last frame
|
|
98
|
+
- [ ] No frame carries two contradictory numbers (a stat in the headline vs. a different one in the UI beneath it)
|
|
99
|
+
- [ ] Frames from two different scenes were compared (a frozen render passes duration, frame-count and audio-hash checks)
|
|
100
|
+
- [ ] Audio verified by measurement — ~12–15 dB speech-over-bed, peak <0 dBFS — not by "it sounds fine"
|
|
101
|
+
|
|
102
|
+
**Client work** — when the product is someone else's, these are not optional
|
|
103
|
+
- [ ] Every claim is one the customer's own site/app makes. "Built on X's guidelines" is not an endorsement; "iOS in the works" is not a shipped app
|
|
104
|
+
- [ ] Ratings, install counts and prices are quoted exactly or left out
|
|
105
|
+
- [ ] No real third party is named in a negative or bias-implying context — even if the product's own output does it; use the product's neutral state instead
|
|
106
|
+
- [ ] Humour is aimed at the problem, never at an identifiable real business or person (use a fictional `.example` domain)
|
|
107
|
+
- [ ] No fear-selling on money/health/safety/family topics, and no authority claimed beyond what the customer claims
|
|
108
|
+
- [ ] Licence obligations (e.g. map attribution) stay on screen for the full runtime
|
|
109
|
+
- [ ] Real names, faces and emails visible in the customer's own screenshots were a deliberate decision, not an accident
|
|
110
|
+
- [ ] Before flagging the customer's data as inconsistent, the whole asset was read — a national total beside a per-row figure is not a contradiction
|
|
@@ -98,6 +98,27 @@ Not an accessibility afterthought: captions are how the hook, the loop, and the
|
|
|
98
98
|
- **Cards are timed text over footage** — never a card UI, table, chip row, or frosted panel (`vidfarm qa` flags those as slop).
|
|
99
99
|
- **Max ~3 standalone cards per video:** one for the loop, one for the payoff, one for the bait.
|
|
100
100
|
|
|
101
|
+
#### Caption styling is MEASURED off the background, never hardcoded
|
|
102
|
+
|
|
103
|
+
A white rounded caption plate copied from another video onto a near-black stage is a bright slab the design never asked for — it dominates the frame and reads as a UI element pasted over the video. So measure what is actually behind the caption band, then pick one of three treatments:
|
|
104
|
+
|
|
105
|
+
| Background behind the caption band | Treatment |
|
|
106
|
+
|---|---|
|
|
107
|
+
| Dark and calm (luma < ~70, variation < ~42) | **Light type, NO plate** — the plate is dead weight |
|
|
108
|
+
| Bright and calm (luma > ~160, variation < ~42) | **Dark type, NO plate** |
|
|
109
|
+
| Busy / mid-tone / moving colour | **Plate** — nothing else stays readable |
|
|
110
|
+
|
|
111
|
+
- **The active-word highlight colour follows the treatment.** A deep red active word is fine on a white plate and unreadable against near-black; on a dark treatment it has to become a bright accent. Pick `--color` / `--active-color` / `--background-style` on `vidfarm captions generate` by measuring, not by taste.
|
|
112
|
+
- **ONE treatment per video, not per caption.** Styling that flips every few seconds reads as a bug, not as intelligence. Re-measure per scene only if the footage genuinely changes character.
|
|
113
|
+
- **Measure the COMPOSITED value, not the source file.** When the background is a moving image, walk the caption band's rect back through the background's own transform (Ken Burns scale/pan) into the source image, crop, then apply the composition's own darkening stack — gradient overlay, vignette, layer opacity — so the number is what the viewer sees. Sampling the raw source is the mistake: it reported a mid-grey for a stage that renders near-black.
|
|
114
|
+
- **Verify it for free in a rendered frame.** On the reference video the model predicted `luma 11, variation 5` and the rendered file measured `luma 16, variation 7` — same bucket, and it correctly shipped plate-free. Grab a frame where no caption is showing and measure the band there.
|
|
115
|
+
|
|
116
|
+
#### On-screen text and captions must not say the same thing at once
|
|
117
|
+
|
|
118
|
+
Animated text and diagrams on the page competing with voiceover captions is a real, nameable defect: the two are fighting for the same visual space and the same attention, as if they aren't aware of each other. **Display text carries the argument; captions carry only the parts of the narration the screen does NOT show.** When both render the same words, the viewer reads one line twice in two sizes while hearing it once, and the frame feels like two videos overlaid.
|
|
119
|
+
|
|
120
|
+
The mechanical fix: compare each caption phrase against the words on screen in that scene and **drop the caption when overlap is ≥60% of its content words** (words longer than 2 chars, case- and punctuation-normalised). On the reference video that suppressed **10 of 27 tiles**, and the hook and end card came out **entirely caption-free** — that is correct, not a bug, and worth saying out loud because an author who doesn't expect it will think captions broke. Second-order benefit: the surviving captions inherit wider time windows (the narrowest tile went from **0.41s to 0.71s**), so it buys readability as well as calm.
|
|
121
|
+
|
|
101
122
|
## Part II — the rules
|
|
102
123
|
|
|
103
124
|
### Rule 1 — every video is standalone. There is no part two.
|
|
@@ -147,9 +168,19 @@ Verbatim captions in the font regime and the safe zone · frame 0 works as hook
|
|
|
147
168
|
|
|
148
169
|
**Production**
|
|
149
170
|
- [ ] Verbatim captions, weight 700–900, safe zone, one line at a time
|
|
171
|
+
- [ ] Caption colour / active colour / plate were chosen by measuring the composited background behind the band, not copied from another video
|
|
172
|
+
- [ ] One caption treatment for the whole video, and the active-word colour is readable on it
|
|
173
|
+
- [ ] No caption repeats the words already on screen in that scene (≥60% overlap → drop the caption)
|
|
150
174
|
- [ ] ≤3 standalone cards, no slop furniture, `vidfarm qa` otherwise clean
|
|
151
175
|
- [ ] In a batch: this variant differs from its siblings by more than one noun
|
|
152
176
|
|
|
177
|
+
**Whole-video review** — on the render, not the plan; scenes built one at a time pass individually and drift as a sequence
|
|
178
|
+
- [ ] A contact sheet of ~12 stills was read as an image: consistent margins, one type scale, one accent colour, one illustration style
|
|
179
|
+
- [ ] Pacing is deliberate — not N identically-long beats, and no single hold that stalls it — and no join is jarring
|
|
180
|
+
- [ ] No dead band under top-anchored content; no frame rests empty >0.5s; the end card is settled ≥2s before the last frame
|
|
181
|
+
- [ ] Frames from two different scenes were compared (a frozen render passes duration, frame-count and audio-hash checks)
|
|
182
|
+
- [ ] Audio verified by measurement — ~12–15 dB speech-over-bed, peak <0 dBFS — not by "it sounds fine"
|
|
183
|
+
|
|
153
184
|
## Diagnosing a flop — read the charges, not the video
|
|
154
185
|
|
|
155
186
|
| What the numbers say | Weak charge | Fix |
|
|
@@ -80,3 +80,10 @@ If you're generating variants with different speakers/avatars, hold the honest-l
|
|
|
80
80
|
- [ ] Any paid/gifted/affiliate relationship is disclosed on screen and in the caption
|
|
81
81
|
- [ ] Frame 0 shows the speaker mid-sentence with a legible caption
|
|
82
82
|
- [ ] In a batch: this variant's *situation* differs from its siblings, not just its wording
|
|
83
|
+
|
|
84
|
+
**Whole-video review** — on the render, not the plan. The bar here is *un*-produced, so drift shows up as the opposite defect: one scene that suddenly looks designed breaks the whole illusion.
|
|
85
|
+
- [ ] A contact sheet of ~12 stills was read as an image — nothing in it looks more produced than the rest
|
|
86
|
+
- [ ] Caption treatment, framing and lighting are consistent end to end; no join is jarring
|
|
87
|
+
- [ ] No frame rests empty >0.5s, and the close is settled ≥2s before the last frame
|
|
88
|
+
- [ ] Frames from two different scenes were compared (a frozen render passes duration, frame-count and audio-hash checks)
|
|
89
|
+
- [ ] Audio verified by measurement — peak <0 dBFS, speech clearly above any bed — not by "it sounds fine"
|
package/SKILL.director.md
CHANGED
|
@@ -213,7 +213,7 @@ Present both harnesses to the director, recommend (A) unless they've asked for p
|
|
|
213
213
|
- **The ART must be CLOSED and SOLIDLY FILLED — this is the other half of surviving the key, and the #1 way stickers come back broken.** Ask an image model for "icons on a green plate" and it will happily draw **outline art**: a colored stroke with the shape's interior left as bare plate. It looks perfect on the sheet, and after the key each sticker is a **rim floating around a see-through hole** (an apple-shaped outline with nothing inside it). Same outcome from a *near-plate* fill (the keyer works on tolerance, not exact match), a translucent/glassy/glowing material, or a soft glow fading into the plate. **You cannot key those pixels back — it has to be in the prompt:** *"every object is a closed, solidly filled shape; outlines must enclose an opaque fill of a different color; no outline-only or hollow art; nothing on the art in the plate color or any near-shade of it; fully opaque, no translucency, glow or drop shadow."* `cutout --generate`, `sticker-pack --generate`, `handoff image` and the `create-overlay` primitive **append that clause for you** with the chosen plate hex — write it yourself only when you prompt a generator directly. After the key, both commands report per-item `hole_pct`/`hollow` (console `⚠ N% hollow`, `--json`, `stickers.json`) — a ring or picture frame reads the same way, so it **warns, never blocks**. Flagged and it shouldn't be? Re-generate with the fill clause; a *near*-plate fill can sometimes be rescued with a lower `--tolerance`; one stubborn item can be lifted with `vidfarm mask --crop …` (ONNX matting ignores fill color).
|
|
214
214
|
- **Transparent GIF is supported, for GIF-only surfaces.** `vidfarm sticker-pack … --output-format gif` (stills) and `vidfarm remove-greenscreen <video> --gif` (animated) emit transparent GIFs. GIF alpha is **1-bit**, so edges go hard — fine for chat/forum/Notion sticker surfaces, worse than PNG/WebP/WebM for compositing on a timeline. Prefer PNG/WebP/WebM unless the destination only eats GIF.
|
|
215
215
|
|
|
216
|
-
**Explainer house style — the defaults to build with unless told otherwise.** **White background / light mode** (plain white stage, no gradients, no dark mode, no photo backdrop), **kinetic word-by-word captions** in dark ink on the light stage (`vidfarm captions generate --style word-pop --color "#111111" --active-color "#7C3AED" --background-style plain` — skip outlines/shadows, they're only needed over busy footage), and **female TTS narration** (`vidfarm tts --voice coral` on OpenAI — `nova` for energy, `sage` for calm; `Kore`/`Leda` on Gemini; any ElevenLabs voice via `vidfarm voices`). **Keep it clean and simple** — one idea on screen at a time, two or three cutouts per beat, one accent color, one font, lots of white space; remove before you add. **Illustrations default to simplicity**: flat vector, simple shapes, minimal detail, 2–3 flat colors, no baked-in text — simple art keys cleanly, trims tight, and stays on-style across the whole cast. State the defaults once so the director can override any of them. Full detail: recipe `recipes/cutout-graphics-for-explainers.md` (“House style — the explainer defaults”).
|
|
216
|
+
**Explainer house style — the defaults to build with unless told otherwise.** **White background / light mode** (plain white stage, no gradients, no dark mode, no photo backdrop), **kinetic word-by-word captions** in dark ink on the light stage (`vidfarm captions generate --style word-pop --color "#111111" --active-color "#7C3AED" --background-style plain` — skip outlines/shadows, they're only needed over busy footage), and **female TTS narration** (`vidfarm tts --voice coral` on OpenAI — `nova` for energy, `sage` for calm; `Kore`/`Leda` on Gemini; any ElevenLabs voice via `vidfarm voices`). **Keep it clean and simple** — one idea on screen at a time, two or three cutouts per beat, one accent color, one font, lots of white space; remove before you add. **Illustrations default to simplicity**: flat vector, simple shapes, minimal detail, 2–3 flat colors, no baked-in text — simple art keys cleanly, trims tight, and stays on-style across the whole cast. State the defaults once so the director can override any of them. Full detail: recipe `recipes/cutout-graphics-for-explainers.md` (“House style — the explainer defaults”). **If the director takes the stage off white**, two things stop being optional: every sticker's **white die-cut rim** has to be stripped (on a dark stage it's a glaring halo and the most obvious bot-made artefact in the frame — recipe → “Stickers on a DARK or photographic stage”), and the caption hexes above stop applying — **caption colour, active-word colour and plate are chosen by measuring the composited background behind the caption band**, one treatment per video (`regimes/short-form.QA_REGIME.md` → “Caption styling is MEASURED off the background”). Related: **on-screen text and captions must not say the same thing at once** — display text carries the argument, captions carry only what the screen doesn't show.
|
|
217
217
|
|
|
218
218
|
**Landscape footage in a fullscreen vertical explainer — use the blurred plate, never bars.** When an explainer is built on **real filmed footage** and the source is 16:9 (or 4:3) on a 9:16 canvas, do not `contain` it (hard black letterbox bars read as an unfinished export) and do not blindly `cover` it (a wide shot loses its left and right thirds). Duplicate the clip: a full-canvas `cover` copy behind, heavily **gaussian-blurred and faded dark**, plus the sharp copy centered as a hero band — optionally zoomed ~1.3× — with its **top and bottom edges feathered** into the blur. Same clip, same timecode, so it reads as one continuous image with a shallow-depth-of-field plane, fullscreen edge to edge, nothing cropped, and clean dark space for the header and captions. Bake it once with ffmpeg into a single 1080×1920 file (free, local) and place it as one ordinary full-canvas layer — layer blur is not an editor property, so the pre-bake is the path that works in the editor, `serve`, and cloud render alike. Copy-paste ffmpeg + HTML recipes, tuning table, and the failure modes: `references/editor-workflows.md` (“The blurred plate — landscape footage, fullscreen, on a vertical canvas”).
|
|
219
219
|
|
|
@@ -251,6 +251,18 @@ The mechanism is deterministic, not luck: rendering is seek-safe, so frame 0 sho
|
|
|
251
251
|
|
|
252
252
|
Full mechanics and editor verbs: `references/editor-workflows.md` (“The opening frame is the post's thumbnail”); poster-state authoring craft: `hyperframes-creative/references/beat-direction.md`.
|
|
253
253
|
|
|
254
|
+
## Judge the WHOLE video, not the parts you built — and never by one frame
|
|
255
|
+
|
|
256
|
+
**Assume your own finished video has a defect you can't see.** That's the observed base rate, not modesty: across a 32-video batch, *every* first-pass video had a real defect that the agent who built it had already reported as "verified, looks good" — dead space under the content, a placeholder that reads as a failed render, contradictory numbers in one frame, a CTA still animating at the last frame.
|
|
257
|
+
|
|
258
|
+
**The cause is how agents build: part by part, each part correct in isolation.** Scene 3 gets authored while scene 3 is the whole world, so every scene passes on its own and the video fails *as a video* — type size jumps between beats, one scene breathes and the next is crammed, the accent colour drifts, a transition lands like a slap because nothing before it moved that fast, one asset is flat vector and the next is photographic. Nobody watches a scene; they watch the sequence. **So before you ship, look at the whole thing at once as a stranger would**, and ask: is it visually balanced (or top-anchored with a dead band below), is the spacing consistent scene to scene, is there ONE type scale / accent colour / illustration style, does the pacing have a deliberate rhythm instead of N identical beats, is anything jarring at the joins, does any frame make two elements compete for the eye, and — the summary question — **does it look like one person made it in one sitting?** When it doesn't, fix the *system* (define the type scale, the margin, the default beat, the palette, then apply it to every scene) rather than patching the one scene that stood out.
|
|
259
|
+
|
|
260
|
+
The mechanical form is a contact sheet, because one image read shows drift that twelve separate checks can't — and it's one command: **`vidfarm stills ./work --sheet`** renders the frames and tiles them into `stills/contact-sheet.png`. **Read the sheet as an image.**
|
|
261
|
+
|
|
262
|
+
**And never verify a video by a single frame.** Frame 0 is the thumbnail so you judge it alone — but a whole class of render bug (an overlay pass missing `-loop 1`, assets outside the composition root so the timeline never runs) freezes *every* frame while duration, frame count and audio hash all still pass, and frame 0 looks perfect. **Always compare two frames from different scenes; they must differ a lot.** Likewise, verify audio by measurement, never "it sounds good" — you can't hear it.
|
|
263
|
+
|
|
264
|
+
Full method — the holistic checklist, the six defects in observed frequency order, the "does it rest there" test, frozen-render verification, audio measurement, and how to brief a revision pass: **`references/reviewing-renders.md`**.
|
|
265
|
+
|
|
254
266
|
## Web AI chat vs. local devcli — know your surface's limits
|
|
255
267
|
|
|
256
268
|
**Desktop users: this is a skill, not an SDK or required runtime.** It gives an AI agent Vidfarm's operating knowledge; it does not proxy requests, add hidden capabilities, or require the agent to use a particular client. Every cloud workflow described here can be performed with ordinary HTTP against the Vidfarm REST API. For convenience and safer composition/file workflows, recommend the `vidfarm` command from `@officexapp/vidfarm-devcli`: it wraps the same REST routes, handles auth, uploads, polling, pagination, local files, and pull/edit/render loops. Use raw REST whenever the user prefers it, the CLI lacks a new route, or the work belongs in an existing service or automation stack.
|
|
@@ -274,6 +286,7 @@ You may be running as the **in-web AI chat** (the /editor copilot, the chat dock
|
|
|
274
286
|
- **The first frame IS the thumbnail — compose it on purpose.** Frame 0 is a single frame of ~30 in the first second, but it's the poster every feed, share link, and paused player freezes on, so **more people see that one frame than watch the video**. It must never be black, empty, mid-fade, or caught mid-animation: put a real visual at `start:0`, have the hook words already on screen at t=0, and never hang a `fade-black`/`fade-white`/`flash` *entrance* on the **first** clip (junction transitions between later clips are fine — this rule is only about the opening). Verify it, don't assume: devcli `vidfarm stills ./work --at 0` renders that exact frame, and `vidfarm qa` flags a blank or fading open.
|
|
275
287
|
- **Ask early: one-time video, or bulk?** "Make me a video about X" and "I need to post daily / give me 20 hook variants" are different jobs, and directors often don't know the second one has a name. Ask once, up front: *"One video, or should we set this up as a repeatable batch?"* Bulk = **scripting mode** (a pinned base fork + a loop that varies ONE thing per variant; a public-raws shelf is the cheapest source of the N), and every batch gets a **`QA_REGIME.md`** — because a loop of fifty videos has no human looking at every frame, and the regime is what replaces those eyes. Don't silently ship a one-off when they asked for volume, or drag someone into a harness when they wanted one clip.
|
|
276
288
|
- **`QA_REGIME.md` is the director's own quality contract, and it's a first-class artifact.** `vidfarm qa`'s built-ins are universal (slop, fonts, the thumbnail frame); a regime is what makes *this* format good — audience, hook shape, banned vocabulary, pacing, compliance line. It lives next to the work, they own it, it stacks: `vidfarm regime init short-form --out ./work/QA_REGIME.md` (bundled bases: `short-form`, `hooks`, `ugc-testimonial`, `explainer`, `product-demo` — each a starting point to **edit**, never a house style), then `vidfarm qa ./work --regime hooks --regime ./brand/HOUSE.md`, and any user file anywhere is valid. Its `checks:` front matter is machine-settled; its `- [ ]` checklist comes back as **review items you answer honestly in your report** — never claim a video passed the half the CLI can't judge. When a batch teaches you something, **write it back into the regime**: that's the artifact that compounds. Details in `references/automation-and-local-dev.md` ("Scripting mode"), format in `regimes/README.md`.
|
|
289
|
+
- **A video is judged as a SEQUENCE, so review it as one.** Agents build scene by scene and each scene passes in isolation while the video drifts — inconsistent margins, three type sizes, an accent colour that wanders, beats that are all the same length, a jarring join. Tile a dozen stills into one contact sheet (`vidfarm stills ./work --sheet`) and read it as an image before you call anything done, fix drift by defining the system rather than patching the odd scene out, and remember that **your own confident "verified, looks good" is the single least reliable signal in this workflow** — it was wrong on every video of a 32-video batch. Method: `references/reviewing-renders.md`.
|
|
277
290
|
- **On devcli, QA every video you produce: `vidfarm qa ./work`.** Free, instant, local-only — it blocklists exactly the slop above plus first-frame/thumbnail and font-regime/safe-zone drift, and prints a concrete fix per finding. **Feedback, not a gate**: it exits 0 even on findings, never runs automatically, and is a blocklist (unusual/stylized compositions pass untouched), so there's no reason not to run it before every publish. `--json` for scripted batches, `--strict` only if you want a CI failure. **Web-chat copilot: this command does not exist for you** (devcli-only, no REST twin) — apply the standard by hand, and when handing a heavy job to a local coding agent, tell them to run `vidfarm qa`.
|
|
278
291
|
- **Every production adheres to the TikTok-native caption standard.** On-screen text lives inside the readable safe zone (**~8%–85%** of a 9:16 frame — never pinned to the top/bottom edges the phone UI clips), uses the composition's bold font regime (**Montserrat** default / TikTok Sans, weight **700–900**, ~36–64px on a 1080-wide frame), and uses **exactly one of four valid backgrounds**: outline/stroke (`background_style:"outline"`, the default), plain + shadow (`"plain"`), an active-word highlight pill (`set_captions` `spotlight`/`karaoke` — the only legitimate pill *in the whole frame*, static labels included), or a tight-hugging solid band (`"highlight-solid"`, radius ≤8px, no border/shadow/gradient/blur). Decomposed forks often inherit the source's edge-pinned caption in an off-regime font — fix it, don't inherit it. Local devcli renders auto-normalize position + font family only (never the slop), so author it correctly. Full rules in `references/editor-workflows.md` ("Social-native visual standard" + "TikTok-native caption standard").
|
|
279
292
|
- **Where the web chat struggles: complex, long, multi-step transformations.** A full multi-scene re-theme, an iterative render-critique-iterate loop, heavy scripted or batch work, or anything needing a real filesystem and many sequential tool calls will hit context limits, turn/timeout ceilings, and the web editor's constraints (CSS/declarative motion only — JS animation adapters are stripped on save). Don't grind a big transformation one layer at a time in a chat turn and stall.
|
|
@@ -292,6 +305,7 @@ Read only the relevant reference file for the current task.
|
|
|
292
305
|
- Getting-started interviews, **consultations**, strategy docs, onboarding flow: `references/onboarding.md`
|
|
293
306
|
- Primitive routes such as TTS, STT, music, overlays, background removal, product placement: `references/primitives.md`
|
|
294
307
|
- **Hooks, retention, and why a video travels — the four charges: `references/hooks-and-virality.md`.** Read it before writing ANY hook, caption script, or re-theme, and before a hook-variant batch. It is the craft harness; the rest of the skill is mechanics.
|
|
308
|
+
- **Reviewing a finished render — the holistic pass, the six common defects, frozen-render and audio verification: `references/reviewing-renders.md`.** Read it before you report a video as done, and before you grade someone else's.
|
|
295
309
|
- Bulk / N-variant production: recipe `recipes/bulk-scripting-with-a-regime.md`; the `QA_REGIME.md` format and the bundled bases: `regimes/README.md` (+ the regime files beside it, readable as-is)
|
|
296
310
|
- Complete REST API map and raw-HTTP conventions: `references/rest-api.md`. Load it only when the user asks for REST, an endpoint/schema, direct HTTP integration, or exhaustive API coverage. For the entire specification, follow its domain links and load every listed reference; do not preload them into ordinary director conversations.
|
|
297
311
|
|
|
@@ -334,6 +348,7 @@ Choose the narrowest path that satisfies the request.
|
|
|
334
348
|
- Submission routes are generally not idempotent. Especially for renders and expensive primitives, check status before retrying.
|
|
335
349
|
- In the web editor, use CSS/declarative motion only. Script-bearing HTML is stripped or rejected there.
|
|
336
350
|
- **Never render or approve without judging frame 0 as a standalone still.** It is the thumbnail everywhere the post appears; an empty/black opening frame ships a dead post. See “The FIRST FRAME is the thumbnail”.
|
|
351
|
+
- **Never judge the VIDEO by one frame, and never report a render as reviewed without the holistic pass.** Compare frames from at least two different scenes (a frozen render passes every other check), read a contact sheet for balance/spacing/style/pacing drift, and state separately what you measured vs. what you judged. See “Judge the WHOLE video”.
|
|
337
352
|
|
|
338
353
|
## Recommended Recipes
|
|
339
354
|
|
|
@@ -1249,6 +1264,8 @@ Short-form is watched on a phone, and the phone's UI eats the frame's edges. **N
|
|
|
1249
1264
|
| 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 |
|
|
1250
1265
|
| 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 |
|
|
1251
1266
|
|
|
1267
|
+
**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".
|
|
1268
|
+
|
|
1252
1269
|
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.
|
|
1253
1270
|
|
|
1254
1271
|
**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.
|
|
@@ -1587,6 +1604,146 @@ A video with replies gets shown again; a video with none dies at its first audie
|
|
|
1587
1604
|
|
|
1588
1605
|
**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).
|
|
1589
1606
|
|
|
1607
|
+
## Reviewing a render — look at the whole video, and never trust one frame
|
|
1608
|
+
|
|
1609
|
+
**Assume your own finished video has a defect you can't see, because you built it.** This is not humility, it's the observed base rate: across a 32-video bespoke batch, **every single first-pass video had a real defect that the agent who built it had already reported as "verified, looks good."** Dead space under the content, a placeholder that reads as a failed render, two contradictory numbers 200px apart, a CTA still animating when the video ends. None of these are subtle. All of them survived a confident self-review.
|
|
1610
|
+
|
|
1611
|
+
The reason is structural, not sloppiness: **an agent builds a video the way it builds code — part by part, each part correct in isolation.** Scene 3 is written while scene 3 is the whole world. So each scene passes on its own and the video fails as a video: the type jumps two sizes between beats, one scene breathes and the next is crammed to the margins, the accent colour drifts, a transition lands like a slap because nothing before it moved that fast. Nobody watches a scene. They watch the sequence.
|
|
1612
|
+
|
|
1613
|
+
So the review has two jobs, and they need two different passes:
|
|
1614
|
+
|
|
1615
|
+
1. **The holistic pass** — does this read as ONE video, made by one person, on purpose?
|
|
1616
|
+
2. **The defect pass** — is any individual frame broken in one of the six ways frames are usually broken?
|
|
1617
|
+
|
|
1618
|
+
Do them in that order. The holistic pass is the one agents skip, and it is the one that separates "technically correct" from "good."
|
|
1619
|
+
|
|
1620
|
+
---
|
|
1621
|
+
|
|
1622
|
+
## Pass 1 — the holistic pass: watch it as one object
|
|
1623
|
+
|
|
1624
|
+
**Before you look for defects, look at the video the way a stranger will: all at once, start to finish, with no memory of how it was built.** You cannot do this from the code, from the storyboard, or from the scene you just edited — you have to look at the actual frames, in order, side by side.
|
|
1625
|
+
|
|
1626
|
+
```bash
|
|
1627
|
+
# ONE command — renders the stills AND tiles them into ./work/stills/contact-sheet.png.
|
|
1628
|
+
# No MP4 render needed; free, local, in-process.
|
|
1629
|
+
vidfarm stills ./work --sheet # default timestamps = midpoint of each scene clip (cap 8)
|
|
1630
|
+
vidfarm stills ./work --sheet --at 0,2,4,6,8,10,12,14,16,18,20,22 # or pick them yourself
|
|
1631
|
+
# then READ ./work/stills/contact-sheet.png as an image
|
|
1632
|
+
# --sheet-out <file> relocates it; --sheet-width <px> for bigger tiles (default 320)
|
|
1633
|
+
|
|
1634
|
+
# already have the MP4? same idea, straight off the file
|
|
1635
|
+
for t in 0 2 4 6 8 10 12 14 16 18 20 22; do
|
|
1636
|
+
ffmpeg -y -ss $t -i final.mp4 -frames:v 1 "qa/f$(printf %03d $t).png"; done
|
|
1637
|
+
ffmpeg -y -pattern_type glob -i "qa/f*.png" \
|
|
1638
|
+
-vf "scale=300:-1,tile=4x3:margin=6:padding=6:color=0x999999" -frames:v 1 qa/sheet.png
|
|
1639
|
+
```
|
|
1640
|
+
|
|
1641
|
+
**The tile sheet is the point.** One image read, twelve frames, and the eye picks up drift instantly that no per-scene check can see. Read it as an image — not the filenames, not the HTML that produced it. (The still filenames are zero-padded seconds, so a glob stays in chronological order.)
|
|
1642
|
+
|
|
1643
|
+
Then answer these, out loud, in your report:
|
|
1644
|
+
|
|
1645
|
+
- **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.
|
|
1646
|
+
- **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.
|
|
1647
|
+
- **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.
|
|
1648
|
+
- **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.
|
|
1649
|
+
- **Rhythm and pacing.** Do scene durations form a deliberate pattern (a fast open, a longer explanation, a fast close), or is every scene the same length because a loop wrote them? Same-length beats are hypnotic in the bad way. Conversely, one 9-second hold in a video of 2-second cuts stalls it dead.
|
|
1650
|
+
- **Nothing jarring at the joins.** Watch each transition specifically. A cut from a dark scene to a white one is a flash in the face; a scale-up entrance immediately after a scale-up exit reads as a stutter; two consecutive scenes whose subjects sit in the same screen position with different content look like a glitch, not a cut. Where a join is harsh, either match the two frames either side of it (colour, position, energy) or make the harshness deliberate and rhythmic.
|
|
1651
|
+
- **One idea per moment.** Across the whole sheet, is there any frame where two things compete for the eye — display text over captions saying the same words, a busy background under type, two headlines superimposed at a handoff? At the video level this shows up as a *density* problem: some beats carry three elements and some carry one.
|
|
1652
|
+
- **Does it look like one person made it in one sitting?** The summary question. If the honest answer is "it looks assembled," name specifically which scenes don't belong and fix them toward the majority, don't average everything.
|
|
1653
|
+
|
|
1654
|
+
**When something is off, fix it globally, not locally.** The instinct after spotting drift is to patch the one scene that stands out. Usually the right fix is to define the rule (two headline sizes, one accent, 8% margins, 2.5s default beat) and apply it across every scene — including the ones that already looked fine. A video is a system; patching one node keeps the system inconsistent.
|
|
1655
|
+
|
|
1656
|
+
**Build order helps too, if you're still building.** Author the shared system first — type scale, palette, margins, motion vocabulary, default beat length — as one thing that every scene reads from, then fill the scenes. Scenes written first and harmonised later almost never fully converge.
|
|
1657
|
+
|
|
1658
|
+
---
|
|
1659
|
+
|
|
1660
|
+
## Pass 2 — the defect pass: what actually goes wrong, in frequency order
|
|
1661
|
+
|
|
1662
|
+
From the same 32-video batch, ranked by how often it happened. Look for these specifically; they are what your own review misses.
|
|
1663
|
+
|
|
1664
|
+
1. **Large flat dead regions.** Content top-anchored with an empty band below it. Agents do this constantly and never notice, because during authoring the element is the subject and the emptiness is just "background."
|
|
1665
|
+
2. **A placeholder empty state that reads as a missing asset.** A big empty dashed rectangle held for two seconds looks exactly like the render failed. If a scene's job is "an empty inbox," it still has to look designed, not broken.
|
|
1666
|
+
3. **Two contradictory numbers in one frame.** Especially on anything data-shaped — a stat in the headline and a different one in the visual beneath it.
|
|
1667
|
+
4. **A CTA or end card still building when the video ends.** The final state must be **settled at least 2 seconds before the last frame**, or the loop-around cuts it off and the ask never lands.
|
|
1668
|
+
5. **Two headlines superimposed at a scene handoff.** The outgoing scene's text hasn't left when the incoming one arrives. Fix at the timing level: exit at `nextIn − 0.18`, duration `0.24`, ease `power2.out` — a slow-leaving `power2.in` is what causes the overlap in the first place.
|
|
1669
|
+
6. **Type colliding with a busy background layer** exactly at the moment it's spoken. Particles, pins, footage detail — legible in the still you checked, unreadable at the second the word lands.
|
|
1670
|
+
|
|
1671
|
+
**If a frame looks empty, check whether it RESTS there.** Sample at 0.2–0.25s intervals through that transition. A transient near-empty wipe frame is fine and normal; anything holding empty for **>0.5s** is a hole in the video.
|
|
1672
|
+
|
|
1673
|
+
```bash
|
|
1674
|
+
vidfarm stills ./work --at 6.0,6.2,6.4,6.6,6.8,7.0 # is it a wipe, or a hole?
|
|
1675
|
+
```
|
|
1676
|
+
|
|
1677
|
+
---
|
|
1678
|
+
|
|
1679
|
+
## Never verify a video by one frame
|
|
1680
|
+
|
|
1681
|
+
**This is the failure mode that survives every check you'd think to run.** Whole classes of render bug produce a video where *every frame is identical* — the timeline never ran — while duration, frame count, file size and audio hash all come out exactly right. Frame 0 looks perfect, so a single-frame check passes and you ship a frozen video.
|
|
1682
|
+
|
|
1683
|
+
Two real causes, both silent:
|
|
1684
|
+
|
|
1685
|
+
- **A watermark/overlay pass without `-loop 1` on a single-frame PNG input.** The frame-sync collapses the whole video onto one frame. Five videos shipped this way before it was caught.
|
|
1686
|
+
- **Assets outside the composition root.** Only `<style>`/`<script>` *inside* the `data-composition-id` root execute, and sibling relative files may not resolve — fonts, images, even the animation library itself. The timeline never starts; frame 0 still renders fine because frame 0 is the static DOM.
|
|
1687
|
+
|
|
1688
|
+
**The rule that catches both: always compare two frames from different scenes.** They must differ a lot. And when you've applied any pass over an existing video (watermark, overlay, dedupe, re-encode), also compare each output frame against **its own** input frame at the same timestamp — that difference should be tiny. Two checks, opposite directions:
|
|
1689
|
+
|
|
1690
|
+
```bash
|
|
1691
|
+
# consecutive/distant frames must DIFFER (motion preserved)
|
|
1692
|
+
vidfarm stills ./work --at 0,4,9,14
|
|
1693
|
+
# after an overlay pass: same timestamp, before vs after, must be NEARLY IDENTICAL
|
|
1694
|
+
ffmpeg -y -ss 7 -i clean.mp4 -frames:v 1 a.png
|
|
1695
|
+
ffmpeg -y -ss 7 -i final.mp4 -frames:v 1 b.png
|
|
1696
|
+
ffmpeg -i a.png -i b.png -filter_complex "psnr" -f null - # very high PSNR = only the mark changed
|
|
1697
|
+
```
|
|
1698
|
+
|
|
1699
|
+
This sits directly beside the frame-0 rule and is its necessary counterweight: **frame 0 is the thumbnail, so judge it alone — but never judge the VIDEO by it.** The two rules are checking different things, and an agent that only internalises the first one has a perfect blind spot for a frozen render.
|
|
1700
|
+
|
|
1701
|
+
---
|
|
1702
|
+
|
|
1703
|
+
## Verify audio by measurement, not by ear
|
|
1704
|
+
|
|
1705
|
+
**You cannot hear the render.** Do not report "the mix sounds good" — you have no way to know it, and it is the claim that most often turns out false. Measure instead.
|
|
1706
|
+
|
|
1707
|
+
- **Speech-over-bed separation: target 12–15 dB.** Measure the RMS of the mix across the spans where words actually occur, minus the RMS of a bed-only stretch. Word spans come free from the transcription you're already running for captions (`vidfarm stt <file> --engine whisper` → word timings).
|
|
1708
|
+
- **Peak below 0 dBFS.** A mix that clips reads as amateur instantly on a phone speaker.
|
|
1709
|
+
- **Beware "separation" numbers computed over the music-only tail** — they measure the wrong thing (bed alone vs. bed alone) and over-report by a wide margin. Don't retune a mix based on one.
|
|
1710
|
+
|
|
1711
|
+
```bash
|
|
1712
|
+
ffmpeg -i final.mp4 -af "volumedetect" -f null - # peak + mean over the whole file
|
|
1713
|
+
ffmpeg -i final.mp4 -ss 3.1 -t 1.4 -af "volumedetect" -f null - # a span where a word is spoken
|
|
1714
|
+
```
|
|
1715
|
+
|
|
1716
|
+
**Narration timing gotchas that produce a correct-looking, wrong-sounding video:**
|
|
1717
|
+
|
|
1718
|
+
- **Never `adelay` the voiceover.** Whisper's word timings — and therefore every caption you generated from them — are relative to the raw `vo.wav`. Delaying the VO desyncs every caption in the video while the file still plays fine. Use `apad` + `atrim` to place it instead.
|
|
1719
|
+
- **Scene handoffs can leave ~0.3s of silence.** Extend each clip's audio ~0.35s into the next.
|
|
1720
|
+
- **Whisper's default model is English-only and will hallucinate fluent English over another language.** Non-English narration needs `--model large-v3 --language <code>`. The output looks like a clean transcript, so this one ships silently.
|
|
1721
|
+
- **`vidfarm tts` reads stdin** — always redirect `</dev/null` when calling it inside a shell loop, or the loop eats its own input.
|
|
1722
|
+
|
|
1723
|
+
---
|
|
1724
|
+
|
|
1725
|
+
## The revision pass — how to fix what review found
|
|
1726
|
+
|
|
1727
|
+
**Spawn a fresh pass rather than re-litigating with the context that produced the defect.** If you're handing fixes to a subagent (or picking the work back up yourself later), the brief that works:
|
|
1728
|
+
|
|
1729
|
+
- **State it as N targeted fixes and nothing else.** "The video is good — you are making three specific fixes." Open-ended "improve it" turns a working video into a different, differently-broken video.
|
|
1730
|
+
- **Edit the generator, not the generated output.** If a script produced `composition.html`, fix the script. Check first that a generator exists — some compositions are hand-authored.
|
|
1731
|
+
- **Back up before overwriting** — keep `<slug>-v1.mp4`. Re-renders are cheap locally; a lost good version isn't.
|
|
1732
|
+
- **Keep audio bit-identical unless audio is the defect.** Reuse the existing `vo.wav` / word timings rather than re-recording; a re-record retimes every caption for no reason.
|
|
1733
|
+
- **Give the PROBLEM, not just your proposed solution.** Repeatedly, the agent handed a described defect found a better fix than the one specified — using an app's own collapsed UI state instead of a redaction box, a type safe-zone solver instead of a scrim, making a document's *arrival* the spectacle instead of cutting the document. Say what's wrong and at what timestamp; let the fix be found.
|
|
1734
|
+
- **Then sweep for the same class of problem** across the rest of the video, and report what else turned up. Defects of a given kind are rarely solitary — they come from a habit.
|
|
1735
|
+
|
|
1736
|
+
---
|
|
1737
|
+
|
|
1738
|
+
## Report both halves honestly
|
|
1739
|
+
|
|
1740
|
+
When you hand back a render, say what you **measured** and what you **judged**, separately:
|
|
1741
|
+
|
|
1742
|
+
- Machine-settled: `vidfarm qa ./work` findings, `vidfarm lint`, durations, peak dBFS, frame-difference checks.
|
|
1743
|
+
- Human-judgment: the holistic pass above — balance, spacing, type continuity, colour coherence, pacing, joins — plus the regime's `- [ ]` review items.
|
|
1744
|
+
|
|
1745
|
+
**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.
|
|
1746
|
+
|
|
1590
1747
|
## Download a video from a website (Vidfarm fetches it for you — paid plans)
|
|
1591
1748
|
|
|
1592
1749
|
**Yes, Vidfarm can download videos off the web for the user.** When someone pastes a YouTube / TikTok / Instagram / X (Twitter) / other supported post URL and wants the actual file — "grab this video", "save this clip", "get me that audio" — you do **not** need yt-dlp, a browser extension, or an apology. One primitive does it and returns a durable Vidfarm-hosted asset:
|
|
@@ -2053,7 +2210,7 @@ The licensed harness also carries the **generative build workflow** guidance (ch
|
|
|
2053
2210
|
| `vidfarm raws search "…"` / `raws match "…"` / `raws list` / `raws sources` | (local library; NL→criteria via local agent or provider key) | search/reuse the raws library |
|
|
2054
2211
|
| `vidfarm raws preset list\|run\|save` / `raws export <ids…> --to <dir>` | (local library) | saved queries; copy raw MP4s out |
|
|
2055
2212
|
| `vidfarm lint <dir\|composition.html>` | (local static validation) | pre-publish composition check: timing, overlaps, preset names, media src |
|
|
2056
|
-
| `vidfarm stills <dir> [--at 0,2.5,…]` | (local in-process render of PNG frames) | visually verify an edit without a full render |
|
|
2213
|
+
| `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 |
|
|
2057
2214
|
| `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 |
|
|
2058
2215
|
| `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 |
|
|
2059
2216
|
| `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) |
|
|
@@ -2108,6 +2265,8 @@ What it flags:
|
|
|
2108
2265
|
|
|
2109
2266
|
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.
|
|
2110
2267
|
|
|
2268
|
+
**`vidfarm qa` is a static DOM check — it 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`.
|
|
2269
|
+
|
|
2111
2270
|
**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.
|
|
2112
2271
|
|
|
2113
2272
|
## Cost mode — the devcli's money-saving guardrail
|
|
@@ -2133,6 +2292,15 @@ The four modes, quoted as **cost per finished video**. The first two are spend p
|
|
|
2133
2292
|
|
|
2134
2293
|
**Narration defaults to the FREE local voice in minimize AND hybrid.** A bare `vidfarm tts "…"` runs the keyless local Kokoro-82M engine in both of those modes — you no longer have to remember `--engine local`. A run **opts out** of that default by asking for a premium voice (`--style`, `--provider`, `--model`, `--own-key`, or a non-Kokoro `--voice` like `alloy`/`Kore`/an ElevenLabs id), by passing `--cloud`/`--engine byok`, or by being in `rich-ai`/`pure-videogen`. If the local engine isn't installed on the machine (it needs `pip install kokoro-onnx soundfile` + a ~340MB model on first use), the run **falls back** to the user's provider key / cloud instead of failing — it prints the reason on stderr so you can tell the user why the voice changed.
|
|
2135
2294
|
|
|
2295
|
+
**Narration gotchas that ship a correct-looking, wrong-sounding video.** Each of these produces output that passes every structural check:
|
|
2296
|
+
|
|
2297
|
+
- **Never `adelay` the voiceover to position it.** Whisper's word timings — and therefore every caption generated from them — are relative to the raw `vo.wav`. An `adelay` desyncs every caption in the video while the file still plays perfectly. Use `apad` + `atrim`.
|
|
2298
|
+
- **Whisper's default model is English-only and hallucinates fluent English over other languages.** Non-English narration needs `--model large-v3 --language <code>`; without it you get a clean, confident, entirely invented transcript.
|
|
2299
|
+
- **`vidfarm tts` reads stdin** — redirect `</dev/null` when calling it inside a shell loop, or the loop consumes its own input.
|
|
2300
|
+
- **Check the brand/product name's pronunciation** before you render 20 variants with it. TTS engines mangle proper nouns (Kokoro reads *Genki* as "Jenki"); respell it phonetically in the TTS input and confirm with a whisper round-trip — you're already running whisper for the caption timings.
|
|
2301
|
+
- **For a calm, unhurried read, render line by line** and concatenate the takes with measured silences, rather than one continuous pass. A single pass reads rushed however slow the copy is, because the pauses are TTS filler rather than real beats.
|
|
2302
|
+
- **Verify the mix by measurement, not by ear** — you can't hear the render. Target **12–15 dB** of speech-over-bed separation measured across the actual word spans, peak below **0 dBFS**. A "separation" figure computed over the music-only tail measures bed-vs-bed and over-reports badly; don't retune against it. See `references/reviewing-renders.md`.
|
|
2303
|
+
|
|
2136
2304
|
Precedence: `--cost-mode <m>` flag → `VIDFARM_COST_MODE` env → the saved `cost-mode` → default (hybrid, flagged as "not set"). When nothing is saved and a billed op runs, the CLI prints a "no preference set — ask the user" nudge instead of silently spending, so the default posture really is *ask before you spend*.
|
|
2137
2305
|
|
|
2138
2306
|
**Agent-memory handoff.** After the user picks, offer to remember it across sessions — but the destination depends on the agent, so ask: Claude Code → `CLAUDE.md` (or its memory dir); Codex / OpenCode / most others → `AGENTS.md`; or a note file the user names. `vidfarm cost-mode <choice>` already persists the devcli-side preference; agent memory is the extra step that survives a fresh checkout. In the **web app UI** there is no memory file — ask each time unless the user states a standing preference for the session.
|
|
@@ -2803,6 +2971,7 @@ Use this when a coding agent is doing the work locally or the user wants a repro
|
|
|
2803
2971
|
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.
|
|
2804
2972
|
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.
|
|
2805
2973
|
7. Render with `vidfarm render <forkId> --dir ./work --wait`.
|
|
2974
|
+
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`.
|
|
2806
2975
|
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*.
|
|
2807
2976
|
9. Approve the finished MP4 with `vidfarm approve --video <url|./final.mp4> --caption "..."`. This prints the shareable `share_url`.
|
|
2808
2977
|
|
|
@@ -2876,6 +3045,21 @@ done
|
|
|
2876
3045
|
|
|
2877
3046
|
Free, offline, no wallet. Variant 1 is the `standard` preset as authored (skew 2%, zoom 3%, rotate 2°, speed +2%, saturation +4%); later variants get jittered magnitudes and flipped signs, so they differ from the original **and from each other**. One variant per account — two accounts posting the same variant defeats the point. Reuse one `--seed` per source so the batch is reproducible.
|
|
2878
3047
|
|
|
3048
|
+
### 5c. Eyeball the renders — the loop cannot do this for you
|
|
3049
|
+
|
|
3050
|
+
**A batch is exactly where "the agent passed its own broken work" compounds**: nobody is watching variant #37, and `vidfarm qa` is a static DOM check that never sees a rendered pixel. So add one cheap visual pass over the output — a contact sheet per video, read as an image:
|
|
3051
|
+
|
|
3052
|
+
```bash
|
|
3053
|
+
for MP4 in renders/*.mp4; do
|
|
3054
|
+
S="$(basename "$MP4" .mp4)"; mkdir -p "qa/$S"
|
|
3055
|
+
for t in 0 3 6 9 12 15; do ffmpeg -y -ss $t -i "$MP4" -frames:v 1 "qa/$S/f$(printf %03d $t).png"; done
|
|
3056
|
+
ffmpeg -y -pattern_type glob -i "qa/$S/f*.png" \
|
|
3057
|
+
-vf "scale=320:-1,tile=3x2:margin=6:padding=6:color=0x999999" -frames:v 1 "qa/$S-sheet.png"
|
|
3058
|
+
done
|
|
3059
|
+
```
|
|
3060
|
+
|
|
3061
|
+
Read the sheets. In a batch you're looking for two different things: **per-video** defects (dead regions, a placeholder that reads as a failed render, contradictory numbers, an unlanded CTA, superimposed headlines at a handoff) and **cross-video** drift (variants that no longer look like siblings, or look *too* identical to be N distinct posts). Also compare two frames from different scenes in at least a sample of the renders — a systematic frozen-render bug in the loop will produce N broken files that all pass duration/frame-count checks. Full method: `references/reviewing-renders.md`.
|
|
3062
|
+
|
|
2879
3063
|
### 6. Answer the review items — don't skip this
|
|
2880
3064
|
|
|
2881
3065
|
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.
|
|
@@ -2898,13 +3082,15 @@ The mechanical trio — **generate on a chroma plate → key it out → trim to
|
|
|
2898
3082
|
|
|
2899
3083
|
**Unless the director asks for something else, build every explainer this way. Don't ask, just do it, and mention the defaults once so they can override.** The whole point of the house style is that explainers read as *clean, bright, and easy* — a busy explainer is a failed explainer.
|
|
2900
3084
|
|
|
2901
|
-
- **White background, light mode.** A plain white (or near-white `#FFFFFF`–`#FAFAFA`) stage. No dark mode, no gradients, no photographic backdrop, no texture. Light mode reads cleaner on every feed, keeps cutout stickers legible, and makes flat-vector art look intentional. Set the composition/scene background to white first, before placing anything.
|
|
3085
|
+
- **White background, light mode.** A plain white (or near-white `#FFFFFF`–`#FAFAFA`) stage. No dark mode, no gradients, no photographic backdrop, no texture. Light mode reads cleaner on every feed, keeps cutout stickers legible, and makes flat-vector art look intentional. Set the composition/scene background to white first, before placing anything. **This is the default, not a law** — a director who asks for a dark or photographic stage gets one, but it changes two things mechanically: the stickers need their white die-cut rim stripped, and the caption treatment has to be re-measured. Both are documented in **"Stickers on a DARK or photographic stage"** below.
|
|
2902
3086
|
- **Kinetic captions.** Narration is always captioned word-by-word (`vidfarm captions generate ./work --style word-pop`). Because the stage is white, **override the preset's dark-canvas colors to dark ink on light**:
|
|
2903
3087
|
```
|
|
2904
3088
|
vidfarm captions generate ./work --style word-pop \
|
|
2905
3089
|
--color "#111111" --active-color "#7C3AED" --background-style plain --max-words 4
|
|
2906
3090
|
```
|
|
2907
3091
|
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.
|
|
3092
|
+
|
|
3093
|
+
**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.
|
|
2908
3094
|
- **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.
|
|
2909
3095
|
- **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.
|
|
2910
3096
|
|
|
@@ -2986,6 +3172,48 @@ vidfarm remove-greenscreen ./mascot.mp4 --gif --gif-fps 12 --gif-width 480 # AN
|
|
|
2986
3172
|
|
|
2987
3173
|
GIF alpha is **1-bit** — a pixel is fully opaque or fully gone, so antialiased edges go hard and semi-transparent shadows/glows disappear (`--gif-alpha <0..255>` moves where that line falls). That's the format, not the key. **For anything going onto a composition, prefer PNG/WebP (still) or transparent WebM (clip);** reach for GIF only when the destination demands it.
|
|
2988
3174
|
|
|
3175
|
+
### Stickers on a DARK or photographic stage — strip the white die-cut rim
|
|
3176
|
+
|
|
3177
|
+
The house style above puts stickers on a **white** stage, and on white the thing this section is about is invisible. The moment the stage goes dark, photographic, or coloured, every sticker arrives wearing a **white die-cut rim** — a 4–12px light halo tracing its silhouette — and that halo is the single most obvious "a bot made this" artefact in the frame. The art stops reading as an element in the scene and starts reading as a cutout pasted on top of it.
|
|
3178
|
+
|
|
3179
|
+
**Why the rim is there:** it's a PRINT convention. Real die-cut vinyl needs a white border so the blade has something to cut along, so sticker art is drawn with one, so generators reproduce it. It has no purpose whatsoever in a video composition. **This is a different failure from the `⚠ N% hollow` flag** `sticker-pack` prints — hollow means outline-only art whose interior got keyed away (fix it in the prompt, see above); the rim is extra art that was drawn on purpose and has to be removed after the key.
|
|
3180
|
+
|
|
3181
|
+
**The fix:** delete exactly the band of light pixels **connected to the transparent edge**, by morphological reconstruction inward from the boundary. Interior whites — an eyeball's sclera, a screen highlight, a paper label — are enclosed by linework, so they are not connected to the edge and survive untouched. Local, free, `numpy` + `scipy` + `PIL`:
|
|
3182
|
+
|
|
3183
|
+
```python
|
|
3184
|
+
def strip_rim(img, light=188):
|
|
3185
|
+
a = np.array(img.convert("RGBA")).astype(np.int16)
|
|
3186
|
+
solid = a[..., 3] > 128
|
|
3187
|
+
is_light = (a[..., :3].mean(axis=2) > light) & solid
|
|
3188
|
+
seed = is_light & ndimage.binary_dilation(~solid, iterations=3) # light AND touching transparency
|
|
3189
|
+
if seed.any():
|
|
3190
|
+
rim = ndimage.binary_propagation(seed, mask=is_light) # flood through light only
|
|
3191
|
+
a[..., 3][rim] = 0
|
|
3192
|
+
return Image.fromarray(a.astype(np.uint8), "RGBA")
|
|
3193
|
+
```
|
|
3194
|
+
|
|
3195
|
+
**Test LIGHTNESS, not per-channel whiteness — this is the gotcha that makes the whole thing non-obvious.** The rim is **not white**. It is white **contaminated with the chroma plate**, because the plate fringes into it during keying. Measured on a magenta-plate pack, the outermost solid ring averaged **R≈250, G≈205, B≈250** — the green channel is nowhere near white. A per-channel test like `(rgb > 224).all(axis=2)` therefore misses the rim on every sticker whose edge is even slightly anti-aliased: in testing it stripped **1 of 4** stickers, and because the one it did strip looked *different* from its three siblings, the result read as broken art rather than as a bad threshold. `rgb.mean(axis=2) > ~188` catches all four.
|
|
3196
|
+
|
|
3197
|
+
**Then clean up what stripping leaves behind.** Removing the rim produces two artefacts, and both read to a viewer as "the sticker didn't mask properly":
|
|
3198
|
+
|
|
3199
|
+
1. **A dotted halo** — surviving specks along the old rim edge. Measured **213** and **164** stray connected components of 3–20px each on two different stickers of the same pack.
|
|
3200
|
+
2. **A bright blob** — a large uniform light region the art *enclosed* is no longer visually held in by the rim. Measured at 7,983px (a ring's centre) and 9,582px (a stamp's paper plate).
|
|
3201
|
+
|
|
3202
|
+
```python
|
|
3203
|
+
def clean(img, speck=0.008, blob=0.015, light=200):
|
|
3204
|
+
# 1. drop connected components smaller than `speck` of the largest
|
|
3205
|
+
# 2. for each enclosed light region larger than `blob` of the sticker area:
|
|
3206
|
+
# holes = binary_fill_holes(m) & ~m
|
|
3207
|
+
# if holes.sum() < m.sum() * 0.02: # solid fill, no detail -> it is background
|
|
3208
|
+
# set alpha 0 there
|
|
3209
|
+
```
|
|
3210
|
+
|
|
3211
|
+
**The discriminator is worth remembering on its own: an enclosed light region is only background if it has NO internal detail, and its holes are the giveaway.** An eyeball's sclera is riddled with drawn veins and a pupil → many holes → keep it. A ring's centre is a flat fill → no holes → punch it transparent. Area alone gets this wrong in both directions.
|
|
3212
|
+
|
|
3213
|
+
**When you recolour a pack to a palette** (a duotone or luminance ramp onto a brand accent), **cap the top of the ramp** — e.g. `accent + 0.66·(white − accent)` — so interior whites land as a light tint instead of glaring pure white against a flat two-colour design. Uncapped, every kept interior white becomes the brightest pixel in the frame, which undoes the point of the ramp.
|
|
3214
|
+
|
|
3215
|
+
> **Known gap:** none of this is in the CLI. `vidfarm sticker-pack` / `vidfarm cutout` have no `--strip-rim` (or equivalent) flag today, so on a dark stage you run the two passes above yourself as a local post-step. A `--strip-rim` flag on both commands, defaulting off, is the right long-term home for it.
|
|
3216
|
+
|
|
2989
3217
|
### The guided sequence (prompt harness)
|
|
2990
3218
|
|
|
2991
3219
|
**Step 0 — Decide the cast of stickers.** With the director, list every element the explainer needs as its own cutout: the hero subject, each labelled prop, each icon/arrow/emoji, any mascot, any full-frame backdrop. Each becomes one transparent PNG. Stickers are reusable — generate once, reuse across scenes. **If the cast is more than two or three items, make it a PACK** (one sheet, split locally — see "A sticker pack" above) rather than N separate `cutout` calls.
|
|
@@ -3020,7 +3248,7 @@ GIF alpha is **1-bit** — a pixel is fully opaque or fully gone, so antialiased
|
|
|
3020
3248
|
|
|
3021
3249
|
**Both are image-only.** A moving subject has no single bounding box — matte a video clip with `vidfarm remove-background <video>` or key a flat backdrop with `vidfarm remove-greenscreen <video>` (→ transparent WebM/mov).
|
|
3022
3250
|
|
|
3023
|
-
**Step 2 — Show the director each cutout, get corrections.** Cutouts are cheap to regenerate. Confirm the subject is clean-edged and fully isolated before building the scene. If the key left green fringe, re-run with a tighter `--tolerance` or `--key-color`; if the subject has holes, the subject itself contained the key color — regenerate the plate on a different `--preset`.
|
|
3251
|
+
**Step 2 — Show the director each cutout, get corrections.** Cutouts are cheap to regenerate. Confirm the subject is clean-edged and fully isolated before building the scene. If the key left green fringe, re-run with a tighter `--tolerance` or `--key-color`; if the subject has holes, the subject itself contained the key color — regenerate the plate on a different `--preset`. **If the stage isn't white, strip the white die-cut rim here**, before anything is staged — see "Stickers on a DARK or photographic stage" above.
|
|
3024
3252
|
|
|
3025
3253
|
**Step 3 — Stage them on the composition.** Fork/seed a working composition (`vidfarm pull` or `vidfarm serve`), **set the stage to a white light-mode background first** (house style), then drop each cutout as an **image layer**, sized and positioned deliberately:
|
|
3026
3254
|
```
|
package/SKILL.md
CHANGED
|
@@ -118,6 +118,7 @@ For composition *authoring* craft (motion, keyframes, scene design), Vidfarm shi
|
|
|
118
118
|
- **Structure before polish — the four charges, written before the timeline.** 🪝 **Hook**: the first line is a complete clause naming a *situation*, not a label; on screen at `start:0` (chunk 1 is read before any audio). 🔄 **Loop**: one open question by 0:10, stated on screen, **closing inside this video**, with an answer the viewer can't guess. 😍 **Payoff**: shown, not summarized, before the final beat. 🎣 **Bait**: one ask, final beat + post caption. Banned openers: throat-clearing, a logo, a title card, a fade from black. Full harness: `references/hooks-and-virality.md`; checkable form: `vidfarm regime show hooks`.
|
|
119
119
|
- **The first frame is the thumbnail.** Frame 0 is one frame of ~30 in the first second, but it's the poster every feed and share link freezes on — so it's seen by more people than the video is. Never open on black, an empty frame, or a fade-up: a real visual at `start:0`, the hook words already up, and no *entrance* transition on the first clip (junction transitions between later clips are fine). Check it with `vidfarm stills <dir> --at 0`.
|
|
120
120
|
- **On devcli, `vidfarm qa <dir>` before every render.** Free, instant, local-only blocklist for the slop above + the first frame + the font regime. Feedback, not a gate (exits 0, never automatic). No REST/web equivalent.
|
|
121
|
+
- **Review the finished video as a WHOLE, and never by one frame.** Agents build scene by scene, so every scene passes alone while the video drifts as a sequence — shifting margins, three type sizes, a wandering accent colour, N identically-long beats, a jarring join, a dead band under top-anchored content. Tile ~12 stills into one contact sheet — one command, `vidfarm stills <dir> --sheet` — and **read it as an image**: balance, spacing, one type scale, one palette, deliberate pacing, clean joins — then fix the *system*, not the one odd scene. Compare frames from two different scenes (a frozen render passes duration, frame-count and audio-hash checks), verify audio by measurement (~12–15 dB speech-over-bed, peak <0 dBFS) rather than "sounds fine", and report what you measured separately from what you judged. Your own "verified, looks good" is the least reliable signal here — it was wrong on every video of a 32-video batch. Method: `references/reviewing-renders.md`.
|
|
121
122
|
- **Ask about deduplication before you publish or bulk-produce.** "Is this going out more than once — several accounts, another platform, a re-post later? How many copies?" Ask *before* the render or the batch, not after: dedupe is a post-render ffmpeg pass, so answering early keeps it at **render once → dedupe N** instead of paying for a second render per slot. Then post each variant to a **different** account — two accounts posting the same variant defeats the point.
|
|
122
123
|
- **Ask one-time vs bulk before you build.** Volume = **scripting mode**: a pinned base fork, a loop varying ONE thing, and a **`QA_REGIME.md`** — the director's own written standard, because a fifty-video loop has no human watching every frame. `vidfarm regime init short-form --out ./work/QA_REGIME.md` (bases: `short-form`, `hooks`, `ugc-testimonial`, `explainer`, `product-demo`), then `vidfarm qa ./work --regime <name|path>` — stackable, any user file valid, auto-discovered from the work dir. Its `checks:` are machine-settled; its `- [ ]` items come back for **you** to answer honestly.
|
|
123
124
|
- **Caption regime is mandatory**: an imported display font (Montserrat default / TikTok Sans), weight 700–900, ~36–64px on a 1080-wide frame, inside the 8%–85% safe zone, and exactly one of four backgrounds — `outline`, `plain`, an active-word `spotlight`/`karaoke` pill, or a tight-hugging `highlight-solid` band (radius ≤8px, no border/shadow/gradient/blur).
|
package/dist/src/cli.js
CHANGED
|
@@ -736,6 +736,14 @@ Local media engines & toolchain (all local, free, no account — no cloud key ne
|
|
|
736
736
|
("did my edit look right") without a full render
|
|
737
737
|
--at 0,2.5,7 Timestamps (default: midpoint of each scene clip, cap 8)
|
|
738
738
|
--out <dir> Output dir (default <dir>/stills)
|
|
739
|
+
--sheet ALSO tile them into one contact sheet PNG — the
|
|
740
|
+
whole-video review pass. Read it as ONE image:
|
|
741
|
+
scene-by-scene building drifts (uneven margins,
|
|
742
|
+
3 type sizes, a wandering accent colour, N equal
|
|
743
|
+
beats, a jarring join) and only a side-by-side
|
|
744
|
+
sheet shows it
|
|
745
|
+
--sheet-out <file> Sheet path (default <out>/contact-sheet.png)
|
|
746
|
+
--sheet-width <px> Per-tile width in the sheet (default 320)
|
|
739
747
|
doctor Health-check the local toolchain: node, (local)
|
|
740
748
|
ffmpeg, hyperframes engines, Chrome, API key
|
|
741
749
|
(whoami), provider keys, agent CLI, poisoned
|
|
@@ -2390,6 +2398,7 @@ Rules:
|
|
|
2390
2398
|
- NO HTML SLOP. You are editing HTML, but the output is a social video, not a web page. THE TEST IS THE NATIVE-EDITOR TEST: could you have made this element with the tools inside TikTok's own editor? That toolset is a font, a color, a stroke/outline, a soft shadow, a tight text box, alignment, opacity, rotation, animation presets — plus stickers, emoji, drawn marks and clips. It has NO padded capsule, NO border, NO gradient fill, NO blur panel, NO card. If you reached past it, cut it. Never author landing-page furniture: CTA "buttons" (a filled/gradient rounded capsule with action copy like "Sign Up for a Free Trial →"), benefit chip/badge rows ("✓ No Credit Card Needed"), bordered/shadowed/frosted cards holding a headline + URL, gradient text fills, feature grids, bulleted lists, or web-default fonts (Inter/Roboto/Arial/system-ui). AND NOT A SINGLE PILL EITHER: one lonely rounded, padded, filled capsule around a static stat or label — "10 hrs / week", "STEP 2", "EP.01", "+40%" — is a web badge, and being the only one on screen does not make it native. The ONLY legitimate capsule in a video is the active-word spotlight/karaoke caption highlight, because it moves with the spoken word. Emphasize a stat the way the editor would: bigger, heavier, ALL-CAPS, an accent color, a hand-drawn circle or underline, or its own beat on screen. Rule of thumb on anything holding words: border-radius over ~8px PLUS a background fill PLUS padding = a badge; drop the fill or drop the radius until the band hugs the glyphs. None of this appears in a real TikTok, and nothing in a video is clickable — say it as timed text on the footage instead. Arrows, scribble/underline marks, italics, ALL-CAPS, single-word color pops, emoji, transparent cut-out stickers, and mock social UI (iMessage bubbles, comment cards) are all fine. Captions use an imported family (Montserrat default / TikTok Sans / Abel / Source Code Pro / Yesteryear) at weight 700-900, ~36-64px on a 1080-wide frame, inside the 8%-85% safe zone, with exactly one of four backgrounds: outline, plain, an active-word spotlight/karaoke pill, or a tight-hugging solid band (radius <=8px, no border/shadow/gradient/blur).
|
|
2391
2399
|
- STRUCTURE BEFORE POLISH — THE FOUR CHARGES, WRITTEN BEFORE YOU TOUCH THE TIMELINE. Most agent-made videos fail on structure, not polish, because the timeline is the fun part so it gets built first and the words get retrofitted. Invert it: (1) HOOK — write the opening line as text first: a complete clause (subject + verb), no jargon, naming a SITUATION ("I've quit six businesses") not a label ("anonymity"); it goes on screen at start:0, because caption chunk 1 is read before any audio and muted autoplay is the default. Banned openings: throat-clearing ("so I was thinking", "here's the thing"), a logo, a title card, a fade from black, context before the claim. (2) LOOP — one open question by 0:10, said ON SCREEN, closing INSIDE this video (state the timestamp it closes at; if you can't, there is no loop), and the withheld answer must be one the viewer CANNOT supply themselves — a formally-correct loop with a guessable answer passes every mechanical check and dies in the field. (3) PAYOFF — shown, not summarized, ≥5 uninterrupted seconds, landing BEFORE the final beat; the payoff is not the CTA. (4) BAIT — one ask in the final beat and in the post caption; never a DM funnel, "follow for part two", or ragebait. Then build the timeline. Re-theming a decomposed template: viral_dna already names the source's hook/retention/payoff — rebuild each charge for the new subject, never flatten the loop into a product statement. Full craft harness: the vidfarm skill's references/hooks-and-virality.md. Checkable form: \`vidfarm regime show hooks\`.
|
|
2392
2400
|
- THE FIRST FRAME IS THE THUMBNAIL. Frame 0 is one frame of ~30 in the first second, but every feed card, share link, and paused player freezes on it — more people see that frame than watch the video. It must never be black, empty, mid-fade, or mid-animation: a real visual at start:0 (\`vidfarm retime . --layer <key> --start 0\`), the hook words already on screen at t=0, and NO entrance transition on the FIRST clip (\`vidfarm transitions set . --layer <key> --in none\`; junction transitions between later clips are fine). Look at the actual pixels before you render: \`vidfarm stills . --at 0\`.
|
|
2401
|
+
- REVIEW THE WHOLE VIDEO AS ONE OBJECT — AND NEVER JUDGE IT BY ONE FRAME. Assume your own finished video has a defect you cannot see: across a 32-video batch, EVERY first-pass video had a real defect its own author had already reported as "verified, looks good". The cause is structural — you build scene by scene, each scene correct while it is the whole world, so every scene passes alone and the video fails as a SEQUENCE: margins shift between beats, headline sizes drift, the accent color wanders, one asset is flat vector and the next is photographic, every beat is the same length, a join lands like a slap. Nobody watches a scene; they watch the sequence. So before you call anything done, tile ~12 stills into ONE contact sheet and READ IT AS AN IMAGE — one command: \`vidfarm stills . --sheet\` (add \`--at 0,2,4,…\` to pick timestamps; writes stills/contact-sheet.png). Check: visual balance (no dead band under top-anchored content), consistent spacing/margins, ONE type scale, ONE accent color, ONE illustration style, deliberate pacing rather than N identical beats, nothing jarring at the joins, no frame where two elements compete for the eye — and the summary question, does it look like one person made it in one sitting? Fix drift by defining the SYSTEM (type scale, margin, palette, default beat) and applying it to every scene, not by patching the one scene that stood out. The defects that actually ship, in observed frequency order: large flat dead regions · a placeholder empty state that reads as a failed render · two contradictory numbers in one frame · a CTA still building at the last frame (settle it >=2s before the end) · two headlines superimposed at a scene handoff (exit at nextIn-0.18, duration 0.24, ease power2.out) · type colliding with a busy background exactly as it is spoken. If a frame looks empty, sample 0.2s apart to see whether it RESTS there — a transient wipe frame is fine, >0.5s is a hole. AND ALWAYS COMPARE TWO FRAMES FROM DIFFERENT SCENES: a frozen render (an overlay/watermark pass missing \`-loop 1\`, or assets outside the composition root so the timeline never runs) makes every frame identical while duration, frame count and audio hash all still pass, and frame 0 looks perfect. Verify audio by MEASUREMENT, never "it sounds fine" (you cannot hear it): ~12-15 dB speech-over-bed separation across the actual word spans, peak <0 dBFS. Never \`adelay\` the VO — whisper word timings and every caption built from them are relative to the raw vo.wav; use apad+atrim. Report what you MEASURED separately from what you JUDGED. Full method: the vidfarm skill's references/reviewing-renders.md.
|
|
2393
2402
|
- ONE-TIME OR BULK? Ask before you build. If the director wants volume (daily posting, N variants, hook tests), that's SCRIPTING MODE: pin this fork as the base, vary exactly ONE thing per variant, and install a QA_REGIME.md — \`vidfarm regime init short-form --out ./QA_REGIME.md\` (bases: short-form, hooks, ugc-testimonial, explainer, product-demo), then EDIT it with them. It is their own written quality standard, and it exists because nobody watches variant #37 as carefully as #1. \`vidfarm qa .\` picks up ./QA_REGIME.md automatically; \`--regime <name|path>\` adds more (they stack, and any file of theirs anywhere is valid). Its \`checks:\` are machine-settled; its \`- [ ]\` items come back for YOU to answer honestly in your report — never claim a pass on the half the CLI can't judge. When a batch teaches you something, write it back into the regime.
|
|
2394
2403
|
- DEDUPLICATE BEFORE YOU PUBLISH — AND ASK FIRST. Social platforms fingerprint every upload, so the same render posted twice (a second account, another platform, a re-post next month) gets the later copy suppressed as duplicate/reused content. BEFORE you render for publication, and before any bulk run, ASK the director: "do you want deduplicated copies for posting, and how many?" Ask THEN, not after — dedupe is a post-render ffmpeg pass, so the correct order is RENDER ONCE → DEDUPE N, and deciding late means paying for a second render. Run it on the EXPORTED file: \`vidfarm dedupe ./final.mp4\` (one copy) or \`vidfarm dedupe ./final.mp4 --variants N --seed <slug> --out-dir ./posts\` (N copies, one per account/slot). Free, offline, no wallet — it never re-renders the composition. The default \`standard\` preset is skew 2%, zoom 3%, rotate 2°, speed +2%, saturation +4%, plus contrast/brightness/hue/grain, a container-metadata strip and a per-variant CRF walk; invisible to a viewer, and each variant differs from the original AND from its siblings. Post each variant to a DIFFERENT account — two accounts posting the same variant defeats the point. A rotate forces a bigger centre-crop to hide the black corners (~6.7% on a tall frame at 2°) and the CLI says so; pass \`--rotate 0\` when framing matters more. Cloud twin: \`POST /api/v1/primitives/media/dedupe\`.
|
|
2395
2404
|
- QA EVERY VIDEO BEFORE YOU RENDER: run \`vidfarm qa .\` in this directory. It's free, instant, and local — a blocklist for the slop above plus the first frame, the font regime, and the safe zone, with a concrete fix per finding. It's feedback, not a gate (exits 0 even on findings, never runs automatically) and a blocklist, not an allowlist, so stylized or hand-made work passes untouched. Fix what's real, ignore what's a deliberate style call. \`--json\` for scripted batches.
|
|
@@ -10264,20 +10273,32 @@ async function runStillsCommand(argv) {
|
|
|
10264
10273
|
options: {
|
|
10265
10274
|
at: { type: "string" },
|
|
10266
10275
|
out: { type: "string" },
|
|
10276
|
+
sheet: { type: "boolean", default: false },
|
|
10277
|
+
"sheet-out": { type: "string" },
|
|
10278
|
+
"sheet-width": { type: "string" },
|
|
10267
10279
|
json: { type: "boolean", default: false }
|
|
10268
10280
|
}
|
|
10269
10281
|
});
|
|
10270
10282
|
const target = parsed.positionals[0];
|
|
10271
10283
|
if (!target)
|
|
10272
|
-
throw new Error("stills requires a composition path: `vidfarm stills <dir-or-composition.html> [--at 0,2.5,7] [--out <dir>]`.");
|
|
10284
|
+
throw new Error("stills requires a composition path: `vidfarm stills <dir-or-composition.html> [--at 0,2.5,7] [--sheet] [--out <dir>]`.");
|
|
10273
10285
|
const htmlPath = resolveCompositionHtmlPath(target);
|
|
10274
10286
|
const json = Boolean(parsed.values.json);
|
|
10275
10287
|
const at = parsed.values.at
|
|
10276
10288
|
? String(parsed.values.at).split(",").map((value) => parseTimeToSeconds(value.trim())).filter((value) => Number.isFinite(value))
|
|
10277
10289
|
: undefined;
|
|
10290
|
+
const sheetWidthRaw = parsed.values["sheet-width"] ? Number.parseInt(String(parsed.values["sheet-width"]), 10) : undefined;
|
|
10278
10291
|
if (!json)
|
|
10279
10292
|
console.log(`${DIM}Rendering stills in-process (free, local Chrome capture)…${RESET}`);
|
|
10280
|
-
const result = await renderCompositionStills({
|
|
10293
|
+
const result = await renderCompositionStills({
|
|
10294
|
+
htmlPath,
|
|
10295
|
+
at,
|
|
10296
|
+
outDir: parsed.values.out,
|
|
10297
|
+
quiet: json,
|
|
10298
|
+
sheet: Boolean(parsed.values.sheet),
|
|
10299
|
+
sheetPath: parsed.values["sheet-out"],
|
|
10300
|
+
...(Number.isFinite(sheetWidthRaw) ? { sheetTileWidth: sheetWidthRaw } : {})
|
|
10301
|
+
});
|
|
10281
10302
|
if (json) {
|
|
10282
10303
|
printJson(result);
|
|
10283
10304
|
return;
|
|
@@ -10286,6 +10307,10 @@ async function runStillsCommand(argv) {
|
|
|
10286
10307
|
console.log(` ${GREEN}${still.path}${RESET} ${DIM}(requested ${still.requested_sec}s → captured ${still.captured_sec}s)${RESET}`);
|
|
10287
10308
|
}
|
|
10288
10309
|
console.log(`${DIM}${result.stills.length} still(s) at ${result.grid_fps}fps grid accuracy — open them to verify the edit before rendering.${RESET}`);
|
|
10310
|
+
if (result.sheet) {
|
|
10311
|
+
console.log(` ${GREEN}${result.sheet}${RESET} ${DIM}(contact sheet)${RESET}`);
|
|
10312
|
+
console.log(`${DIM}READ the contact sheet as ONE image — that is how you catch what per-scene checks miss: uneven margins, a wandering type scale or accent colour, N identically-long beats, a jarring join, dead space under top-anchored content. Fix drift by defining the system, not by patching the one odd scene.${RESET}`);
|
|
10313
|
+
}
|
|
10289
10314
|
}
|
|
10290
10315
|
// ── Agent skill ───────────────────────────────────────────────────────────────
|
|
10291
10316
|
// Install the latest director skill onto disk as a Claude Code / agent skill so
|
|
@@ -17,10 +17,12 @@
|
|
|
17
17
|
// to 2/1fps for long timelines) instead of the composition's native 30fps.
|
|
18
18
|
// Requested timestamps are snapped to that grid; the actually-captured time
|
|
19
19
|
// is reported next to each written PNG.
|
|
20
|
+
import { spawn } from "node:child_process";
|
|
20
21
|
import { copyFileSync, cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
21
22
|
import os from "node:os";
|
|
22
23
|
import path from "node:path";
|
|
23
24
|
import { parseHTML } from "linkedom";
|
|
25
|
+
import { resolveFfmpeg } from "../services/clip-curation/ffmpeg.js";
|
|
24
26
|
import { inspectComposition } from "./composition-edit.js";
|
|
25
27
|
const MAX_DEFAULT_STILLS = 8;
|
|
26
28
|
const MAX_GRID_FRAMES = 600;
|
|
@@ -143,6 +145,62 @@ function stageCompositionProject(htmlPath, stagedHtml) {
|
|
|
143
145
|
}
|
|
144
146
|
return projectDir;
|
|
145
147
|
}
|
|
148
|
+
/**
|
|
149
|
+
* Choose a near-square tile grid for N frames, biased to a wider-than-tall
|
|
150
|
+
* sheet (frames are usually portrait, so extra columns read better than extra
|
|
151
|
+
* rows on a screen).
|
|
152
|
+
*/
|
|
153
|
+
export function pickTileGrid(count) {
|
|
154
|
+
const cols = Math.max(1, Math.ceil(Math.sqrt(count)));
|
|
155
|
+
return { cols, rows: Math.max(1, Math.ceil(count / cols)) };
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Tile the rendered stills into ONE contact sheet.
|
|
159
|
+
*
|
|
160
|
+
* This exists because reviewing a video frame-by-frame does not surface the
|
|
161
|
+
* defects that actually ship. Sequence-level drift — margins that shift scene
|
|
162
|
+
* to scene, three headline sizes, a wandering accent colour, N identical beat
|
|
163
|
+
* lengths, a jarring join — is only visible when the frames sit side by side,
|
|
164
|
+
* and an agent reading one tiled image sees it immediately while N separate
|
|
165
|
+
* image reads bury it. See the vidfarm skill's references/reviewing-renders.md.
|
|
166
|
+
*
|
|
167
|
+
* Frames are copied to sequential names first: globbing the output dir would
|
|
168
|
+
* pick up stills left over from a previous run at different timestamps.
|
|
169
|
+
*/
|
|
170
|
+
async function writeContactSheet(stills, outPath, tileWidth) {
|
|
171
|
+
const ffmpeg = await resolveFfmpeg();
|
|
172
|
+
const stageDir = mkdtempSync(path.join(os.tmpdir(), "vidfarm-sheet-"));
|
|
173
|
+
try {
|
|
174
|
+
stills.forEach((still, index) => {
|
|
175
|
+
copyFileSync(still.path, path.join(stageDir, `f${String(index + 1).padStart(4, "0")}.png`));
|
|
176
|
+
});
|
|
177
|
+
const { cols, rows } = pickTileGrid(stills.length);
|
|
178
|
+
mkdirSync(path.dirname(outPath), { recursive: true });
|
|
179
|
+
const args = [
|
|
180
|
+
"-hide_banner", "-v", "error", "-y",
|
|
181
|
+
"-i", path.join(stageDir, "f%04d.png"),
|
|
182
|
+
"-vf", `scale=${tileWidth}:-2,tile=${cols}x${rows}:margin=8:padding=8:color=0x999999`,
|
|
183
|
+
"-frames:v", "1",
|
|
184
|
+
outPath
|
|
185
|
+
];
|
|
186
|
+
await new Promise((resolve, reject) => {
|
|
187
|
+
const child = spawn(ffmpeg, args, { stdio: ["ignore", "ignore", "pipe"] });
|
|
188
|
+
let stderr = "";
|
|
189
|
+
child.stderr?.on("data", (chunk) => { stderr += String(chunk); });
|
|
190
|
+
child.on("error", reject);
|
|
191
|
+
child.on("close", (code) => {
|
|
192
|
+
if (code === 0)
|
|
193
|
+
resolve();
|
|
194
|
+
else
|
|
195
|
+
reject(new Error(`ffmpeg tile failed (exit ${code})${stderr.trim() ? `: ${stderr.trim()}` : ""}`));
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
return outPath;
|
|
199
|
+
}
|
|
200
|
+
finally {
|
|
201
|
+
rmSync(stageDir, { recursive: true, force: true });
|
|
202
|
+
}
|
|
203
|
+
}
|
|
146
204
|
export async function renderCompositionStills(input) {
|
|
147
205
|
const html = readFileSync(input.htmlPath, "utf8");
|
|
148
206
|
if (!html.includes("data-composition-id=")) {
|
|
@@ -230,13 +288,19 @@ export async function renderCompositionStills(input) {
|
|
|
230
288
|
copyFileSync(path.join(framesDir, frameFiles[frameIndex]), dest);
|
|
231
289
|
stills.push({ requested_sec: Number(t.toFixed(3)), captured_sec: capturedSec, path: dest });
|
|
232
290
|
}
|
|
291
|
+
let sheet;
|
|
292
|
+
if (input.sheet && stills.length > 0) {
|
|
293
|
+
const sheetPath = path.resolve(input.sheetPath ?? path.join(outDir, "contact-sheet.png"));
|
|
294
|
+
sheet = await writeContactSheet(stills, sheetPath, Math.max(80, Math.round(input.sheetTileWidth ?? 320)));
|
|
295
|
+
}
|
|
233
296
|
return {
|
|
234
297
|
ok: true,
|
|
235
298
|
composition: input.htmlPath,
|
|
236
299
|
out_dir: outDir,
|
|
237
300
|
grid_fps: gridFps,
|
|
238
301
|
duration_seconds: durationSeconds,
|
|
239
|
-
stills
|
|
302
|
+
stills,
|
|
303
|
+
...(sheet ? { sheet } : {})
|
|
240
304
|
};
|
|
241
305
|
}
|
|
242
306
|
finally {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@officexapp/vidfarm-devcli",
|
|
3
|
-
"version": "0.21.
|
|
3
|
+
"version": "0.21.34",
|
|
4
4
|
"description": "Local bridge for the Vidfarm Trackpad Editor. `vidfarm serve <template_id>` boots the FULL editor on localhost (disk-backed records/storage, free in-process render); edit composition.html on disk (Claude Code, Codex, etc.) and the browser live-morphs it.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|