@officexapp/vidfarm-devcli 0.21.24 → 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 +35 -7
- package/.agents/skills/vidfarm/references/assets-and-sourcing.md +26 -0
- package/.agents/skills/vidfarm/references/automation-and-local-dev.md +37 -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 +171 -16
- package/SKILL.md +14 -2
- package/clipper.md +3 -2
- package/dist/src/cli.js +412 -17
- package/dist/src/devcli/cost-mode.js +66 -26
- package/dist/src/devcli/sequence.js +619 -0
- package/dist/src/services/sequence-prompts.js +463 -0
- package/package.json +4 -2
- 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
|
@@ -79,8 +79,13 @@ Account (persisted login — points the CLI + \`serve\` at cloud prod):
|
|
|
79
79
|
logout Clear the persisted credential
|
|
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
|
-
commands respect
|
|
83
|
-
|
|
82
|
+
commands respect, quoted per finished video:
|
|
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).
|
|
87
|
+
Spend is billed to YOUR AI provider keys (BYOK).
|
|
88
|
+
No arg = show current + explain them simply.
|
|
84
89
|
--clear forgets it · --note "<why>" annotates the save.
|
|
85
90
|
Override per-run with --cost-mode <m> / VIDFARM_COST_MODE;
|
|
86
91
|
in "minimize" a billed op is refused until you add --yes.
|
|
@@ -120,9 +125,17 @@ Discover & inspiration (browse the viral-video catalog, add your own source):
|
|
|
120
125
|
--limit <n> Max results
|
|
121
126
|
--cursor <cursor> Continue from next_cursor
|
|
122
127
|
--sort <mode> relevance, wow, automation, or recent
|
|
123
|
-
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.
|
|
124
135
|
--query <text> Keyword/vector query over title + metadata
|
|
125
|
-
--category <
|
|
136
|
+
--category <name> Filter ONE category. Takes a curated shelf
|
|
137
|
+
("scroll-stoppers", "greenscreen", "reaction")
|
|
138
|
+
or an exact taxonomy key (emotion:confused).
|
|
126
139
|
--type <raw_type> Filter one raw family (greenscreen, b_roll, ...)
|
|
127
140
|
--freshness-days <n> Only raws newer than N days
|
|
128
141
|
--bookmark <raw_id> Save that public raw into /raws/public/* → POST /api/v1/public-raws/:rawId/bookmark
|
|
@@ -185,6 +198,35 @@ Generate AI media and drop it on the timeline (for local coding agents):
|
|
|
185
198
|
--no-wait Return the job id instead of polling to the URL
|
|
186
199
|
--place <dir> After generating, place it into that composition
|
|
187
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
|
|
188
230
|
inpaint <image|url> Masked image EDIT — replace ONLY the painted region → POST /api/v1/primitives/images/inpaint
|
|
189
231
|
--mask <png|@file|url> Alpha-mask PNG: TRANSPARENT pixels = the editable region (required)
|
|
190
232
|
--prompt <text> Whole-image edit instruction (what to put in the mask)
|
|
@@ -207,6 +249,28 @@ Generate AI media and drop it on the timeline (for local coding agents):
|
|
|
207
249
|
--out <file> Download the transparent PNG to this path
|
|
208
250
|
--no-wait Return the job id instead of polling to the URL
|
|
209
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
|
|
210
274
|
remove-greenscreen <image|video|url> Key out a FLAT solid background from an
|
|
211
275
|
IMAGE (→ transparent PNG/WebP) or VIDEO (→ transparent
|
|
212
276
|
WebM/VP9 alpha), auto-detecting the media kind.
|
|
@@ -686,13 +750,20 @@ Cost spectrum (default to the cheapest approach that works; see SKILL.director.m
|
|
|
686
750
|
$10+ Heavy AI generation (many/long AI clips, custom characters).
|
|
687
751
|
Notes: image gen is cheap (use freely); AI VIDEO gen is expensive (ask the user
|
|
688
752
|
first). Decompose is a one-time ~$0.10 — or skip it by forking a decomposed template.
|
|
689
|
-
|
|
690
|
-
(
|
|
753
|
+
Per-video budget by mode: minimize = $0 (explicitly free), hybrid = ~$0.01-$1,
|
|
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)
|
|
691
761
|
respects it: minimize refuses billed spend without --yes and points you at the free
|
|
692
|
-
local path;
|
|
762
|
+
local path; the others run but print each op's cost. FREE local engines never gate
|
|
693
763
|
(local render, tts --engine local, stt --engine whisper, remove-greenscreen --local).
|
|
694
764
|
In minimize AND hybrid, 'vidfarm tts' DEFAULTS to the free local Kokoro voice — ask for
|
|
695
|
-
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.
|
|
696
767
|
|
|
697
768
|
Escape hatch — call ANY route directly:
|
|
698
769
|
api <METHOD> <path> Raw REST call with auth + pretty errors
|
|
@@ -843,6 +914,11 @@ async function main() {
|
|
|
843
914
|
case "generate":
|
|
844
915
|
await runGenerateCommand(rest);
|
|
845
916
|
return;
|
|
917
|
+
case "sequence":
|
|
918
|
+
case "storyboard":
|
|
919
|
+
case "previz":
|
|
920
|
+
await runSequenceCommand(rest);
|
|
921
|
+
return;
|
|
846
922
|
case "inpaint":
|
|
847
923
|
await runInpaintCommand(rest);
|
|
848
924
|
return;
|
|
@@ -850,6 +926,11 @@ async function main() {
|
|
|
850
926
|
case "overlay":
|
|
851
927
|
await runCreateOverlayCommand(rest);
|
|
852
928
|
return;
|
|
929
|
+
case "avatar":
|
|
930
|
+
case "talking-head":
|
|
931
|
+
case "spokesperson":
|
|
932
|
+
await runAvatarCommand(rest);
|
|
933
|
+
return;
|
|
853
934
|
case "remove-greenscreen":
|
|
854
935
|
case "remove-background-greenscreen":
|
|
855
936
|
case "rmbg-green":
|
|
@@ -1228,7 +1309,7 @@ function commonOptions() {
|
|
|
1228
1309
|
local: { type: "boolean", default: false },
|
|
1229
1310
|
cloud: { type: "boolean", default: false },
|
|
1230
1311
|
both: { type: "boolean", default: false },
|
|
1231
|
-
// Money-saving preference: minimize | hybrid | pure-
|
|
1312
|
+
// Money-saving preference: minimize | hybrid | rich-ai | pure-videogen. Flag > VIDFARM_COST_MODE
|
|
1232
1313
|
// env > `vidfarm cost-mode` stored value > default (hybrid). Billed commands
|
|
1233
1314
|
// gate on it; `--yes`/-y confirms a billed spend when the mode is "minimize".
|
|
1234
1315
|
"cost-mode": { type: "string" },
|
|
@@ -2628,10 +2709,25 @@ async function runPublicRawsCommand(argv) {
|
|
|
2628
2709
|
category: { type: "string" },
|
|
2629
2710
|
type: { type: "string" },
|
|
2630
2711
|
"freshness-days": { type: "string" },
|
|
2631
|
-
bookmark: { type: "string" }
|
|
2712
|
+
bookmark: { type: "string" },
|
|
2713
|
+
categories: { type: "boolean", default: false }
|
|
2632
2714
|
}
|
|
2633
2715
|
});
|
|
2634
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
|
+
}
|
|
2635
2731
|
const bookmark = parsed.values.bookmark?.trim();
|
|
2636
2732
|
if (bookmark) {
|
|
2637
2733
|
const result = await apiRequest({
|
|
@@ -4147,6 +4243,74 @@ async function runGenerateCommand(argv) {
|
|
|
4147
4243
|
console.log(`${DIM}Place it with: vidfarm place <dir> --src "${mediaUrl}" --kind ${mediaType} --at <time>|--replace <layer_key>${RESET}`);
|
|
4148
4244
|
}
|
|
4149
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
|
+
}
|
|
4150
4314
|
// ── Inpaint: masked image edit ───────────────────────────────────────────────
|
|
4151
4315
|
// The devcli twin of the web /tools/image page. The browser page rasterizes painted
|
|
4152
4316
|
// strokes into an alpha mask (transparent = editable), composes one provider
|
|
@@ -4468,6 +4632,236 @@ async function runCreateOverlayCommand(argv) {
|
|
|
4468
4632
|
console.log(`${DIM}Place it with: vidfarm place <dir> --src "${mediaUrl}" --kind image --at <time>|--replace <layer_key>${RESET}`);
|
|
4469
4633
|
}
|
|
4470
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
|
+
}
|
|
4471
4865
|
// ── Green-screen (chroma-key) background removal ──────────────────────────────
|
|
4472
4866
|
// The devcli twin of the remove-greenscreen primitive: key out a flat, solid
|
|
4473
4867
|
// background from an IMAGE (→ transparent PNG/WebP) OR a VIDEO (→ transparent
|
|
@@ -5151,7 +5545,7 @@ async function runTtsCommand(argv) {
|
|
|
5151
5545
|
/** Set when the cost-mode default ran Kokoro and it failed — so the keyless
|
|
5152
5546
|
* path below doesn't pointlessly retry the same engine. */
|
|
5153
5547
|
let kokoroError = null;
|
|
5154
|
-
if (!engine && !wantCloud && costMode.mode !== "
|
|
5548
|
+
if (!engine && !wantCloud && costMode.mode !== "rich-ai" && costMode.mode !== "pure-videogen" && !premium) {
|
|
5155
5549
|
try {
|
|
5156
5550
|
await runLocalKokoroTts({
|
|
5157
5551
|
text,
|
|
@@ -6833,15 +7227,16 @@ async function runWhoamiCommand(argv) {
|
|
|
6833
7227
|
console.log(`${GREEN}${BOLD}${who}${RESET} → ${ctx.host}`);
|
|
6834
7228
|
console.log(` plan ${customer.isPaidPlan ? `${GREEN}paid${plan}${RESET}` : `${DIM}free${plan}${RESET}`}`);
|
|
6835
7229
|
console.log(` credential ${DIM}${stored ? "persisted login (vidfarm login)" : "env/flag key — not persisted (run vidfarm login to persist)"}${RESET}`);
|
|
6836
|
-
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}` : ""}`);
|
|
6837
7231
|
return;
|
|
6838
7232
|
}
|
|
6839
7233
|
emitResult(result, ctx.json);
|
|
6840
7234
|
}
|
|
6841
|
-
// `vidfarm cost-mode [minimize|hybrid|rich-ai]` — show or set the
|
|
6842
|
-
// preference every billed command respects. No arg = show current +
|
|
6843
|
-
//
|
|
6844
|
-
// 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.
|
|
6845
7240
|
async function runCostModeCommand(argv) {
|
|
6846
7241
|
const parsed = parseArgs({
|
|
6847
7242
|
args: argv,
|
|
@@ -6884,7 +7279,7 @@ async function runCostModeCommand(argv) {
|
|
|
6884
7279
|
console.log("");
|
|
6885
7280
|
console.log(costModeExplainer());
|
|
6886
7281
|
console.log("");
|
|
6887
|
-
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}`);
|
|
6888
7283
|
console.log(`${DIM}Forget it: vidfarm cost-mode --clear · override per-run: --cost-mode <m> or VIDFARM_COST_MODE.${RESET}`);
|
|
6889
7284
|
if (!resolved.isSet) {
|
|
6890
7285
|
console.log(`${DIM}Nothing saved yet — ask the user which one they want before spending AI credits.${RESET}`);
|