@officexapp/vidfarm-devcli 0.21.39 → 0.21.42

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.
@@ -131,9 +131,14 @@ export const COST_MODE_BLURB = {
131
131
  "bgm|sfx|image|vector|icon|video. Check the KEYLESS sources first: Openverse (CC/CC0 " +
132
132
  "music, SFX, images) and iconify (icons) need no account at all. A free Pixabay key adds " +
133
133
  "photos/vectors/stock-video and may already be saved (check vidfarm provider-keys). " +
134
+ 'For any ICON, STICKER, illustration, 3D prop or Lottie, use vidfarm iconscout "<meaning>" ' +
135
+ "--free before even considering AI image generation: free IconScout assets cost $0 (a credit " +
136
+ "line is the only price) and arrive as clean transparent vectors. " +
134
137
  "Billed cloud/AI generation is refused unless you re-confirm it (--yes).",
135
138
  hybrid: "Hybrid (recommended) — roughly $0.01–$1 per video. Free where it's free, spend AI " +
136
139
  "credits only where they clearly win (a hero shot, a voice you can't fake locally). " +
140
+ 'Never GENERATE an icon, sticker, illustration or 3D prop — vidfarm iconscout "<meaning>" ' +
141
+ "buys a designer's finished asset for a fraction of one AI attempt, with no prompt loop. " +
137
142
  "Billed ops run but each prints its cost so nothing is a surprise. Charges land on " +
138
143
  "your own AI provider keys (BYOK).",
139
144
  "rich-ai": "Rich AI — $1+ per video. AI video generation is spent on REUSABLE greenscreen raws " +
@@ -168,6 +173,9 @@ export function costModeExplainer() {
168
173
  " `vidfarm add-provider-key <provider> <key>` or at vidfarm.cc/settings/developer.",
169
174
  "Tip: before paying to generate music/SFX/images/video, try the free stock catalog —",
170
175
  ' vidfarm media search "<meaning>" --type bgm|sfx|image|vector|icon|video.',
176
+ " For icons, STICKERS, illustrations, 3D props and Lottie, IconScout beats AI generation on price",
177
+ ' and on quality in EVERY mode — vidfarm iconscout "<meaning>" --style sticker (search is free;',
178
+ " free assets cost $0 with a credit line, premium downloads are a few cents on the wallet).",
171
179
  " Check the keyless sources first — Openverse (CC/CC0 music, SFX, images) and iconify (icons)",
172
180
  " need no account. A free Pixabay key adds photos/vectors/stock-video (it may already be saved —",
173
181
  " check `vidfarm provider-keys` or vidfarm.cc/settings/developer; add via `vidfarm add-provider-key pixabay <key>`)."
@@ -26,17 +26,22 @@ export const EXPERIMENT_MODES = ["creative", "structured"];
26
26
  export const CORE_METRICS = ["views", "comments", "clicks", "buys"];
27
27
  export const DEFAULT_METRIC = "comments";
28
28
  /** The starting format menu, offered during planning — NOT discovered at build
29
- * time. Deliberately copywriting-led: in every one of these the words carry the
29
+ * time. **Read the script's grammatical person to pick one**: copy written in the
30
+ * first person ("I tried to…") wants the speaker's own world (POV b-roll, talking
31
+ * head, process); copy written in the third person ("when you try to…") wants a
32
+ * meme recaption — a greenscreen reaction raw over a background image. Either way
33
+ * the finish is the same: TikTok-native type, kinetic captions, free Kokoro VO. Deliberately copywriting-led: in every one of these the words carry the
30
34
  * persuasion and the footage only has to hold attention, which is what makes
31
35
  * them fast, cheap and repeatable. All seven are sourceable for ~$0 from
32
36
  * `vidfarm public-raws --categories`. */
33
37
  export const EASY_FORMATS = [
34
- { key: "b-roll", label: "kinetic captions over b-roll footage", note: "the workhorse start here" },
35
- { key: "talking-head", label: "talking head", note: "only if the user will film themselves; strongest trust signal" },
36
- { key: "process", label: "process footage", note: "high watch-through, needs no narration" },
38
+ { key: "b-roll", label: "kinetic captions over b-roll footage", note: "1st person — the workhorse, start here" },
39
+ { key: "talking-head", label: "talking head", note: "1st person — only if they'll film themselves; strongest trust signal" },
40
+ { key: "process", label: "process footage", note: "1st person — high watch-through, needs no narration" },
41
+ { key: "meme-recaption", label: "meme recaption", note: "3rd person (\"when you try to…\") — greenscreen raw over a background image, blur optional" },
37
42
  { key: "loop-background", label: "loop background footage", note: "cheapest of all; the copy is the entire video" },
38
43
  { key: "satisfying", label: "satisfying footage", note: "strong retention, weak topical fit" },
39
- { key: "lifestyle", label: "lifestyle footage", note: "best for identity and status angles" },
44
+ { key: "lifestyle", label: "lifestyle footage", note: "identity and status angles" },
40
45
  { key: "pov-quote", label: "POV quote aesthetic", note: "pure copywriting; a natural fit for hook tests" }
41
46
  ];
42
47
  /** What to take when the director has no opinion — never stall on this choice. */
@@ -24,7 +24,81 @@
24
24
  //
25
25
  // Pure DOM (linkedom) — no ffmpeg, no network, no Chrome. Local-only: this is a
26
26
  // devcli capability with no cloud/REST equivalent.
27
+ import { createHash } from "node:crypto";
28
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
29
+ import path from "node:path";
27
30
  import { parseHTML } from "linkedom";
31
+ // ── Revision governor ────────────────────────────────────────────────────────
32
+ //
33
+ // `vidfarm qa` is feedback, not a gate — which is exactly what makes it a loop
34
+ // hazard for an autonomous agent: qa lists findings → agent edits → qa lists
35
+ // findings → forever. Some findings are deliberate style choices that will never
36
+ // "clear", so the loop has no natural base case.
37
+ //
38
+ // This is that base case. The governor counts how many times the composition has
39
+ // CHANGED between qa passes (re-running qa on an untouched file is free), and at
40
+ // the limit qa stops handing out new work: it withholds the finding list and says
41
+ // stop. ONE revision is the default: the first pass names the real slop, the fix
42
+ // clears it, and a second round is nearly always an agent polishing its own taste
43
+ // rather than removing a defect. The human raises the ceiling with
44
+ // `--max-revisions <n>` when a video genuinely earns another round.
45
+ export const QA_STATE_FILENAME = ".vidfarm-qa-state.json";
46
+ /** Revisions of one composition before qa stops feeding the loop. */
47
+ export const DEFAULT_MAX_REVISIONS = 1;
48
+ function hashComposition(html) {
49
+ return createHash("sha256").update(html).digest("hex").slice(0, 16);
50
+ }
51
+ /**
52
+ * Record this qa pass and report where the composition stands against the
53
+ * revision limit. `max <= 0` disables the governor entirely.
54
+ *
55
+ * Deliberately forgiving: an unreadable or corrupt state file is treated as a
56
+ * fresh start, never an error — a QA helper must not be the thing that breaks a
57
+ * build.
58
+ */
59
+ export function recordQaPass(htmlPath, html, opts) {
60
+ const max = opts.max ?? DEFAULT_MAX_REVISIONS;
61
+ const workDir = path.dirname(htmlPath);
62
+ const statePath = path.join(workDir, QA_STATE_FILENAME);
63
+ const composition = path.basename(htmlPath);
64
+ const hash = hashComposition(html);
65
+ const disabled = !Number.isFinite(max) || max <= 0;
66
+ let previous = null;
67
+ if (!opts.reset && existsSync(statePath)) {
68
+ try {
69
+ const parsed = JSON.parse(readFileSync(statePath, "utf8"));
70
+ // A different composition in the same folder starts its own count.
71
+ if (parsed && parsed.composition === composition)
72
+ previous = parsed;
73
+ }
74
+ catch {
75
+ previous = null;
76
+ }
77
+ }
78
+ const changed = previous ? previous.hash !== hash : false;
79
+ const revisions = previous ? previous.revisions + (changed ? 1 : 0) : 0;
80
+ const next = {
81
+ composition,
82
+ hash,
83
+ revisions,
84
+ first_seen: previous?.first_seen ?? opts.now,
85
+ last_seen: opts.now
86
+ };
87
+ try {
88
+ writeFileSync(statePath, `${JSON.stringify(next, null, 2)}\n`, "utf8");
89
+ }
90
+ catch {
91
+ // A read-only work dir just means no governor. Don't fail the check.
92
+ }
93
+ return {
94
+ revisions,
95
+ max,
96
+ limit_reached: !disabled && revisions >= max,
97
+ changed_since_last_run: changed,
98
+ state_path: statePath,
99
+ disabled
100
+ };
101
+ }
28
102
  /**
29
103
  * Build the "now go watch it" directive. `dir` is substituted into the commands
30
104
  * so the agent can paste them; the JSON default is the generic placeholder.
@@ -106,6 +106,109 @@ export function readPackDoc(ref, name = DEFAULT_PACK) {
106
106
  const doc = resolvePackDoc(ref, name);
107
107
  return { doc, contents: readFileSync(doc.abs, "utf8") };
108
108
  }
109
+ export const PACK_TOPICS = [
110
+ { topic: "content-ideas", aliases: ["ideas", "idea", "angles", "what-to-post", "content"], doc: "references/content-ideas.md",
111
+ blurb: "The 50-frame angle bank — answer \"what should I post?\" for a whole month" },
112
+ { topic: "meme-recaption", aliases: ["meme", "recaption", "meme-caption"], doc: "references/editor-workflows.md", heading: "Writing a meme recaption",
113
+ blurb: "Recaption a meme at a pain or a win the niche knows — the cold-viewer test" },
114
+ { topic: "product-explainer", aliases: ["product-explainers"], doc: "harnesses/product-explainer.HARNESS.md",
115
+ blurb: "The product-explainer harness — the bundled base for explaining what a product does" },
116
+ { topic: "product-demo", aliases: ["demo"], doc: "harnesses/product-demo.HARNESS.md",
117
+ blurb: "The product-demo harness — showing the product in use" },
118
+ { topic: "explainer-cutouts", aliases: ["cutout", "cutouts", "stickers", "sticker", "explainer"], doc: "recipes/cutout-graphics-for-explainers.md",
119
+ blurb: "Explainers built from transparent cutout stickers — house style, sheets, keying, dark stages" },
120
+ { topic: "hooks", aliases: ["hook", "virality", "viral", "charges"], doc: "references/hooks-and-virality.md",
121
+ blurb: "The four charges — hook, loop, payoff, bait — written BEFORE the timeline" },
122
+ { topic: "density", aliases: ["cut", "cutting", "too-long", "pacing"], doc: "references/hooks-and-virality.md", heading: "Density",
123
+ blurb: "Every second must earn its place — the deletion test and cut-on-sight list" },
124
+ { topic: "captions", aliases: ["caption", "subtitles", "safe-zone"], doc: "references/editor-workflows.md", heading: "TikTok-native caption standard",
125
+ blurb: "The caption standard — safe zone, negative space, font regime, the four valid backgrounds" },
126
+ { topic: "first-frame", aliases: ["thumbnail", "poster"], doc: "references/editor-workflows.md", heading: "The first frame is the thumbnail",
127
+ blurb: "t=0 is the thumbnail everywhere — compose it as a designed still" },
128
+ { topic: "no-slop", aliases: ["slop", "visual-standard", "html-slop"], doc: "references/editor-workflows.md", heading: "Social-native visual standard",
129
+ blurb: "No HTML slop — what a social-native frame may and may not contain" },
130
+ { topic: "blurred-plate", aliases: ["letterbox", "landscape-vertical", "bars"], doc: "references/editor-workflows.md", heading: "The blurred plate",
131
+ blurb: "Landscape footage fullscreen on a vertical canvas, without black bars" },
132
+ { topic: "ken-burns", aliases: ["pan-zoom", "still-motion"], doc: "references/editor-workflows.md", heading: "Ken Burns",
133
+ blurb: "Animate a still image instead of paying for AI video" },
134
+ { topic: "paintbrushes", aliases: ["three-paintbrushes", "replication"], doc: "references/editor-workflows.md", heading: "The three paintbrushes",
135
+ blurb: "Clip reuse vs. image gen vs. AI video — and the two replication harnesses" },
136
+ { topic: "avatar", aliases: ["talking-head", "spokesperson", "presenter"], doc: "references/primitives.md", heading: "Primitive: talking_avatar",
137
+ blurb: "\"Create an avatar\" = a talking head WITH audio, keyed off greenscreen" },
138
+ { topic: "dedupe", aliases: ["deduplicate", "repost", "variants"], doc: "references/primitives.md", heading: "Primitive: media_dedupe",
139
+ blurb: "Deduplicate an exported MP4 before posting it again or to another platform" },
140
+ { topic: "review", aliases: ["reviewing", "judge", "holistic", "qa-review"], doc: "references/reviewing-renders.md",
141
+ blurb: "Judge the WHOLE video before you report it done — never by one frame" },
142
+ { topic: "harness", aliases: ["harnesses", "regime"], doc: "harnesses/README.md",
143
+ blurb: "What a HARNESS.md is, its format, and the three phrasings that mean one" },
144
+ { topic: "bulk", aliases: ["batch", "volume", "scripting", "daily-posting"], doc: "recipes/bulk-scripting-with-a-harness.md",
145
+ blurb: "Produce N different videos in one run — scripting mode with a harness" },
146
+ { topic: "onboarding", aliases: ["consultation", "coldstart", "offer", "strategy"], doc: "references/onboarding.md",
147
+ blurb: "The cold-start interview and the brainstorm/* consultation chain" }
148
+ ];
149
+ export function listPackTopics() {
150
+ return [...PACK_TOPICS].sort((a, b) => (a.topic < b.topic ? -1 : 1));
151
+ }
152
+ /** Match a spoken topic name. Returns null (not an error) so callers can fall back to file lookup. */
153
+ export function resolvePackTopic(ref) {
154
+ const needle = ref.trim().toLowerCase().replace(/[\s_]+/g, "-").replace(/\.md$/, "");
155
+ return PACK_TOPICS.find((entry) => entry.topic === needle || entry.aliases.includes(needle)) ?? null;
156
+ }
157
+ /**
158
+ * Slice one Markdown section out of a doc: from the heading that contains
159
+ * `heading` down to the next heading of the same or higher level. Returns null
160
+ * when the heading is gone (the pack was edited) so the caller can fall back to
161
+ * the whole file rather than printing nothing.
162
+ */
163
+ export function extractSection(contents, heading) {
164
+ const needle = heading.trim().toLowerCase();
165
+ const lines = contents.split("\n");
166
+ const level = (line) => (line.match(/^(#{1,6})\s/)?.[1].length ?? 0);
167
+ const start = lines.findIndex((line) => level(line) > 0 && line.toLowerCase().includes(needle));
168
+ if (start < 0)
169
+ return null;
170
+ const depth = level(lines[start]);
171
+ let end = lines.length;
172
+ for (let i = start + 1; i < lines.length; i += 1) {
173
+ const at = level(lines[i]);
174
+ if (at > 0 && at <= depth) {
175
+ end = i;
176
+ break;
177
+ }
178
+ }
179
+ return { heading: lines[start].replace(/^#+\s*/, "").trim(), body: lines.slice(start, end).join("\n").trimEnd() };
180
+ }
181
+ /** A topic's text, already sliced to its section when it names one. */
182
+ export function readPackTopic(topic, name = DEFAULT_PACK) {
183
+ const { doc, contents } = readPackDoc(topic.doc, name);
184
+ if (!topic.heading)
185
+ return { doc, contents, whole: true };
186
+ const section = extractSection(contents, topic.heading);
187
+ if (!section)
188
+ return { doc, contents, whole: true };
189
+ return { doc, heading: section.heading, contents: section.body, whole: false };
190
+ }
191
+ export function loadIdeaBank(name = DEFAULT_PACK) {
192
+ const { contents } = readPackDoc("references/content-ideas.md", name);
193
+ const section = extractSection(contents, "The 50 frames");
194
+ const lines = (section?.body ?? contents).split("\n");
195
+ const frames = [];
196
+ const families = [];
197
+ let family = "";
198
+ for (const line of lines) {
199
+ const label = line.match(/^\*\*(.+?)\*\*\s*$/);
200
+ if (label) {
201
+ family = label[1].split(/\s+[—-]\s+/)[0].trim();
202
+ if (family && !families.includes(family))
203
+ families.push(family);
204
+ continue;
205
+ }
206
+ const bullet = line.match(/^-\s+(.+?)\s*$/);
207
+ if (bullet && family)
208
+ frames.push({ frame: bullet[1].trim(), family });
209
+ }
210
+ return { frames, families };
211
+ }
109
212
  /**
110
213
  * Grep the pack. This is the affordance that makes a local copy genuinely
111
214
  * better than the network one: "where does it say anything about greenscreen"
package/experiments.md CHANGED
@@ -75,7 +75,7 @@ Example — twelve channels, same round of 8 angles:
75
75
 
76
76
  **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.
77
77
 
78
- **d) Which video format do we start on?** Offer the menu in §5 — the seven copywriting-led formats — and take a pick, or take the default (b-roll with kinetic captions) if they have no opinion. **Ask this in the interview, not at build time**, because the format is what the first round holds constant while it varies the angle. Ask the talking-head question explicitly here too: *"are you willing to be on camera?"* — the answer changes the whole menu, and it is cheap to ask once.
78
+ **d) Which video format do we start on?** Offer the menu in §5 — the copywriting-led formats — and take a pick, or take the default (b-roll with kinetic captions) if they have no opinion. **Ask this in the interview, not at build time**, because the format is what the first round holds constant while it varies the angle. Ask the talking-head question explicitly here too: *"are you willing to be on camera?"* — the answer changes the whole menu, and it is cheap to ask once.
79
79
 
80
80
  **e) Who does the editing?** Their own agent (you), or a distributed task force of gigworkers. This decides the mode — see §3.
81
81
 
@@ -244,8 +244,39 @@ Pick one for the round:
244
244
  | **satisfying footage** | oddly-satisfying visuals | strong retention, weak topical fit |
245
245
  | **lifestyle footage** | aspirational day-in-the-life | best for identity and status angles |
246
246
  | **POV quote aesthetic** | static/slow plate, one quoted line | pure copywriting; a natural fit for hook tests |
247
+ | **meme recaption** | a greenscreen reaction clip over a background image | for 3rd-person copy ("when you try to…"); see the person rule below |
247
248
 
248
- 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.
249
+ All of these 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.
250
+
251
+ ### Pick the PERSON first — it picks the visual for you
252
+
253
+ Before you argue about footage, read the script's opening words. **The grammatical person the copy is written in decides which visual family works**, and getting this backwards is why a good line lands flat.
254
+
255
+ **First person — "I tried to…", "I spent 3 months…", "here's what I learned"**
256
+
257
+ The viewer is being told a personal story, so the visual has to be *the speaker's world*:
258
+
259
+ - **POV b-roll** — hands, screen, desk, walking shots: what the narrator would actually see
260
+ - **talking head** — the strongest version, if the user will film themselves
261
+ - **process footage** — the thing being done, from the doer's angle
262
+ - Lifestyle and satisfying footage work here too, as long as they read as *someone's* footage
263
+
264
+ **Third person — "when you try to…", "nobody tells you that…", "POV: your first…"**
265
+
266
+ This is observed, not confessed, so it wants a **meme recaption**: a reaction/character clip that plays the situation out, with your line written over it.
267
+
268
+ - Pull the clip from the Vidfarm **greenscreen raws** — `vidfarm public-raws --category greenscreen`
269
+ - Key it and place it over a **background image** (`vidfarm remove-greenscreen`, then composite). A screenshot, product shot, or scene photo all work
270
+ - **Blur the background or don't** — blur when the subject must pop or the plate is busy; skip it when the background *is* the joke and needs to be readable
271
+ - The character carries the emotion; your caption carries the claim
272
+
273
+ **Both families share the same finish, every time:**
274
+
275
+ - **TikTok-native type** — TikTok Sans or the Montserrat 700–900 caption regime, never a "designed" web font
276
+ - **Kinetic captions** — 3–5-word cues paged in time, not a static wall of text
277
+ - **Free Kokoro voiceover** — `vidfarm tts "…"` on the local voice, $0 in `minimize`/`hybrid`. Add it by default: a voice raises watch-through even over silent footage, and it costs nothing. Swap in a premium voice or the user's own recording only when a round is specifically testing voice
278
+
279
+ The quick rule: **if the copy says "I", show the speaker's world. If it says "you", show someone else's reaction.**
249
280
 
250
281
  **Speed to productivity beats polish at the start.** If the director has no opinion, do not stall: take **b-roll footage with kinetic captions**, write it into the plan as the constant, and move. The format is cheap to change in a later round — and "which format?" is itself one of the two best second variables to test (§4).
251
282
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@officexapp/vidfarm-devcli",
3
- "version": "0.21.39",
3
+ "version": "0.21.42",
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": {
@@ -104,6 +104,9 @@
104
104
  "test:stickers": "node --import tsx --test test/sticker-pack.test.ts test/plate-key.test.ts",
105
105
  "test:social-recycle": "node --import tsx --test test/social-recycle.test.ts",
106
106
  "test:dedupe": "node --import tsx --test test/dedupe-recipe.test.ts",
107
+ "test:iconscout": "node --import tsx --test test/iconscout.test.ts",
108
+ "test:billing": "node --import tsx --test test/billing-wallet-cas.test.ts",
109
+ "test:skill-docs": "node --import tsx --test test/skill-docs.test.ts",
107
110
  "check:skills": "node scripts/build-director-skill-rollup.mjs --check && node scripts/check-skill-routes.mjs && node scripts/check-skill-nav.mjs",
108
111
  "benchmark:editor-chat": "node --import tsx scripts/benchmark-editor-chat-harness.mjs",
109
112
  "cdk:deploy:prod-serverless": "npm run build && dotenv -e .env.production -- npx aws-cdk deploy --app 'node dist/infra/cdk/bin/vidfarm-prod.js'",