@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/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 the three simply.
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 (feed / categories) → GET /api/v1/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 <key> Filter one public-raw category (e.g. emotion:confused)
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+. All of it is billed to YOUR OWN AI provider keys (BYOK) — add them
693
- with 'vidfarm add-provider-key <provider> <key>' or at vidfarm.cc/settings/developer.
694
- Set 'vidfarm cost-mode <minimize|hybrid|rich-ai>' once and every billed command
695
- (generate, music, decompose, cloud render/TTS/STT/greenscreen, create, replicate)
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; hybrid/rich-ai run but print each op's cost. FREE local engines never gate
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 to opt out.
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-ai. Flag > VIDFARM_COST_MODE
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 !== "pure-ai" && !premium) {
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 <minimize|hybrid|rich-ai>)${RESET}` : ""}`);
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 money-saving
6847
- // preference every billed command respects. No arg = show current + explain the
6848
- // three simply so an agent can relay them to the user (and be reminded to ask
6849
- // about saving the choice into whatever agent memory it has). `--clear` forgets it.
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 <minimize|hybrid|rich-ai>${RESET}`);
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}`);