@officexapp/vidfarm-devcli 0.21.36 → 0.21.38
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/references/automation-and-local-dev.md +1 -0
- package/SKILL.md +3 -0
- package/crowdsourcing.md +66 -0
- package/dist/src/cli.js +298 -0
- package/dist/src/devcli/experiments.js +685 -0
- package/dist/src/devcli/local-frontend-server.js +92 -6
- package/experiment.md +327 -0
- package/package.json +4 -1
|
@@ -235,6 +235,24 @@ function serveAsset(res, pathname) {
|
|
|
235
235
|
return true;
|
|
236
236
|
}
|
|
237
237
|
// ── reverse proxy to the cloud host ─────────────────────────────────────────
|
|
238
|
+
/** How long to wait for upstream RESPONSE HEADERS before giving up (504). */
|
|
239
|
+
const PROXY_HEADERS_TIMEOUT_MS = 30_000;
|
|
240
|
+
function errorText(error) {
|
|
241
|
+
return error instanceof Error ? error.message : String(error);
|
|
242
|
+
}
|
|
243
|
+
/** Stream a finished render file. Same rule as the proxy pipe: a disk read
|
|
244
|
+
* error (file deleted mid-download, bad sector) must fail THIS response, not
|
|
245
|
+
* raise an uncaught 'error' that takes the whole serve process down. */
|
|
246
|
+
function pipeFile(res, filePath) {
|
|
247
|
+
const stream = createReadStream(filePath);
|
|
248
|
+
stream.on("error", (error) => {
|
|
249
|
+
console.warn(`[vidfarm] serve: could not stream ${filePath} (${errorText(error)})`);
|
|
250
|
+
res.destroy();
|
|
251
|
+
});
|
|
252
|
+
res.on("close", () => stream.destroy());
|
|
253
|
+
res.on("error", () => stream.destroy());
|
|
254
|
+
stream.pipe(res);
|
|
255
|
+
}
|
|
238
256
|
// Hop-by-hop headers are stripped so we forward a clean request/response.
|
|
239
257
|
const HOP_BY_HOP = new Set([
|
|
240
258
|
"connection", "keep-alive", "proxy-authenticate", "proxy-authorization",
|
|
@@ -267,15 +285,40 @@ async function proxyToCloud(req, res, host, pathWithSearch, auth, bodyOverride)
|
|
|
267
285
|
const method = req.method ?? "GET";
|
|
268
286
|
const body = method === "GET" || method === "HEAD" ? undefined : (bodyOverride ?? await readBody(req));
|
|
269
287
|
let upstream;
|
|
288
|
+
// Bound the wait for RESPONSE HEADERS only. A cloud route that never answers
|
|
289
|
+
// used to hang the browser forever with no error — the page shell paints from
|
|
290
|
+
// disk, then a data call spins for good. The BODY stays unbounded on purpose:
|
|
291
|
+
// proxied SSE (/api/v1/editor-chat) and media downloads are long-lived by
|
|
292
|
+
// design, so a body deadline would cut healthy streams.
|
|
293
|
+
const headersAbort = new AbortController();
|
|
294
|
+
const headersTimer = setTimeout(() => headersAbort.abort(), PROXY_HEADERS_TIMEOUT_MS);
|
|
270
295
|
try {
|
|
271
|
-
upstream = await fetch(target, {
|
|
296
|
+
upstream = await fetch(target, {
|
|
297
|
+
method,
|
|
298
|
+
headers,
|
|
299
|
+
body: body,
|
|
300
|
+
redirect: "manual",
|
|
301
|
+
signal: headersAbort.signal
|
|
302
|
+
});
|
|
272
303
|
}
|
|
273
304
|
catch (error) {
|
|
274
|
-
|
|
305
|
+
const timedOut = headersAbort.signal.aborted;
|
|
306
|
+
res.statusCode = timedOut ? 504 : 502;
|
|
275
307
|
res.setHeader("content-type", "application/json");
|
|
276
|
-
res.end(JSON.stringify({
|
|
308
|
+
res.end(JSON.stringify({
|
|
309
|
+
error: timedOut ? "upstream_timeout" : "upstream_unreachable",
|
|
310
|
+
detail: timedOut
|
|
311
|
+
? `${host} sent no response headers within ${PROXY_HEADERS_TIMEOUT_MS / 1000}s`
|
|
312
|
+
: error instanceof Error ? error.message : String(error),
|
|
313
|
+
host
|
|
314
|
+
}));
|
|
277
315
|
return;
|
|
278
316
|
}
|
|
317
|
+
finally {
|
|
318
|
+
// Headers are in (or the request failed) — from here the body streams for
|
|
319
|
+
// as long as it needs to.
|
|
320
|
+
clearTimeout(headersTimer);
|
|
321
|
+
}
|
|
279
322
|
res.statusCode = upstream.status;
|
|
280
323
|
upstream.headers.forEach((value, key) => {
|
|
281
324
|
if (HOP_BY_HOP.has(key.toLowerCase()))
|
|
@@ -287,7 +330,30 @@ async function proxyToCloud(req, res, host, pathWithSearch, auth, bodyOverride)
|
|
|
287
330
|
res.setHeader("x-vidfarm-upstream", host);
|
|
288
331
|
if (upstream.body) {
|
|
289
332
|
// Stream the response (covers SSE from /api/v1/editor-chat too).
|
|
290
|
-
|
|
333
|
+
//
|
|
334
|
+
// An upstream body can break AFTER the headers arrive: a CloudFront/Lambda
|
|
335
|
+
// connection reset, or undici's own body timeout on a proxied stream that
|
|
336
|
+
// idles. That surfaces as an 'error' on this stream — and an UNHANDLED
|
|
337
|
+
// 'error' is an uncaught exception, which killed the entire serve process.
|
|
338
|
+
// The visible symptom was the reported one: the editor page paints, the
|
|
339
|
+
// server dies mid-load, and every remaining request hangs forever. So both
|
|
340
|
+
// ends of the pipe get error handling, and each end tears the other down.
|
|
341
|
+
const stream = Readable.fromWeb(upstream.body);
|
|
342
|
+
stream.on("error", (error) => {
|
|
343
|
+
console.warn(`[vidfarm] serve: upstream stream broke for ${pathWithSearch} (${errorText(error)})`);
|
|
344
|
+
// Headers are already on the wire, so there is no status left to send —
|
|
345
|
+
// cut the response so the browser reports a failed request instead of
|
|
346
|
+
// waiting on a body that will never arrive.
|
|
347
|
+
res.destroy();
|
|
348
|
+
});
|
|
349
|
+
// The browser navigating away or cancelling must not leave us pulling the
|
|
350
|
+
// upstream body forever.
|
|
351
|
+
res.on("close", () => stream.destroy());
|
|
352
|
+
res.on("error", (error) => {
|
|
353
|
+
console.warn(`[vidfarm] serve: client stream broke for ${pathWithSearch} (${errorText(error)})`);
|
|
354
|
+
stream.destroy();
|
|
355
|
+
});
|
|
356
|
+
stream.pipe(res);
|
|
291
357
|
}
|
|
292
358
|
else {
|
|
293
359
|
res.end();
|
|
@@ -709,8 +775,28 @@ function renderLoginRequiredPage(pathname) {
|
|
|
709
775
|
</html>
|
|
710
776
|
`;
|
|
711
777
|
}
|
|
778
|
+
// Last-resort net. Every known stream path now handles its own errors, but a
|
|
779
|
+
// local dev server must never disappear on a stray async throw — the user sees
|
|
780
|
+
// only a page that stopped loading, with no clue the server is gone. Log it
|
|
781
|
+
// loudly and stay up; this process is a stateless proxy plus a static file
|
|
782
|
+
// server, so there is no shared state left corrupt by a failed request.
|
|
783
|
+
let crashGuardInstalled = false;
|
|
784
|
+
function installServeCrashGuard() {
|
|
785
|
+
if (crashGuardInstalled)
|
|
786
|
+
return;
|
|
787
|
+
crashGuardInstalled = true;
|
|
788
|
+
process.on("uncaughtException", (error) => {
|
|
789
|
+
console.error(`[vidfarm] serve: recovered from an uncaught error — ${errorText(error)}`);
|
|
790
|
+
if (error instanceof Error && error.stack)
|
|
791
|
+
console.error(error.stack);
|
|
792
|
+
});
|
|
793
|
+
process.on("unhandledRejection", (reason) => {
|
|
794
|
+
console.error(`[vidfarm] serve: recovered from an unhandled rejection — ${errorText(reason)}`);
|
|
795
|
+
});
|
|
796
|
+
}
|
|
712
797
|
export function startLocalFrontendServer(opts) {
|
|
713
798
|
const host = opts.host.replace(/\/+$/, "");
|
|
799
|
+
installServeCrashGuard();
|
|
714
800
|
const server = createServer((req, res) => {
|
|
715
801
|
void handleRequest(req, res, host, opts.auth).catch((error) => {
|
|
716
802
|
if (!res.headersSent) {
|
|
@@ -816,7 +902,7 @@ async function handleRequest(req, res, host, auth) {
|
|
|
816
902
|
res.setHeader("content-type", "video/mp4");
|
|
817
903
|
res.setHeader("content-length", String(statSync(record.outputPath).size));
|
|
818
904
|
res.setHeader("content-disposition", `inline; filename="vidfarm-${path.basename(record.outputPath)}"`);
|
|
819
|
-
|
|
905
|
+
pipeFile(res, record.outputPath);
|
|
820
906
|
return;
|
|
821
907
|
}
|
|
822
908
|
// 3b. Studio-contract render endpoints (the sealed StudioApp's Export
|
|
@@ -909,7 +995,7 @@ async function handleRequest(req, res, host, auth) {
|
|
|
909
995
|
res.setHeader("content-type", STUDIO_RENDER_MIME[path.extname(job.outputPath)] ?? "video/mp4");
|
|
910
996
|
res.setHeader("content-length", String(statSync(job.outputPath).size));
|
|
911
997
|
res.setHeader("content-disposition", `inline; filename="${filename}"`);
|
|
912
|
-
|
|
998
|
+
pipeFile(res, job.outputPath);
|
|
913
999
|
return;
|
|
914
1000
|
}
|
|
915
1001
|
}
|
package/experiment.md
ADDED
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
# Experiment.md
|
|
2
|
+
|
|
3
|
+
How to run ad experiments on vidfarm.cc.
|
|
4
|
+
|
|
5
|
+
**You are an AI agent, and from here on you are a viral short-form scientist and a direct-response ads practitioner.** Your job is not "make a nice video." Your job is to **find the winning ad** — the one creative that best produces the outcome your human actually wants — and then to find out *why* it won so you can build more like it.
|
|
6
|
+
|
|
7
|
+
Two habits separate a scientist from a video editor:
|
|
8
|
+
|
|
9
|
+
1. You **change one thing at a time** when you are trying to learn, and you change **everything at once** when you are trying to discover.
|
|
10
|
+
2. You **write down what you posted and what came back**, every single time, in `EXPERIMENTS_DIARY.md`. An experiment nobody logged is an experiment that never happened.
|
|
11
|
+
|
|
12
|
+
Everything below is the method. Read it all before you plan the first epoch — the plan is the part the user approves, and a bad plan wastes a week of posting capacity.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## 0. The interview — do this first, it takes five minutes
|
|
17
|
+
|
|
18
|
+
Do not open the editor yet. You cannot size an experiment without these answers.
|
|
19
|
+
|
|
20
|
+
**a) What is the desired outcome (the KPI)?** Ask directly: *"When this works, what number went up?"*
|
|
21
|
+
|
|
22
|
+
The four core metrics, in funnel order:
|
|
23
|
+
|
|
24
|
+
| Metric | What it really tells you |
|
|
25
|
+
|---|---|
|
|
26
|
+
| **views** | the algorithm likes the video enough to keep serving it — a distribution signal, not an interest signal |
|
|
27
|
+
| **comments** | interest gauge, and a traffic hoop the viewer chose to jump through — the richest early-stage intel |
|
|
28
|
+
| **clicks** | traffic intent — they left the app for you |
|
|
29
|
+
| **buys** | the verified win, and the slowest, rarest event |
|
|
30
|
+
|
|
31
|
+
Bookmarks, shares, watch-through, follows and profile visits are all good supporting signals. Do not over-index on any list, including this one — **any metric can be the north star, it depends on what the user wants.** Sometimes views alone is genuinely enough.
|
|
32
|
+
|
|
33
|
+
**Default recommendation: optimize for comments.** Most Vidfarm customers are founders, and comments are the cheapest early-stage intel that exists — people tell you their objection, their use case and their price sensitivity in public, unprompted. Views tell you the algorithm approved; comments tell you *humans* did.
|
|
34
|
+
|
|
35
|
+
**If the user says "sales", push back once — politely, then obey.** Buys are an end-of-funnel event. At small traffic you will get zero-to-two buys per video, which is statistically indistinguishable from noise, so every round teaches you nothing and the whole evolution stalls. Say exactly that, and propose views/comments/clicks until traffic is real. Once they have volume, switching the north star to buys is correct and you should do it. If they hear the argument and still want buys, that is their call — run it and log it.
|
|
36
|
+
|
|
37
|
+
**b) Which platforms and how many channels?** Ask for a list, not a yes/no. Short-form distribution today includes TikTok, YouTube Shorts, Instagram Reels, Facebook Reels, LinkedIn, X (Twitter) video, Snapchat Spotlight, Pinterest Idea Pins, and anything else they hold.
|
|
38
|
+
|
|
39
|
+
**Channel count is the single most important number in this whole document, because it is your testing capacity.** One channel can carry about one test video per day without looking spammy. So:
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
capacity per epoch ≈ number of channels
|
|
43
|
+
epochs per round = ceil(videos in the round / capacity per epoch)
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Example — four channels (2× TikTok, 1× YouTube Shorts, 1× Instagram):
|
|
47
|
+
|
|
48
|
+
- capacity = **4 videos/day**
|
|
49
|
+
- a round of 8 angles = 8 / 4 = **2 epochs (2 days)**
|
|
50
|
+
|
|
51
|
+
Example — twelve channels, same round of 8 angles:
|
|
52
|
+
|
|
53
|
+
- 8 slots go to the angle experiment
|
|
54
|
+
- **4 slots are still empty**, so run a second experiment in parallel the same day
|
|
55
|
+
|
|
56
|
+
**c) What are we selling, and to whom?** If they cannot answer in one sentence, do not guess — run the Vidfarm consultation first (`https://vidfarm.cc/skill.md` → the `brainstorm/*` chain: cold-start interview → awareness stages → angles → hooks). That chain exists precisely to produce the raw material this document then tests.
|
|
57
|
+
|
|
58
|
+
**d) Who does the editing?** Their own agent (you), or a distributed task force of gigworkers. This decides the mode — see §3.
|
|
59
|
+
|
|
60
|
+
**e) How long is this campaign?** Experiments run for weeks or months, not one afternoon. Say so up front so the user expects an evolution, not a single delivery.
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## 1. Vocabulary — use these words exactly
|
|
65
|
+
|
|
66
|
+
- **epoch** — one posting cycle. **Typically a day.** For slower or higher-stakes tests it can be a week or a month. One epoch = one batch of videos going live across the available channels.
|
|
67
|
+
- **round** — one experiment on one variable, start to finish. A round may span several epochs, because the round may contain more videos than you have channels.
|
|
68
|
+
- **capacity** — videos you can post per epoch = your channel count. Your evolutionary speed is capacity, nothing else.
|
|
69
|
+
- **composition params** — the knobs a short-form ad is built out of (see §2). Every one of them is either a **constant** or a **variable** in a structured round.
|
|
70
|
+
- **baseline checkpoint** — the current best-known configuration. Every round starts from it and, if the round wins, replaces it.
|
|
71
|
+
- **outlier** — a video that beat its round's median by a wide margin on the north-star metric. Outliers are the raw material of the next structured round.
|
|
72
|
+
|
|
73
|
+
---
|
|
74
|
+
|
|
75
|
+
## 2. The composition params
|
|
76
|
+
|
|
77
|
+
These are the dimensions an ad varies along. Hold them constant or make them the variable:
|
|
78
|
+
|
|
79
|
+
- **short-form video format** — kinetic captions over b-roll, talking head, process footage, satisfying footage, POV quote aesthetic, greenscreen reaction, slideshow, skit, demo screen-capture, street interview…
|
|
80
|
+
- **selling angle** — which problem, promise or identity the ad is sold on (and at which awareness level)
|
|
81
|
+
- **written hook** — the first on-screen line
|
|
82
|
+
- **visual hook** — what is on screen in the first frame and first second
|
|
83
|
+
- **curiosity loop** — the open question you plant, and when it closes
|
|
84
|
+
- **payoff reward** — what the viewer actually receives for staying
|
|
85
|
+
- **engagement bait** — the one ask (comment prompt, poll, "wrong answers only", link-in-bio)
|
|
86
|
+
- **and arbitrarily more** — pacing, voice (own / AI / silent), music, length, caption identity, CTA wording, aspect, first-comment copy. Add params as you learn what moves your KPI. This list is a starting kit, not a schema.
|
|
87
|
+
|
|
88
|
+
Two Vidfarm systems already encode most of this and you should reuse them instead of inventing vocabulary: the **four charges** (hook / loop / payoff / bait) in `references/hooks-and-virality.md`, and the **HARNESS.md** format, which is exactly "the constants, written down." A harness *is* the constants of a structured round — `vidfarm harness init short-form --out ./work/HARNESS.md`, then `vidfarm qa ./work --harness <name|path>` to hold the batch to it.
|
|
89
|
+
|
|
90
|
+
### The default baseline checkpoint: awareness levels
|
|
91
|
+
|
|
92
|
+
When you know nothing, the honest position is: **we do not know whether this product sells better problem-unaware, problem-aware, solution-aware, product-aware or most-aware.** That uncertainty is the biggest one on the board and it is upstream of everything else — the hook, the format and the bait all change when awareness changes.
|
|
93
|
+
|
|
94
|
+
So the **default first structured experiment is angle, spanning the awareness levels**:
|
|
95
|
+
|
|
96
|
+
- **variable:** selling angle (one per awareness level, or several per level)
|
|
97
|
+
- **constants:** format, written-hook pattern, visual hook, loop, payoff, bait, voice, length
|
|
98
|
+
|
|
99
|
+
That is the strong starting checkpoint. It is not arbitrary — it is the cheapest way to eliminate the largest unknown.
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## 3. The two modes
|
|
104
|
+
|
|
105
|
+
### Creative Mode — **the default**
|
|
106
|
+
|
|
107
|
+
Test **N videos regardless of their composition params.** Every video may differ in format, hook, angle, pacing, everything. You are not isolating a variable; you are **searching a wide space fast** for outliers.
|
|
108
|
+
|
|
109
|
+
- **Best for:** new users, cold starts, any product with no performance history at all.
|
|
110
|
+
- **Editing:** thrives with **distributed task forces** — other people's AI agents (gigworkers) doing the editing. Variance between editors is a *feature* here: it widens the search. Set up the loop from `https://vidfarm.cc/crowdsourcing.md` (client POV — a Dollar Platoon vending machine, one task per video, batches of 7 because that is a week of content).
|
|
111
|
+
- **Typical shape:** the user brings the selling angles; the gigworkers take creative liberty on everything else. You get diverse videos, and diversity gets you to an outlier fastest.
|
|
112
|
+
- **What you learn:** *that* something works. Not yet why.
|
|
113
|
+
|
|
114
|
+
**Creative Mode is the default, but confirm with the user before assuming it.** If they already have a proven winner and want to sharpen it, structured is correct from the start.
|
|
115
|
+
|
|
116
|
+
### Structured Mode
|
|
117
|
+
|
|
118
|
+
Hold **N params constant and vary ideally exactly one.** Everything else is pinned by a written harness.
|
|
119
|
+
|
|
120
|
+
- **Best for:** reverse-engineering a known outlier, and squeezing a proven concept.
|
|
121
|
+
- **Editing:** **your own AI agents**, not a task force. Structured rounds do **not** survive distributed delivery — the between-editor variance is far larger than the effect you are trying to measure, so a "constant" that ten different agents each interpret differently is not a constant, and the round returns noise. If you must distribute a structured round, ship the identical base fork and let the workers change only the one variable — nothing else.
|
|
122
|
+
- **What you learn:** *why* it works, transferably.
|
|
123
|
+
|
|
124
|
+
**Discipline:** one variable per round. Two variables is not "more efficient", it is an unreadable result. If you truly need to move two, run them as **two parallel rounds** on separate channel slots (§4), not as one mixed round.
|
|
125
|
+
|
|
126
|
+
### The distilled loop — the whole method in five lines
|
|
127
|
+
|
|
128
|
+
1. Quick interview with the user to determine **selling angles** worth testing.
|
|
129
|
+
2. Run **Creative Mode** with gigworkers, in bulk, to get many creative treatments of those angles.
|
|
130
|
+
3. Publish, and watch for **outliers** — with **comments** as the metric you optimize for (early-stage intel).
|
|
131
|
+
4. Run **Structured Mode** on the outliers to reverse-engineer what makes them win, then push that further.
|
|
132
|
+
5. Repeat, until you hold a **library of winning ads** you can re-cut and re-run indefinitely.
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## 4. Running the evolution
|
|
137
|
+
|
|
138
|
+
### Filling an epoch
|
|
139
|
+
|
|
140
|
+
Capacity is a budget you spend every day, and unspent capacity is gone forever. If a round does not fill the day, fill the rest with something.
|
|
141
|
+
|
|
142
|
+
Worked example — **twelve channels**, day 1:
|
|
143
|
+
|
|
144
|
+
| slots | experiment | variable | constants |
|
|
145
|
+
|---|---|---|---|
|
|
146
|
+
| 8 | Round A | selling angle (8 angles across awareness levels) | format, hooks, loop, payoff, bait |
|
|
147
|
+
| 4 | Round B | video format (4 formats) | angle (pinned to the current best), hooks, loop, payoff, bait |
|
|
148
|
+
|
|
149
|
+
Both rounds finish in **one epoch**. Day 2 you read results: say **3 angles win** and **1 format clearly wins**.
|
|
150
|
+
|
|
151
|
+
Now you **combine**: 3 winning angles × 1 winning format = **3 videos** for the next round. That is your new baseline checkpoint being tested. But it only consumes 3 of 12 slots — **9 are free**. Choose deliberately:
|
|
152
|
+
|
|
153
|
+
- **more angle data** — re-run angle variants for a larger sample and more confidence in the 3 winners
|
|
154
|
+
- **a new variable** — start a hooks round, or a bait round, on top of the new baseline
|
|
155
|
+
- **one-off theories** — mixed-variable long shots, a wild format, a competitor's structure. Log them as one-offs so they never contaminate a structured round's read.
|
|
156
|
+
|
|
157
|
+
**If the user asks "what should I test next?", the answer is almost always formats or hooks.** They are the two params with the biggest measured effect after angle, and both are cheap to vary.
|
|
158
|
+
|
|
159
|
+
### Sample size and honesty
|
|
160
|
+
|
|
161
|
+
Short-form results are noisy. Guardrails:
|
|
162
|
+
|
|
163
|
+
- **Never** promote a winner off one post. Two posts minimum per variant before you believe it, three is better.
|
|
164
|
+
- Compare within the **same epoch and same platform** where you can — the algorithm's mood is not constant across days or apps.
|
|
165
|
+
- A variant with 3× the median on the north star is an outlier worth pursuing. A variant 20% above median is noise. Say which one you are looking at.
|
|
166
|
+
- Record **losers** as carefully as winners. "Problem-unaware never worked here" is a real, reusable finding.
|
|
167
|
+
- Report what you **measured** separately from what you **judge**. Never dress up a hunch as a result.
|
|
168
|
+
|
|
169
|
+
### Publishing hygiene
|
|
170
|
+
|
|
171
|
+
Posting one render to several channels is exactly the case platform de-duplication punishes — the second copy gets suppressed and your experiment records a false loser. So for any video going to more than one channel:
|
|
172
|
+
|
|
173
|
+
```bash
|
|
174
|
+
vidfarm dedupe ./out.mp4 --variants 4 # free, local ffmpeg, no re-render
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
One variant per channel, and never the same variant on two accounts. Ask about this **before** the batch, not after.
|
|
178
|
+
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
## 5. The default video format — for speed
|
|
182
|
+
|
|
183
|
+
**Speed to productivity matters more than polish at the start.** If an experiment is ready but no format is chosen yet, **do not stall on the format decision.** Default to **kinetic captions over easy visuals**:
|
|
184
|
+
|
|
185
|
+
- b-roll footage
|
|
186
|
+
- talking head (only if the user is willing to film themselves)
|
|
187
|
+
- process footage
|
|
188
|
+
- loop background footage
|
|
189
|
+
- satisfying footage
|
|
190
|
+
- lifestyle footage
|
|
191
|
+
- POV quote aesthetic
|
|
192
|
+
|
|
193
|
+
All seven are sourceable for ~$0 from the free public raws catalog — `vidfarm public-raws --categories`, then `--category <shelf>`. A shelf is also a ready-made clip pool for fanning one composition into N variants.
|
|
194
|
+
|
|
195
|
+
Audio, in ascending order of quality and effort:
|
|
196
|
+
|
|
197
|
+
1. **Pure captions, no audio** — music added at posting time inside the platform's own editor (this also gets you the platform's trending-audio boost, for free)
|
|
198
|
+
2. **AI voiceover** — `vidfarm tts "…"` (free local Kokoro in `minimize`/`hybrid`)
|
|
199
|
+
3. **The user's own voice** — ideal. Real voice out-performs synthetic on nearly every KPI. Ask for it.
|
|
200
|
+
|
|
201
|
+
Caption regime, safe zones and the no-HTML-slop standard all still apply — see the Non-negotiables in `https://vidfarm.cc/skill.md`. An experiment does not license slop.
|
|
202
|
+
|
|
203
|
+
---
|
|
204
|
+
|
|
205
|
+
## 6. Plan first, get approval, only then produce
|
|
206
|
+
|
|
207
|
+
This is the sequence. Do not skip forward.
|
|
208
|
+
|
|
209
|
+
**Step 1 — write the plan.** Present it to the user *before* any editing. It must contain:
|
|
210
|
+
|
|
211
|
+
- the **north-star metric**, and why (including the push-back if they said "sales")
|
|
212
|
+
- the **channel inventory** and the resulting capacity per epoch
|
|
213
|
+
- the **mode** (creative or structured) and why
|
|
214
|
+
- the **epochs and rounds**: what runs on which day, in which slots
|
|
215
|
+
- per round: the **variable**, the **constants**, and the **justification for the priority** — why this variable is worth the capacity before the others
|
|
216
|
+
- what a **win** looks like numerically, decided *before* posting
|
|
217
|
+
- who edits: you, or a gigworker task force
|
|
218
|
+
|
|
219
|
+
**Step 2 — get explicit approval.** A plan is free to redo. A batch of rendered videos is not.
|
|
220
|
+
|
|
221
|
+
**Step 3 — only after approval, break every video out into its own standalone handoff document.** One document per video. Each one must be **complete on its own** — the actual production work in the Vidfarm devcli editor runs over many hours, often asynchronously, and possibly by a distributed task force who have none of your context and cannot ask you a question.
|
|
222
|
+
|
|
223
|
+
A handoff document contains: the angle, the exact written hook, the visual hook, the loop, the payoff, the bait, the format, the length, the audio choice, the sourcing hints, the constants it must not touch, and the round + slot it belongs to. Mint the sourcing halves with `vidfarm handoff image --theme "<what>" --items "a,b,c"` and `vidfarm handoff raws --keywords "<kw>,<kw>"`, and pin the constants with a `HARNESS.md`.
|
|
224
|
+
|
|
225
|
+
**Static is the requirement.** If a handoff document needs a follow-up conversation to be actionable, it is not a handoff document.
|
|
226
|
+
|
|
227
|
+
---
|
|
228
|
+
|
|
229
|
+
## 7. `EXPERIMENTS_DIARY.md` — the source of truth
|
|
230
|
+
|
|
231
|
+
**Keep this file. It is not optional, and it is not a summary of the chat.** It is a standalone, decoupled record that outlives every session, and it is what lets you decide the next round correctly weeks from now.
|
|
232
|
+
|
|
233
|
+
Create it at the root of the work directory on the first epoch, and append to it forever. Never rewrite history in it — append corrections.
|
|
234
|
+
|
|
235
|
+
Results arrive through several channels, and the diary does not care which:
|
|
236
|
+
|
|
237
|
+
- the **user reports manually** — screenshots or numbers pasted in chat
|
|
238
|
+
- **flockposter.com** — pulled automatically when the accounts are connected (see the `flockposter` skill)
|
|
239
|
+
- the **vidfarm.cc email channel scheduling** flow
|
|
240
|
+
- **gigworkers** reporting on the client's behalf
|
|
241
|
+
|
|
242
|
+
Because sources vary, always record **where a number came from and when it was read.** A 24-hour view count and a 7-day view count are different measurements and must never be compared as if they were the same.
|
|
243
|
+
|
|
244
|
+
### Format
|
|
245
|
+
|
|
246
|
+
````markdown
|
|
247
|
+
# Experiments Diary — <product>
|
|
248
|
+
|
|
249
|
+
## Setup
|
|
250
|
+
- North-star metric: comments (secondary: views, clicks)
|
|
251
|
+
- Channels (capacity 4/epoch): tiktok_a, tiktok_b, yt_shorts_a, ig_a
|
|
252
|
+
- Mode: creative
|
|
253
|
+
- Baseline checkpoint: kinetic captions over b-roll, no VO, 22s, "wrong answers only" bait
|
|
254
|
+
- Analytics source: flockposter (connected 2026-08-15)
|
|
255
|
+
|
|
256
|
+
## Round 1 — selling angle (awareness levels)
|
|
257
|
+
- Mode: structured · Variable: angle · Constants: format, hooks, loop, payoff, bait
|
|
258
|
+
- Justification: awareness level is the largest unknown and is upstream of every other param
|
|
259
|
+
- Videos: 8 · Capacity 4/epoch → 2 epochs
|
|
260
|
+
- Win condition: any angle at ≥3× median comments
|
|
261
|
+
|
|
262
|
+
### Epoch 1 — 2026-08-16
|
|
263
|
+
| slot | video | angle | channel | posted |
|
|
264
|
+
|---|---|---|---|---|
|
|
265
|
+
| 1 | v001 | problem-unaware: "you are losing 10 hrs/wk" | tiktok_a | ✅ |
|
|
266
|
+
| 2 | v002 | solution-aware: "AI editors vs freelancers" | tiktok_b | ✅ |
|
|
267
|
+
| 3 | v003 | product-aware: "vidfarm vs capcut" | yt_shorts_a | ✅ |
|
|
268
|
+
| 4 | v004 | most-aware: "the $0 plan does this" | ig_a | ✅ |
|
|
269
|
+
|
|
270
|
+
### Results — read 2026-08-18, source: flockposter, age: 48h
|
|
271
|
+
| video | views | comments | clicks | buys | note |
|
|
272
|
+
|---|---|---|---|---|---|
|
|
273
|
+
| v001 | 14,200 | 61 | 38 | 0 | **outlier** — comments 4.1× median |
|
|
274
|
+
| v002 | 3,100 | 12 | 9 | 0 | |
|
|
275
|
+
| v003 | 2,800 | 9 | 14 | 1 | |
|
|
276
|
+
| v004 | 1,900 | 4 | 3 | 0 | weakest |
|
|
277
|
+
|
|
278
|
+
**Finding:** problem-unaware wins decisively on comments. Most-aware is dead — stop spending capacity on it.
|
|
279
|
+
**Next:** Round 2 = structured on v001 — hold the angle, vary the written hook ×6.
|
|
280
|
+
````
|
|
281
|
+
|
|
282
|
+
Anything is fine as long as every entry answers: what did we post, what varied, what was held constant, what came back, from where, when, and what did we decide.
|
|
283
|
+
|
|
284
|
+
### If the devcli is installed, do not hand-maintain this file
|
|
285
|
+
|
|
286
|
+
`vidfarm experiment` owns the ledger, the arithmetic and the method lint — nothing else. It does **not** wrap posting, channels, briefs or constants, because those commands already exist.
|
|
287
|
+
|
|
288
|
+
```bash
|
|
289
|
+
vidfarm experiment --init --metric comments --channels "tiktok_a,tiktok_b,yt_a,ig_a"
|
|
290
|
+
vidfarm experiment round --videos 8 --variable angle --mode structured \
|
|
291
|
+
--constants "format,hooks,loop,payoff,bait" --why "awareness is the largest unknown"
|
|
292
|
+
vidfarm experiment log v001 --posted --channel tiktok_a
|
|
293
|
+
vidfarm experiment log v001 --views 14200 --comments 61 --source flockposter --age 48h
|
|
294
|
+
vidfarm experiment # sizing, ranking vs median, outliers, findings
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
Reading is the default; there are exactly two writes, `round` and `log`. `--json` on the read gives you the parsed setup, the per-round analysis and the findings.
|
|
298
|
+
|
|
299
|
+
It sizes the round for you (`8 videos ÷ 4 channels = 2 epochs`), computes the median and marks anything at ≥3× as an outlier, and lints the method: two variables in one structured round, a winner promoted off one post, results read at mixed ages, a structured round handed to gigworkers, unspent capacity, an end-of-funnel north star with no traffic behind it. Feedback, not a gate — it exits 0, like `vidfarm qa`.
|
|
300
|
+
|
|
301
|
+
The rest of the loop stays where it already lives: **`vidfarm channels`** (the channel list, therefore capacity) · **`vidfarm harness`** + **`vidfarm qa`** (the constants, and holding a batch to them) · **`vidfarm handoff`** (the per-video briefs) · **`vidfarm dedupe`** (one variant per channel) · **`vidfarm approve`** + **`vidfarm schedule`** (the actual posting). `experiment log --posted` only *records* that a slot went live; it does not post anything.
|
|
302
|
+
|
|
303
|
+
Also keep the operational lists on disk, not in memory — the channel inventory, and if you are crowdsourcing, `dollarplatoon-gigs.md` (one row per gig: id, client, invite link, video type, price, date last checked). Re-derive them when they look stale.
|
|
304
|
+
|
|
305
|
+
---
|
|
306
|
+
|
|
307
|
+
## 8. Long-running campaigns
|
|
308
|
+
|
|
309
|
+
These run for weeks or months. At the top of every session:
|
|
310
|
+
|
|
311
|
+
1. Read `EXPERIMENTS_DIARY.md` **first**. It, not your memory, is the state.
|
|
312
|
+
2. Read the current baseline checkpoint. Everything you produce this epoch starts from it.
|
|
313
|
+
3. Pull results for anything posted and not yet logged.
|
|
314
|
+
4. Decide the round, then plan → approve → handoff (§6).
|
|
315
|
+
|
|
316
|
+
At the end of every epoch, append the results and the decision. Then update the baseline checkpoint **only if a variant beat it on the north star, with more than one post behind it.**
|
|
317
|
+
|
|
318
|
+
When you accumulate winners, you are building the real deliverable: **a library of proven ads you can re-cut, re-dedupe and re-run indefinitely.** That library — not any single viral video — is what the whole method is for.
|
|
319
|
+
|
|
320
|
+
---
|
|
321
|
+
|
|
322
|
+
## Where to go next
|
|
323
|
+
|
|
324
|
+
- **Making the videos:** `https://vidfarm.cc/skill.md` (and the director skill it points to — hooks, harnesses, review, cost mode, interactive vs autonomous)
|
|
325
|
+
- **Crowdsourcing the editing (Creative Mode, client POV):** `https://vidfarm.cc/crowdsourcing.md`
|
|
326
|
+
- **The agentic clipper loop programme:** `https://vidfarm.cc/clipper.md`
|
|
327
|
+
- **Scheduling and analytics:** the `flockposter` skill, or the Vidfarm email channel scheduling flow
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@officexapp/vidfarm-devcli",
|
|
3
|
-
"version": "0.21.
|
|
3
|
+
"version": "0.21.38",
|
|
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": {
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"dist/src/devcli/cost-mode.js",
|
|
19
19
|
"dist/src/devcli/dedupe-local.js",
|
|
20
20
|
"dist/src/devcli/doctor.js",
|
|
21
|
+
"dist/src/devcli/experiments.js",
|
|
21
22
|
"dist/src/devcli/handoff.js",
|
|
22
23
|
"dist/src/devcli/greenscreen-local.js",
|
|
23
24
|
"dist/src/devcli/hyperframes-cli.js",
|
|
@@ -63,6 +64,8 @@
|
|
|
63
64
|
"SKILL.md",
|
|
64
65
|
"SKILL.director.md",
|
|
65
66
|
"clipper.md",
|
|
67
|
+
"crowdsourcing.md",
|
|
68
|
+
"experiment.md",
|
|
66
69
|
"update.md",
|
|
67
70
|
"!readme.secret.md",
|
|
68
71
|
"!**/*.secret.*"
|