@officexapp/vidfarm-devcli 0.21.38 → 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.
- package/.agents/skills/editor-capabilities/SKILL.md +16 -0
- package/.agents/skills/vidfarm/SKILL.md +28 -4
- package/.agents/skills/vidfarm/harnesses/README.md +1 -0
- package/.agents/skills/vidfarm/harnesses/explainer.HARNESS.md +11 -0
- package/.agents/skills/vidfarm/harnesses/product-demo.HARNESS.md +2 -0
- package/.agents/skills/vidfarm/harnesses/product-explainer.HARNESS.md +242 -0
- package/.agents/skills/vidfarm/recipes/bulk-scripting-with-a-harness.md +1 -1
- package/.agents/skills/vidfarm/recipes/cutout-graphics-for-explainers.md +2 -0
- package/.agents/skills/vidfarm/recipes/onboard-a-new-director.md +1 -0
- package/.agents/skills/vidfarm/references/automation-and-local-dev.md +21 -9
- package/.agents/skills/vidfarm/references/content-ideas.md +111 -0
- package/.agents/skills/vidfarm/references/editor-workflows.md +36 -3
- package/.agents/skills/vidfarm/references/onboarding.md +2 -0
- package/.agents/skills/vidfarm/references/primitives.md +67 -0
- package/.agents/skills/vidfarm-media/SKILL.md +50 -0
- package/SKILL.director.md +270 -16
- package/SKILL.md +3 -3
- package/crowdsourcing.md +49 -1
- package/dist/src/cli.js +461 -35
- package/dist/src/devcli/cost-mode.js +8 -0
- package/dist/src/devcli/experiments.js +257 -31
- package/dist/src/devcli/qa-check.js +74 -0
- package/dist/src/devcli/skill-docs.js +103 -0
- package/{experiment.md → experiments.md} +129 -31
- package/package.json +5 -2
package/dist/src/cli.js
CHANGED
|
@@ -19,7 +19,7 @@ import { formatCompositionLintIssues, lintCompositionHtml } from "./services/com
|
|
|
19
19
|
import { resolveFfmpeg } from "./services/clip-curation/ffmpeg.js";
|
|
20
20
|
import { parseHyperframesJson, runHyperframesCommand } from "./devcli/hyperframes-cli.js";
|
|
21
21
|
import { renderCompositionStills } from "./devcli/stills.js";
|
|
22
|
-
import { extractCompositionFacts, formatQaReport, formatWatchTheVideoNotice, qaCompositionHtml, watchTheVideoDirective } from "./devcli/qa-check.js";
|
|
22
|
+
import { extractCompositionFacts, formatQaReport, formatWatchTheVideoNotice, qaCompositionHtml, recordQaPass, watchTheVideoDirective } from "./devcli/qa-check.js";
|
|
23
23
|
import { HARNESS_FILENAME, discoverHarness, formatHarnessReport, isLegacyHarnessFilename, listBuiltinHarnesses, loadAndEvaluateHarness, mergeHarnessIntoReport, parseHarness, resolveHarnessPath } from "./devcli/harness.js";
|
|
24
24
|
import { removeGreenscreenLocal, localGreenscreenAvailable, defaultGreenscreenOutPath, GREENSCREEN_PRESETS, trimTransparentBorders, cropImageRegion } from "./devcli/greenscreen-local.js";
|
|
25
25
|
import { dedupeMediaLocal, localDedupeAvailable, defaultDedupeOutPath, describeLocalDedupe, DEDUPE_PRESETS, DEDUPE_DEFAULT_PRESET, isDedupePresetName } from "./devcli/dedupe-local.js";
|
|
@@ -29,7 +29,7 @@ import { runDoctorCommand } from "./devcli/doctor.js";
|
|
|
29
29
|
import { findFreePort } from "./devcli/port-utils.js";
|
|
30
30
|
import { scanLocalServers } from "./devcli/process-scan.js";
|
|
31
31
|
import { runSkillsCommand } from "./devcli/skills.js";
|
|
32
|
-
import { AmbiguousDocRef, DEFAULT_PACK, bundledPackDir, listPackDocs, readPackDoc, searchPackDocs } from "./devcli/skill-docs.js";
|
|
32
|
+
import { AmbiguousDocRef, DEFAULT_PACK, bundledPackDir, listPackDocs, listPackTopics, loadIdeaBank, readPackDoc, readPackTopic, resolvePackTopic, searchPackDocs } from "./devcli/skill-docs.js";
|
|
33
33
|
import { initTelemetry, reportCliCrash } from "./devcli/telemetry.js";
|
|
34
34
|
import { resolveLocalDataDir, localBackendAvailable, LocalModeUnavailableError, localApiRequest } from "./devcli/local-backend.js";
|
|
35
35
|
import { startLocalFrontendServer, serveShellsPresent } from "./devcli/local-frontend-server.js";
|
|
@@ -38,7 +38,7 @@ import { CostModeBlockedError, assertBilledAllowed, clearStoredCostMode, costMod
|
|
|
38
38
|
import { INTERACTION_MODES, INTERACTION_MODE_BLURB, clearStoredInteractionMode, interactionModeExplainer, interactionModeSummaryLine, interactiveOfferLine, normalizeInteractionMode, resolveInteractionMode, writeStoredInteractionMode } from "./devcli/interaction-mode.js";
|
|
39
39
|
import { buildImageHandoff, buildRawsHandoff, formatHandoff } from "./devcli/handoff.js";
|
|
40
40
|
import { STORYBOARD_FILENAME, readStoryboard, renderStoryboardScaffold, storyboardStage } from "./devcli/storyboard.js";
|
|
41
|
-
import { CORE_METRICS, DEFAULT_METRIC, EXPERIMENTS_FILENAME, EXPERIMENT_MODES, analyzeRound, appendResult, appendRound, capacityOf, epochsNeeded, findRoundIndex, lintDiary, markPosted, readDiary, renderDiaryScaffold, writeDiary } from "./devcli/experiments.js";
|
|
41
|
+
import { CORE_METRICS, DEFAULT_FORMAT, DEFAULT_METRIC, EASY_FORMATS, EXPERIMENTS_FILENAME, EXPERIMENT_MODES, analyzeRound, appendResult, appendRound, capacityOf, epochsNeeded, findRoundIndex, lintDiary, markPosted, readDiary, renderDiaryScaffold, writeDiary } from "./devcli/experiments.js";
|
|
42
42
|
// vidfarm-devcli — command-line bridge for the Vidfarm video studio. The
|
|
43
43
|
// `serve` command boots the FULL editor locally (single origin, disk-backed
|
|
44
44
|
// records + storage) so power users edit compositions on disk while a browser
|
|
@@ -132,21 +132,33 @@ Account (persisted login — points the CLI + \`serve\` at cloud prod):
|
|
|
132
132
|
--title/--format/--message/--arc/--audience Frontmatter for --init
|
|
133
133
|
--json The parsed manifest (frames, statuses, warnings)
|
|
134
134
|
experiment [dir] AD TESTING — read the campaign's EXPERIMENTS_DIARY.md,
|
|
135
|
-
size each round (videos ÷
|
|
135
|
+
size each round (videos ÷ capacity = epochs), rank the
|
|
136
136
|
north-star metric, flag outliers vs the median, and lint
|
|
137
137
|
the method (two variables in one structured round, a
|
|
138
138
|
winner promoted off one post, results read at mixed ages,
|
|
139
139
|
a structured round handed to gigworkers, unspent capacity).
|
|
140
|
-
Feedback, not a gate. Method: vidfarm.cc/
|
|
140
|
+
Feedback, not a gate. Method: vidfarm.cc/experiments.md
|
|
141
141
|
(alias: experiments)
|
|
142
|
-
--init Scaffold the diary
|
|
143
|
-
|
|
142
|
+
--init Scaffold the diary. --channels takes PER-CHANNEL RATES —
|
|
143
|
+
"tiktok_a x2, tiktok_b, yt_a 1/day, li_a 3/week, fb_a paused"
|
|
144
|
+
— and capacity becomes the SUM of them, so round slots are
|
|
145
|
+
dealt out in proportion instead of round-robin.
|
|
146
|
+
(--metric/--channels/--mode/--product/
|
|
147
|
+
--format/--baseline/--editors · --force overwrites).
|
|
148
|
+
Prints the copywriting-led FORMAT MENU (b-roll · talking
|
|
149
|
+
head · process · loop background · satisfying · lifestyle ·
|
|
150
|
+
POV quote) when --format is omitted — the format is a
|
|
151
|
+
planning decision, not a build-time discovery.
|
|
144
152
|
round --videos <n> Append a planned round + its epoch slot tables
|
|
145
153
|
[--variable angle] [--constants "format,hooks"] [--mode structured|creative]
|
|
146
|
-
[--slots "v001|angle,…"] [--why "…"] [--win "…"] [--title "…"]
|
|
154
|
+
[--slots "v001|angle,…"] [--format "…"] [--why "…"] [--win "…"] [--title "…"]
|
|
147
155
|
log <video> Record what happened: --views/--comments/--clicks/--buys
|
|
148
|
-
[--source flockposter] [--age 48h] [--note "…"]
|
|
149
|
-
or --posted [--channel <id>] to mark a slot live.
|
|
156
|
+
--channel <id> [--source flockposter] [--age 48h] [--note "…"]
|
|
157
|
+
[--round N], or --posted [--channel <id>] to mark a slot live.
|
|
158
|
+
ALWAYS pass --channel: account health moves numbers by
|
|
159
|
+
multiples, so each video is ranked against ITS OWN account's
|
|
160
|
+
median (1.3×acct), and a reading from a second account counts
|
|
161
|
+
as the retest that clears account-health-confound.
|
|
150
162
|
--json Parsed setup + per-round analysis + findings
|
|
151
163
|
It does NOT re-wrap what already exists: capacity comes from
|
|
152
164
|
"vidfarm channels", constants from "vidfarm harness", briefs
|
|
@@ -682,6 +694,32 @@ Speech (TTS/STT) — LOCAL-FIRST on your own AI key; --cloud is the explicit bac
|
|
|
682
694
|
--free List the $0 local Kokoro voices instead (--all lists both rosters)
|
|
683
695
|
--own-key List voices on your own saved ElevenLabs key (default: platform account)
|
|
684
696
|
--limit <n> | --json Show more / raw JSON
|
|
697
|
+
media <type> "<query>" Search the FREE stock catalog — no AI spend, no wallet cost.
|
|
698
|
+
type: image|vector|icon|video|bgm|sfx. Openverse (CC images,
|
|
699
|
+
music, SFX) and iconify (icons) are KEYLESS; a free Pixabay
|
|
700
|
+
key adds photos/vectors/stock video.
|
|
701
|
+
--provider <p> Force pixabay|openverse|iconify|iconscout
|
|
702
|
+
--limit <n> | --json
|
|
703
|
+
iconscout "<query>" DESIGNER icons, STICKERS, illustrations, 3D and Lottie —
|
|
704
|
+
THE CHEAP ALTERNATIVE TO AI IMAGE GENERATION. Reach for this
|
|
705
|
+
BEFORE 'generate' / 'image': an AI sticker costs cents per
|
|
706
|
+
attempt, needs a prompt loop, and rarely returns a clean
|
|
707
|
+
transparent vector; IconScout returns a finished SVG or
|
|
708
|
+
transparent PNG on the first try. Search is FREE.
|
|
709
|
+
Works with NO key — vidfarm's own IconScout subscription
|
|
710
|
+
serves premium downloads for a tiny wallet charge.
|
|
711
|
+
--asset <a> icon (default)|illustration|3d|lottie|ai_image
|
|
712
|
+
--style <s> sticker|flat|line|glyph|gradient|isometric|rounded|doodle|
|
|
713
|
+
dualtone|colored-outline|tile (--style sticker = STICKERS)
|
|
714
|
+
--free Free assets only ($0, credit required). --premium for the
|
|
715
|
+
paid catalog (no credit line needed)
|
|
716
|
+
--sort <s> relevant (default)|popular|latest|featured
|
|
717
|
+
--limit <n> --page <n> --json
|
|
718
|
+
iconscout get <uuid> Download ONE asset to a durable vidfarm URL you can place.
|
|
719
|
+
--format <f> icon svg|png · illustration svg|png|eps · 3d png|gltf|glb|
|
|
720
|
+
obj|fbx|blend · lottie json|lottie|gif|mp4 (default: first valid)
|
|
721
|
+
--size <px> Pixel size for raster formats (default 512; vectors ignore it)
|
|
722
|
+
--out <file> Also save the file locally
|
|
685
723
|
stt <file|url> Video or audio → transcript (alias: transcribe). LOCAL: local ffmpeg demux +
|
|
686
724
|
Returns BOTH formats: the simple subtitle your provider key (gemini labels
|
|
687
725
|
version (plain text + timed SRT cues) and speakers; openai/openrouter give
|
|
@@ -775,20 +813,30 @@ Local media engines & toolchain (all local, free, no account — no cloud key ne
|
|
|
775
813
|
lint <dir|composition.html> Validate a composition on disk (same (local)
|
|
776
814
|
checks the /editor chat preflights); prints
|
|
777
815
|
errors/warnings, exit 1 on errors [--json]
|
|
778
|
-
qa <dir|composition.html> Social-native QA pass —
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
816
|
+
qa <dir|composition.html> Social-native QA pass — OPTIONAL. (local, devcli-only)
|
|
817
|
+
Blocklists "HTML slop" (CTA buttons, benefit
|
|
818
|
+
chip rows, frosted cards, gradient text,
|
|
819
|
+
web-page classes/fonts) and checks the caption
|
|
820
|
+
font regime + safe zone. Feedback only: exits 0
|
|
821
|
+
even when it finds slop, and it never runs
|
|
822
|
+
automatically. Blocklist, not allowlist —
|
|
823
|
+
unusual styles pass untouched. Skip it freely;
|
|
824
|
+
watching the render is the review that counts
|
|
786
825
|
--harness <name|path> Grade against a HARNESS.md too (repeatable —
|
|
787
826
|
they stack). A built-in name or ANY file path.
|
|
788
827
|
<dir>/HARNESS.md is picked up automatically
|
|
789
828
|
--no-harness Skip HARNESS.md auto-discovery
|
|
790
829
|
--json Machine-readable findings (rule/severity/fix)
|
|
791
830
|
--strict Also exit 1 when slop is found (for CI)
|
|
831
|
+
--max-revisions <n> REVISION GOVERNOR (default 1, 0 disables). qa counts
|
|
832
|
+
how many times the composition CHANGED between passes
|
|
833
|
+
— re-running on an untouched file is free — and at the
|
|
834
|
+
limit it WITHHOLDS the findings and tells you to ship
|
|
835
|
+
or ask the human. That's the base case that stops a
|
|
836
|
+
qa -> fix -> qa loop: one pass names the slop, one fix
|
|
837
|
+
clears it, and what's left is nearly always taste, not
|
|
838
|
+
a defect. Raise it when the human asks for another round
|
|
839
|
+
--reset-revisions Start the revision count over
|
|
792
840
|
harness <sub> HARNESS.md — the reusable AI harness for ONE (local, devcli-only)
|
|
793
841
|
format or template: what makes it special,
|
|
794
842
|
written down so an agent can reproduce it.
|
|
@@ -797,7 +845,7 @@ Local media engines & toolchain (all local, free, no account — no cloud key ne
|
|
|
797
845
|
template_id" all land here
|
|
798
846
|
harness list [--json] Bundled starting points: short-form,
|
|
799
847
|
hooks, ugc-testimonial, explainer,
|
|
800
|
-
product-demo
|
|
848
|
+
product-demo, product-explainer
|
|
801
849
|
harness show <name|path> Print one [--dna <strand>] to print just
|
|
802
850
|
one strand (viral_dna, visual_dna, …)
|
|
803
851
|
harness init <name> [--out <p>] Copy one next to your work, then EDIT it
|
|
@@ -877,10 +925,31 @@ Account:
|
|
|
877
925
|
whoami Show the authenticated account → GET /api/v1/user/me
|
|
878
926
|
provider-keys List saved AI provider keys → GET /api/v1/user/me/provider-keys
|
|
879
927
|
add-provider-key <provider> <secret> Save an AI provider key → POST /api/v1/user/me/provider-keys
|
|
928
|
+
provider: openai|gemini|openrouter|perplexity|nvidia|elevenlabs|pixabay|iconscout
|
|
929
|
+
(iconscout takes BOTH values as one secret: <client_id>:<client_secret>)
|
|
880
930
|
|
|
881
931
|
Agent skill (the director knowledge — a full copy SHIPS INSIDE this CLI):
|
|
932
|
+
ideas [topic] "What should I post?" — the 50-frame ANGLE BANK (local — offline, free, no AI)
|
|
933
|
+
(the rise of · what everyone gets wrong · then vs
|
|
934
|
+
now · one decision that changed everything · …).
|
|
935
|
+
One offer poured into 50 frames is 50 DIFFERENT
|
|
936
|
+
videos, not 50 rewrites of one. It hands over the
|
|
937
|
+
frames; you fill them with the director's offer,
|
|
938
|
+
then write hook/loop/payoff/bait per pick
|
|
939
|
+
--topic "<offer>" Print each frame already filled with your topic
|
|
940
|
+
--family <name> One family only (arc, contrast, pivot, …)
|
|
941
|
+
--families List the families and their sizes
|
|
942
|
+
--count <n> Sample N, spread across families (default 20 with --topic)
|
|
943
|
+
--json Machine-readable frames (+ starter lines)
|
|
944
|
+
skill topics The craft this pack knows, by SPOKEN name — (local — offline, no account)
|
|
945
|
+
meme-recaption, product-explainer, captions,
|
|
946
|
+
first-frame, blurred-plate, density, avatar, …
|
|
947
|
+
each mapped to its file and section
|
|
882
948
|
skill ls List every file in the bundled pack, with sizes (local — offline, no account)
|
|
883
|
-
skill show <path>
|
|
949
|
+
skill show <path|topic> Print one file — or just the SECTION a topic
|
|
950
|
+
names: \`skill show meme-recaption\` prints the
|
|
951
|
+
recaption method, not the 650-line reference it
|
|
952
|
+
lives in. Shorthand works too: \`skill show
|
|
884
953
|
primitives\`, \`skill show harnesses/README.md\`
|
|
885
954
|
skill search "<term>" Grep all of it — the fastest way to find the one
|
|
886
955
|
paragraph you need without loading a whole file
|
|
@@ -1227,6 +1296,11 @@ async function main() {
|
|
|
1227
1296
|
case "media":
|
|
1228
1297
|
await runMediaCommand(rest);
|
|
1229
1298
|
return;
|
|
1299
|
+
case "iconscout":
|
|
1300
|
+
case "icons":
|
|
1301
|
+
case "stickers":
|
|
1302
|
+
await runIconScoutCommand(rest);
|
|
1303
|
+
return;
|
|
1230
1304
|
case "stt":
|
|
1231
1305
|
case "transcribe":
|
|
1232
1306
|
await runSttCommand(rest);
|
|
@@ -1330,6 +1404,12 @@ async function main() {
|
|
|
1330
1404
|
case "experiments":
|
|
1331
1405
|
await runExperimentCommand(rest);
|
|
1332
1406
|
return;
|
|
1407
|
+
// "what should I post?" — the 50-frame angle bank, read off the bundled pack.
|
|
1408
|
+
case "content-ideas":
|
|
1409
|
+
case "idea":
|
|
1410
|
+
case "ideas":
|
|
1411
|
+
await runIdeasCommand(rest);
|
|
1412
|
+
return;
|
|
1333
1413
|
case "update-skill":
|
|
1334
1414
|
case "skill":
|
|
1335
1415
|
await runUpdateSkillCommand(rest);
|
|
@@ -2497,15 +2577,17 @@ Rules:
|
|
|
2497
2577
|
- If narration must be customized, default to premium ElevenLabs first, then the user's own ElevenLabs path, then BYOK OpenAI/Gemini/OpenRouter. If captions or scenes were timed to the old VO, retime them to the new narration.
|
|
2498
2578
|
- NO HTML SLOP. You are editing HTML, but the output is a social video, not a web page. THE TEST IS THE NATIVE-EDITOR TEST: could you have made this element with the tools inside TikTok's own editor? That toolset is a font, a color, a stroke/outline, a soft shadow, a tight text box, alignment, opacity, rotation, animation presets — plus stickers, emoji, drawn marks and clips. It has NO padded capsule, NO border, NO gradient fill, NO blur panel, NO card. If you reached past it, cut it. Never author landing-page furniture: CTA "buttons" (a filled/gradient rounded capsule with action copy like "Sign Up for a Free Trial →"), benefit chip/badge rows ("✓ No Credit Card Needed"), bordered/shadowed/frosted cards holding a headline + URL, gradient text fills, feature grids, bulleted lists, or web-default fonts (Inter/Roboto/Arial/system-ui). AND NOT A SINGLE PILL EITHER: one lonely rounded, padded, filled capsule around a static stat or label — "10 hrs / week", "STEP 2", "EP.01", "+40%" — is a web badge, and being the only one on screen does not make it native. The ONLY legitimate capsule in a video is the active-word spotlight/karaoke caption highlight, because it moves with the spoken word. Emphasize a stat the way the editor would: bigger, heavier, ALL-CAPS, an accent color, a hand-drawn circle or underline, or its own beat on screen. Rule of thumb on anything holding words: border-radius over ~8px PLUS a background fill PLUS padding = a badge; drop the fill or drop the radius until the band hugs the glyphs. None of this appears in a real TikTok, and nothing in a video is clickable — say it as timed text on the footage instead. Arrows, scribble/underline marks, italics, ALL-CAPS, single-word color pops, emoji, transparent cut-out stickers, and mock social UI (iMessage bubbles, comment cards) are all fine. Captions use an imported family (Montserrat default / TikTok Sans / Abel / Source Code Pro / Yesteryear) at weight 700-900, ~36-64px on a 1080-wide frame, inside the 8%-85% safe zone, with exactly one of four backgrounds: outline, plain, an active-word spotlight/karaoke pill, or a tight-hugging solid band (radius <=8px, no border/shadow/gradient/blur).
|
|
2499
2579
|
- STRUCTURE BEFORE POLISH — THE FOUR CHARGES, WRITTEN BEFORE YOU TOUCH THE TIMELINE. Most agent-made videos fail on structure, not polish, because the timeline is the fun part so it gets built first and the words get retrofitted. Invert it: (1) HOOK — write the opening line as text first: a complete clause (subject + verb), no jargon, naming a SITUATION ("I've quit six businesses") not a label ("anonymity"); it goes on screen at start:0, because caption chunk 1 is read before any audio and muted autoplay is the default. Banned openings: throat-clearing ("so I was thinking", "here's the thing"), a logo, a title card, a fade from black, context before the claim. (2) LOOP — one open question by 0:10, said ON SCREEN, closing INSIDE this video (state the timestamp it closes at; if you can't, there is no loop), and the withheld answer must be one the viewer CANNOT supply themselves — a formally-correct loop with a guessable answer passes every mechanical check and dies in the field. (3) PAYOFF — shown, not summarized, ≥5 uninterrupted seconds, landing BEFORE the final beat; the payoff is not the CTA. (4) BAIT — one ask in the final beat and in the post caption; a keyword comment ask ("comment CLIPPER and I'll send the breakdown") is standard and allowed, but never "follow for part two", ragebait, or an earnings/health claim traded for the reply. Then build the timeline. Re-theming a decomposed template: viral_dna already names the source's hook/retention/payoff — rebuild each charge for the new subject, never flatten the loop into a product statement. Full craft harness: the vidfarm skill's references/hooks-and-virality.md. Checkable form: \`vidfarm harness show hooks\`.
|
|
2580
|
+
- ORIENT THE COLD VIEWER IN THE FIRST 3 SECONDS — THE VIEWER HAS NO CONTEXT AND DID NOT CHOOSE THIS VIDEO. Distinct from the hook: the hook makes them WANT to watch, orientation makes the watching POSSIBLE. A stranger mid-scroll must be able to answer three things by ~3s — what am I looking at (the CATEGORY noun), who is it for, and why is this on my screen (the situation). The failure is not a bad first frame, it is a good video that BEGINS AT BEAT TWO, and the author cannot see it because the author already knows what the thing is. Signatures, each a rebuild not a polish: a pronoun with no referent ("it just works", "this changes everything", "here's how they do it"); starting at step three (the process already running, the dashboard already full); a metaphor whose subject only lands at 6s; insider vocabulary, a product's own feature name, or an ACRONYM in the first line; a detail crop that reads as texture until you know the whole. Instead, the opening beat is BOTH channels at once: an EASY IMAGE (one large subject, already moving, legible at a glance and at thumbnail scale — a relevant die-cut sticker names the category before a word is read) AND an EASY LINE (first spoken sentence one clause, <=12 words, everyday words, concrete noun + verb, no subordinate clause, brand name said once plainly, and the CATEGORY named: "X is a language app that…"). Give the SITUATION, not the label — "the end of the month, and your receipts are in a shoebox" orients, "expense automation" does not. THIS IS NOT AN INTRO AND COSTS NO EXTRA SECONDS: it replaces the wind-up sentence, it never precedes it, and it never licenses a logo, a title card or a fade from black. Test it on the render, not the script: play the first 3 seconds ONLY to somebody with no context and stop — they should say what kind of thing it is and roughly who it is for. "Something about audio" is a fail. Fullest form: \`vidfarm harness show product-explainer\` (Rule 0).
|
|
2500
2581
|
- THE FIRST FRAME IS THE THUMBNAIL. Frame 0 is one frame of ~30 in the first second, but every feed card, share link, and paused player freezes on it — more people see that frame than watch the video. It must never be black, empty, mid-fade, or mid-animation: a real visual at start:0 (\`vidfarm retime . --layer <key> --start 0\`), the hook words already on screen at t=0, and NO entrance transition on the FIRST clip (\`vidfarm transitions set . --layer <key> --in none\`; junction transitions between later clips are fine). Look at the actual pixels before you render: \`vidfarm stills . --at 0\`.
|
|
2501
2582
|
- REVIEW THE WHOLE VIDEO AS ONE OBJECT — AND NEVER JUDGE IT BY ONE FRAME. Assume your own finished video has a defect you cannot see: across a 32-video batch, EVERY first-pass video had a real defect its own author had already reported as "verified, looks good". The cause is structural — you build scene by scene, each scene correct while it is the whole world, so every scene passes alone and the video fails as a SEQUENCE: margins shift between beats, headline sizes drift, the accent color wanders, one asset is flat vector and the next is photographic, every beat is the same length, a join lands like a slap. Nobody watches a scene; they watch the sequence. So before you call anything done, tile ~12 stills into ONE contact sheet and READ IT AS AN IMAGE — one command: \`vidfarm stills . --sheet\` (add \`--at 0,2,4,…\` to pick timestamps; writes stills/contact-sheet.png). Check: visual balance (no dead band under top-anchored content), consistent spacing/margins, ONE type scale, ONE accent color, ONE illustration style, deliberate pacing rather than N identical beats, nothing jarring at the joins, no frame where two elements compete for the eye — and the summary question, does it look like one person made it in one sitting? Fix drift by defining the SYSTEM (type scale, margin, palette, default beat) and applying it to every scene, not by patching the one scene that stood out. The defects that actually ship, in observed frequency order: large flat dead regions · a placeholder empty state that reads as a failed render · two contradictory numbers in one frame · a CTA still building at the last frame (settle it >=2s before the end) · two headlines superimposed at a scene handoff (exit at nextIn-0.18, duration 0.24, ease power2.out) · type colliding with a busy background exactly as it is spoken. If a frame looks empty, sample 0.2s apart to see whether it RESTS there — a transient wipe frame is fine, >0.5s is a hole. AND ALWAYS COMPARE TWO FRAMES FROM DIFFERENT SCENES: a frozen render (an overlay/watermark pass missing \`-loop 1\`, or assets outside the composition root so the timeline never runs) makes every frame identical while duration, frame count and audio hash all still pass, and frame 0 looks perfect. Verify audio by MEASUREMENT, never "it sounds fine" (you cannot hear it): ~12-15 dB speech-over-bed separation across the actual word spans, peak <0 dBFS. Never \`adelay\` the VO — whisper word timings and every caption built from them are relative to the raw vo.wav; use apad+atrim. Report what you MEASURED separately from what you JUDGED. Full method: the vidfarm skill's references/reviewing-renders.md.
|
|
2502
|
-
- THE HARNESS IS A FIRST-CLASS ARTIFACT — KNOW THE THREE PHRASINGS. A HARNESS.md is the reusable AI harness for ONE format or template: what makes it special, written down so an agent reproduces it without the director in the room. "Create me a harness" → \`vidfarm harness init short-form --out ./HARNESS.md\` (bases: short-form, hooks, ugc-testimonial, explainer, product-demo), then EDIT it with them. "Update the harness for this format" → open the file and write the new rule in WITH ITS REASON on the same line. "Give me the harness for this template_id" → they mean the DECOMPOSITION: \`vidfarm harness derive <forkId>\` distils this fork's viral/visual/structural/audio/build DNA into an editable HARNESS.md, keyed the same way the decompose JSON is (\`## Viral DNA\` → \`viral_dna\`, …); \`vidfarm harness show <ref> --dna visual\` prints one strand. \`vidfarm qa .\` picks up ./HARNESS.md automatically; \`--harness <name|path>\` adds more (they stack, and any file of theirs anywhere is valid). Its \`checks:\` are machine-settled; its \`- [ ]\` items come back for YOU to answer honestly in your report — never claim a pass on the half the CLI can't judge. ONE-TIME OR BULK? Ask before you build: if the director wants volume (daily posting, N variants, hook tests), that's SCRIPTING MODE — pin this fork as the base, vary exactly ONE thing per variant, and install a harness, because nobody watches variant #37 as carefully as #1. When a batch teaches you something, write it back into the harness — the compositions are disposable, the harness compounds. (Formerly QA_REGIME.md; \`vidfarm regime …\` still works as an alias. NOTE: the \`.harness/\` directory beside this file is machine-generated context regenerated on every pull — never hand-edit it. HARNESS.md is the one you own.)
|
|
2583
|
+
- THE HARNESS IS A FIRST-CLASS ARTIFACT — KNOW THE THREE PHRASINGS. A HARNESS.md is the reusable AI harness for ONE format or template: what makes it special, written down so an agent reproduces it without the director in the room. "Create me a harness" → \`vidfarm harness init short-form --out ./HARNESS.md\` (bases: short-form, hooks, ugc-testimonial, explainer, product-demo, product-explainer), then EDIT it with them. "Update the harness for this format" → open the file and write the new rule in WITH ITS REASON on the same line. "Give me the harness for this template_id" → they mean the DECOMPOSITION: \`vidfarm harness derive <forkId>\` distils this fork's viral/visual/structural/audio/build DNA into an editable HARNESS.md, keyed the same way the decompose JSON is (\`## Viral DNA\` → \`viral_dna\`, …); \`vidfarm harness show <ref> --dna visual\` prints one strand. \`vidfarm qa .\` picks up ./HARNESS.md automatically; \`--harness <name|path>\` adds more (they stack, and any file of theirs anywhere is valid). Its \`checks:\` are machine-settled; its \`- [ ]\` items come back for YOU to answer honestly in your report — never claim a pass on the half the CLI can't judge. ONE-TIME OR BULK? Ask before you build: if the director wants volume (daily posting, N variants, hook tests), that's SCRIPTING MODE — pin this fork as the base, vary exactly ONE thing per variant, and install a harness, because nobody watches variant #37 as carefully as #1. When a batch teaches you something, write it back into the harness — the compositions are disposable, the harness compounds. (Formerly QA_REGIME.md; \`vidfarm regime …\` still works as an alias. NOTE: the \`.harness/\` directory beside this file is machine-generated context regenerated on every pull — never hand-edit it. HARNESS.md is the one you own.)
|
|
2503
2584
|
- DEDUPLICATE BEFORE YOU PUBLISH — AND ASK FIRST. Social platforms fingerprint every upload, so the same render posted twice (a second account, another platform, a re-post next month) gets the later copy suppressed as duplicate/reused content. BEFORE you render for publication, and before any bulk run, ASK the director: "do you want deduplicated copies for posting, and how many?" Ask THEN, not after — dedupe is a post-render ffmpeg pass, so the correct order is RENDER ONCE → DEDUPE N, and deciding late means paying for a second render. Run it on the EXPORTED file: \`vidfarm dedupe ./final.mp4\` (one copy) or \`vidfarm dedupe ./final.mp4 --variants N --seed <slug> --out-dir ./posts\` (N copies, one per account/slot). Free, offline, no wallet — it never re-renders the composition. The default \`standard\` preset is skew 2%, zoom 3%, rotate 2°, speed +2%, saturation +4%, plus contrast/brightness/hue/grain, a container-metadata strip and a per-variant CRF walk; invisible to a viewer, and each variant differs from the original AND from its siblings. Post each variant to a DIFFERENT account — two accounts posting the same variant defeats the point. A rotate forces a bigger centre-crop to hide the black corners (~6.7% on a tall frame at 2°) and the CLI says so; pass \`--rotate 0\` when framing matters more. Cloud twin: \`POST /api/v1/primitives/media/dedupe\`.
|
|
2504
2585
|
- QA EVERY VIDEO BEFORE YOU RENDER: run \`vidfarm qa .\` in this directory. It's free, instant, and local — a blocklist for the slop above plus the first frame, the font regime, and the safe zone, with a concrete fix per finding. It's feedback, not a gate (exits 0 even on findings, never runs automatically) and a blocklist, not an allowlist, so stylized or hand-made work passes untouched. Fix what's real, ignore what's a deliberate style call. \`--json\` for scripted batches.
|
|
2505
2586
|
|
|
2506
2587
|
The three paintbrushes (Vidfarm is thrift-first — do NOT spend AI credits on every scene):
|
|
2507
2588
|
- Paint each REPLACE beat with one of three brushes, cheapest first: (1) RAW CLIPS remixed from existing footage (\`vidfarm raws search\` the local/library first, then \`vidfarm raws scan <url|file>\` to hunt new ones — free local compute); (2) HTML/JS HYPERFRAMES — animate text/images/logos/stickers/charts with CSS or JS adapters (anime.js/GSAP/Lottie/Three) for titles, kinetic captions, data beats, brand cards; (3) PURE AI GENERATION (\`vidfarm generate image|video\`) — most expensive, AI video especially, last resort for beats no clip or hyperframe can cover.
|
|
2508
2589
|
- A background video + a foreground video (greenscreen / picture-in-picture) covers most "video meme" formats with zero generation.
|
|
2590
|
+
- IF THE DIRECTOR GAVE YOU A WEBSITE (a product explainer / client brand video), THAT SITE IS THE FIRST ASSET LIBRARY — harvest before you buy or generate, unless they said not to use their site art. Take product screenshots and UI states, brand illustrations, mascots, spot art and icons, the palette and their own words, any animated WebP/GIF (free motion footage — extract frames to a sprite sheet), an SPA's manifest.json screenshots[], and best of all a screen recording in the JS bundle (that is A-roll). \`vidfarm capture <url>\` renders the page and pulls assets; \`vidfarm mask <image> --crop x,y,w,h\` cuts ONE element out of a screenshot into a snug transparent PNG with local matting for $0 (\`--flat <hex>\` on a solid background), repeatable to lift a whole cast out of one image; then place + keyframes it like any sticker. The order is HARVEST -> ICONSCOUT CATALOG -> GENERATE, and it is on-brand by construction, free, and truthful (a screen off their live site cannot claim what they do not claim). Two limits: harvest the ASSETS, never the landing-page furniture (no CTA capsules, feature grids, pricing cards, chip rows — a video is not a web page), and treat obvious stock photography as licensed to THEM, not to you. Say in your report which assets came from the client's own site. Full rule: \`vidfarm harness show product-explainer\` (Rule 5b).
|
|
2509
2591
|
- Reusable asset library (logos, stickers, reactions, b-roll, a-roll, brand media kit): have an opinion on when/where to reuse — but anchor every asset choice to the viral DNA + harness above. Reskinning past a load-bearing beat kills what made the format work. AI-generate a reusable element ONCE, then reuse it; don't regenerate per-scene.
|
|
2510
2592
|
|
|
2511
2593
|
Two replication harnesses — pick one and name it to the user (default A). When \`replication-harness.json\` is present it holds a CONCRETE, per-beat version of both plans (see "Replication plan" below) — prefer its beat assignments over freestyling; this prose is the fallback when it's absent:
|
|
@@ -4513,7 +4595,10 @@ async function runGenerateCommand(argv) {
|
|
|
4513
4595
|
estimate: "cheap, ~$0.01–$0.05",
|
|
4514
4596
|
freeAlternative: 'free stock image/vector — vidfarm media search "<meaning>" --type image|vector ($0). ' +
|
|
4515
4597
|
"Openverse (CC images) is keyless and always on — check it first; a free Pixabay key adds " +
|
|
4516
|
-
"photos/vectors and may already be saved (check vidfarm provider-keys)"
|
|
4598
|
+
"photos/vectors and may already be saved (check vidfarm provider-keys). " +
|
|
4599
|
+
"If the image is an ICON, STICKER, illustration, 3D prop or Lottie, do NOT generate it at all — " +
|
|
4600
|
+
'vidfarm iconscout "<meaning>" --style sticker returns a designer\'s finished transparent asset ' +
|
|
4601
|
+
"for $0 (free tier, credit required) or a few cents, with no prompt loop",
|
|
4517
4602
|
interactiveAlternative: 'hand the user an image brief — vidfarm handoff image --theme "<what>" --items "a,b,c" — they run it ' +
|
|
4518
4603
|
"in a free web generator (meta.ai / ChatGPT / Gemini) and drop the PNG back; needing several graphics " +
|
|
4519
4604
|
"makes it ONE sticker sheet that vidfarm sticker-pack splits for $0"
|
|
@@ -7684,7 +7769,10 @@ async function runMediaCommand(argv) {
|
|
|
7684
7769
|
const items = Array.isArray(body.items) ? body.items : [];
|
|
7685
7770
|
console.log(`${DIM}${items.length} ${type} result(s) for "${query}" via [${(body.providers_used ?? []).join(", ") || "none"}]${RESET}`);
|
|
7686
7771
|
if (!items.length) {
|
|
7687
|
-
console.log(`${DIM}No results. Try a broader query, another --type, or a specific --provider (pixabay|openverse|iconify).${RESET}`);
|
|
7772
|
+
console.log(`${DIM}No results. Try a broader query, another --type, or a specific --provider (pixabay|openverse|iconify|iconscout).${RESET}`);
|
|
7773
|
+
if (type === "icon" || type === "vector") {
|
|
7774
|
+
console.log(`${DIM}(for icons/STICKERS/illustrations/3D/Lottie try 'vidfarm iconscout "${query}" --style sticker' — a much deeper designer catalog, and far cheaper than generating one with AI.)${RESET}`);
|
|
7775
|
+
}
|
|
7688
7776
|
if (type === "image" || type === "vector" || type === "video") {
|
|
7689
7777
|
console.log(`${DIM}(photos/vectors/video need YOUR OWN free Pixabay key — get one at https://pixabay.com/api/docs/ then save it: 'vidfarm add-provider-key pixabay <key>'. openverse covers CC images keyless.)${RESET}`);
|
|
7690
7778
|
}
|
|
@@ -7698,6 +7786,124 @@ async function runMediaCommand(argv) {
|
|
|
7698
7786
|
}
|
|
7699
7787
|
console.log(`${DIM}Attribution shown for CC-BY-style items — include it when you use them. Pixabay/CC0 need none.${RESET}`);
|
|
7700
7788
|
}
|
|
7789
|
+
// IconScout — designer icons / STICKERS / illustrations / 3D / Lottie.
|
|
7790
|
+
//
|
|
7791
|
+
// This is the CHEAP path, and the CLI says so loudly: an AI image attempt costs
|
|
7792
|
+
// cents and a prompt loop for a sticker a designer already drew. Search is free,
|
|
7793
|
+
// so there is no cost-mode gate on it; only `iconscout get` can spend, and only
|
|
7794
|
+
// on a PREMIUM asset running on the platform subscription.
|
|
7795
|
+
async function runIconScoutCommand(argv) {
|
|
7796
|
+
const ASSETS = ["icon", "illustration", "3d", "lottie", "ai_image"];
|
|
7797
|
+
const parsed = parseArgs({
|
|
7798
|
+
args: argv,
|
|
7799
|
+
allowPositionals: true,
|
|
7800
|
+
options: {
|
|
7801
|
+
...commonOptions(),
|
|
7802
|
+
asset: { type: "string", short: "a" },
|
|
7803
|
+
style: { type: "string" },
|
|
7804
|
+
format: { type: "string", short: "f" },
|
|
7805
|
+
sort: { type: "string" },
|
|
7806
|
+
free: { type: "boolean" },
|
|
7807
|
+
premium: { type: "boolean" },
|
|
7808
|
+
limit: { type: "string" },
|
|
7809
|
+
page: { type: "string" },
|
|
7810
|
+
size: { type: "string" },
|
|
7811
|
+
out: { type: "string" }
|
|
7812
|
+
}
|
|
7813
|
+
});
|
|
7814
|
+
const ctx = commonContext(parsed.values);
|
|
7815
|
+
const positionals = parsed.positionals.slice();
|
|
7816
|
+
const sub = (positionals[0] ?? "").toLowerCase();
|
|
7817
|
+
// ── iconscout get <uuid> — download one asset ────────────────────────────
|
|
7818
|
+
if (sub === "get" || sub === "download") {
|
|
7819
|
+
const uuid = positionals[1];
|
|
7820
|
+
if (!uuid)
|
|
7821
|
+
throw new Error('iconscout get requires an asset uuid (from `vidfarm iconscout "<query>"`).');
|
|
7822
|
+
const size = Number(parsed.values.size ?? "512") || 512;
|
|
7823
|
+
const body = { uuid };
|
|
7824
|
+
if (parsed.values.format)
|
|
7825
|
+
body.format = String(parsed.values.format).toLowerCase();
|
|
7826
|
+
if (parsed.values.size) {
|
|
7827
|
+
body.width = size;
|
|
7828
|
+
body.height = size;
|
|
7829
|
+
}
|
|
7830
|
+
const res = await apiRequest({
|
|
7831
|
+
method: "POST", host: ctx.host, path: "/api/v1/primitives/iconscout/download", auth: ctx.auth, body
|
|
7832
|
+
});
|
|
7833
|
+
assertApiOk(res, "iconscout get");
|
|
7834
|
+
const out = (res.json ?? {});
|
|
7835
|
+
if (ctx.json) {
|
|
7836
|
+
printJson(out);
|
|
7837
|
+
return;
|
|
7838
|
+
}
|
|
7839
|
+
console.log(`${GREEN}${out.url}${RESET}`);
|
|
7840
|
+
const cost = out.billed ? `$${out.charged_usd} wallet` : out.is_free ? "$0 (free asset)" : "$0 (your own IconScout key)";
|
|
7841
|
+
console.log(`${DIM} ${out.name} · ${out.asset}/${out.format} · ${out.size_bytes} bytes · ${cost}${RESET}`);
|
|
7842
|
+
if (out.attribution_required && out.attribution) {
|
|
7843
|
+
console.log(`${RED} ⚠ credit required: ${out.attribution}${RESET}`);
|
|
7844
|
+
}
|
|
7845
|
+
if (parsed.values.out) {
|
|
7846
|
+
const dest = path.resolve(process.cwd(), String(parsed.values.out));
|
|
7847
|
+
const fileRes = await fetch(out.url, { headers: buildAuthHeaders(ctx.auth) });
|
|
7848
|
+
if (!fileRes.ok || !fileRes.body)
|
|
7849
|
+
throw new Error(`Could not save the asset: ${fileRes.status}.`);
|
|
7850
|
+
await pipeline(Readable.fromWeb(fileRes.body), createWriteStream(dest));
|
|
7851
|
+
console.log(`${DIM} saved → ${dest}${RESET}`);
|
|
7852
|
+
}
|
|
7853
|
+
console.log(`${DIM}Place it with: vidfarm set-media <dir> --src "${out.url}" --replace <layer_key>${RESET}`);
|
|
7854
|
+
return;
|
|
7855
|
+
}
|
|
7856
|
+
// ── iconscout "<query>" — search ─────────────────────────────────────────
|
|
7857
|
+
const query = (sub === "search" || sub === "find" ? positionals.slice(1) : positionals).join(" ").trim();
|
|
7858
|
+
if (!query) {
|
|
7859
|
+
console.error(`${RED}iconscout: a search query is required${RESET}`);
|
|
7860
|
+
console.error(`${DIM}e.g. vidfarm iconscout "pineapple" --style sticker | vidfarm iconscout "rocket" --asset 3d${RESET}`);
|
|
7861
|
+
process.exit(2);
|
|
7862
|
+
}
|
|
7863
|
+
const asset = String(parsed.values.asset ?? "icon").toLowerCase();
|
|
7864
|
+
if (!ASSETS.includes(asset)) {
|
|
7865
|
+
console.error(`${RED}iconscout: --asset must be one of: ${ASSETS.join(", ")}${RESET}`);
|
|
7866
|
+
process.exit(2);
|
|
7867
|
+
}
|
|
7868
|
+
const qs = new URLSearchParams({ q: query, asset });
|
|
7869
|
+
if (parsed.values.free)
|
|
7870
|
+
qs.set("price", "free");
|
|
7871
|
+
else if (parsed.values.premium)
|
|
7872
|
+
qs.set("price", "premium");
|
|
7873
|
+
if (parsed.values.style)
|
|
7874
|
+
qs.set("style", String(parsed.values.style).toLowerCase());
|
|
7875
|
+
if (parsed.values.format)
|
|
7876
|
+
qs.set("format", String(parsed.values.format).toLowerCase());
|
|
7877
|
+
if (parsed.values.sort)
|
|
7878
|
+
qs.set("sort", String(parsed.values.sort).toLowerCase());
|
|
7879
|
+
qs.set("limit", String(Number(parsed.values.limit ?? "20") || 20));
|
|
7880
|
+
qs.set("page", String(Number(parsed.values.page ?? "1") || 1));
|
|
7881
|
+
const res = await apiRequest({
|
|
7882
|
+
method: "GET", host: ctx.host, path: `/api/v1/primitives/iconscout/search?${qs.toString()}`, auth: ctx.auth
|
|
7883
|
+
});
|
|
7884
|
+
assertApiOk(res, "iconscout search");
|
|
7885
|
+
const body = (res.json ?? {});
|
|
7886
|
+
if (ctx.json) {
|
|
7887
|
+
printJson(body);
|
|
7888
|
+
return;
|
|
7889
|
+
}
|
|
7890
|
+
const items = Array.isArray(body.items) ? body.items : [];
|
|
7891
|
+
console.log(`${DIM}${items.length} of ${body.total ?? items.length} ${asset} result(s) for "${query}" (page ${body.page ?? 1}/${body.last_page ?? 1})${RESET}`);
|
|
7892
|
+
if (!items.length) {
|
|
7893
|
+
console.log(`${DIM}No results. Try a broader query, another --asset, or drop --style.${RESET}`);
|
|
7894
|
+
return;
|
|
7895
|
+
}
|
|
7896
|
+
for (const it of items) {
|
|
7897
|
+
const price = it.is_free ? `${GREEN}free${RESET}` : `${DIM}premium${RESET}`;
|
|
7898
|
+
console.log(`${GREEN}${it.uuid}${RESET} ${it.name} [${price}${DIM}]${RESET}`);
|
|
7899
|
+
console.log(`${DIM} ${it.asset} · formats ${(it.formats ?? []).join("/")} · preview ${it.preview_url ?? "—"}${RESET}`);
|
|
7900
|
+
}
|
|
7901
|
+
const perDownload = Number(body.download_usd ?? 0);
|
|
7902
|
+
console.log(`${DIM}Download one: vidfarm iconscout get <uuid> --format svg${RESET}`);
|
|
7903
|
+
if (body.account === "platform" && perDownload > 0) {
|
|
7904
|
+
console.log(`${DIM}Free assets cost $0 (credit required). Premium downloads run on vidfarm's IconScout subscription at ~$${perDownload} wallet each — still far cheaper than generating one with AI.${RESET}`);
|
|
7905
|
+
}
|
|
7906
|
+
}
|
|
7701
7907
|
async function runSttCommand(argv) {
|
|
7702
7908
|
const parsed = parseArgs({
|
|
7703
7909
|
args: argv,
|
|
@@ -9175,7 +9381,7 @@ async function runStoryboardCommand(argv) {
|
|
|
9175
9381
|
}
|
|
9176
9382
|
// `vidfarm experiment [dir]` — the ad-testing ledger, and only the ledger.
|
|
9177
9383
|
//
|
|
9178
|
-
// The method is https://vidfarm.cc/
|
|
9384
|
+
// The method is https://vidfarm.cc/experiments.md: rounds of videos that vary one
|
|
9179
9385
|
// composition param (structured) or vary everything (creative), posted across the
|
|
9180
9386
|
// channels the director holds, read back against one north-star metric. This
|
|
9181
9387
|
// command owns the three things nothing else in the CLI owns — the
|
|
@@ -9198,6 +9404,7 @@ async function runExperimentCommand(argv) {
|
|
|
9198
9404
|
channels: { type: "string" },
|
|
9199
9405
|
mode: { type: "string" },
|
|
9200
9406
|
product: { type: "string" },
|
|
9407
|
+
format: { type: "string" },
|
|
9201
9408
|
baseline: { type: "string" },
|
|
9202
9409
|
editors: { type: "string" },
|
|
9203
9410
|
// round
|
|
@@ -9245,6 +9452,7 @@ async function runExperimentCommand(argv) {
|
|
|
9245
9452
|
const body = renderDiaryScaffold({
|
|
9246
9453
|
product: parsed.values.product,
|
|
9247
9454
|
metric: parsed.values.metric?.toLowerCase(),
|
|
9455
|
+
format: parsed.values.format,
|
|
9248
9456
|
channels: asList(parsed.values.channels) ?? [],
|
|
9249
9457
|
mode: mode,
|
|
9250
9458
|
baseline: parsed.values.baseline,
|
|
@@ -9256,6 +9464,15 @@ async function runExperimentCommand(argv) {
|
|
|
9256
9464
|
return printJson({ ok: true, created: target });
|
|
9257
9465
|
console.log(`${GREEN}Diary created:${RESET} ${target}`);
|
|
9258
9466
|
console.log(`${DIM}Fill the channel list from ${BOLD}vidfarm channels${RESET}${DIM} — the channel count IS your testing capacity per epoch.${RESET}`);
|
|
9467
|
+
if (!parsed.values.format) {
|
|
9468
|
+
// The format is a PLANNING decision. Print the menu now rather than let it
|
|
9469
|
+
// get discovered at build time, one drifting video at a time.
|
|
9470
|
+
console.log(`${DIM}Format defaulted to ${BOLD}${DEFAULT_FORMAT}${RESET}${DIM}. Offer the director the menu (--format sets it):${RESET}`);
|
|
9471
|
+
for (const f of EASY_FORMATS)
|
|
9472
|
+
console.log(` ${DIM}·${RESET} ${f.label.padEnd(38)} ${DIM}${f.note}${RESET}`);
|
|
9473
|
+
// The script's grammatical person is the fastest way to pick from that menu.
|
|
9474
|
+
console.log(`${DIM}Rule of thumb: copy that says ${BOLD}"I"${RESET}${DIM} → show the speaker's world (POV b-roll / talking head / process). Copy that says ${BOLD}"you"${RESET}${DIM} → meme recaption (greenscreen raw over a background image). Both finish with TikTok-native type, kinetic captions and free Kokoro VO.${RESET}`);
|
|
9475
|
+
}
|
|
9259
9476
|
console.log(`${DIM}Then plan the first round: ${BOLD}vidfarm experiment round --variable angle --videos 8 --why "awareness level is the largest unknown"${RESET}`);
|
|
9260
9477
|
return;
|
|
9261
9478
|
}
|
|
@@ -9282,6 +9499,7 @@ async function runExperimentCommand(argv) {
|
|
|
9282
9499
|
mode: mode,
|
|
9283
9500
|
variable: parsed.values.variable,
|
|
9284
9501
|
constants: asList(parsed.values.constants),
|
|
9502
|
+
format: parsed.values.format ?? read.manifest.setup.format,
|
|
9285
9503
|
justification: parsed.values.why,
|
|
9286
9504
|
win: parsed.values.win,
|
|
9287
9505
|
videos,
|
|
@@ -9289,6 +9507,7 @@ async function runExperimentCommand(argv) {
|
|
|
9289
9507
|
capacity,
|
|
9290
9508
|
slots,
|
|
9291
9509
|
channels: read.manifest.setup.channels,
|
|
9510
|
+
channelPlans: read.manifest.setup.channelPlans,
|
|
9292
9511
|
date: today
|
|
9293
9512
|
};
|
|
9294
9513
|
const next = appendRound(readFileSync(read.absPath, "utf8"), input);
|
|
@@ -9298,7 +9517,7 @@ async function runExperimentCommand(argv) {
|
|
|
9298
9517
|
return printJson({ ok: true, round: number, videos, capacity, epochs: Number.isFinite(epochs) ? epochs : null, path: read.absPath });
|
|
9299
9518
|
console.log(`${GREEN}Round ${number} planned${RESET} ${DIM}${read.absPath}${RESET}`);
|
|
9300
9519
|
console.log(` ${DIM}mode${RESET} ${mode}${input.variable ? ` ${DIM}variable${RESET} ${input.variable}` : ""}`);
|
|
9301
|
-
console.log(` ${DIM}sizing${RESET} ${videos} videos ÷ ${capacity || "?"}
|
|
9520
|
+
console.log(` ${DIM}sizing${RESET} ${videos} videos ÷ ${capacity || "?"} slots/epoch = ${BOLD}${Number.isFinite(epochs) ? epochs : "?"} epoch(s)${RESET}`);
|
|
9302
9521
|
if (mode === "structured") {
|
|
9303
9522
|
const rel = path.relative(process.cwd(), dir);
|
|
9304
9523
|
const shown = !rel ? "." : rel.startsWith("..") ? dir : rel;
|
|
@@ -9342,6 +9561,7 @@ async function runExperimentCommand(argv) {
|
|
|
9342
9561
|
const written = appendResult(source, read.manifest, roundIndex, {
|
|
9343
9562
|
video,
|
|
9344
9563
|
metrics,
|
|
9564
|
+
channel: parsed.values.channel,
|
|
9345
9565
|
note: parsed.values.note,
|
|
9346
9566
|
source: parsed.values.source ?? read.manifest.setup.source,
|
|
9347
9567
|
age: parsed.values.age,
|
|
@@ -9374,6 +9594,7 @@ async function runExperimentCommand(argv) {
|
|
|
9374
9594
|
mode: a.round.mode ?? read.manifest.setup.mode,
|
|
9375
9595
|
variable: a.round.variable,
|
|
9376
9596
|
constants: a.round.constants,
|
|
9597
|
+
format: a.round.format ?? read.manifest.setup.format ?? null,
|
|
9377
9598
|
metric: a.metric,
|
|
9378
9599
|
videosPlanned: a.videosPlanned,
|
|
9379
9600
|
epochsNeeded: Number.isFinite(a.epochsNeeded ?? NaN) ? a.epochsNeeded : null,
|
|
@@ -9382,6 +9603,8 @@ async function runExperimentCommand(argv) {
|
|
|
9382
9603
|
unposted: a.unposted,
|
|
9383
9604
|
awaiting: a.awaiting,
|
|
9384
9605
|
median: a.median ?? null,
|
|
9606
|
+
channelMedians: a.channelMedians,
|
|
9607
|
+
retested: a.retested,
|
|
9385
9608
|
scored: a.scored,
|
|
9386
9609
|
outliers: a.outliers,
|
|
9387
9610
|
finding: a.round.finding,
|
|
@@ -9395,15 +9618,20 @@ async function runExperimentCommand(argv) {
|
|
|
9395
9618
|
console.log(`${YELLOW}No ${EXPERIMENTS_FILENAME} in ${dir}.${RESET}`);
|
|
9396
9619
|
console.log(`${DIM}The diary is the source of truth for a campaign that runs for weeks — start it before the first post:${RESET}`);
|
|
9397
9620
|
console.log(` ${BOLD}vidfarm experiment --init --metric comments --channels "$(vidfarm channels --json | jq -r '.channels[].destination_id' | paste -sd,)"${RESET}`);
|
|
9398
|
-
console.log(`${DIM}Method: https://vidfarm.cc/
|
|
9621
|
+
console.log(`${DIM}Method: https://vidfarm.cc/experiments.md${RESET}`);
|
|
9399
9622
|
return;
|
|
9400
9623
|
}
|
|
9401
9624
|
const setup = read.manifest.setup;
|
|
9402
9625
|
const capacity = capacityOf(setup);
|
|
9403
9626
|
console.log(`${BOLD}Experiments${RESET} ${DIM}${read.absPath}${RESET}`);
|
|
9404
9627
|
console.log(` ${DIM}north star${RESET} ${setup.metric ?? `${DEFAULT_METRIC} (assumed)`}`);
|
|
9405
|
-
|
|
9628
|
+
// Echo what the diary literally says ("li_a 3/week"), never the derived float.
|
|
9629
|
+
const rates = setup.channelPlans.length
|
|
9630
|
+
? setup.channelPlans.map((p) => p.raw.trim()).join(", ")
|
|
9631
|
+
: setup.channels.join(", ");
|
|
9632
|
+
console.log(` ${DIM}capacity${RESET} ${capacity || "?"} video(s)/epoch ${DIM}${rates}${RESET}`);
|
|
9406
9633
|
console.log(` ${DIM}mode${RESET} ${setup.mode ?? "creative (assumed)"}${setup.editors ? ` ${DIM}editors${RESET} ${setup.editors}` : ""}`);
|
|
9634
|
+
console.log(` ${DIM}format${RESET} ${setup.format ?? `${DEFAULT_FORMAT} (assumed)`}`);
|
|
9407
9635
|
if (setup.baseline)
|
|
9408
9636
|
console.log(` ${DIM}baseline${RESET} ${setup.baseline}`);
|
|
9409
9637
|
for (const a of analyses) {
|
|
@@ -9411,17 +9639,22 @@ async function runExperimentCommand(argv) {
|
|
|
9411
9639
|
console.log("");
|
|
9412
9640
|
console.log(` ${BOLD}Round ${n}${a.round.title ? ` — ${a.round.title}` : ""}${RESET} ${DIM}${a.round.mode ?? setup.mode ?? ""}${a.round.variable ? ` · variable: ${a.round.variable}` : ""}${RESET}`);
|
|
9413
9641
|
if (a.videosPlanned !== undefined) {
|
|
9414
|
-
console.log(` ${DIM}sizing${RESET} ${a.videosPlanned} videos ÷ ${a.capacity || "?"} = ${Number.isFinite(a.epochsNeeded ?? NaN) ? a.epochsNeeded : "?"} epoch(s) · ${a.epochsRun} run · ${a.postedCount} posted`);
|
|
9642
|
+
console.log(` ${DIM}sizing${RESET} ${a.videosPlanned} videos ÷ ${a.capacity || "?"} slots/epoch = ${Number.isFinite(a.epochsNeeded ?? NaN) ? a.epochsNeeded : "?"} epoch(s) · ${a.epochsRun} run · ${a.postedCount} posted`);
|
|
9415
9643
|
}
|
|
9416
9644
|
if (a.scored.length > 0) {
|
|
9417
|
-
|
|
9645
|
+
const perAccount = Object.entries(a.channelMedians);
|
|
9646
|
+
console.log(` ${DIM}${a.metric} — round median ${a.median ?? "?"}${perAccount.length ? ` · per-account: ${perAccount.map(([c, m]) => `${c} ${m}`).join(" · ")}` : ""}${RESET}`);
|
|
9418
9647
|
for (const s of a.scored) {
|
|
9419
9648
|
const mark = a.outliers.includes(s) ? `${GREEN}★${RESET}` : s === a.weakest ? `${DIM}·${RESET}` : " ";
|
|
9420
9649
|
const ratio = s.ratio !== undefined ? `${s.ratio.toFixed(1)}×` : "";
|
|
9421
9650
|
// When a round was read at more than one age, the same video appears once
|
|
9422
9651
|
// per reading — label them, or the list looks like a duplicate row.
|
|
9423
9652
|
const age = a.readContexts.length > 1 && s.age ? ` @${s.age}` : "";
|
|
9424
|
-
|
|
9653
|
+
// The account-normalized ratio is the trustworthy one — mark it so nobody
|
|
9654
|
+
// reads a cross-account raw ranking as a result.
|
|
9655
|
+
const shown = s.channelRatio !== undefined ? `${s.channelRatio.toFixed(1)}×acct` : ratio;
|
|
9656
|
+
const chan = s.channel ? ` ${s.channel}` : "";
|
|
9657
|
+
console.log(` ${mark} ${s.video.padEnd(8)} ${String(s.value).padStart(7)} ${DIM}${shown.padStart(9)}${age}${chan}${s.variant ? ` ${s.variant}` : ""}${RESET}`);
|
|
9425
9658
|
}
|
|
9426
9659
|
}
|
|
9427
9660
|
if (a.unposted.length > 0)
|
|
@@ -9443,7 +9676,7 @@ async function runExperimentCommand(argv) {
|
|
|
9443
9676
|
for (const w of read.manifest.warnings)
|
|
9444
9677
|
console.log(` ${YELLOW}warn${RESET} ${DIM}${w.message}${w.line ? ` (line ${w.line})` : ""}${RESET}`);
|
|
9445
9678
|
console.log("");
|
|
9446
|
-
console.log(`${DIM}Feedback, not a gate. Method: https://vidfarm.cc/
|
|
9679
|
+
console.log(`${DIM}Feedback, not a gate. Method: https://vidfarm.cc/experiments.md${RESET}`);
|
|
9447
9680
|
}
|
|
9448
9681
|
// `vidfarm mode [interactive|autonomous]` — show or set whether the HUMAN is in
|
|
9449
9682
|
// the loop. Independent of cost mode: any cost mode runs either way, and the
|
|
@@ -9598,7 +9831,7 @@ async function runAddProviderKeyCommand(argv) {
|
|
|
9598
9831
|
const provider = parsed.positionals[0];
|
|
9599
9832
|
const secret = parsed.positionals[1];
|
|
9600
9833
|
if (!provider || !secret)
|
|
9601
|
-
throw new Error("add-provider-key requires <provider> <secret> (provider: openai|gemini|openrouter|perplexity|nvidia|elevenlabs|pixabay).");
|
|
9834
|
+
throw new Error("add-provider-key requires <provider> <secret> (provider: openai|gemini|openrouter|perplexity|nvidia|elevenlabs|pixabay|iconscout). IconScout packs BOTH values into one secret: <client_id>:<client_secret>.");
|
|
9602
9835
|
const ctx = commonContext(parsed.values);
|
|
9603
9836
|
const result = await apiRequest({ method: "POST", host: ctx.host, path: "/api/v1/user/me/provider-keys", auth: ctx.auth, body: { provider, secret, label: parsed.values.label } });
|
|
9604
9837
|
assertApiOk(result, "add-provider-key");
|
|
@@ -11050,7 +11283,10 @@ async function runQaCommand(argv) {
|
|
|
11050
11283
|
"no-harness": { type: "boolean", default: false },
|
|
11051
11284
|
// Pre-rename aliases; `harness` is the name everywhere else now.
|
|
11052
11285
|
regime: { type: "string", multiple: true },
|
|
11053
|
-
"no-regime": { type: "boolean", default: false }
|
|
11286
|
+
"no-regime": { type: "boolean", default: false },
|
|
11287
|
+
// Revision governor — the base case that stops a qa→fix→qa loop.
|
|
11288
|
+
"max-revisions": { type: "string" },
|
|
11289
|
+
"reset-revisions": { type: "boolean", default: false }
|
|
11054
11290
|
}
|
|
11055
11291
|
});
|
|
11056
11292
|
const target = parsed.positionals[0];
|
|
@@ -11058,6 +11294,19 @@ async function runQaCommand(argv) {
|
|
|
11058
11294
|
throw new Error("qa requires a composition path: `vidfarm qa <dir-or-composition.html> [--harness <name|path>] [--json] [--strict]`.");
|
|
11059
11295
|
const htmlPath = resolveCompositionHtmlPath(target);
|
|
11060
11296
|
const html = readFileSync(htmlPath, "utf8");
|
|
11297
|
+
// Count this pass BEFORE anything is printed: at the limit qa withholds the
|
|
11298
|
+
// finding list, because handing an agent a fresh list of things to fix is the
|
|
11299
|
+
// fuel the revise-forever loop runs on.
|
|
11300
|
+
const maxRevisionsRaw = parsed.values["max-revisions"];
|
|
11301
|
+
const maxRevisions = maxRevisionsRaw !== undefined ? Number(maxRevisionsRaw) : undefined;
|
|
11302
|
+
if (maxRevisionsRaw !== undefined && !Number.isFinite(maxRevisions)) {
|
|
11303
|
+
throw new Error(`--max-revisions must be a number (0 disables the limit), got "${maxRevisionsRaw}".`);
|
|
11304
|
+
}
|
|
11305
|
+
const governor = recordQaPass(htmlPath, html, {
|
|
11306
|
+
max: maxRevisions,
|
|
11307
|
+
reset: Boolean(parsed.values["reset-revisions"]),
|
|
11308
|
+
now: new Date().toISOString()
|
|
11309
|
+
});
|
|
11061
11310
|
let report = qaCompositionHtml(html);
|
|
11062
11311
|
// Harness resolution, in precedence order: explicit --harness flags
|
|
11063
11312
|
// (stackable) → VIDFARM_HARNESS (a default for a whole scripting run) → the
|
|
@@ -11093,11 +11342,31 @@ async function runQaCommand(argv) {
|
|
|
11093
11342
|
watch_the_video: watchTheVideoDirective(pastablePath(workDir), render ? pastablePath(render) : null)
|
|
11094
11343
|
};
|
|
11095
11344
|
if (parsed.values.json) {
|
|
11096
|
-
printJson(report);
|
|
11345
|
+
printJson({ ...report, revision_governor: governor });
|
|
11097
11346
|
if (!report.ok && parsed.values.strict)
|
|
11098
11347
|
process.exitCode = 1;
|
|
11099
11348
|
return;
|
|
11100
11349
|
}
|
|
11350
|
+
// ---- the base case ------------------------------------------------------
|
|
11351
|
+
// Findings are withheld, not deleted: --max-revisions raises the ceiling and
|
|
11352
|
+
// --reset-revisions starts the count over, both printed here so nothing is
|
|
11353
|
+
// hidden from a human who genuinely wants another pass.
|
|
11354
|
+
if (governor.limit_reached) {
|
|
11355
|
+
const issues = report.errors.length + report.warnings.length;
|
|
11356
|
+
console.log("");
|
|
11357
|
+
console.log(`${RED}${BOLD}■ REVISION LIMIT REACHED — STOP REVISING THIS VIDEO.${RESET}`);
|
|
11358
|
+
console.log(` ${DIM}${governor.revisions} revision(s) of ${path.basename(htmlPath)}, limit ${governor.max}.${RESET}`);
|
|
11359
|
+
console.log("");
|
|
11360
|
+
console.log(` ${issues === 0 ? "No findings this pass." : `${issues} finding(s) withheld.`} ${DIM}Past the limit, what's left is nearly always taste, not a defect — and re-running qa to find more is how an agent loops forever.${RESET}`);
|
|
11361
|
+
console.log("");
|
|
11362
|
+
console.log(` ${BOLD}Do one of these instead:${RESET}`);
|
|
11363
|
+
console.log(` ${DIM}1.${RESET} Ship it. ${DIM}Watch the render once, then publish.${RESET}`);
|
|
11364
|
+
console.log(` ${DIM}2.${RESET} Ask the human. ${DIM}Show them the video and let them call it.${RESET}`);
|
|
11365
|
+
console.log(` ${DIM}3.${RESET} Genuinely need another pass? ${BOLD}vidfarm qa ${pastablePath(path.dirname(htmlPath))} --max-revisions ${governor.max + 1}${RESET}${DIM} (or --reset-revisions to start over).${RESET}`);
|
|
11366
|
+
console.log("");
|
|
11367
|
+
console.log(formatWatchTheVideoNotice(report.watch_the_video, { yellow: YELLOW, dim: DIM, reset: RESET, bold: BOLD }));
|
|
11368
|
+
return;
|
|
11369
|
+
}
|
|
11101
11370
|
const body = formatQaReport(report, { red: RED, yellow: YELLOW, green: GREEN, dim: DIM, reset: RESET });
|
|
11102
11371
|
if (body)
|
|
11103
11372
|
console.log(body);
|
|
@@ -11127,6 +11396,10 @@ async function runQaCommand(argv) {
|
|
|
11127
11396
|
console.log(`${DIM}Checked mechanics only — nothing here grades the hook, the loop, the payoff, or the bait.${RESET}`);
|
|
11128
11397
|
console.log(`${DIM}Structure: vidfarm harness show hooks · grade this video: vidfarm qa <dir> --harness short-form${RESET}`);
|
|
11129
11398
|
}
|
|
11399
|
+
if (!governor.disabled && governor.revisions > 0) {
|
|
11400
|
+
const left = governor.max - governor.revisions;
|
|
11401
|
+
console.log(`${DIM}Revision ${governor.revisions} of ${governor.max}${left === 1 ? " — one more, then qa stops and you ship or ask the human." : "."}${RESET}`);
|
|
11402
|
+
}
|
|
11130
11403
|
// Last word on every run, clean ones included: this tool never saw the video.
|
|
11131
11404
|
console.log(formatWatchTheVideoNotice(report.watch_the_video, { yellow: YELLOW, dim: DIM, reset: RESET, bold: BOLD }));
|
|
11132
11405
|
if (!report.ok && parsed.values.strict)
|
|
@@ -11605,6 +11878,131 @@ function devcliVersion() {
|
|
|
11605
11878
|
* genuinely offline. Every surface below says so rather than letting an offline
|
|
11606
11879
|
* read imply an offline entitlement.
|
|
11607
11880
|
*/
|
|
11881
|
+
/**
|
|
11882
|
+
* `vidfarm ideas` — the content-ideas angle bank, offline and free.
|
|
11883
|
+
*
|
|
11884
|
+
* WHY A COMMAND AND NOT JUST A DOC: "give me content ideas for <offer>" is one
|
|
11885
|
+
* of the most common asks a director makes, and the honest answer is volume —
|
|
11886
|
+
* 20+ titled ideas, each a DIFFERENT subject rather than 20 rewrites of one.
|
|
11887
|
+
* The bank of 50 reusable frames is what makes that possible without an AI call
|
|
11888
|
+
* and without inventing a list from memory (which drifts, repeats, and forgets
|
|
11889
|
+
* the contrast/pivot shapes entirely). The frames are parsed straight out of
|
|
11890
|
+
* the bundled reference, so the CLI and the skill can never disagree.
|
|
11891
|
+
*
|
|
11892
|
+
* It deliberately does NOT generate the titles: filling a frame needs the
|
|
11893
|
+
* director's offer, audience and vocabulary, which is the agent's job (or the
|
|
11894
|
+
* human's). This hands over the instrument and the loop, for $0.
|
|
11895
|
+
*/
|
|
11896
|
+
/**
|
|
11897
|
+
* Join a frame to the director's topic with the connector English wants.
|
|
11898
|
+
* "the history of" + X is "the history of X", but "the biggest mistakes" + X is
|
|
11899
|
+
* "the biggest mistakes IN X" and "then vs now" + X is "X: then vs now". Naive
|
|
11900
|
+
* concatenation produces lines an agent then has to un-mangle, which defeats
|
|
11901
|
+
* the point of handing it a starter at all.
|
|
11902
|
+
*/
|
|
11903
|
+
function joinIdeaFrame(frame, topic) {
|
|
11904
|
+
const lower = frame.toLowerCase();
|
|
11905
|
+
if (/\bof$/.test(lower))
|
|
11906
|
+
return `${frame} ${topic}`;
|
|
11907
|
+
if (lower.includes(" vs "))
|
|
11908
|
+
return `${topic}: ${frame}`;
|
|
11909
|
+
if (/^(the biggest|the chain reaction)/.test(lower) || lower.startsWith("one "))
|
|
11910
|
+
return `${frame} in ${topic}`;
|
|
11911
|
+
if (/^(the hidden side|the untold story|the complete|the rabbit hole)/.test(lower))
|
|
11912
|
+
return `${frame} of ${topic}`;
|
|
11913
|
+
return `${frame} — ${topic}`;
|
|
11914
|
+
}
|
|
11915
|
+
async function runIdeasCommand(argv) {
|
|
11916
|
+
const parsed = parseArgs({
|
|
11917
|
+
args: argv,
|
|
11918
|
+
allowPositionals: true,
|
|
11919
|
+
options: {
|
|
11920
|
+
json: { type: "boolean", default: false },
|
|
11921
|
+
family: { type: "string" },
|
|
11922
|
+
topic: { type: "string" },
|
|
11923
|
+
count: { type: "string" },
|
|
11924
|
+
families: { type: "boolean", default: false }
|
|
11925
|
+
}
|
|
11926
|
+
});
|
|
11927
|
+
const json = Boolean(parsed.values.json);
|
|
11928
|
+
const topic = String(parsed.values.topic ?? parsed.positionals.join(" ") ?? "").trim();
|
|
11929
|
+
const { frames, families } = loadIdeaBank();
|
|
11930
|
+
if (!frames.length)
|
|
11931
|
+
throw new Error("No bundled content-ideas reference found next to this devcli install. Fetch it with `vidfarm skills add vidfarm`.");
|
|
11932
|
+
if (parsed.values.families) {
|
|
11933
|
+
if (json)
|
|
11934
|
+
return printJson({ ok: true, families, frames: frames.length });
|
|
11935
|
+
console.log(`${DIM}${frames.length} frames in ${families.length} families:${RESET}`);
|
|
11936
|
+
for (const family of families) {
|
|
11937
|
+
console.log(` ${GREEN}${family.padEnd(22)}${RESET} ${DIM}${frames.filter((entry) => entry.family === family).length} frames${RESET}`);
|
|
11938
|
+
}
|
|
11939
|
+
console.log(`\n${DIM}vidfarm ideas --family contrast · vidfarm ideas --topic "<your offer>"${RESET}`);
|
|
11940
|
+
return;
|
|
11941
|
+
}
|
|
11942
|
+
const familyFilter = String(parsed.values.family ?? "").trim().toLowerCase();
|
|
11943
|
+
let picked = familyFilter ? frames.filter((entry) => entry.family.toLowerCase().includes(familyFilter)) : frames;
|
|
11944
|
+
if (familyFilter && !picked.length) {
|
|
11945
|
+
throw new Error(`No family matches "${familyFilter}". Families: ${families.join(", ")} (see \`vidfarm ideas --families\`).`);
|
|
11946
|
+
}
|
|
11947
|
+
const requested = Number(parsed.values.count ?? (topic ? 20 : picked.length));
|
|
11948
|
+
const limit = Number.isFinite(requested) && requested > 0 ? Math.min(requested, picked.length) : picked.length;
|
|
11949
|
+
if (limit < picked.length) {
|
|
11950
|
+
// Spread the sample across families rather than truncating the list — a
|
|
11951
|
+
// top-N slice would hand back eight arc frames and no contrast frames, and
|
|
11952
|
+
// the variety across shapes is the entire value of the bank.
|
|
11953
|
+
const buckets = families.map((family) => picked.filter((entry) => entry.family === family)).filter((bucket) => bucket.length);
|
|
11954
|
+
const spread = [];
|
|
11955
|
+
for (let round = 0; spread.length < limit; round += 1) {
|
|
11956
|
+
let addedThisRound = false;
|
|
11957
|
+
for (const bucket of buckets) {
|
|
11958
|
+
if (round >= bucket.length)
|
|
11959
|
+
continue;
|
|
11960
|
+
spread.push(bucket[round]);
|
|
11961
|
+
addedThisRound = true;
|
|
11962
|
+
if (spread.length >= limit)
|
|
11963
|
+
break;
|
|
11964
|
+
}
|
|
11965
|
+
if (!addedThisRound)
|
|
11966
|
+
break;
|
|
11967
|
+
}
|
|
11968
|
+
picked = spread;
|
|
11969
|
+
}
|
|
11970
|
+
if (json) {
|
|
11971
|
+
return printJson({
|
|
11972
|
+
ok: true,
|
|
11973
|
+
source: "bundled",
|
|
11974
|
+
offline: true,
|
|
11975
|
+
topic: topic || null,
|
|
11976
|
+
total_frames: frames.length,
|
|
11977
|
+
families,
|
|
11978
|
+
frames: picked.map((entry) => ({ frame: entry.frame, family: entry.family, starter: topic ? joinIdeaFrame(entry.frame, topic) : null })),
|
|
11979
|
+
method: "references/content-ideas.md"
|
|
11980
|
+
});
|
|
11981
|
+
}
|
|
11982
|
+
console.log(`${BOLD}Content ideas — the angle bank${RESET} ${DIM}(${picked.length} of ${frames.length} frames · offline, free, no AI call)${RESET}`);
|
|
11983
|
+
if (topic)
|
|
11984
|
+
console.log(`${DIM}Topic: ${RESET}${topic}`);
|
|
11985
|
+
console.log("");
|
|
11986
|
+
let group = "";
|
|
11987
|
+
for (const entry of picked) {
|
|
11988
|
+
if (entry.family !== group) {
|
|
11989
|
+
group = entry.family;
|
|
11990
|
+
console.log(` ${BOLD}${group}${RESET}`);
|
|
11991
|
+
}
|
|
11992
|
+
console.log(topic ? ` ${GREEN}${joinIdeaFrame(entry.frame, topic)}${RESET}` : ` ${GREEN}${entry.frame}${RESET}`);
|
|
11993
|
+
}
|
|
11994
|
+
console.log("");
|
|
11995
|
+
if (topic) {
|
|
11996
|
+
console.log(`${DIM}Those are FRAMES filled with your topic, not finished ideas. Sharpen each into a real title${RESET}`);
|
|
11997
|
+
console.log(`${DIM}("The one pricing mistake that killed our first 400 orders"), then write hook/loop/payoff/bait.${RESET}`);
|
|
11998
|
+
}
|
|
11999
|
+
else {
|
|
12000
|
+
console.log(`${DIM}Pour ONE topic into a frame to get a video: 'the rise of' + your niche. Add --topic "<offer>" to see it.${RESET}`);
|
|
12001
|
+
}
|
|
12002
|
+
console.log(`${DIM}Method + frame→format notes: vidfarm skill show content-ideas · families: vidfarm ideas --families${RESET}`);
|
|
12003
|
+
console.log(`${DIM}Never ship a frame as a hook — the four charges still apply: vidfarm skill show hooks${RESET}`);
|
|
12004
|
+
console.log(`${DIM}Producing the whole set is a batch: vidfarm skill show bulk (one frame per video).${RESET}`);
|
|
12005
|
+
}
|
|
11608
12006
|
async function runSkillDocsCommand(sub, argv) {
|
|
11609
12007
|
const parsed = parseArgs({
|
|
11610
12008
|
args: argv,
|
|
@@ -11645,9 +12043,37 @@ async function runSkillDocsCommand(sub, argv) {
|
|
|
11645
12043
|
console.log(`${DIM}This is documentation, not entitlement: the free-local half (clips, hyperframes, \`vidfarm serve\` render, qa, harnesses, dedupe, local TTS) runs offline; AI generation, hosted render, recycle, download and marketplace still need \`vidfarm login\` and a network call.${RESET}`);
|
|
11646
12044
|
return true;
|
|
11647
12045
|
}
|
|
12046
|
+
if (sub === "topics" || sub === "topic" || sub === "craft") {
|
|
12047
|
+
// The spoken-name index. `skill ls` answers "what files exist"; this answers
|
|
12048
|
+
// "what does this thing KNOW", which is the question an agent actually has.
|
|
12049
|
+
const topics = listPackTopics();
|
|
12050
|
+
if (json)
|
|
12051
|
+
return printJson({ ok: true, pack, source: "bundled", offline: true, topics }), true;
|
|
12052
|
+
console.log(`${DIM}Craft topics in the bundled ${pack} pack — say the name, get the section (offline, no account):${RESET}`);
|
|
12053
|
+
for (const entry of topics) {
|
|
12054
|
+
const where = entry.heading ? `${entry.doc} § ${entry.heading}` : entry.doc;
|
|
12055
|
+
console.log(` ${GREEN}${entry.topic.padEnd(20)}${RESET} ${entry.blurb}`);
|
|
12056
|
+
console.log(` ${" ".repeat(20)} ${DIM}${where}${RESET}`);
|
|
12057
|
+
}
|
|
12058
|
+
console.log(`\n${DIM}vidfarm skill show <topic> · the 50-frame idea bank has its own command: vidfarm ideas${RESET}`);
|
|
12059
|
+
return true;
|
|
12060
|
+
}
|
|
11648
12061
|
if (sub === "show" || sub === "read" || sub === "cat") {
|
|
11649
12062
|
if (!ref)
|
|
11650
|
-
throw new Error('skill show requires a file: `vidfarm skill show references/primitives.md` (shorthand like "primitives"
|
|
12063
|
+
throw new Error('skill show requires a file or topic: `vidfarm skill show references/primitives.md` (shorthand like "primitives", or a topic like "meme-recaption" — `vidfarm skill topics`).');
|
|
12064
|
+
// A spoken topic wins over fuzzy file matching: "meme-recaption" should print
|
|
12065
|
+
// the 25-line section, not the 650-line reference it lives in.
|
|
12066
|
+
const topic = resolvePackTopic(ref);
|
|
12067
|
+
if (topic) {
|
|
12068
|
+
const read = readPackTopic(topic, pack);
|
|
12069
|
+
if (json) {
|
|
12070
|
+
return printJson({ ok: true, pack, topic: topic.topic, path: read.doc.rel, heading: read.heading ?? null, whole_file: read.whole, source: "bundled", contents: read.contents }), true;
|
|
12071
|
+
}
|
|
12072
|
+
process.stdout.write(read.contents.endsWith("\n") ? read.contents : `${read.contents}\n`);
|
|
12073
|
+
if (!read.whole)
|
|
12074
|
+
console.log(`\n${DIM}— one section of ${read.doc.rel}. Whole file: vidfarm skill show ${read.doc.rel}${RESET}`);
|
|
12075
|
+
return true;
|
|
12076
|
+
}
|
|
11651
12077
|
let resolved;
|
|
11652
12078
|
try {
|
|
11653
12079
|
resolved = readPackDoc(ref, pack);
|