@officexapp/vidfarm-devcli 0.21.26 → 0.21.27
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/vidfarm/SKILL.md +26 -6
- package/.agents/skills/vidfarm/references/assets-and-sourcing.md +26 -0
- package/.agents/skills/vidfarm/references/automation-and-local-dev.md +27 -6
- package/.agents/skills/vidfarm/references/core-workflows.md +1 -1
- package/.agents/skills/vidfarm/references/editor-workflows.md +44 -2
- package/.agents/skills/vidfarm/references/primitives.md +28 -0
- package/.agents/skills/vidfarm-media/SKILL.md +3 -1
- package/.agents/skills/vidfarm-media/references/tts.md +1 -1
- package/SKILL.director.md +152 -15
- package/SKILL.md +6 -2
- package/clipper.md +3 -2
- package/dist/src/cli.js +409 -19
- package/dist/src/devcli/cost-mode.js +46 -18
- package/dist/src/devcli/sequence.js +619 -0
- package/dist/src/services/sequence-prompts.js +463 -0
- package/package.json +3 -1
- package/public/assets/file-directory-app.js +26 -26
- package/public/assets/homepage-client-app.js +12 -12
- package/public/serve-shells/editor.html +66 -3
- package/public/serve-shells/library-files.html +60 -3
- package/public/serve-shells/library-raws.html +60 -3
- package/public/serve-shells/tools-clipper.html +60 -3
- package/public/serve-shells/tools-image.html +1069 -483
- package/public/serve-shells/tools-video.html +140 -14
package/dist/src/cli.js
CHANGED
|
@@ -80,9 +80,12 @@ Account (persisted login — points the CLI + \`serve\` at cloud prod):
|
|
|
80
80
|
whoami Show the logged-in account, host, plan, and cost mode
|
|
81
81
|
cost-mode [mode] Show or set the money-saving preference all billed
|
|
82
82
|
commands respect, quoted per finished video:
|
|
83
|
-
minimize = $0 · hybrid = ~$0.01-$1 · rich-ai = $1
|
|
83
|
+
minimize = $0 · hybrid = ~$0.01-$1 · rich-ai = $1+
|
|
84
|
+
(AI video gen → REUSABLE greenscreen raws, remixed
|
|
85
|
+
with hyperframes) · pure-videogen = $5+ (script →
|
|
86
|
+
storyboard → frame-by-frame generated scenes).
|
|
84
87
|
Spend is billed to YOUR AI provider keys (BYOK).
|
|
85
|
-
No arg = show current + explain
|
|
88
|
+
No arg = show current + explain them simply.
|
|
86
89
|
--clear forgets it · --note "<why>" annotates the save.
|
|
87
90
|
Override per-run with --cost-mode <m> / VIDFARM_COST_MODE;
|
|
88
91
|
in "minimize" a billed op is refused until you add --yes.
|
|
@@ -122,9 +125,17 @@ Discover & inspiration (browse the viral-video catalog, add your own source):
|
|
|
122
125
|
--limit <n> Max results
|
|
123
126
|
--cursor <cursor> Continue from next_cursor
|
|
124
127
|
--sort <mode> relevance, wow, automation, or recent
|
|
125
|
-
public-raws [query...] Browse PUBLIC RAWS
|
|
128
|
+
public-raws [query...] Browse PUBLIC RAWS — free, cleared B-roll you → GET /api/v1/public-raws
|
|
129
|
+
can drop straight into a composition.
|
|
130
|
+
--categories LIST THE SHELVES first (curated categories + → GET /api/v1/public-raws/categories
|
|
131
|
+
live counts + what each is FOR). The cheapest
|
|
132
|
+
way to find footage: pick a shelf, then pull
|
|
133
|
+
it with --category. Great for bulk scripting —
|
|
134
|
+
one shelf is a ready-made clip pool.
|
|
126
135
|
--query <text> Keyword/vector query over title + metadata
|
|
127
|
-
--category <
|
|
136
|
+
--category <name> Filter ONE category. Takes a curated shelf
|
|
137
|
+
("scroll-stoppers", "greenscreen", "reaction")
|
|
138
|
+
or an exact taxonomy key (emotion:confused).
|
|
128
139
|
--type <raw_type> Filter one raw family (greenscreen, b_roll, ...)
|
|
129
140
|
--freshness-days <n> Only raws newer than N days
|
|
130
141
|
--bookmark <raw_id> Save that public raw into /raws/public/* → POST /api/v1/public-raws/:rawId/bookmark
|
|
@@ -187,6 +198,35 @@ Generate AI media and drop it on the timeline (for local coding agents):
|
|
|
187
198
|
--no-wait Return the job id instead of polling to the URL
|
|
188
199
|
--place <dir> After generating, place it into that composition
|
|
189
200
|
--at <time> | --replace <layer_key> Placement (fill gap vs replace scene)
|
|
201
|
+
sequence <step> STORYBOARD-DRIVEN VIDEO — brief → character card → storyboard
|
|
202
|
+
sheet → shot prompt → AI video → final cut. Resumable:
|
|
203
|
+
all state lives in sequence.json, rerunning a finished
|
|
204
|
+
step is a no-op. (aliases: storyboard, previz)
|
|
205
|
+
init "<brief>" Scaffold sequence.json + PLAN_TASK.md from one line
|
|
206
|
+
--dir <path> Project folder (default: vidfarm-sequence-<brief>)
|
|
207
|
+
--parts <n> Shots to chain; part 2+ extends the one before (default 1)
|
|
208
|
+
--panels <n> Panels per storyboard sheet (default 8)
|
|
209
|
+
--duration <s> Seconds per part (default 15); --aspect-ratio <r> (16:9)
|
|
210
|
+
--style "<look>" Final-film style lock, e.g. "painterly anime, crisp daylight"
|
|
211
|
+
--board-style <k> rough = monochrome previz (default) | final = rendered in
|
|
212
|
+
the final look, so the board doubles as a style check
|
|
213
|
+
--character <name> --character-description "<silhouette, wardrobe>"
|
|
214
|
+
--no-laban Drop the Laban weight/time/space/flow movement block
|
|
215
|
+
plan Write PLAN_TASK.md for a desktop agent to direct the
|
|
216
|
+
sequence properly; --check validates the plan instead
|
|
217
|
+
characters One identity sheet per character [cheap, ~$0.05 each]
|
|
218
|
+
boards One storyboard sheet per part [cheap, ~$0.05 each]
|
|
219
|
+
prompts Compose the video prompts (free, local, deterministic)
|
|
220
|
+
--print <kind>:<id> Preview a prompt without spending: board:part-01,
|
|
221
|
+
shot:part-01, card:<slug>
|
|
222
|
+
animate Generate video per part [EXPENSIVE, ~$1+ each]
|
|
223
|
+
--resolution <r> --audio --provider <p> --model <m>
|
|
224
|
+
assemble ffmpeg-concat the parts into renders/final.mp4 [free]
|
|
225
|
+
--music <file|url> Lay a music bed under the cut; --out <path>
|
|
226
|
+
run All of the above in order, with a confirm gate before
|
|
227
|
+
each spend. --yes runs it fully autonomously.
|
|
228
|
+
status What's done, what's next (the default step)
|
|
229
|
+
--force Redo a completed step; --part <id> / --only <slug> scope it
|
|
190
230
|
inpaint <image|url> Masked image EDIT — replace ONLY the painted region → POST /api/v1/primitives/images/inpaint
|
|
191
231
|
--mask <png|@file|url> Alpha-mask PNG: TRANSPARENT pixels = the editable region (required)
|
|
192
232
|
--prompt <text> Whole-image edit instruction (what to put in the mask)
|
|
@@ -209,6 +249,28 @@ Generate AI media and drop it on the timeline (for local coding agents):
|
|
|
209
249
|
--out <file> Download the transparent PNG to this path
|
|
210
250
|
--no-wait Return the job id instead of polling to the URL
|
|
211
251
|
--place <dir> --at <time>|--replace <layer_key> Fuse onto a composition
|
|
252
|
+
avatar "<who they are>" CREATE AN AVATAR — a TALKING HEAD video with lip-synced
|
|
253
|
+
spoken audio, generated on a forced greenscreen plate and
|
|
254
|
+
chroma-keyed in the same job → transparent presenter WebM
|
|
255
|
+
you composite over any background. (aliases: talking-head,
|
|
256
|
+
spokesperson) → POST /api/v1/primitives/videos/create-avatar
|
|
257
|
+
--say "<line>" What the avatar SAYS out loud, verbatim (alias --script)
|
|
258
|
+
--voice <desc> Delivery note, e.g. "warm, upbeat, mid-tempo"
|
|
259
|
+
--ref <url|@file|id> Likeness reference — headshot or character sprite card
|
|
260
|
+
(repeatable) → input_references
|
|
261
|
+
--duration <s> Clip length in seconds; --resolution 720p|1080p
|
|
262
|
+
--aspect-ratio <r> e.g. 9:16 (match where it lands)
|
|
263
|
+
--preset <name> Plate color: green (default) | digital-green | blue |
|
|
264
|
+
white | black | magenta; --key-color <hex> overrides
|
|
265
|
+
--tolerance <0..1> Key radius (default 0.3); --softness <0..1> feather (0.1)
|
|
266
|
+
--no-despill Skip edge color-fringe suppression
|
|
267
|
+
--local Generate in the cloud (your keys), key on YOUR machine
|
|
268
|
+
with bundled ffmpeg for free
|
|
269
|
+
--keep-greenscreen Stop at the raw greenscreen MP4; don't key it at all
|
|
270
|
+
--provider <p> --model <m> Video provider/model overrides (omit to auto-pick)
|
|
271
|
+
--out <file> Write the finished avatar here
|
|
272
|
+
--no-wait Return the job id instead of polling to the URL
|
|
273
|
+
--place <dir> --at <time>|--replace <layer_key> Fuse onto a composition
|
|
212
274
|
remove-greenscreen <image|video|url> Key out a FLAT solid background from an
|
|
213
275
|
IMAGE (→ transparent PNG/WebP) or VIDEO (→ transparent
|
|
214
276
|
WebM/VP9 alpha), auto-detecting the media kind.
|
|
@@ -689,15 +751,19 @@ Cost spectrum (default to the cheapest approach that works; see SKILL.director.m
|
|
|
689
751
|
Notes: image gen is cheap (use freely); AI VIDEO gen is expensive (ask the user
|
|
690
752
|
first). Decompose is a one-time ~$0.10 — or skip it by forking a decomposed template.
|
|
691
753
|
Per-video budget by mode: minimize = $0 (explicitly free), hybrid = ~$0.01-$1,
|
|
692
|
-
rich-ai = $1
|
|
693
|
-
with
|
|
694
|
-
|
|
695
|
-
|
|
754
|
+
rich-ai = $1+ (AI video gen buys REUSABLE greenscreen raws — keyed once, then remixed
|
|
755
|
+
with hyperframes HTML/CSS motion and saved for later hybrid/minimize videos),
|
|
756
|
+
pure-videogen = $5+ (no reuse at all: text script → image storyboard → frame-by-frame
|
|
757
|
+
generation of every scene). All of it is billed to YOUR OWN AI provider keys (BYOK) —
|
|
758
|
+
add them with 'vidfarm add-provider-key <provider> <key>' or vidfarm.cc/settings/developer.
|
|
759
|
+
Set 'vidfarm cost-mode <minimize|hybrid|rich-ai|pure-videogen>' once and every billed
|
|
760
|
+
command (generate, music, decompose, cloud render/TTS/STT/greenscreen, create, replicate)
|
|
696
761
|
respects it: minimize refuses billed spend without --yes and points you at the free
|
|
697
|
-
local path;
|
|
762
|
+
local path; the others run but print each op's cost. FREE local engines never gate
|
|
698
763
|
(local render, tts --engine local, stt --engine whisper, remove-greenscreen --local).
|
|
699
764
|
In minimize AND hybrid, 'vidfarm tts' DEFAULTS to the free local Kokoro voice — ask for
|
|
700
|
-
a premium voice (--style/--voice/--provider/--own-key/--cloud) or rich-ai
|
|
765
|
+
a premium voice (--style/--voice/--provider/--own-key/--cloud), or be in rich-ai /
|
|
766
|
+
pure-videogen, to opt out.
|
|
701
767
|
|
|
702
768
|
Escape hatch — call ANY route directly:
|
|
703
769
|
api <METHOD> <path> Raw REST call with auth + pretty errors
|
|
@@ -848,6 +914,11 @@ async function main() {
|
|
|
848
914
|
case "generate":
|
|
849
915
|
await runGenerateCommand(rest);
|
|
850
916
|
return;
|
|
917
|
+
case "sequence":
|
|
918
|
+
case "storyboard":
|
|
919
|
+
case "previz":
|
|
920
|
+
await runSequenceCommand(rest);
|
|
921
|
+
return;
|
|
851
922
|
case "inpaint":
|
|
852
923
|
await runInpaintCommand(rest);
|
|
853
924
|
return;
|
|
@@ -855,6 +926,11 @@ async function main() {
|
|
|
855
926
|
case "overlay":
|
|
856
927
|
await runCreateOverlayCommand(rest);
|
|
857
928
|
return;
|
|
929
|
+
case "avatar":
|
|
930
|
+
case "talking-head":
|
|
931
|
+
case "spokesperson":
|
|
932
|
+
await runAvatarCommand(rest);
|
|
933
|
+
return;
|
|
858
934
|
case "remove-greenscreen":
|
|
859
935
|
case "remove-background-greenscreen":
|
|
860
936
|
case "rmbg-green":
|
|
@@ -1233,7 +1309,7 @@ function commonOptions() {
|
|
|
1233
1309
|
local: { type: "boolean", default: false },
|
|
1234
1310
|
cloud: { type: "boolean", default: false },
|
|
1235
1311
|
both: { type: "boolean", default: false },
|
|
1236
|
-
// Money-saving preference: minimize | hybrid | pure-
|
|
1312
|
+
// Money-saving preference: minimize | hybrid | rich-ai | pure-videogen. Flag > VIDFARM_COST_MODE
|
|
1237
1313
|
// env > `vidfarm cost-mode` stored value > default (hybrid). Billed commands
|
|
1238
1314
|
// gate on it; `--yes`/-y confirms a billed spend when the mode is "minimize".
|
|
1239
1315
|
"cost-mode": { type: "string" },
|
|
@@ -2633,10 +2709,25 @@ async function runPublicRawsCommand(argv) {
|
|
|
2633
2709
|
category: { type: "string" },
|
|
2634
2710
|
type: { type: "string" },
|
|
2635
2711
|
"freshness-days": { type: "string" },
|
|
2636
|
-
bookmark: { type: "string" }
|
|
2712
|
+
bookmark: { type: "string" },
|
|
2713
|
+
categories: { type: "boolean", default: false }
|
|
2637
2714
|
}
|
|
2638
2715
|
});
|
|
2639
2716
|
const ctx = commonContext(parsed.values);
|
|
2717
|
+
// `--categories` is the map BEFORE the territory: the curated shelves with
|
|
2718
|
+
// live counts. Start here when you don't know what footage exists — pick a
|
|
2719
|
+
// shelf, then re-run with --category <slug> to pull its clips.
|
|
2720
|
+
if (parsed.values.categories) {
|
|
2721
|
+
const result = await apiRequest({
|
|
2722
|
+
method: "GET",
|
|
2723
|
+
host: ctx.host,
|
|
2724
|
+
path: "/api/v1/public-raws/categories",
|
|
2725
|
+
auth: ctx.auth
|
|
2726
|
+
});
|
|
2727
|
+
assertApiOk(result, "public-raws categories");
|
|
2728
|
+
emitResult(result, ctx.json, [["Browse ", `${ctx.host}/discover/raws/categories`]]);
|
|
2729
|
+
return;
|
|
2730
|
+
}
|
|
2640
2731
|
const bookmark = parsed.values.bookmark?.trim();
|
|
2641
2732
|
if (bookmark) {
|
|
2642
2733
|
const result = await apiRequest({
|
|
@@ -4152,6 +4243,74 @@ async function runGenerateCommand(argv) {
|
|
|
4152
4243
|
console.log(`${DIM}Place it with: vidfarm place <dir> --src "${mediaUrl}" --kind ${mediaType} --at <time>|--replace <layer_key>${RESET}`);
|
|
4153
4244
|
}
|
|
4154
4245
|
}
|
|
4246
|
+
// ── Sequence: storyboard-driven AI video pipeline ────────────────────────────
|
|
4247
|
+
// `vidfarm sequence` is a multi-step, resumable director's workflow: brief →
|
|
4248
|
+
// character card → storyboard sheet → shot prompt → AI video → final cut. The
|
|
4249
|
+
// orchestration + disk state live in devcli/sequence.ts and the prompt grammar
|
|
4250
|
+
// in services/sequence-prompts.ts; this wrapper only supplies the cloud seam
|
|
4251
|
+
// (auth, primitive jobs, cost gating, downloads) so there is exactly one copy
|
|
4252
|
+
// of the REST plumbing.
|
|
4253
|
+
/** Submit a primitive generate job and poll it to a finished media URL. */
|
|
4254
|
+
async function generatePrimitiveMedia(ctx, kind, payload) {
|
|
4255
|
+
const tracer = `devcli-seq-${Date.now().toString(36)}`;
|
|
4256
|
+
const route = kind === "video" ? "/api/v1/primitives/videos/generate" : "/api/v1/primitives/images/generate";
|
|
4257
|
+
const submit = await apiRequest({ method: "POST", host: ctx.host, path: route, auth: ctx.auth, body: { tracer, payload } });
|
|
4258
|
+
assertApiOk(submit, `generate ${kind}`);
|
|
4259
|
+
const jobId = submit.json?.job_id;
|
|
4260
|
+
if (!jobId)
|
|
4261
|
+
throw new Error(`generate ${kind} did not return a job id.`);
|
|
4262
|
+
const job = await pollPrimitiveJob(ctx, jobId);
|
|
4263
|
+
const url = resolveJobMediaUrl(job);
|
|
4264
|
+
if (!url) {
|
|
4265
|
+
throw new Error(`generate ${kind} ${String(job?.status ?? "did not finish")} — no media URL (job ${jobId}).`);
|
|
4266
|
+
}
|
|
4267
|
+
return { url, jobId };
|
|
4268
|
+
}
|
|
4269
|
+
async function runSequenceCommand(argv) {
|
|
4270
|
+
// Parsed loosely: the subcommand flags live in devcli/sequence.ts, and this
|
|
4271
|
+
// pass only needs the shared host/auth/cost-mode options.
|
|
4272
|
+
const parsed = parseArgs({ args: argv, allowPositionals: true, strict: false, options: commonOptions() });
|
|
4273
|
+
const ctx = commonContext(parsed.values);
|
|
4274
|
+
const { runSequenceCommand: run } = await import("./devcli/sequence.js");
|
|
4275
|
+
await run(argv, {
|
|
4276
|
+
json: ctx.json,
|
|
4277
|
+
yes: ctx.yes,
|
|
4278
|
+
guardBilled: (input) => guardBilled(ctx, input),
|
|
4279
|
+
resolveRefs: (values) => resolveReferenceUrls(ctx, values),
|
|
4280
|
+
async generateImage(input) {
|
|
4281
|
+
const payload = { prompt: input.prompt };
|
|
4282
|
+
if (input.aspectRatio)
|
|
4283
|
+
payload.aspect_ratio = input.aspectRatio;
|
|
4284
|
+
if (input.provider)
|
|
4285
|
+
payload.provider = input.provider;
|
|
4286
|
+
if (input.model)
|
|
4287
|
+
payload.model = input.model;
|
|
4288
|
+
if (input.refs.length)
|
|
4289
|
+
payload.prompt_attachments = input.refs.slice(0, 16);
|
|
4290
|
+
return generatePrimitiveMedia(ctx, "image", payload);
|
|
4291
|
+
},
|
|
4292
|
+
async generateVideo(input) {
|
|
4293
|
+
const payload = {
|
|
4294
|
+
prompt: input.prompt,
|
|
4295
|
+
duration: Math.round(input.durationSeconds)
|
|
4296
|
+
};
|
|
4297
|
+
if (input.aspectRatio)
|
|
4298
|
+
payload.aspect_ratio = input.aspectRatio;
|
|
4299
|
+
if (input.resolution)
|
|
4300
|
+
payload.resolution = input.resolution;
|
|
4301
|
+
if (input.audio)
|
|
4302
|
+
payload.generate_audio = true;
|
|
4303
|
+
if (input.provider)
|
|
4304
|
+
payload.provider = input.provider;
|
|
4305
|
+
if (input.model)
|
|
4306
|
+
payload.model = input.model;
|
|
4307
|
+
if (input.refs.length)
|
|
4308
|
+
payload.input_references = input.refs.slice(0, 8);
|
|
4309
|
+
return generatePrimitiveMedia(ctx, "video", payload);
|
|
4310
|
+
},
|
|
4311
|
+
download: (url, dest) => downloadUrlToFile(url, dest, ctx.auth.apiKey ? { "vidfarm-api-key": ctx.auth.apiKey } : undefined)
|
|
4312
|
+
});
|
|
4313
|
+
}
|
|
4155
4314
|
// ── Inpaint: masked image edit ───────────────────────────────────────────────
|
|
4156
4315
|
// The devcli twin of the web /tools/image page. The browser page rasterizes painted
|
|
4157
4316
|
// strokes into an alpha mask (transparent = editable), composes one provider
|
|
@@ -4473,6 +4632,236 @@ async function runCreateOverlayCommand(argv) {
|
|
|
4473
4632
|
console.log(`${DIM}Place it with: vidfarm place <dir> --src "${mediaUrl}" --kind image --at <time>|--replace <layer_key>${RESET}`);
|
|
4474
4633
|
}
|
|
4475
4634
|
}
|
|
4635
|
+
// ── avatar: talking-head presenter, keyed off its greenscreen ────────────────
|
|
4636
|
+
// "Create an avatar" is ALWAYS this: a generated VIDEO of a talking head with
|
|
4637
|
+
// lip-synced spoken audio, shot on a flat key-color plate, then chroma-keyed so
|
|
4638
|
+
// the presenter composites over whatever is behind them. The devcli twin of the
|
|
4639
|
+
// talking_avatar primitive.
|
|
4640
|
+
//
|
|
4641
|
+
// The generation leg is always the cloud primitive (it rides the caller's own
|
|
4642
|
+
// saved video-provider keys — BYOK, same as the web app). The KEYING leg is
|
|
4643
|
+
// where local/cloud differ:
|
|
4644
|
+
// default → the same job keys it in the cloud → browser-ready transparent
|
|
4645
|
+
// WebM (VP9 alpha, audio preserved) you can drop straight in.
|
|
4646
|
+
// --local → the job returns the raw greenscreen MP4 and bundled ffmpeg keys
|
|
4647
|
+
// it on your machine for free (ProRes .mov fallback if this
|
|
4648
|
+
// ffmpeg build can't write VP9 alpha).
|
|
4649
|
+
// --keep-greenscreen → stop at the plate; don't key at all.
|
|
4650
|
+
async function runAvatarCommand(argv) {
|
|
4651
|
+
const parsed = parseArgs({
|
|
4652
|
+
args: argv,
|
|
4653
|
+
allowPositionals: true,
|
|
4654
|
+
options: {
|
|
4655
|
+
...commonOptions(),
|
|
4656
|
+
prompt: { type: "string" },
|
|
4657
|
+
// What the avatar says out loud. `--script` is the alias.
|
|
4658
|
+
say: { type: "string" },
|
|
4659
|
+
script: { type: "string" },
|
|
4660
|
+
voice: { type: "string" },
|
|
4661
|
+
// Likeness references: headshot, character sprite card, brand photo.
|
|
4662
|
+
ref: { type: "string", multiple: true },
|
|
4663
|
+
duration: { type: "string" },
|
|
4664
|
+
resolution: { type: "string" },
|
|
4665
|
+
"aspect-ratio": { type: "string" },
|
|
4666
|
+
provider: { type: "string" },
|
|
4667
|
+
model: { type: "string" },
|
|
4668
|
+
// Plate + keying knobs (mirror remove-greenscreen).
|
|
4669
|
+
preset: { type: "string" },
|
|
4670
|
+
"key-color": { type: "string" },
|
|
4671
|
+
tolerance: { type: "string" },
|
|
4672
|
+
softness: { type: "string" },
|
|
4673
|
+
"no-despill": { type: "boolean", default: false },
|
|
4674
|
+
local: { type: "boolean", default: false },
|
|
4675
|
+
"keep-greenscreen": { type: "boolean", default: false },
|
|
4676
|
+
out: { type: "string" },
|
|
4677
|
+
"no-wait": { type: "boolean", default: false },
|
|
4678
|
+
tracer: { type: "string" },
|
|
4679
|
+
// Fused placement onto a local composition.html (same as `generate`).
|
|
4680
|
+
place: { type: "string" },
|
|
4681
|
+
at: { type: "string" },
|
|
4682
|
+
replace: { type: "string" },
|
|
4683
|
+
track: { type: "string" },
|
|
4684
|
+
transition: { type: "string" },
|
|
4685
|
+
"transition-duration": { type: "string" },
|
|
4686
|
+
"transition-out": { type: "string" },
|
|
4687
|
+
"transition-out-duration": { type: "string" },
|
|
4688
|
+
"layer-key": { type: "string" }
|
|
4689
|
+
}
|
|
4690
|
+
});
|
|
4691
|
+
const ctx = commonContext(parsed.values);
|
|
4692
|
+
guardBilled(ctx, {
|
|
4693
|
+
label: "AI talking-avatar video (generation with audio + chroma key)",
|
|
4694
|
+
estimate: "expensive, ~$0.50–$1.50 (AI video)",
|
|
4695
|
+
freeAlternative: 'reuse a free talking-head clip — vidfarm public-raws --category talking-head — or key an existing greenscreen clip with vidfarm remove-greenscreen --local ($0)'
|
|
4696
|
+
});
|
|
4697
|
+
const prompt = (parsed.values.prompt ?? parsed.positionals.join(" ")).trim();
|
|
4698
|
+
if (!prompt) {
|
|
4699
|
+
throw new Error('avatar requires a description of WHO the avatar is: `vidfarm avatar "friendly 30-something founder in a hoodie" --say "Here is why we built this."` — describe the person, not the background (the greenscreen plate is forced for you).');
|
|
4700
|
+
}
|
|
4701
|
+
const script = (parsed.values.say ?? parsed.values.script ?? "").trim();
|
|
4702
|
+
const refs = await resolveReferenceUrls(ctx, parsed.values.ref);
|
|
4703
|
+
const presetRaw = parsed.values.preset?.trim().toLowerCase();
|
|
4704
|
+
if (presetRaw && !GREENSCREEN_PRESETS[presetRaw]) {
|
|
4705
|
+
throw new Error(`Unknown --preset "${presetRaw}". Choose one of: ${Object.keys(GREENSCREEN_PRESETS).join(", ")}, or pass --key-color <hex>.`);
|
|
4706
|
+
}
|
|
4707
|
+
// --local keys on this machine, so ask the cloud job for the raw plate.
|
|
4708
|
+
const keyLocally = Boolean(parsed.values.local);
|
|
4709
|
+
const keepPlate = Boolean(parsed.values["keep-greenscreen"]);
|
|
4710
|
+
const payload = { prompt };
|
|
4711
|
+
if (script)
|
|
4712
|
+
payload.script = script;
|
|
4713
|
+
if (parsed.values.voice)
|
|
4714
|
+
payload.voice = parsed.values.voice;
|
|
4715
|
+
if (refs.length)
|
|
4716
|
+
payload.input_references = refs.slice(0, 8);
|
|
4717
|
+
if (parsed.values.duration)
|
|
4718
|
+
payload.duration = Math.round(Number(parsed.values.duration));
|
|
4719
|
+
if (parsed.values.resolution)
|
|
4720
|
+
payload.resolution = parsed.values.resolution;
|
|
4721
|
+
if (parsed.values["aspect-ratio"])
|
|
4722
|
+
payload.aspect_ratio = parsed.values["aspect-ratio"];
|
|
4723
|
+
if (parsed.values.provider)
|
|
4724
|
+
payload.provider = parsed.values.provider;
|
|
4725
|
+
if (parsed.values.model)
|
|
4726
|
+
payload.model = parsed.values.model;
|
|
4727
|
+
if (presetRaw)
|
|
4728
|
+
payload.preset = presetRaw;
|
|
4729
|
+
if (parsed.values["key-color"])
|
|
4730
|
+
payload.key_color = parsed.values["key-color"];
|
|
4731
|
+
if (parsed.values.tolerance)
|
|
4732
|
+
payload.tolerance = Number(parsed.values.tolerance);
|
|
4733
|
+
if (parsed.values.softness)
|
|
4734
|
+
payload.softness = Number(parsed.values.softness);
|
|
4735
|
+
if (parsed.values["no-despill"])
|
|
4736
|
+
payload.despill = false;
|
|
4737
|
+
if (keyLocally || keepPlate)
|
|
4738
|
+
payload.keep_greenscreen = true;
|
|
4739
|
+
const tracer = parsed.values.tracer ?? `devcli-avatar-${Date.now().toString(36)}`;
|
|
4740
|
+
const submit = await apiRequest({ method: "POST", host: ctx.host, path: "/api/v1/primitives/videos/create-avatar", auth: ctx.auth, body: { tracer, payload } });
|
|
4741
|
+
assertApiOk(submit, "avatar");
|
|
4742
|
+
const jobId = submit.json?.job_id;
|
|
4743
|
+
const wait = !parsed.values["no-wait"];
|
|
4744
|
+
if (!wait || !jobId) {
|
|
4745
|
+
if (!ctx.json && jobId)
|
|
4746
|
+
console.log(`${DIM}Queued ${jobId} (tracer ${tracer}). Poll: vidfarm api GET /api/v1/user/me/jobs/${jobId} — or drop --no-wait to auto-poll.${RESET}`);
|
|
4747
|
+
emitResult(submit, ctx.json);
|
|
4748
|
+
return;
|
|
4749
|
+
}
|
|
4750
|
+
if (!ctx.json) {
|
|
4751
|
+
console.log(`${DIM}Generating talking avatar${script ? ` saying "${script.slice(0, 60)}${script.length > 60 ? "…" : ""}"` : ""} on a greenscreen plate (${jobId})… polling every 5s.${RESET}`);
|
|
4752
|
+
}
|
|
4753
|
+
const job = await pollPrimitiveJob(ctx, jobId);
|
|
4754
|
+
const mediaUrl = resolveJobMediaUrl(job);
|
|
4755
|
+
const status = String(job?.status ?? "");
|
|
4756
|
+
if (!mediaUrl) {
|
|
4757
|
+
if (ctx.json) {
|
|
4758
|
+
printJson({ ok: false, job_id: jobId, status, job });
|
|
4759
|
+
}
|
|
4760
|
+
else {
|
|
4761
|
+
console.log(`${RED}Avatar ${status || "did not finish"} — no media URL. Check your video-provider key and wallet.${RESET}`);
|
|
4762
|
+
printJson(job);
|
|
4763
|
+
}
|
|
4764
|
+
process.exitCode = 1;
|
|
4765
|
+
return;
|
|
4766
|
+
}
|
|
4767
|
+
// Local keying leg: pull the plate down and run the free ffmpeg keyer on it.
|
|
4768
|
+
let localOut = null;
|
|
4769
|
+
if (keyLocally && !keepPlate) {
|
|
4770
|
+
const downloadDir = mkdtempSync(path.join(tmpdir(), "vidfarm-avatar-"));
|
|
4771
|
+
const platePath = path.join(downloadDir, "avatar-greenscreen.mp4");
|
|
4772
|
+
try {
|
|
4773
|
+
if (!ctx.json)
|
|
4774
|
+
console.log(`${DIM}Downloading the greenscreen plate…${RESET}`);
|
|
4775
|
+
await downloadUrlToFile(mediaUrl, platePath);
|
|
4776
|
+
const preset = presetRaw ? GREENSCREEN_PRESETS[presetRaw] : undefined;
|
|
4777
|
+
const outPath = parsed.values.out
|
|
4778
|
+
? path.resolve(process.cwd(), String(parsed.values.out))
|
|
4779
|
+
: path.resolve(process.cwd(), "avatar.webm");
|
|
4780
|
+
mkdirSync(path.dirname(outPath), { recursive: true });
|
|
4781
|
+
if (!ctx.json)
|
|
4782
|
+
console.log(`${DIM}Keying the avatar off its greenscreen locally with ffmpeg (free)…${RESET}`);
|
|
4783
|
+
localOut = await removeGreenscreenLocal({
|
|
4784
|
+
sourcePath: platePath,
|
|
4785
|
+
mediaType: "video",
|
|
4786
|
+
outputPath: outPath,
|
|
4787
|
+
keyColor: parsed.values["key-color"] ?? preset?.key_color,
|
|
4788
|
+
tolerance: parsed.values.tolerance !== undefined ? Number(parsed.values.tolerance) : preset?.tolerance,
|
|
4789
|
+
softness: parsed.values.softness !== undefined ? Number(parsed.values.softness) : preset?.softness,
|
|
4790
|
+
despill: !parsed.values["no-despill"]
|
|
4791
|
+
});
|
|
4792
|
+
}
|
|
4793
|
+
finally {
|
|
4794
|
+
rmSync(downloadDir, { recursive: true, force: true });
|
|
4795
|
+
}
|
|
4796
|
+
}
|
|
4797
|
+
// Whatever the caller ends up placing/saving: the local cut-out if we keyed
|
|
4798
|
+
// here, otherwise the cloud result (transparent WebM, or the plate with
|
|
4799
|
+
// --keep-greenscreen).
|
|
4800
|
+
const finalSrc = localOut?.outputPath ?? mediaUrl;
|
|
4801
|
+
let outPath = localOut?.outputPath ?? null;
|
|
4802
|
+
if (!localOut && parsed.values.out) {
|
|
4803
|
+
outPath = path.resolve(process.cwd(), String(parsed.values.out));
|
|
4804
|
+
await downloadUrlToFile(mediaUrl, outPath);
|
|
4805
|
+
}
|
|
4806
|
+
let placement = null;
|
|
4807
|
+
if (parsed.values.place) {
|
|
4808
|
+
const htmlPath = resolveCompositionHtmlPath(String(parsed.values.place));
|
|
4809
|
+
placement = placeMediaOnDisk({
|
|
4810
|
+
htmlPath,
|
|
4811
|
+
src: finalSrc,
|
|
4812
|
+
kind: "video",
|
|
4813
|
+
at: parsed.values.at ? parseTimeToSeconds(String(parsed.values.at)) : undefined,
|
|
4814
|
+
replace: parsed.values.replace,
|
|
4815
|
+
track: parsed.values.track ? Number(parsed.values.track) : undefined,
|
|
4816
|
+
transition: parsed.values.transition,
|
|
4817
|
+
transitionDuration: parsed.values["transition-duration"] ? Number(parsed.values["transition-duration"]) : undefined,
|
|
4818
|
+
transitionOut: parsed.values["transition-out"],
|
|
4819
|
+
transitionOutDuration: parsed.values["transition-out-duration"] ? Number(parsed.values["transition-out-duration"]) : undefined,
|
|
4820
|
+
layerKey: parsed.values["layer-key"]
|
|
4821
|
+
});
|
|
4822
|
+
if (!ctx.json)
|
|
4823
|
+
console.log(`${GREEN}Placed as ${placement.layerKey} in ${htmlPath}. Run \`vidfarm publish --dir ${path.dirname(htmlPath)}\` (or it live-morphs under \`serve\`).${RESET}`);
|
|
4824
|
+
}
|
|
4825
|
+
const transparent = !keepPlate;
|
|
4826
|
+
if (ctx.json) {
|
|
4827
|
+
printJson({
|
|
4828
|
+
ok: true,
|
|
4829
|
+
job_id: jobId,
|
|
4830
|
+
media_url: mediaUrl,
|
|
4831
|
+
transparent,
|
|
4832
|
+
keyed: keepPlate ? "none" : keyLocally ? "local" : "cloud",
|
|
4833
|
+
format: localOut?.format ?? (keepPlate ? "mp4" : "webm"),
|
|
4834
|
+
out: outPath,
|
|
4835
|
+
placement,
|
|
4836
|
+
script: script || null,
|
|
4837
|
+
greenscreen_source_url: job?.result?.greenscreen_source_url ?? null
|
|
4838
|
+
});
|
|
4839
|
+
return;
|
|
4840
|
+
}
|
|
4841
|
+
if (keepPlate) {
|
|
4842
|
+
console.log(`${GREEN}Avatar greenscreen plate ready:${RESET} ${mediaUrl}`);
|
|
4843
|
+
console.log(`${DIM}Key it whenever you like: vidfarm remove-greenscreen "${mediaUrl}" --local${RESET}`);
|
|
4844
|
+
}
|
|
4845
|
+
else if (localOut) {
|
|
4846
|
+
const label = localOut.format === "webm" ? "Transparent talking avatar (WebM)" : "Transparent talking avatar (ProRes 4444 .mov)";
|
|
4847
|
+
console.log(`${GREEN}${label} ready:${RESET} ${localOut.outputPath}`);
|
|
4848
|
+
if (!localOut.webmAlpha) {
|
|
4849
|
+
console.log(`${YELLOW}Note:${RESET} ${DIM}this ffmpeg build can't write VP9 alpha, so a ProRes .mov alpha master was produced — transparent and free, but not browser/editor-playable. Re-run without ${RESET}--local${DIM} for a browser-ready WebM.${RESET}`);
|
|
4850
|
+
}
|
|
4851
|
+
console.log(`${DIM}Greenscreen plate (re-keyable): ${mediaUrl}${RESET}`);
|
|
4852
|
+
}
|
|
4853
|
+
else {
|
|
4854
|
+
console.log(`${GREEN}Transparent talking avatar ready:${RESET} ${mediaUrl}`);
|
|
4855
|
+
if (outPath)
|
|
4856
|
+
console.log(`${DIM}Saved to ${outPath}${RESET}`);
|
|
4857
|
+
const plate = job?.result?.greenscreen_source_url;
|
|
4858
|
+
if (plate)
|
|
4859
|
+
console.log(`${DIM}Greenscreen plate (re-key at a different tolerance for free): ${plate}${RESET}`);
|
|
4860
|
+
}
|
|
4861
|
+
if (!placement) {
|
|
4862
|
+
console.log(`${DIM}Drop it over a background with: vidfarm place <dir> --src "${finalSrc}" --kind video --at <time> (the alpha survives into the render).${RESET}`);
|
|
4863
|
+
}
|
|
4864
|
+
}
|
|
4476
4865
|
// ── Green-screen (chroma-key) background removal ──────────────────────────────
|
|
4477
4866
|
// The devcli twin of the remove-greenscreen primitive: key out a flat, solid
|
|
4478
4867
|
// background from an IMAGE (→ transparent PNG/WebP) OR a VIDEO (→ transparent
|
|
@@ -5156,7 +5545,7 @@ async function runTtsCommand(argv) {
|
|
|
5156
5545
|
/** Set when the cost-mode default ran Kokoro and it failed — so the keyless
|
|
5157
5546
|
* path below doesn't pointlessly retry the same engine. */
|
|
5158
5547
|
let kokoroError = null;
|
|
5159
|
-
if (!engine && !wantCloud && costMode.mode !== "
|
|
5548
|
+
if (!engine && !wantCloud && costMode.mode !== "rich-ai" && costMode.mode !== "pure-videogen" && !premium) {
|
|
5160
5549
|
try {
|
|
5161
5550
|
await runLocalKokoroTts({
|
|
5162
5551
|
text,
|
|
@@ -6838,15 +7227,16 @@ async function runWhoamiCommand(argv) {
|
|
|
6838
7227
|
console.log(`${GREEN}${BOLD}${who}${RESET} → ${ctx.host}`);
|
|
6839
7228
|
console.log(` plan ${customer.isPaidPlan ? `${GREEN}paid${plan}${RESET}` : `${DIM}free${plan}${RESET}`}`);
|
|
6840
7229
|
console.log(` credential ${DIM}${stored ? "persisted login (vidfarm login)" : "env/flag key — not persisted (run vidfarm login to persist)"}${RESET}`);
|
|
6841
|
-
console.log(` ${costModeSummaryLine(ctx.costMode).replace("cost mode: ", "cost mode ")}${!ctx.costMode.isSet ? ` ${DIM}(set with: vidfarm cost-mode
|
|
7230
|
+
console.log(` ${costModeSummaryLine(ctx.costMode).replace("cost mode: ", "cost mode ")}${!ctx.costMode.isSet ? ` ${DIM}(set with: vidfarm cost-mode <${COST_MODE_DISPLAY_LIST.join("|")}>)${RESET}` : ""}`);
|
|
6842
7231
|
return;
|
|
6843
7232
|
}
|
|
6844
7233
|
emitResult(result, ctx.json);
|
|
6845
7234
|
}
|
|
6846
|
-
// `vidfarm cost-mode [minimize|hybrid|rich-ai]` — show or set the
|
|
6847
|
-
// preference every billed command respects. No arg = show current +
|
|
6848
|
-
//
|
|
6849
|
-
// about saving the choice into whatever agent memory it has).
|
|
7235
|
+
// `vidfarm cost-mode [minimize|hybrid|rich-ai|pure-videogen]` — show or set the
|
|
7236
|
+
// money-saving preference every billed command respects. No arg = show current +
|
|
7237
|
+
// explain the modes simply so an agent can relay them to the user (and be
|
|
7238
|
+
// reminded to ask about saving the choice into whatever agent memory it has).
|
|
7239
|
+
// `--clear` forgets it.
|
|
6850
7240
|
async function runCostModeCommand(argv) {
|
|
6851
7241
|
const parsed = parseArgs({
|
|
6852
7242
|
args: argv,
|
|
@@ -6889,7 +7279,7 @@ async function runCostModeCommand(argv) {
|
|
|
6889
7279
|
console.log("");
|
|
6890
7280
|
console.log(costModeExplainer());
|
|
6891
7281
|
console.log("");
|
|
6892
|
-
console.log(`${DIM}Set it: ${BOLD}vidfarm cost-mode
|
|
7282
|
+
console.log(`${DIM}Set it: ${BOLD}vidfarm cost-mode <${COST_MODE_DISPLAY_LIST.join("|")}>${RESET}`);
|
|
6893
7283
|
console.log(`${DIM}Forget it: vidfarm cost-mode --clear · override per-run: --cost-mode <m> or VIDFARM_COST_MODE.${RESET}`);
|
|
6894
7284
|
if (!resolved.isSet) {
|
|
6895
7285
|
console.log(`${DIM}Nothing saved yet — ask the user which one they want before spending AI credits.${RESET}`);
|