@officexapp/vidfarm-devcli 0.21.56 → 0.21.57
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 +26 -2
- package/.agents/skills/vidfarm/harnesses/README.md +1 -0
- package/.agents/skills/vidfarm/references/automation-and-local-dev.md +6 -1
- package/.agents/skills/vidfarm/references/reviewing-renders.md +11 -2
- package/SKILL.director.md +43 -5
- package/SKILL.md +5 -2
- package/dist/src/cli.js +96 -1
- package/dist/src/devcli/local-frontend-server.js +10 -2
- package/dist/src/devcli/local-render.js +28 -2
- package/dist/src/devcli/qa-check.js +27 -1
- package/dist/src/lib/engine-globals.js +138 -0
- package/dist/src/lib/frozen-render.js +130 -0
- package/dist/src/services/composition-lint.js +16 -0
- package/experimental/engaging-chat-convo.md +1370 -0
- package/package.json +2 -1
- package/update.md +15 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: vidfarm
|
|
3
|
-
skill_version: 0.21.
|
|
3
|
+
skill_version: 0.21.57
|
|
4
4
|
description: Use Vidfarm as a director. Run a strategy **consultation** (the `brainstorm/*` chain — cold-start interview, awareness stages, persuasive angles, hooks, product placement). Answer "give me content ideas" / "what should I post" / "I need 30 videos this month" from the bundled idea banks (50 content frames x 5 awareness stages x 44 problem angles). Browse/add inspiration videos, browse the free public raws catalog BY CATEGORY (curated shelves like scroll-stoppers/greenscreen/reaction — the cheapest way to source footage for one video, and a ready-made clip pool for bulk scripting N variants), fork a template into a composition, edit it in the Trackpad Editor (timeline-based like Premiere/DaVinci), auto-decompose source video into scenes, render to MP4, approve into a shareable post, and schedule it. Includes login, provider keys, discovery, versioning, uploads/downloads, and billing. Every step is available as raw REST; `vidfarm-devcli` wraps those routes and composes the file-backed scripting flows.
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -397,6 +397,27 @@ The mechanism is deterministic, not luck: rendering is seek-safe, so frame 0 sho
|
|
|
397
397
|
|
|
398
398
|
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`.
|
|
399
399
|
|
|
400
|
+
## Reserved globals — the engine owns them, and assigning one ships a still image
|
|
401
|
+
|
|
402
|
+
**Never assign `window.__player`, `window.__hf`, `window.__playerReady`, `window.__renderReady`, or `window.__hyperframes` from composition code.** The render engine installs all of them. A composition that overwrites one destroys the frame-capture bridge, and the failure is invisible: the renderer stalls 45 seconds, downgrades the error to a warning, exits **0**, and writes an MP4 with the **correct duration, correct frame count and correct audio, in which nothing moves.** Four paid deliverables shipped that way in one night before anyone looked at the pixels.
|
|
403
|
+
|
|
404
|
+
**Motion has exactly one mechanism — you register a paused timeline and the engine SEEKS it:**
|
|
405
|
+
|
|
406
|
+
```html
|
|
407
|
+
<script>
|
|
408
|
+
window.__timelines = window.__timelines || {}; // this line is the ONLY __ global you write
|
|
409
|
+
var tl = gsap.timeline({ paused: true }); // paused, built synchronously
|
|
410
|
+
tl.to('#box', { x: 880, duration: 4, ease: 'none' }, 0);
|
|
411
|
+
window.__timelines['main'] = tl; // key === the root's data-composition-id, exactly
|
|
412
|
+
</script>
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
- **Need imperative per-frame drawing** (canvas, a custom renderer)? Do *not* invent a seek API. Register a paused **driver** timeline and hang your draw call off its `onUpdate`: `gsap.timeline({ paused: true, onUpdate: function () { draw(driver.time()); } })`, then give it a dummy tween of the full duration. The engine seeks the driver; `onUpdate` fires at the seeked time.
|
|
416
|
+
- **No timeline at all** is the same failure with a different cause. A composition that builds a GSAP/anime timeline and never registers it renders static too.
|
|
417
|
+
- **`window.__timelines` is the one exception** — writing it is required. Nothing else in that namespace is yours.
|
|
418
|
+
|
|
419
|
+
**Three tools enforce this, so a mistake costs seconds instead of a night:** `vidfarm lint <dir>` and `vidfarm qa <dir>` both report `engine_owned_global_assigned` as an **error**, `vidfarm render --target local` **refuses to start**, and every local render ends with an automatic **`vidfarm motion-check`** that fails the render when the finished MP4 never moves (`--allow-static` for a title card that holds still on purpose). Run `vidfarm motion-check <file.mp4>` by hand on anything those did not produce — a cloud render, a watermarked master, an ffmpeg re-encode.
|
|
420
|
+
|
|
400
421
|
## Judge the WHOLE video, not the parts you built — and never by one frame
|
|
401
422
|
|
|
402
423
|
**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.
|
|
@@ -405,7 +426,7 @@ Full mechanics and editor verbs: `references/editor-workflows.md` (“The openin
|
|
|
405
426
|
|
|
406
427
|
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.**
|
|
407
428
|
|
|
408
|
-
**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. **
|
|
429
|
+
**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 assigned engine-owned global, 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. **Settle it with `vidfarm motion-check <file.mp4>`, then compare two frames from different scenes by eye; they must differ a lot.** Likewise, verify audio by measurement, never "it sounds good" — you can't hear it.
|
|
409
430
|
|
|
410
431
|
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`**.
|
|
411
432
|
|
|
@@ -510,6 +531,7 @@ Why they exist: the prompts you find on the `/discover` pages are tuned to **one
|
|
|
510
531
|
| `https://vidfarm.cc/experimental/ugc-reaction-greenscreen.md` | Sell an app with three streams cut against each other: `ugc-reaction` raws, a Display Greenscreen device whose screen carries the customer's real demo, and the demo itself. One actor across every beat via `actor_<uuid>`, a per-frame tracked screen insert, captions inside the platform-chrome-safe core, and two exports from one render (voiceover-only to publish, voiceover+music to review) |
|
|
511
532
|
| `https://vidfarm.cc/experimental/sticker-slideshow-tips.md` | A tips **carousel** — the deliverable is N still slides, and the 3.0s-per-slide MP4 is only the playable preview. Die-cut cutouts on a paper page or a photo background, a literal "Tips for…" cover, the specificity ladder, one slide shilled from the middle and written so it survives deleting the brand name, four background modes, and a measured WCAG contrast gate on the exported PNGs |
|
|
512
533
|
| `https://vidfarm.cc/experimental/wall-text-pov-ugc.md` | One unbroken ambient take + one static block of unplated type. No cuts, no voiceover, no subtitles, nothing animated. The retention engine is arithmetic — `duration = words / 8`, so one play lands the viewer at the **halfway mark**, committed and one pass from done — floored at 8s so a trending sound gets a real phrase of a track. The four speaker frames, MIRROR vs TURN, a density pass that treats padding as the fatal failure, casting the scene dark so the type needs no plate (measured), ping-ponging the plate for a seamless loop, and three gates. Runs at $0 |
|
|
534
|
+
| `https://vidfarm.cc/experimental/engaging-chat-convo.md` | A chat-thread **carousel** — N still screenshots of ONE text conversation at 3.0s each; the slides are the deliverable and the MP4 is only the preview. Pure type on pure black with **no phone chrome at all** (no status bar, avatar, header, timestamps or keyboard), because a crop containing only bubbles cannot be *wrong* about a detail — and one wrong detail makes a reader re-read the whole thread as fake. The engine is two voices that stay separable with the **colour removed**; `>>>` skip cards buy time jumps for free; the story opens mid-mistake, reveals something in the middle that recasts what came before, breaks one character's register in the last act, and ends mid-action with no resolution, because the bait is the missing ending. The offer is named **once**, lowercase, mid-bubble, as a plot fact a character mentions about their own life while the other reacts sceptically. Ships a voice-drift detector that names the slide where the two voices merge. Runs at $0 |
|
|
513
535
|
| `https://vidfarm.cc/experimental/animated-sticker-story.md` | A narrated **paper puppet theater** — one full-bleed parchment stage that never cuts, a cast of die-cut stickers, and every element moved by ONE paused GSAP timeline (MotionPathPlugin for walks; paths in absolute canvas coordinates, never `align:"self"`). The three-node rig, the seven moves (ENTER / WALK / CROWD / BEAT / STAMP / DRAW / CAMERA), buying SHEETS rather than stickers so one art class survives, generating art without shadows and adding one CSS drop-shadow, kinetic captions that animate **colour only** on whisper word timings, a two-pass build that MEASURES where the drawing is quietest before placing any type, a feathered paper wash that is not a plate, and the offer named once as a wordmark on the last beat. Desktop-only. ~$0.25 in `hybrid`, $0 in `minimize` |
|
|
514
536
|
|
|
515
537
|
Fetch one as plain markdown and follow it end to end; do not skim it into a summary.
|
|
@@ -562,6 +584,8 @@ The File Index above says what each file *is*; this says which one a given ask m
|
|
|
562
584
|
- Treat `forkId` as an unguessable bearer token for read access.
|
|
563
585
|
- Submission routes are generally not idempotent. Especially for renders and expensive primitives, check status before retrying.
|
|
564
586
|
- In the web editor, use CSS/declarative motion only. Script-bearing HTML is stripped or rejected there.
|
|
587
|
+
- **Never assign `window.__player` / `window.__hf` / `window.__playerReady` / `window.__renderReady` / `window.__hyperframes`.** The engine owns them; assigning one writes a correct-length MP4 of a still image and exits 0. Register `window.__timelines["<data-composition-id>"] = gsap.timeline({ paused: true })` and let the engine seek it. See “Reserved globals”.
|
|
588
|
+
- **Never call a render verified until `vidfarm motion-check <file.mp4>` has passed.** A frozen render matches on duration, frame count, file size and audio hash, and frame 0 looks perfect.
|
|
565
589
|
- **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”.
|
|
566
590
|
- **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”.
|
|
567
591
|
|
|
@@ -125,6 +125,7 @@ you follow. Read one **before** you build that format, and stack it on a base.
|
|
|
125
125
|
| `wall-text-pov-ugc` | One unbroken ambient take + one static block of unplated type. `duration = words / 8` |
|
|
126
126
|
| `ugc-reaction-greenscreen` | Reaction cutaways + a keyed device carrying the customer's real app demo |
|
|
127
127
|
| `sticker-slideshow-tips` | A tips carousel — N still slides at 3.0s; the slides are the deliverable |
|
|
128
|
+
| `engaging-chat-convo` | A text thread as a carousel — N chat screenshots at 3.0s, two voices, no ending |
|
|
128
129
|
| `animated-sticker-story` | A narrated paper puppet theater on one parchment stage, one GSAP timeline |
|
|
129
130
|
| `google-news-to-video` | Timely newsjack — news-search finds the STORY, video-search the VISUALS |
|
|
130
131
|
| `unique-product-explainers` | N customer URLs → N videos that do not converge. Differentiation as an input |
|
|
@@ -140,6 +140,8 @@ vidfarm render "$FORK_ID" --dir ./work --target cloud --tracer "batch-2026-07-09
|
|
|
140
140
|
|
|
141
141
|
`--dir` may also point directly at `composition.html`. Local renders don't push `./work` to the cloud — `vidfarm publish` (or the cloud-target render) does that. Render prep automatically downloads a long source once and cuts the timeline's segments from it, so referencing the same long raw N times is fine.
|
|
142
142
|
|
|
143
|
+
**A local render is gated at both ends, because a broken composition still produces a plausible MP4.** Before it starts, it refuses any composition that assigns an engine-owned global (`window.__player`, `window.__hf`, …) — that mistake writes a correct-length video of a still image and exits 0. After it finishes, it runs `vidfarm motion-check` on the output and **fails with exit 1** if not one sampled frame pair differs. Pass `--allow-static` when the video is meant to hold still. A cloud render has no such gate: run `vidfarm motion-check <file.mp4>` on the downloaded result yourself.
|
|
144
|
+
|
|
143
145
|
Best practices:
|
|
144
146
|
|
|
145
147
|
- Prefer one canonical base fork per automation run, then branch from that fork if you need variants.
|
|
@@ -268,7 +270,8 @@ The licensed harness also carries the **generative build workflow** guidance (ch
|
|
|
268
270
|
| `vidfarm remove-video-captions <forkId>` (alias: `ghostcut`) | `GET .../compositions/:forkId/remove-video-captions` | subtitle-removal status |
|
|
269
271
|
| `vidfarm snapshot <forkId>` | `POST .../compositions/:forkId/versions` | save composition fork version |
|
|
270
272
|
| `vidfarm versions <forkId>` | `GET .../compositions/:forkId/versions` | list versions |
|
|
271
|
-
| `vidfarm render <forkId> [--dir <dir\|composition.html>] [--out <path>] [--target local\|cloud] [--wait]` | local (default): renders ON YOUR MACHINE via bundled hyperframes, free; `--target cloud`: `PUT/PATCH working files, then POST .../compositions/:forkId/render` (billed) | script-friendly render to MP4; no `--dir` uses the fork's current working state |
|
|
273
|
+
| `vidfarm render <forkId> [--dir <dir\|composition.html>] [--out <path>] [--target local\|cloud] [--wait]` | local (default): renders ON YOUR MACHINE via bundled hyperframes, free; `--target cloud`: `PUT/PATCH working files, then POST .../compositions/:forkId/render` (billed) | script-friendly render to MP4; no `--dir` uses the fork's current working state. A **local** render refuses to start on a composition that assigns an engine-owned global, and **fails (exit 1)** when the finished MP4 never moves — `--allow-static` for a card that holds still on purpose |
|
|
274
|
+
| `vidfarm motion-check <video.mp4> [--json] [--fps N]` | (local ffmpeg, no route) | **does the finished video actually MOVE?** Exits 1 when every sampled frame pair is identical. A frozen render matches on duration, frame count, file size and audio hash and has a perfect frame 0, so nothing else catches it. Runs automatically at the end of `render --target local` |
|
|
272
275
|
| `vidfarm render-status <forkId> <renderId>` | `GET .../compositions/:forkId/renders/:renderId` | poll a render |
|
|
273
276
|
| `vidfarm visibility <forkId> <private\|public>` | `PATCH .../compositions/:forkId/visibility` | set visibility |
|
|
274
277
|
| `vidfarm clone <forkId>` | `POST .../compositions/:forkId/clone` | clone a fork |
|
|
@@ -363,6 +366,8 @@ What it flags:
|
|
|
363
366
|
| `layout-template` | error | The frame composed like a **page**: 3+ stacked text blocks in one container ending in a call to action — headline + subheading + CTA, the web hero/modal body. Fires with or without the box around it, because the STACK is the tell. Three stacked lines with no CTA are a legitimate title card and pass |
|
|
364
367
|
| `modal-scrim` | error | A full-frame backdrop that is **blurred AND dimmed** — the website-modal staging that pushes the picture back so a floating block pops. Blur alone passes (the blurred fill behind a 16:9 clip in a 9:16 frame is a real technique); it needs the dimming too, and a bed faded under `opacity:0.25` is an ambient texture wash, not staging, so it passes as well |
|
|
365
368
|
| `gradient-text` | error | `background-clip:text` gradient headline fills |
|
|
369
|
+
| `engine-owned-global` | error | A `<script>` assigns `window.__player` / `__hf` / `__playerReady` / `__renderReady` / `__hyperframes`. **The only rule here that is about correctness, not taste** — assigning one destroys frame capture, and the render still exits 0 with a correct-length MP4 in which nothing moves. Register `window.__timelines["<data-composition-id>"] = gsap.timeline({ paused: true })` instead. Same check runs in `vidfarm lint` (`engine_owned_global_assigned`) and blocks `vidfarm render --target local` |
|
|
370
|
+
| `missing-timeline-registry` | warn | A GSAP/anime timeline is built and never registered on `window.__timelines` — the engine has nothing to seek, so the render is a still image |
|
|
366
371
|
| `clickable-element` | error/warn | `<button>`, `<form>`, `<input>`; `<a href>` warns |
|
|
367
372
|
| `web-framework-classes` | error/warn | Bootstrap/Tailwind class tokens (`btn`, `badge`, `card`, `hero`, `col-*`, `rounded-full`, `shadow-lg`, `backdrop-blur`, `bg-gradient-to-*`) or a linked CSS framework. A `<script>` CDN for GSAP/anime.js is fine |
|
|
368
373
|
| `page-structure` / `bullet-list` | error/warn | `<nav>`/`<header>`/`<footer>`/`<table>`; a `<ul>` with visible bullet markers |
|
|
@@ -76,12 +76,21 @@ vidfarm stills ./work --at 6.0,6.2,6.4,6.6,6.8,7.0 # is it a wipe, or a hole?
|
|
|
76
76
|
|
|
77
77
|
**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.
|
|
78
78
|
|
|
79
|
-
|
|
79
|
+
**Settle it mechanically, first, every time — one command:**
|
|
80
80
|
|
|
81
|
+
```bash
|
|
82
|
+
vidfarm motion-check ./final.mp4 # exits 1 and says why when nothing in the video ever changes
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
It samples the video and compares consecutive frames. `vidfarm render --target local` now runs it automatically and **fails the render** on a frozen output (`--allow-static` for a title card that is meant to hold still). Run it by hand on anything that did not come out of that command — a cloud render, a watermarked master, an ffmpeg re-encode.
|
|
86
|
+
|
|
87
|
+
Three real causes, all silent:
|
|
88
|
+
|
|
89
|
+
- **The composition assigned an engine-owned global.** `window.__player` and `window.__hf` belong to the engine; assigning either destroys the frame-capture bridge, and the renderer then stalls 45 s, downgrades the failure to a warning, and writes a correct-length MP4 of a still image with exit code 0. **Four paid deliverables shipped this way in one night.** `vidfarm lint` and `vidfarm qa` both call this an error now, and a local render refuses to start. Never write those globals — see SKILL.md § "Reserved globals".
|
|
81
90
|
- **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.
|
|
82
91
|
- **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.
|
|
83
92
|
|
|
84
|
-
**
|
|
93
|
+
**Then look, too: 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:
|
|
85
94
|
|
|
86
95
|
```bash
|
|
87
96
|
# consecutive/distant frames must DIFFER (motion preserved)
|
package/SKILL.director.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: vidfarm
|
|
3
|
-
skill_version: 0.21.
|
|
3
|
+
skill_version: 0.21.57
|
|
4
4
|
description: Use Vidfarm as a director. Run a strategy **consultation** (the `brainstorm/*` chain — cold-start interview, awareness stages, persuasive angles, hooks, product placement). Answer "give me content ideas" / "what should I post" / "I need 30 videos this month" from the bundled idea banks (50 content frames x 5 awareness stages x 44 problem angles). Browse/add inspiration videos, browse the free public raws catalog BY CATEGORY (curated shelves like scroll-stoppers/greenscreen/reaction — the cheapest way to source footage for one video, and a ready-made clip pool for bulk scripting N variants), fork a template into a composition, edit it in the Trackpad Editor (timeline-based like Premiere/DaVinci), auto-decompose source video into scenes, render to MP4, approve into a shareable post, and schedule it. Includes login, provider keys, discovery, versioning, uploads/downloads, and billing. Every step is available as raw REST; `vidfarm-devcli` wraps those routes and composes the file-backed scripting flows.
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -397,6 +397,27 @@ The mechanism is deterministic, not luck: rendering is seek-safe, so frame 0 sho
|
|
|
397
397
|
|
|
398
398
|
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`.
|
|
399
399
|
|
|
400
|
+
## Reserved globals — the engine owns them, and assigning one ships a still image
|
|
401
|
+
|
|
402
|
+
**Never assign `window.__player`, `window.__hf`, `window.__playerReady`, `window.__renderReady`, or `window.__hyperframes` from composition code.** The render engine installs all of them. A composition that overwrites one destroys the frame-capture bridge, and the failure is invisible: the renderer stalls 45 seconds, downgrades the error to a warning, exits **0**, and writes an MP4 with the **correct duration, correct frame count and correct audio, in which nothing moves.** Four paid deliverables shipped that way in one night before anyone looked at the pixels.
|
|
403
|
+
|
|
404
|
+
**Motion has exactly one mechanism — you register a paused timeline and the engine SEEKS it:**
|
|
405
|
+
|
|
406
|
+
```html
|
|
407
|
+
<script>
|
|
408
|
+
window.__timelines = window.__timelines || {}; // this line is the ONLY __ global you write
|
|
409
|
+
var tl = gsap.timeline({ paused: true }); // paused, built synchronously
|
|
410
|
+
tl.to('#box', { x: 880, duration: 4, ease: 'none' }, 0);
|
|
411
|
+
window.__timelines['main'] = tl; // key === the root's data-composition-id, exactly
|
|
412
|
+
</script>
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
- **Need imperative per-frame drawing** (canvas, a custom renderer)? Do *not* invent a seek API. Register a paused **driver** timeline and hang your draw call off its `onUpdate`: `gsap.timeline({ paused: true, onUpdate: function () { draw(driver.time()); } })`, then give it a dummy tween of the full duration. The engine seeks the driver; `onUpdate` fires at the seeked time.
|
|
416
|
+
- **No timeline at all** is the same failure with a different cause. A composition that builds a GSAP/anime timeline and never registers it renders static too.
|
|
417
|
+
- **`window.__timelines` is the one exception** — writing it is required. Nothing else in that namespace is yours.
|
|
418
|
+
|
|
419
|
+
**Three tools enforce this, so a mistake costs seconds instead of a night:** `vidfarm lint <dir>` and `vidfarm qa <dir>` both report `engine_owned_global_assigned` as an **error**, `vidfarm render --target local` **refuses to start**, and every local render ends with an automatic **`vidfarm motion-check`** that fails the render when the finished MP4 never moves (`--allow-static` for a title card that holds still on purpose). Run `vidfarm motion-check <file.mp4>` by hand on anything those did not produce — a cloud render, a watermarked master, an ffmpeg re-encode.
|
|
420
|
+
|
|
400
421
|
## Judge the WHOLE video, not the parts you built — and never by one frame
|
|
401
422
|
|
|
402
423
|
**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.
|
|
@@ -405,7 +426,7 @@ Full mechanics and editor verbs: `references/editor-workflows.md` (“The openin
|
|
|
405
426
|
|
|
406
427
|
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.**
|
|
407
428
|
|
|
408
|
-
**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. **
|
|
429
|
+
**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 assigned engine-owned global, 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. **Settle it with `vidfarm motion-check <file.mp4>`, then compare two frames from different scenes by eye; they must differ a lot.** Likewise, verify audio by measurement, never "it sounds good" — you can't hear it.
|
|
409
430
|
|
|
410
431
|
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`**.
|
|
411
432
|
|
|
@@ -510,6 +531,7 @@ Why they exist: the prompts you find on the `/discover` pages are tuned to **one
|
|
|
510
531
|
| `https://vidfarm.cc/experimental/ugc-reaction-greenscreen.md` | Sell an app with three streams cut against each other: `ugc-reaction` raws, a Display Greenscreen device whose screen carries the customer's real demo, and the demo itself. One actor across every beat via `actor_<uuid>`, a per-frame tracked screen insert, captions inside the platform-chrome-safe core, and two exports from one render (voiceover-only to publish, voiceover+music to review) |
|
|
511
532
|
| `https://vidfarm.cc/experimental/sticker-slideshow-tips.md` | A tips **carousel** — the deliverable is N still slides, and the 3.0s-per-slide MP4 is only the playable preview. Die-cut cutouts on a paper page or a photo background, a literal "Tips for…" cover, the specificity ladder, one slide shilled from the middle and written so it survives deleting the brand name, four background modes, and a measured WCAG contrast gate on the exported PNGs |
|
|
512
533
|
| `https://vidfarm.cc/experimental/wall-text-pov-ugc.md` | One unbroken ambient take + one static block of unplated type. No cuts, no voiceover, no subtitles, nothing animated. The retention engine is arithmetic — `duration = words / 8`, so one play lands the viewer at the **halfway mark**, committed and one pass from done — floored at 8s so a trending sound gets a real phrase of a track. The four speaker frames, MIRROR vs TURN, a density pass that treats padding as the fatal failure, casting the scene dark so the type needs no plate (measured), ping-ponging the plate for a seamless loop, and three gates. Runs at $0 |
|
|
534
|
+
| `https://vidfarm.cc/experimental/engaging-chat-convo.md` | A chat-thread **carousel** — N still screenshots of ONE text conversation at 3.0s each; the slides are the deliverable and the MP4 is only the preview. Pure type on pure black with **no phone chrome at all** (no status bar, avatar, header, timestamps or keyboard), because a crop containing only bubbles cannot be *wrong* about a detail — and one wrong detail makes a reader re-read the whole thread as fake. The engine is two voices that stay separable with the **colour removed**; `>>>` skip cards buy time jumps for free; the story opens mid-mistake, reveals something in the middle that recasts what came before, breaks one character's register in the last act, and ends mid-action with no resolution, because the bait is the missing ending. The offer is named **once**, lowercase, mid-bubble, as a plot fact a character mentions about their own life while the other reacts sceptically. Ships a voice-drift detector that names the slide where the two voices merge. Runs at $0 |
|
|
513
535
|
| `https://vidfarm.cc/experimental/animated-sticker-story.md` | A narrated **paper puppet theater** — one full-bleed parchment stage that never cuts, a cast of die-cut stickers, and every element moved by ONE paused GSAP timeline (MotionPathPlugin for walks; paths in absolute canvas coordinates, never `align:"self"`). The three-node rig, the seven moves (ENTER / WALK / CROWD / BEAT / STAMP / DRAW / CAMERA), buying SHEETS rather than stickers so one art class survives, generating art without shadows and adding one CSS drop-shadow, kinetic captions that animate **colour only** on whisper word timings, a two-pass build that MEASURES where the drawing is quietest before placing any type, a feathered paper wash that is not a plate, and the offer named once as a wordmark on the last beat. Desktop-only. ~$0.25 in `hybrid`, $0 in `minimize` |
|
|
514
536
|
|
|
515
537
|
Fetch one as plain markdown and follow it end to end; do not skim it into a summary.
|
|
@@ -562,6 +584,8 @@ The File Index above says what each file *is*; this says which one a given ask m
|
|
|
562
584
|
- Treat `forkId` as an unguessable bearer token for read access.
|
|
563
585
|
- Submission routes are generally not idempotent. Especially for renders and expensive primitives, check status before retrying.
|
|
564
586
|
- In the web editor, use CSS/declarative motion only. Script-bearing HTML is stripped or rejected there.
|
|
587
|
+
- **Never assign `window.__player` / `window.__hf` / `window.__playerReady` / `window.__renderReady` / `window.__hyperframes`.** The engine owns them; assigning one writes a correct-length MP4 of a still image and exits 0. Register `window.__timelines["<data-composition-id>"] = gsap.timeline({ paused: true })` and let the engine seek it. See “Reserved globals”.
|
|
588
|
+
- **Never call a render verified until `vidfarm motion-check <file.mp4>` has passed.** A frozen render matches on duration, frame count, file size and audio hash, and frame 0 looks perfect.
|
|
565
589
|
- **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”.
|
|
566
590
|
- **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”.
|
|
567
591
|
|
|
@@ -2241,12 +2265,21 @@ vidfarm stills ./work --at 6.0,6.2,6.4,6.6,6.8,7.0 # is it a wipe, or a hole?
|
|
|
2241
2265
|
|
|
2242
2266
|
**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.
|
|
2243
2267
|
|
|
2244
|
-
|
|
2268
|
+
**Settle it mechanically, first, every time — one command:**
|
|
2269
|
+
|
|
2270
|
+
```bash
|
|
2271
|
+
vidfarm motion-check ./final.mp4 # exits 1 and says why when nothing in the video ever changes
|
|
2272
|
+
```
|
|
2245
2273
|
|
|
2274
|
+
It samples the video and compares consecutive frames. `vidfarm render --target local` now runs it automatically and **fails the render** on a frozen output (`--allow-static` for a title card that is meant to hold still). Run it by hand on anything that did not come out of that command — a cloud render, a watermarked master, an ffmpeg re-encode.
|
|
2275
|
+
|
|
2276
|
+
Three real causes, all silent:
|
|
2277
|
+
|
|
2278
|
+
- **The composition assigned an engine-owned global.** `window.__player` and `window.__hf` belong to the engine; assigning either destroys the frame-capture bridge, and the renderer then stalls 45 s, downgrades the failure to a warning, and writes a correct-length MP4 of a still image with exit code 0. **Four paid deliverables shipped this way in one night.** `vidfarm lint` and `vidfarm qa` both call this an error now, and a local render refuses to start. Never write those globals — see SKILL.md § "Reserved globals".
|
|
2246
2279
|
- **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.
|
|
2247
2280
|
- **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.
|
|
2248
2281
|
|
|
2249
|
-
**
|
|
2282
|
+
**Then look, too: 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:
|
|
2250
2283
|
|
|
2251
2284
|
```bash
|
|
2252
2285
|
# consecutive/distant frames must DIFFER (motion preserved)
|
|
@@ -3050,6 +3083,8 @@ vidfarm render "$FORK_ID" --dir ./work --target cloud --tracer "batch-2026-07-09
|
|
|
3050
3083
|
|
|
3051
3084
|
`--dir` may also point directly at `composition.html`. Local renders don't push `./work` to the cloud — `vidfarm publish` (or the cloud-target render) does that. Render prep automatically downloads a long source once and cuts the timeline's segments from it, so referencing the same long raw N times is fine.
|
|
3052
3085
|
|
|
3086
|
+
**A local render is gated at both ends, because a broken composition still produces a plausible MP4.** Before it starts, it refuses any composition that assigns an engine-owned global (`window.__player`, `window.__hf`, …) — that mistake writes a correct-length video of a still image and exits 0. After it finishes, it runs `vidfarm motion-check` on the output and **fails with exit 1** if not one sampled frame pair differs. Pass `--allow-static` when the video is meant to hold still. A cloud render has no such gate: run `vidfarm motion-check <file.mp4>` on the downloaded result yourself.
|
|
3087
|
+
|
|
3053
3088
|
Best practices:
|
|
3054
3089
|
|
|
3055
3090
|
- Prefer one canonical base fork per automation run, then branch from that fork if you need variants.
|
|
@@ -3178,7 +3213,8 @@ The licensed harness also carries the **generative build workflow** guidance (ch
|
|
|
3178
3213
|
| `vidfarm remove-video-captions <forkId>` (alias: `ghostcut`) | `GET .../compositions/:forkId/remove-video-captions` | subtitle-removal status |
|
|
3179
3214
|
| `vidfarm snapshot <forkId>` | `POST .../compositions/:forkId/versions` | save composition fork version |
|
|
3180
3215
|
| `vidfarm versions <forkId>` | `GET .../compositions/:forkId/versions` | list versions |
|
|
3181
|
-
| `vidfarm render <forkId> [--dir <dir\|composition.html>] [--out <path>] [--target local\|cloud] [--wait]` | local (default): renders ON YOUR MACHINE via bundled hyperframes, free; `--target cloud`: `PUT/PATCH working files, then POST .../compositions/:forkId/render` (billed) | script-friendly render to MP4; no `--dir` uses the fork's current working state |
|
|
3216
|
+
| `vidfarm render <forkId> [--dir <dir\|composition.html>] [--out <path>] [--target local\|cloud] [--wait]` | local (default): renders ON YOUR MACHINE via bundled hyperframes, free; `--target cloud`: `PUT/PATCH working files, then POST .../compositions/:forkId/render` (billed) | script-friendly render to MP4; no `--dir` uses the fork's current working state. A **local** render refuses to start on a composition that assigns an engine-owned global, and **fails (exit 1)** when the finished MP4 never moves — `--allow-static` for a card that holds still on purpose |
|
|
3217
|
+
| `vidfarm motion-check <video.mp4> [--json] [--fps N]` | (local ffmpeg, no route) | **does the finished video actually MOVE?** Exits 1 when every sampled frame pair is identical. A frozen render matches on duration, frame count, file size and audio hash and has a perfect frame 0, so nothing else catches it. Runs automatically at the end of `render --target local` |
|
|
3182
3218
|
| `vidfarm render-status <forkId> <renderId>` | `GET .../compositions/:forkId/renders/:renderId` | poll a render |
|
|
3183
3219
|
| `vidfarm visibility <forkId> <private\|public>` | `PATCH .../compositions/:forkId/visibility` | set visibility |
|
|
3184
3220
|
| `vidfarm clone <forkId>` | `POST .../compositions/:forkId/clone` | clone a fork |
|
|
@@ -3273,6 +3309,8 @@ What it flags:
|
|
|
3273
3309
|
| `layout-template` | error | The frame composed like a **page**: 3+ stacked text blocks in one container ending in a call to action — headline + subheading + CTA, the web hero/modal body. Fires with or without the box around it, because the STACK is the tell. Three stacked lines with no CTA are a legitimate title card and pass |
|
|
3274
3310
|
| `modal-scrim` | error | A full-frame backdrop that is **blurred AND dimmed** — the website-modal staging that pushes the picture back so a floating block pops. Blur alone passes (the blurred fill behind a 16:9 clip in a 9:16 frame is a real technique); it needs the dimming too, and a bed faded under `opacity:0.25` is an ambient texture wash, not staging, so it passes as well |
|
|
3275
3311
|
| `gradient-text` | error | `background-clip:text` gradient headline fills |
|
|
3312
|
+
| `engine-owned-global` | error | A `<script>` assigns `window.__player` / `__hf` / `__playerReady` / `__renderReady` / `__hyperframes`. **The only rule here that is about correctness, not taste** — assigning one destroys frame capture, and the render still exits 0 with a correct-length MP4 in which nothing moves. Register `window.__timelines["<data-composition-id>"] = gsap.timeline({ paused: true })` instead. Same check runs in `vidfarm lint` (`engine_owned_global_assigned`) and blocks `vidfarm render --target local` |
|
|
3313
|
+
| `missing-timeline-registry` | warn | A GSAP/anime timeline is built and never registered on `window.__timelines` — the engine has nothing to seek, so the render is a still image |
|
|
3276
3314
|
| `clickable-element` | error/warn | `<button>`, `<form>`, `<input>`; `<a href>` warns |
|
|
3277
3315
|
| `web-framework-classes` | error/warn | Bootstrap/Tailwind class tokens (`btn`, `badge`, `card`, `hero`, `col-*`, `rounded-full`, `shadow-lg`, `backdrop-blur`, `bg-gradient-to-*`) or a linked CSS framework. A `<script>` CDN for GSAP/anime.js is fine |
|
|
3278
3316
|
| `page-structure` / `bullet-list` | error/warn | `<nav>`/`<header>`/`<footer>`/`<table>`; a `<ul>` with visible bullet markers |
|
package/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: vidfarm
|
|
3
|
-
skill_version: 0.21.
|
|
3
|
+
skill_version: 0.21.57
|
|
4
4
|
description: Entry point for making, editing, and rendering short-form video with Vidfarm. Use when the task is to create a video, ad, explainer, or social clip; to edit or re-theme an existing composition; to source footage or audio; or to run a batch of video variants. Routes to the full director skill and the `vidfarm` CLI.
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -162,6 +162,7 @@ vidfarm publish <forkId> # push edits back to th
|
|
|
162
162
|
| "A UGC ad for my app" / "someone reacting, then the app" / a client's app demo you have to make watchable | three streams cut against each other: `ugc-reaction` raws (ONE actor, via `actor_<uuid>`), a Display Greenscreen device carrying their real demo on its screen, and the demo. Ships a voiceover-only cut to publish + a voiceover+music cut to review | `vidfarm.cc/experimental/ugc-reaction-greenscreen.md` |
|
|
163
163
|
| "A tips slideshow / carousel" / "5 tips for X" / a listicle post | N still slides at exactly 3.0s, die-cut cutouts on a page or a photo, a literal "Tips for…" cover, ONE slide shilled from the middle. **The slides are the deliverable; the MP4 is the preview.** $0–$0.15 | `vidfarm.cc/experimental/sticker-slideshow-tips.md` |
|
|
164
164
|
| "A confession / truth bomb over a POV or ambient scene" / "just text on a video" / a text-story post | ONE unbroken take + ONE static block of unplated type. Nothing animates, nothing is cut. `duration = words / 8` (min 8s) so one play reaches the HALFWAY mark and they loop to finish. Cast the scene DARK so the type needs no plate. $0 | `vidfarm.cc/experimental/wall-text-pov-ugc.md` |
|
|
165
|
+
| "A fake text conversation" / "a chat story" / "a screenshot thread" / funny-or-dramatic DMs | N still chat screenshots at exactly 3.0s, pure type on pure black, **no phone chrome at all**. Two voices that stay separable with the colour removed, `>>>` cards for time jumps, the offer named ONCE mid-thread as a plot fact, and NO ending — it stops mid-action. **The slides are the deliverable; the MP4 is the preview.** $0 | `vidfarm.cc/experimental/engaging-chat-convo.md` |
|
|
165
166
|
| "An animated explainer / a little story that animates" / "like those Vox map animations" / "cutout paper animation" | ONE full-bleed parchment stage, no cuts, a cast of die-cut paper stickers moved by ONE paused GSAP timeline. Narrated, subtitles stay small and kinetic in colour only, the offer named once as a wordmark on the LAST beat. Buy SHEETS, not stickers. Desktop-only (the web editor strips scripts). ~$0.25 hybrid, $0 minimize | `vidfarm.cc/experimental/animated-sticker-story.md` |
|
|
166
167
|
| "Make N videos for N customers / clients" / a batch that must not read as N runs of one template | differentiation is an INPUT, not a hope: assign each variant its own frame before you build any of them, then review frame by frame. $0 | `vidfarm.cc/experimental/unique-product-explainers.md` |
|
|
167
168
|
| "Download this video from `<url>`" | `vidfarm download-video <url>` (paid). Free plan gets a 402 — `vidfarm browse page "<url>"` and save it from their Chrome yourself, else have them download it, then `vidfarm put-file`. Never answer "I can't." | `references/browser-harness.md` |
|
|
@@ -221,6 +222,7 @@ The standards themselves are not optional. `vidfarm qa <dir>` is — it is a fre
|
|
|
221
222
|
- **The first frame is the thumbnail.** A real visual and the hook words at `start:0`. No fade-up, no entrance transition on the first clip. Check with `vidfarm stills <dir> --at 0`.
|
|
222
223
|
- **Cut ruthlessly.** First assembly is 30–50% too long. Delete every beat that the video survives without, then ripple the hole closed. Length is an output, not a target.
|
|
223
224
|
- **Review the whole video, never one frame.** Tile ~12 stills with `vidfarm stills <dir> --sheet` and read it as an image — one type scale, one palette, deliberate pacing, clean joins. Verify audio by measurement (~12–15 dB speech-over-bed, peak <0 dBFS). Report what you measured separately from what you judged. Your own "looks good" is the least reliable signal here.
|
|
225
|
+
- **Prove the render MOVES before you call it done: `vidfarm motion-check <file.mp4>`.** A frozen render has the right duration, frame count, file size and audio, and a perfect frame 0 — every other check you would run passes. `vidfarm render --target local` runs this itself and fails on a still output (`--allow-static` when that is the point).
|
|
224
226
|
- **Use the real trending song.** The render is a review artifact; the user re-attaches the same track from the platform's in-app music library at post time, where it is licensed. Pull it with `vidfarm download-audio <post-url>`, keep it on its own `<audio>` layer at its own `data-volume`, and name the track in your report. Exception: a paid ad placement is not covered — say so once and offer `vidfarm music "<same vibe, same BPM>"`.
|
|
225
227
|
- **Ask about deduplication before the render, not after.** "Is this going out more than once?" Answering early keeps it at render once → dedupe N.
|
|
226
228
|
|
|
@@ -231,6 +233,7 @@ The standards themselves are not optional. `vidfarm qa <dir>` is — it is a fre
|
|
|
231
233
|
- Render only through `POST /api/v1/compositions/:forkId/render`. Never call the renderer directly.
|
|
232
234
|
- Submissions are **not** idempotent. Every render and primitive POST charges again — check status before retrying.
|
|
233
235
|
- In the web editor, CSS/declarative motion only; JS animation adapters are stripped on save. Locally via `vidfarm serve`, the full adapters work.
|
|
236
|
+
- **`window.__player`, `window.__hf`, `window.__playerReady`, `window.__renderReady` and `window.__hyperframes` belong to the engine — never assign them.** Assigning one destroys frame capture: the render exits 0 and writes a correct-length MP4 in which nothing moves. Motion has one mechanism — register a paused timeline and let the engine seek it: `window.__timelines = window.__timelines || {}; window.__timelines["<data-composition-id>"] = gsap.timeline({ paused: true });` (that key must equal the root's `data-composition-id` exactly). For per-frame drawing, hang your draw call off a paused driver timeline's `onUpdate` instead of inventing a seek API. `vidfarm lint` and `vidfarm qa` both error on this, and a local render refuses to start.
|
|
234
237
|
- Treat `forkId` as an unguessable bearer token. Never put provider secrets in composition HTML or JSON.
|
|
235
238
|
|
|
236
239
|
## 8. Deeper knowledge — fetch on demand
|
|
@@ -262,7 +265,7 @@ Otherwise fetch `https://vidfarm.cc/skill-pack/vidfarm/files/<path>`. Load one f
|
|
|
262
265
|
| `recipes/*.md` | `find-and-fork-template` · `retheme-template` · `local-edit-render-approve` · `onboard-a-new-director` · `bulk-scripting-with-a-harness` · `cutout-graphics-for-explainers` |
|
|
263
266
|
| `harnesses/README.md` | anything harness-shaped — start here. Bases: `short-form` · `hooks` · `ugc-testimonial` · `explainer` · `product-demo` · `product-explainer` |
|
|
264
267
|
| `vidfarm.cc/flash-harness.md` | **you are a weak model (§0a), or you are unsure.** The process harness that sits above every format harness: match a decomposed `/discover` template → decompose if nobody has → derive the viral + visual DNA → procure every raw in one pass → transplant. Also at `vidfarm harness show flash-harness`, offline |
|
|
265
|
-
| `vidfarm harness list` | both shelves: the bases above, **plus every format harness from `vidfarm.cc/experimental`** — `flash-harness` (the weak-model process, not a format) · `meme-recaption` · `wall-text-pov-ugc` · `ugc-reaction-greenscreen` · `sticker-slideshow-tips` · `animated-sticker-story` · `google-news-to-video` · `unique-product-explainers`. They ship in the package, so `vidfarm harness show <name>` reads one in full with no fetch, and `vidfarm qa ./work --harness <name>` grades against it. The name is the URL slug |
|
|
268
|
+
| `vidfarm harness list` | both shelves: the bases above, **plus every format harness from `vidfarm.cc/experimental`** — `flash-harness` (the weak-model process, not a format) · `meme-recaption` · `wall-text-pov-ugc` · `ugc-reaction-greenscreen` · `sticker-slideshow-tips` · `engaging-chat-convo` · `animated-sticker-story` · `google-news-to-video` · `unique-product-explainers`. They ship in the package, so `vidfarm harness show <name>` reads one in full with no fetch, and `vidfarm qa ./work --harness <name>` grades against it. The name is the URL slug |
|
|
266
269
|
|
|
267
270
|
Also served at `vidfarm.cc`: `/flash-harness.md` (**the weak-model process harness — read it first if §0a called you weak or unsure**), `/experiments.md` (ad testing), `/marketplace.md` (the marketplace manual — routes you to one of the two side harnesses below), `/marketplace-buyer.md` (**buyer side**: commission videos from the crowd), `/agentic-clipper.md` (**worker side**: "Agentic Clipper" mode — one orchestrator on a long-horizon earning mission, one subagent per task), `/update.md` (upgrade runbook), `/experimental` (format harnesses under live testing — the index a clipper routes tasks against), `/skill/vidfarm-platform` (architecture), `/skill/hyperframes` (composition-authoring craft — route broad "make me a video" asks here first).
|
|
268
271
|
|
package/dist/src/cli.js
CHANGED
|
@@ -868,6 +868,10 @@ Fine timeline control (trackpad-level verbs on a pulled/served composition — l
|
|
|
868
868
|
--target cloud: billed Lambda render (~$0.01-$0.10)
|
|
869
869
|
→ POST .../compositions/:forkId/render (--wait to
|
|
870
870
|
poll; --dir pushes composition.html + .json first)
|
|
871
|
+
A local render REFUSES compositions that assign the
|
|
872
|
+
engine-owned globals (window.__player / window.__hf)
|
|
873
|
+
and FAILS if the finished MP4 never moves — pass
|
|
874
|
+
--allow-static for a deliberately still card
|
|
871
875
|
render-status <forkId> <renderId> Poll one render job → GET .../compositions/:forkId/renders/:renderId
|
|
872
876
|
template run <template_id> <operation_name>
|
|
873
877
|
Run one template operation via REST → POST /api/v1/templates/:templateId/operations/:operationName
|
|
@@ -934,6 +938,7 @@ Local media engines & toolchain (all local, free, no account — no cloud key ne
|
|
|
934
938
|
meme-recaption, wall-text-pov-ugc,
|
|
935
939
|
ugc-reaction-greenscreen,
|
|
936
940
|
sticker-slideshow-tips,
|
|
941
|
+
engaging-chat-convo,
|
|
937
942
|
animated-sticker-story,
|
|
938
943
|
google-news-to-video,
|
|
939
944
|
unique-product-explainers
|
|
@@ -961,6 +966,13 @@ Local media engines & toolchain (all local, free, no account — no cloud key ne
|
|
|
961
966
|
sheet shows it
|
|
962
967
|
--sheet-out <file> Sheet path (default <out>/contact-sheet.png)
|
|
963
968
|
--sheet-width <px> Per-tile width in the sheet (default 320)
|
|
969
|
+
motion-check <video.mp4> DOES IT MOVE? Proves a finished video is not (local ffmpeg)
|
|
970
|
+
a still image. A frozen render has the right
|
|
971
|
+
duration, frame count, audio and frame 0, so
|
|
972
|
+
every other check you would run passes. Exits 1
|
|
973
|
+
when nothing in the video ever changes. Runs
|
|
974
|
+
automatically after vidfarm render --target local
|
|
975
|
+
[--json] [--fps <n>]
|
|
964
976
|
doctor Health-check the local toolchain: node, (local)
|
|
965
977
|
ffmpeg, hyperframes engines, Chrome, API key
|
|
966
978
|
(whoami), provider keys, agent CLI, poisoned
|
|
@@ -1817,6 +1829,13 @@ async function main() {
|
|
|
1817
1829
|
case "stills":
|
|
1818
1830
|
await runStillsCommand(rest);
|
|
1819
1831
|
return;
|
|
1832
|
+
// "does this finished video actually move?" — the check that catches a
|
|
1833
|
+
// frozen render, which passes every other verification.
|
|
1834
|
+
case "motion-check":
|
|
1835
|
+
case "motioncheck":
|
|
1836
|
+
case "frozen-check":
|
|
1837
|
+
await runMotionCheckCommand(rest);
|
|
1838
|
+
return;
|
|
1820
1839
|
case "doctor":
|
|
1821
1840
|
await runDoctorCommand(rest);
|
|
1822
1841
|
return;
|
|
@@ -4188,6 +4207,10 @@ async function runRenderCommand(argv) {
|
|
|
4188
4207
|
out: { type: "string" },
|
|
4189
4208
|
quality: { type: "string" },
|
|
4190
4209
|
"keep-project": { type: "boolean", default: false },
|
|
4210
|
+
// A deliberately static render (a title card, a still loop) is a real
|
|
4211
|
+
// output — this is how you say so. Everything else that never moves is a
|
|
4212
|
+
// bug, so the default is to fail loudly.
|
|
4213
|
+
"allow-static": { type: "boolean", default: false },
|
|
4191
4214
|
wait: { type: "boolean", default: false }
|
|
4192
4215
|
}
|
|
4193
4216
|
});
|
|
@@ -4246,8 +4269,20 @@ async function runRenderCommand(argv) {
|
|
|
4246
4269
|
keepProjectDir: Boolean(parsed.values["keep-project"])
|
|
4247
4270
|
});
|
|
4248
4271
|
const prepCuts = result.prepNotes.filter((note) => note.action !== "skipped");
|
|
4272
|
+
// A frozen render is a FAILED render wearing a successful render's exit
|
|
4273
|
+
// code. Treat it as a failure unless the director said the video is static
|
|
4274
|
+
// on purpose.
|
|
4275
|
+
const frozen = Boolean(result.motion?.frozen) && !parsed.values["allow-static"];
|
|
4249
4276
|
if (ctx.json) {
|
|
4250
|
-
printJson({
|
|
4277
|
+
printJson({
|
|
4278
|
+
ok: !frozen,
|
|
4279
|
+
render_target: "local",
|
|
4280
|
+
output_path: result.outputPath,
|
|
4281
|
+
duration_ms: result.durationMs,
|
|
4282
|
+
media_prep: result.prepNotes,
|
|
4283
|
+
motion: result.motion ?? null,
|
|
4284
|
+
...(frozen ? { error: "frozen_render", message: result.motion?.reason } : {})
|
|
4285
|
+
});
|
|
4251
4286
|
}
|
|
4252
4287
|
else {
|
|
4253
4288
|
if (prepCuts.length > 0) {
|
|
@@ -4255,10 +4290,22 @@ async function runRenderCommand(argv) {
|
|
|
4255
4290
|
}
|
|
4256
4291
|
console.log(`${GREEN}Rendered locally in ${(result.durationMs / 1000).toFixed(1)}s → ${result.outputPath}${RESET}`);
|
|
4257
4292
|
console.log(`${DIM}$0.00 — local Vidfarm render, no cloud charge.${RESET}`);
|
|
4293
|
+
if (frozen && result.motion) {
|
|
4294
|
+
const { formatFrozenRenderFailure } = await import("./lib/frozen-render.js");
|
|
4295
|
+
console.log(formatFrozenRenderFailure(result.motion, { red: RED, bold: BOLD, dim: DIM, reset: RESET }));
|
|
4296
|
+
}
|
|
4297
|
+
else if (result.motion?.skipped) {
|
|
4298
|
+
console.log(`${DIM}Motion not verified (${result.motion.reason}) — check it by eye before publishing.${RESET}`);
|
|
4299
|
+
}
|
|
4300
|
+
else if (result.motion) {
|
|
4301
|
+
console.log(`${DIM}Motion verified: ${result.motion.moving_pairs}/${result.motion.compared_pairs} sampled frame pairs differ — the video moves.${RESET}`);
|
|
4302
|
+
}
|
|
4258
4303
|
// Nudge, not a gate: QA never runs automatically, but every production
|
|
4259
4304
|
// should get a look before it's published.
|
|
4260
4305
|
console.log(`${DIM}Recommended: ${RESET}vidfarm qa ${sourceDir ?? "<dir>"}${DIM} — social-native QA (HTML slop + font regime). Feedback only, free, instant.${RESET}`);
|
|
4261
4306
|
}
|
|
4307
|
+
if (frozen)
|
|
4308
|
+
process.exitCode = 1;
|
|
4262
4309
|
return;
|
|
4263
4310
|
}
|
|
4264
4311
|
if (sourceDir) {
|
|
@@ -12784,6 +12831,54 @@ async function runStillsCommand(argv) {
|
|
|
12784
12831
|
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}`);
|
|
12785
12832
|
}
|
|
12786
12833
|
}
|
|
12834
|
+
// `vidfarm motion-check <video.mp4>` — prove a finished video actually MOVES.
|
|
12835
|
+
//
|
|
12836
|
+
// The one check that catches a whole class of silent render failure: a frozen
|
|
12837
|
+
// render has the right duration, the right frame count, clean audio, and a
|
|
12838
|
+
// perfect frame 0, so every other verification an agent would think to run
|
|
12839
|
+
// passes. This one samples the video and compares consecutive frames. Runs on
|
|
12840
|
+
// any MP4, not just a vidfarm render — including the output of a watermark or
|
|
12841
|
+
// overlay pass, which is where the other known cause lives.
|
|
12842
|
+
async function runMotionCheckCommand(argv) {
|
|
12843
|
+
const parsed = parseArgs({
|
|
12844
|
+
args: argv,
|
|
12845
|
+
allowPositionals: true,
|
|
12846
|
+
options: {
|
|
12847
|
+
json: { type: "boolean", default: false },
|
|
12848
|
+
fps: { type: "string" }
|
|
12849
|
+
}
|
|
12850
|
+
});
|
|
12851
|
+
const target = parsed.positionals[0];
|
|
12852
|
+
if (!target)
|
|
12853
|
+
throw new Error("motion-check requires a video path: `vidfarm motion-check <video.mp4> [--json]`.");
|
|
12854
|
+
const videoPath = path.resolve(process.cwd(), target);
|
|
12855
|
+
if (!existsSync(videoPath))
|
|
12856
|
+
throw new Error(`No such file: ${videoPath}`);
|
|
12857
|
+
const fpsRaw = parsed.values.fps;
|
|
12858
|
+
const fpsGrid = fpsRaw !== undefined ? Number(fpsRaw) : undefined;
|
|
12859
|
+
if (fpsRaw !== undefined && (!Number.isFinite(fpsGrid) || fpsGrid <= 0)) {
|
|
12860
|
+
throw new Error(`--fps must be a positive number, got "${fpsRaw}".`);
|
|
12861
|
+
}
|
|
12862
|
+
const { checkRenderMotion, formatFrozenRenderFailure } = await import("./lib/frozen-render.js");
|
|
12863
|
+
const result = await checkRenderMotion(videoPath, fpsGrid ? { fpsGrid } : {});
|
|
12864
|
+
if (parsed.values.json) {
|
|
12865
|
+
printJson(result);
|
|
12866
|
+
if (result.frozen)
|
|
12867
|
+
process.exitCode = 1;
|
|
12868
|
+
return;
|
|
12869
|
+
}
|
|
12870
|
+
if (result.frozen) {
|
|
12871
|
+
console.log(formatFrozenRenderFailure(result, { red: RED, bold: BOLD, dim: DIM, reset: RESET }));
|
|
12872
|
+
process.exitCode = 1;
|
|
12873
|
+
return;
|
|
12874
|
+
}
|
|
12875
|
+
if (result.skipped) {
|
|
12876
|
+
console.log(`${YELLOW}? Motion not verified${RESET} ${DIM}— ${result.reason}${RESET}`);
|
|
12877
|
+
return;
|
|
12878
|
+
}
|
|
12879
|
+
console.log(`${GREEN}✓ The video moves${RESET} ${DIM}— ${result.moving_pairs}/${result.compared_pairs} sampled frame pairs differ (max change ${result.motion_score.toFixed(1)}%).${RESET}`);
|
|
12880
|
+
console.log(`${DIM}This proves motion exists, not that the video is good. Still watch it: vidfarm stills <dir> --sheet.${RESET}`);
|
|
12881
|
+
}
|
|
12787
12882
|
// ── Agent skill ───────────────────────────────────────────────────────────────
|
|
12788
12883
|
// Install the latest director skill onto disk as a Claude Code / agent skill so
|
|
12789
12884
|
// the user's AI agent can read SKILL.director.md and act. We pull the freshest
|
|
@@ -560,7 +560,7 @@ async function startLocalRender(input) {
|
|
|
560
560
|
void (async () => {
|
|
561
561
|
try {
|
|
562
562
|
const { renderCompositionLocally } = await import("./local-render.js");
|
|
563
|
-
await renderCompositionLocally({
|
|
563
|
+
const outcome = await renderCompositionLocally({
|
|
564
564
|
compositionHtml: compositionHtml,
|
|
565
565
|
outputPath,
|
|
566
566
|
stdio: "capture"
|
|
@@ -569,6 +569,11 @@ async function startLocalRender(input) {
|
|
|
569
569
|
record.outputPath = outputPath;
|
|
570
570
|
record.endedAt = Date.now();
|
|
571
571
|
console.log(`[vidfarm] local render ${renderId} finished (${((record.endedAt - record.startedAt) / 1000).toFixed(1)}s) → ${outputPath}`);
|
|
572
|
+
// The editor keeps the file (a still card is a legal output here), but a
|
|
573
|
+
// frozen render is almost always a bug and must not pass silently.
|
|
574
|
+
if (outcome.motion?.frozen) {
|
|
575
|
+
console.warn(`[vidfarm] ⚠ render ${renderId} NEVER MOVES — ${outcome.motion.reason} Check for window.__player/__hf assignments and a registered window.__timelines entry (vidfarm lint).`);
|
|
576
|
+
}
|
|
572
577
|
}
|
|
573
578
|
catch (error) {
|
|
574
579
|
record.status = "FAILED";
|
|
@@ -638,7 +643,7 @@ async function startStudioRender(input) {
|
|
|
638
643
|
void (async () => {
|
|
639
644
|
try {
|
|
640
645
|
const { renderCompositionLocally } = await import("./local-render.js");
|
|
641
|
-
await renderCompositionLocally({
|
|
646
|
+
const outcome = await renderCompositionLocally({
|
|
642
647
|
compositionHtml,
|
|
643
648
|
outputPath,
|
|
644
649
|
fps,
|
|
@@ -650,6 +655,9 @@ async function startStudioRender(input) {
|
|
|
650
655
|
job.stage = "rendering";
|
|
651
656
|
}
|
|
652
657
|
});
|
|
658
|
+
if (outcome.motion?.frozen) {
|
|
659
|
+
console.warn(`[vidfarm] ⚠ studio render ${job.filename} NEVER MOVES — ${outcome.motion.reason} Check for window.__player/__hf assignments and a registered window.__timelines entry (vidfarm lint).`);
|
|
660
|
+
}
|
|
653
661
|
// A cancel marks the job terminal for the UI but cannot abort the child
|
|
654
662
|
// process — don't resurrect a cancelled job when it eventually finishes.
|
|
655
663
|
if (job.status === "rendering") {
|
|
@@ -20,6 +20,8 @@ import os from "node:os";
|
|
|
20
20
|
import path from "node:path";
|
|
21
21
|
import { prepareProjectMediaForRender } from "../lib/render-media-prep.js";
|
|
22
22
|
import { resolveBundledFfprobe } from "../lib/ffprobe-path.js";
|
|
23
|
+
import { describeEngineGlobalHits, findEngineOwnedGlobalAssignments } from "../lib/engine-globals.js";
|
|
24
|
+
import { checkRenderMotion } from "../lib/frozen-render.js";
|
|
23
25
|
import { runHyperframesCommand } from "./hyperframes-cli.js";
|
|
24
26
|
import { normalizeTikTokCaptionLayout } from "./composition-edit.js";
|
|
25
27
|
// Same invariant as the backend's forceEvenCompositionDimensions: libx264
|
|
@@ -70,6 +72,16 @@ export async function renderCompositionLocally(input) {
|
|
|
70
72
|
if (!input.compositionHtml.includes("data-composition-id=")) {
|
|
71
73
|
throw new Error("Local render requires composition HTML with data-composition-id.");
|
|
72
74
|
}
|
|
75
|
+
// ── Preflight: engine-owned globals ─────────────────────────────────────────
|
|
76
|
+
// Refuse BEFORE spending 45 seconds producing a still image. This is the one
|
|
77
|
+
// authoring mistake whose render "succeeds" in every observable way, so the
|
|
78
|
+
// only place it can be stopped is here.
|
|
79
|
+
if (!input.allowEngineGlobals) {
|
|
80
|
+
const hits = findEngineOwnedGlobalAssignments(input.compositionHtml);
|
|
81
|
+
if (hits.length > 0) {
|
|
82
|
+
throw new Error(`Refusing to render: ${describeEngineGlobalHits(hits)}`);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
73
85
|
const startedAt = Date.now();
|
|
74
86
|
const projectDir = await mkdtemp(path.join(os.tmpdir(), "vidfarm-local-render-"));
|
|
75
87
|
let prepNotes = [];
|
|
@@ -121,13 +133,27 @@ export async function renderCompositionLocally(input) {
|
|
|
121
133
|
if (!existsSync(input.outputPath)) {
|
|
122
134
|
throw new Error(`hyperframes render reported success but wrote no file at ${input.outputPath}`);
|
|
123
135
|
}
|
|
136
|
+
// ── Post-render: does it actually move? ──────────────────────────────────
|
|
137
|
+
// hyperframes downgrades `sub_timeline_readiness_timeout` to a warning and
|
|
138
|
+
// writes the file anyway, so a correct-looking exit 0 is not evidence of a
|
|
139
|
+
// video. One cheap ffmpeg pass settles it. Never throws — a missing ffmpeg
|
|
140
|
+
// comes back `skipped` rather than failing a render that is probably fine.
|
|
141
|
+
let motion;
|
|
142
|
+
if (!input.skipMotionCheck) {
|
|
143
|
+
motion = await checkRenderMotion(input.outputPath);
|
|
144
|
+
if (motion.frozen)
|
|
145
|
+
log("local_render.frozen", { output: input.outputPath, ...motion });
|
|
146
|
+
else if (motion.skipped)
|
|
147
|
+
log("local_render.motion_unverified", { reason: motion.reason });
|
|
148
|
+
}
|
|
124
149
|
const durationMs = Date.now() - startedAt;
|
|
125
|
-
log("local_render.succeeded", { output: input.outputPath, duration_ms: durationMs });
|
|
150
|
+
log("local_render.succeeded", { output: input.outputPath, duration_ms: durationMs, frozen: motion?.frozen ?? null });
|
|
126
151
|
return {
|
|
127
152
|
outputPath: input.outputPath,
|
|
128
153
|
projectDir: input.keepProjectDir ? projectDir : null,
|
|
129
154
|
durationMs,
|
|
130
|
-
prepNotes
|
|
155
|
+
prepNotes,
|
|
156
|
+
...(motion ? { motion } : {})
|
|
131
157
|
};
|
|
132
158
|
}
|
|
133
159
|
finally {
|