@officexapp/vidfarm-devcli 0.21.15 → 0.21.17

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.
Files changed (29) hide show
  1. package/.agents/skills/editor-capabilities/SKILL.md +3 -3
  2. package/.agents/skills/{vidfarm-director → vidfarm}/SKILL.md +40 -7
  3. package/.agents/skills/{vidfarm-director → vidfarm}/references/assets-and-sourcing.md +6 -0
  4. package/.agents/skills/{vidfarm-director → vidfarm}/references/automation-and-local-dev.md +38 -6
  5. package/.agents/skills/{vidfarm-director → vidfarm}/references/core-workflows.md +2 -2
  6. package/.agents/skills/{vidfarm-director → vidfarm}/references/editor-workflows.md +29 -2
  7. package/.agents/skills/{vidfarm-director → vidfarm}/references/rest-api.md +1 -1
  8. package/SKILL.director.md +115 -17
  9. package/SKILL.md +10 -5
  10. package/demo/dist/app.js +207 -207
  11. package/dist/src/cli.js +333 -46
  12. package/dist/src/devcli/cost-mode.js +22 -7
  13. package/dist/src/devcli/doctor.js +12 -2
  14. package/dist/src/devcli/greenscreen-local.js +191 -0
  15. package/dist/src/services/clip-curation/hunt.js +5 -4
  16. package/dist/src/services/clip-curation/media-select.js +76 -30
  17. package/package.json +2 -1
  18. package/public/serve-shells/editor.html +17 -7
  19. package/public/serve-shells/library-files.html +89 -5
  20. package/public/serve-shells/library-raws.html +224 -18
  21. package/public/serve-shells/tools-clipper.html +75 -6
  22. package/public/serve-shells/tools-image.html +174 -7
  23. package/public/serve-shells/tools-video.html +104 -6
  24. /package/.agents/skills/{vidfarm-director → vidfarm}/recipes/find-and-fork-template.md +0 -0
  25. /package/.agents/skills/{vidfarm-director → vidfarm}/recipes/local-edit-render-approve.md +0 -0
  26. /package/.agents/skills/{vidfarm-director → vidfarm}/recipes/onboard-a-new-director.md +0 -0
  27. /package/.agents/skills/{vidfarm-director → vidfarm}/recipes/retheme-template.md +0 -0
  28. /package/.agents/skills/{vidfarm-director → vidfarm}/references/onboarding.md +0 -0
  29. /package/.agents/skills/{vidfarm-director → vidfarm}/references/primitives.md +0 -0
package/dist/src/cli.js CHANGED
@@ -19,6 +19,7 @@ import { formatCompositionLintIssues, lintCompositionHtml } from "./services/com
19
19
  import { resolveFfmpeg } from "./services/clip-curation/ffmpeg.js";
20
20
  import { parseHyperframesJson, runHyperframesCommand } from "./devcli/hyperframes-cli.js";
21
21
  import { renderCompositionStills } from "./devcli/stills.js";
22
+ import { removeGreenscreenLocal, localGreenscreenAvailable, defaultGreenscreenOutPath, GREENSCREEN_PRESETS } from "./devcli/greenscreen-local.js";
22
23
  import { runDoctorCommand } from "./devcli/doctor.js";
23
24
  import { findFreePort } from "./devcli/port-utils.js";
24
25
  import { scanLocalServers } from "./devcli/process-scan.js";
@@ -27,7 +28,7 @@ import { initTelemetry, reportCliCrash } from "./devcli/telemetry.js";
27
28
  import { resolveLocalDataDir, localBackendAvailable, LocalModeUnavailableError, localApiRequest } from "./devcli/local-backend.js";
28
29
  import { startLocalFrontendServer, serveShellsPresent } from "./devcli/local-frontend-server.js";
29
30
  import { readStoredAuth, writeStoredAuth, clearStoredAuth, hostsMatch } from "./devcli/auth-store.js";
30
- import { COST_MODES, CostModeBlockedError, assertBilledAllowed, clearStoredCostMode, costModeExplainer, costModeSummaryLine, normalizeCostMode, resolveCostMode, writeStoredCostMode, COST_MODE_BLURB } from "./devcli/cost-mode.js";
31
+ import { CostModeBlockedError, assertBilledAllowed, clearStoredCostMode, costModeExplainer, costModeSummaryLine, normalizeCostMode, resolveCostMode, writeStoredCostMode, COST_MODE_BLURB, COST_MODE_DISPLAY_LIST, costModeDisplayName } from "./devcli/cost-mode.js";
31
32
  // vidfarm-devcli — command-line bridge for the Vidfarm video studio. The
32
33
  // `serve` command boots the FULL editor locally (single origin, disk-backed
33
34
  // records + storage) so power users edit compositions on disk while a browser
@@ -38,7 +39,7 @@ const DEFAULT_PORT = 4321;
38
39
  // Agent skill files the `update-skill` command can install from the live host
39
40
  // (`GET /skill/<name>`), falling back to the copy bundled in this npm package.
40
41
  const SKILL_TARGETS = {
41
- "vidfarm-director": { route: "/skill/vidfarm-director", bundled: "SKILL.director.md" },
42
+ "vidfarm": { route: "/skill/vidfarm", bundled: "SKILL.director.md" },
42
43
  "vidfarm-platform": { route: "/skill/vidfarm-platform", bundled: "SKILL.platform.md" }
43
44
  };
44
45
  // Most commands below are thin wrappers over ONE Vidfarm REST call (the `→`
@@ -71,7 +72,7 @@ Account (persisted login — points the CLI + \`serve\` at cloud prod):
71
72
  logout Clear the persisted credential
72
73
  whoami Show the logged-in account, host, plan, and cost mode
73
74
  cost-mode [mode] Show or set the money-saving preference all billed
74
- commands respect: minimize | hybrid | pure-ai.
75
+ commands respect: minimize | hybrid | rich-ai.
75
76
  No arg = show current + explain the three simply.
76
77
  --clear forgets it · --note "<why>" annotates the save.
77
78
  Override per-run with --cost-mode <m> / VIDFARM_COST_MODE;
@@ -210,9 +211,11 @@ Generate AI media and drop it on the timeline (for local coding agents):
210
211
  --tolerance <0..1> Key radius (default 0.3); --softness <0..1> edge feather (0.1)
211
212
  --no-despill Skip edge color-fringe suppression
212
213
  --output-format <f> Image only: png|webp
213
- --local Run the keyer in-process for FREE (sharp/ffmpeg, no wallet)
214
- --cloud Force the billed cloud primitive (default when a key is set)
215
- --out <file> Download the transparent result to this path
214
+ --local Key on your machine with bundled ffmpeg for FREE
215
+ (no wallet, no account; cloud-parity chroma key) DEFAULT
216
+ --cloud Force the billed cloud primitive
217
+ --out <file> Write the transparent result to this path (local writes
218
+ <source>.transparent.png/.webm next to it by default)
216
219
  place <dir|composition.html> Insert media into a local composition
217
220
  (fill a gap, replace a scene, or overlay an AUDIO
218
221
  track) — same clip markup the browser editor makes;
@@ -539,8 +542,8 @@ Account:
539
542
 
540
543
  Agent skill (install the latest director skill so your AI agent can act):
541
544
  update-skill DEPRECATED installer alias; installs the canonical
542
- multi-file vidfarm-director pack via \`skills add\`.
543
- Use \`vidfarm skills add vidfarm-director\` directly.
545
+ multi-file vidfarm pack via \`skills add\`.
546
+ Use \`vidfarm skills add vidfarm\` directly.
544
547
  --dir <path> Project root for the canonical pack install
545
548
  --print Print the generated single-file director rollup
546
549
  (copy/share artifact; not the canonical install)
@@ -632,10 +635,10 @@ Cost spectrum (default to the cheapest approach that works; see SKILL.director.m
632
635
  $10+ Heavy AI generation (many/long AI clips, custom characters).
633
636
  Notes: image gen is cheap (use freely); AI VIDEO gen is expensive (ask the user
634
637
  first). Decompose is a one-time ~$0.10 — or skip it by forking a decomposed template.
635
- Set 'vidfarm cost-mode <minimize|hybrid|pure-ai>' once and every billed command
638
+ Set 'vidfarm cost-mode <minimize|hybrid|rich-ai>' once and every billed command
636
639
  (generate, music, decompose, cloud render/TTS/STT/greenscreen, create, replicate)
637
640
  respects it: minimize refuses billed spend without --yes and points you at the free
638
- local path; hybrid/pure-ai run but print each op's cost. FREE local engines never gate
641
+ local path; hybrid/rich-ai run but print each op's cost. FREE local engines never gate
639
642
  (local render, tts --engine local, stt --engine whisper, remove-greenscreen --local).
640
643
 
641
644
  Escape hatch — call ANY route directly:
@@ -673,6 +676,7 @@ const FRONTEND = "\x1b[1m\x1b[36m"; // bold cyan — reserved for openable URLs
673
676
  const BOLD = "\x1b[1m";
674
677
  const DIM = "\x1b[2m";
675
678
  const GREEN = "\x1b[32m";
679
+ const YELLOW = "\x1b[33m";
676
680
  const RED = "\x1b[31m";
677
681
  const RESET = "\x1b[0m";
678
682
  // Referenced by runMarketplaceCommand's SYNCHRONOUS browse path, so it must be
@@ -2974,11 +2978,218 @@ async function runForkCommand(argv) {
2974
2978
  const tId = result.json?.template_id ?? templateId;
2975
2979
  emitResult(result, ctx.json, [["Open editor ", forkId ? editorFrontendUrl(ctx.host, tId, forkId) : null]]);
2976
2980
  }
2981
+ // A weak, UNLICENSED fallback guide for free-tier local decompose. It deliberately
2982
+ // omits Vidfarm's proprietary methodology (that lives in the paid HARNESS.md) and
2983
+ // tells the agent to produce a best-effort decomposition with open reasoning only.
2984
+ const WEAK_FREE_DECOMPOSE_GUIDE = `# Local Decompose — Free Tier (weak, unlicensed)
2985
+
2986
+ You are on Vidfarm's FREE tier. This is a stripped-down, best-effort decompose guide.
2987
+ It does NOT include Vidfarm's licensed decomposition methodology (viral-DNA extraction,
2988
+ editor/replication harnesses, the generative character-card→storyboard→animate
2989
+ workflow). Expect noticeably weaker results than Vidfarm cloud decompose, and expect
2990
+ to burn more of your own desktop-agent tokens iterating to something usable.
2991
+
2992
+ Do your honest best from first principles: watch the source, split it into scenes with
2993
+ timestamps, transcribe any on-screen text into captions, and write a short summary.
2994
+ Fill \`smart-decompose.template.json\` as far as you can — leave the harness/viral-DNA
2995
+ sub-objects mostly empty; you are not licensed to Vidfarm's method for those.
2996
+
2997
+ To get the FULL licensed harness (much better results, runs on your own tokens, and
2998
+ lets you sync the decomposition back to the shared library so others reuse it free),
2999
+ subscribe to Vidfarm and re-run \`vidfarm decompose <forkId> --local\`.
3000
+ `;
3001
+ // The result skeleton the desktop agent fills in and drops at smart-decompose.json.
3002
+ // Kept intentionally sparse — the field docs live in HARNESS.md (paid) or the weak
3003
+ // guide (free); this is just the shape the sync endpoint coerces.
3004
+ const LOCAL_DECOMPOSE_RESULT_TEMPLATE = JSON.stringify({
3005
+ summary: "",
3006
+ durationSeconds: 0,
3007
+ width: 720,
3008
+ height: 1280,
3009
+ scenes: [{ slug: "scene_1", start: 0, duration: 0, label: "", description: "", viral_note: "" }],
3010
+ captions: [{ slug: "caption_1", text: "", start: 0, duration: 0, x: 8, y: 68, width: 84, height: 12, font_size: 0, color: "#ffffff", background: "#000000", background_style: "highlight-translucent", font_family: "", font_weight: 700, viral_note: "" }],
3011
+ viralDna: {},
3012
+ editorHarness: {},
3013
+ replicationHarness: { generative_workflow: { applies: false, steps: [], character_card: {}, storyboard: {}, animation: { default_motion: "ken_burns_static", scenes: [] } } },
3014
+ transcript: null
3015
+ }, null, 2);
3016
+ function buildLocalDecomposeTask(input) {
3017
+ const syncLine = input.paid
3018
+ ? `4. Sync it back to cloud (shared library): \`vidfarm decompose ${input.forkId} --local --sync --dir ${input.dir}\``
3019
+ : `4. (Sync to cloud is PAID-only. Your free decomposition stays local. Subscribe to contribute it back.)`;
3020
+ return `# Decompose task — fork ${input.forkId}
3021
+
3022
+ You are the desktop AI agent. Run this decompose on your OWN tokens.
3023
+
3024
+ Source video: ${input.sourceUrl || "(unknown — pass --source <url> or read it from cloud-video-context.json)"}
3025
+ ${input.contextNote ? `\n${input.contextNote}\n` : ""}
3026
+ Steps:
3027
+ 1. Read ${input.harnessSaved ? "**HARNESS.md** in this folder — it is Vidfarm's LICENSED decomposition methodology (paid). Follow it exactly, including the character-card → storyboard → (ken-burns vs AI-video) generative workflow detection." : "**HARNESS.md** in this folder (free-tier weak guide) and do your honest best."}
3028
+ 2. Analyze the source video (sample frames, read on-screen text, transcribe audio if any).
3029
+ 3. Produce **smart-decompose.json** in this folder, matching the shape in smart-decompose.template.json${input.harnessSaved ? " and every rule in HARNESS.md" : ""}. Fill scenes + captions with real timestamps; ${input.harnessSaved ? "fully populate viralDna, editorHarness, and replicationHarness (including replicationHarness.generative_workflow when the template fits the image→video pipeline)." : "fill what you can."}
3030
+ ${syncLine}
3031
+
3032
+ Do NOT invent footage or audio you cannot see/hear. Ground every scene and caption on a real moment.
3033
+ `;
3034
+ }
3035
+ // Resolve the on-disk staging folder for a local decompose (next to the CWD so the
3036
+ // desktop agent sees the harness, the task, and drops its smart-decompose.json here).
3037
+ function resolveLocalDecomposeDir(forkId, dirFlag) {
3038
+ const raw = typeof dirFlag === "string" && dirFlag.trim() ? dirFlag.trim() : `vidfarm-decompose-${forkId}`;
3039
+ return path.resolve(process.cwd(), raw);
3040
+ }
3041
+ // Read the caller's paid/subscription status from the self endpoint. Used to gate
3042
+ // the licensed harness fetch and to warn free users before a weak local run.
3043
+ async function fetchCustomerPaidStatus(ctx) {
3044
+ try {
3045
+ const res = await apiRequest({ method: "GET", host: ctx.host, path: "/api/v1/user/me", auth: ctx.auth });
3046
+ if (!res.ok || !res.json || typeof res.json !== "object")
3047
+ return { ok: false, paid: false, email: null, planTier: null };
3048
+ const customer = res.json.customer ?? {};
3049
+ return {
3050
+ ok: true,
3051
+ paid: Boolean(customer.isPaidPlan),
3052
+ email: typeof customer.email === "string" ? customer.email : null,
3053
+ planTier: typeof customer.planTier === "string" ? customer.planTier : null
3054
+ };
3055
+ }
3056
+ catch {
3057
+ return { ok: false, paid: false, email: null, planTier: null };
3058
+ }
3059
+ }
3060
+ // Local decompose — the paid-vs-free, prepare-vs-sync workflow behind
3061
+ // `vidfarm decompose <forkId> --local`. PREPARE stages the harness + task + result
3062
+ // skeleton for the desktop agent; SYNC (--sync) posts the agent's produced
3063
+ // smart-decompose.json back to cloud (paid-only). Free users get a weak, unlicensed,
3064
+ // local-only run with an upgrade nudge — so the ethics/licensing live in the tool,
3065
+ // not just the doc, and a customer's agent complies by default.
3066
+ async function runLocalDecomposeCommand(forkId, values) {
3067
+ const ctx = commonContext(values);
3068
+ const dir = resolveLocalDecomposeDir(forkId, values.dir);
3069
+ const resultPath = path.join(dir, "smart-decompose.json");
3070
+ // -------- SYNC phase: push the agent's local result back to the shared library.
3071
+ if (values.sync) {
3072
+ if (!existsSync(resultPath)) {
3073
+ throw new Error(`No decomposition to sync at ${resultPath}. Run \`vidfarm decompose ${forkId} --local\` first, have your agent produce smart-decompose.json, then re-run with --sync.`);
3074
+ }
3075
+ let result;
3076
+ try {
3077
+ result = JSON.parse(readFileSync(resultPath, "utf8"));
3078
+ }
3079
+ catch (e) {
3080
+ throw new Error(`smart-decompose.json is not valid JSON: ${e instanceof Error ? e.message : String(e)}`);
3081
+ }
3082
+ const res = await apiRequest({
3083
+ method: "POST",
3084
+ host: ctx.host,
3085
+ path: `/api/v1/compositions/${encodeURIComponent(forkId)}/auto-decompose/sync`,
3086
+ auth: ctx.auth,
3087
+ body: {
3088
+ result,
3089
+ source_url: typeof values.source === "string" && values.source.trim() ? values.source.trim() : undefined,
3090
+ provider: "local",
3091
+ model: process.env.VIDFARM_LOCAL_DECOMPOSE_MODEL || "desktop-agent"
3092
+ }
3093
+ });
3094
+ if (res.status === 402) {
3095
+ throw new Error("Syncing a local decompose to cloud requires a PAID Vidfarm subscription — the shared template library is a paid, licensed asset. Subscribe to contribute this back, or keep the decomposition local-only.");
3096
+ }
3097
+ assertApiOk(res, "decompose --local --sync");
3098
+ if (ctx.json) {
3099
+ emitResult(res, true);
3100
+ return;
3101
+ }
3102
+ const j = (res.json ?? {});
3103
+ console.log(`${GREEN}Synced decomposition to cloud.${RESET} fork=${j.fork_id ?? forkId} scenes=${j.scene_count ?? "?"} captions=${j.caption_count ?? "?"}${j.generative_workflow_applies ? ` ${BOLD}generative_workflow: applies${RESET}` : ""}`);
3104
+ console.log(`${DIM}The next creator who forks this template gets your decomposition for free.${RESET}`);
3105
+ return;
3106
+ }
3107
+ // -------- PREPARE phase: stage inputs for the desktop agent.
3108
+ const paid = await fetchCustomerPaidStatus(ctx);
3109
+ mkdirSync(dir, { recursive: true });
3110
+ // Pull any existing cloud grounding for the fork (source URL + prior context).
3111
+ let sourceUrl = typeof values.source === "string" && values.source.trim() ? values.source.trim() : "";
3112
+ let contextNote = "";
3113
+ try {
3114
+ const ctxRes = await apiRequest({ method: "GET", host: ctx.host, path: `/api/v1/compositions/${encodeURIComponent(forkId)}/video-context.json`, auth: ctx.auth });
3115
+ if (ctxRes.ok && ctxRes.json && typeof ctxRes.json === "object") {
3116
+ writeFileSync(path.join(dir, "cloud-video-context.json"), JSON.stringify(ctxRes.json, null, 2));
3117
+ const scloud = ctxRes.json.source_url;
3118
+ if (!sourceUrl && typeof scloud === "string" && scloud.trim())
3119
+ sourceUrl = scloud.trim();
3120
+ contextNote = "A prior cloud context was saved to cloud-video-context.json — reuse it where useful.";
3121
+ }
3122
+ }
3123
+ catch {
3124
+ /* fork may not be decomposed yet — fine */
3125
+ }
3126
+ // Fetch the LICENSED harness (paid only). Free users get the weak, unlicensed guide.
3127
+ let harnessSaved = false;
3128
+ if (paid.paid) {
3129
+ try {
3130
+ const hRes = await apiRequest({ method: "GET", host: ctx.host, path: "/api/v1/decompose/harness.md", auth: ctx.auth });
3131
+ if (hRes.ok && typeof hRes.text === "string" && hRes.text.trim().length > 0) {
3132
+ writeFileSync(path.join(dir, "HARNESS.md"), hRes.text);
3133
+ harnessSaved = true;
3134
+ }
3135
+ }
3136
+ catch {
3137
+ /* fall through to the weak guide */
3138
+ }
3139
+ }
3140
+ if (!harnessSaved) {
3141
+ writeFileSync(path.join(dir, "HARNESS.md"), WEAK_FREE_DECOMPOSE_GUIDE);
3142
+ }
3143
+ writeFileSync(path.join(dir, "smart-decompose.template.json"), LOCAL_DECOMPOSE_RESULT_TEMPLATE);
3144
+ writeFileSync(path.join(dir, "DECOMPOSE_TASK.md"), buildLocalDecomposeTask({ forkId, dir, sourceUrl, paid: paid.paid, harnessSaved, contextNote }));
3145
+ if (ctx.json) {
3146
+ emitResult({
3147
+ status: 200,
3148
+ ok: true,
3149
+ json: {
3150
+ ok: true,
3151
+ prepared: true,
3152
+ dir,
3153
+ paid: paid.paid,
3154
+ harness: harnessSaved ? "licensed" : "weak-free",
3155
+ source_url: sourceUrl || null,
3156
+ task: path.join(dir, "DECOMPOSE_TASK.md"),
3157
+ result_expected: resultPath,
3158
+ sync_cmd: paid.paid ? `vidfarm decompose ${forkId} --local --sync --dir ${dir}` : null
3159
+ },
3160
+ text: ""
3161
+ }, true);
3162
+ return;
3163
+ }
3164
+ if (paid.paid) {
3165
+ console.log(`${GREEN}Local decompose prepared (PAID — latest licensed harness fetched).${RESET}`);
3166
+ }
3167
+ else {
3168
+ console.log(`${RED}Local decompose prepared (FREE tier — WEAK, UNLICENSED).${RESET}`);
3169
+ console.log(`${DIM}Heads up: the free local decompose uses a stripped-down method and your OWN desktop-agent tokens. It typically produces WORSE viral-DNA / harness quality than Vidfarm cloud AND burns more tokens iterating. Cloud decompose (\`vidfarm decompose ${forkId}\`) is ~$0.10 one-time, runs Vidfarm's best models, and its result is shared free with the network. You also CANNOT sync a free local decompose back to cloud.${RESET}`);
3170
+ console.log(`${DIM}Subscribe to run the licensed harness locally on your own tokens AND contribute your decompositions back to the shared library.${RESET}`);
3171
+ }
3172
+ console.log("");
3173
+ console.log(` 1. Open ${BOLD}${path.join(dir, "DECOMPOSE_TASK.md")}${RESET} and follow it (your desktop AI agent does the analysis).`);
3174
+ console.log(` 2. Your agent writes ${BOLD}${resultPath}${RESET} matching smart-decompose.template.json${harnessSaved ? " + HARNESS.md" : ""}.`);
3175
+ if (paid.paid)
3176
+ console.log(` 3. Sync back: ${BOLD}vidfarm decompose ${forkId} --local --sync --dir ${dir}${RESET}`);
3177
+ else
3178
+ console.log(` 3. ${DIM}(Sync to cloud is paid-only — subscribe to contribute this back.)${RESET}`);
3179
+ }
2977
3180
  async function runDecomposeCommand(argv) {
2978
- const parsed = parseArgs({ args: argv, allowPositionals: true, options: { ...commonOptions(), mode: { type: "string", default: "smart" }, prompt: { type: "string" } } });
3181
+ const parsed = parseArgs({ args: argv, allowPositionals: true, options: { ...commonOptions(), mode: { type: "string", default: "smart" }, prompt: { type: "string" }, sync: { type: "boolean", default: false }, dir: { type: "string" }, source: { type: "string" } } });
2979
3182
  const forkId = parsed.positionals[0];
2980
3183
  if (!forkId)
2981
3184
  throw new Error("decompose requires a fork id.");
3185
+ // `--local` runs decompose on the user's OWN desktop AI-agent tokens instead of
3186
+ // billing Vidfarm cloud. Paid users pull the latest licensed harness + can sync
3187
+ // the result back to the shared library; free users get a weaker, unlicensed,
3188
+ // local-only path (with an upgrade nudge). See runLocalDecomposeCommand.
3189
+ if (parsed.values.local) {
3190
+ await runLocalDecomposeCommand(forkId, parsed.values);
3191
+ return;
3192
+ }
2982
3193
  const ctx = commonContext(parsed.values);
2983
3194
  guardBilled(ctx, {
2984
3195
  label: "AI decompose (scene split + annotations)",
@@ -4192,10 +4403,17 @@ async function runCreateOverlayCommand(argv) {
4192
4403
  // The devcli twin of the remove-greenscreen primitive: key out a flat, solid
4193
4404
  // background from an IMAGE (→ transparent PNG/WebP) OR a VIDEO (→ transparent
4194
4405
  // WebM/VP9 alpha), auto-detecting the media kind. LOCAL vs CLOUD: --local runs
4195
- // the keyer in-process for FREE (sharp/ffmpeg, no wallet); --cloud runs the
4196
- // billed primitive. Default = cloud (paid), unless there's no cloud key AND the
4197
- // local backend is present (free / offline), in which case it runs local.
4198
- const GREENSCREEN_PRESET_NAMES = new Set(["green", "digital-green", "blue", "white", "black", "magenta"]);
4406
+ // the keyer on your machine with bundled ffmpeg for FREE (no wallet, no
4407
+ // backend same chromakey/despill filter chain as the cloud primitive, so the
4408
+ // output matches); --cloud runs the billed primitive. Default = local (free)
4409
+ // whenever ffmpeg is available, since keying a flat color is deterministic and
4410
+ // cloud-equivalent; the runner falls back to cloud only when ffmpeg is missing.
4411
+ //
4412
+ // NOTE: preset-name validation reads the IMPORTED `GREENSCREEN_PRESETS` (not a
4413
+ // module-local const) on purpose — `void main()` runs near the top of this file
4414
+ // and executes synchronously until its first `await`, so a command handler that
4415
+ // touches a module-scope const declared further down would TDZ. Imported
4416
+ // bindings are initialized before this module's body runs, so they're safe.
4199
4417
  function resolveGreenscreenTarget(values) {
4200
4418
  if (values.local)
4201
4419
  return "local";
@@ -4204,10 +4422,9 @@ function resolveGreenscreenTarget(values) {
4204
4422
  const env = (process.env.VIDFARM_TARGET ?? "").trim().toLowerCase();
4205
4423
  if (env === "local" || env === "cloud")
4206
4424
  return env;
4207
- const hasCloudKey = Boolean(values["api-key"] ?? process.env.VIDFARM_API_KEY);
4208
- if (!hasCloudKey && localBackendAvailable())
4209
- return "local";
4210
- return "cloud";
4425
+ // Free local ffmpeg keyer is cloud-parity prefer it by default (the runner
4426
+ // downgrades to cloud if ffmpeg turns out to be unavailable at run time).
4427
+ return "local";
4211
4428
  }
4212
4429
  async function runRemoveGreenscreenCommand(argv) {
4213
4430
  const parsed = parseArgs({
@@ -4231,25 +4448,41 @@ async function runRemoveGreenscreenCommand(argv) {
4231
4448
  }
4232
4449
  });
4233
4450
  const ctx = commonContext(parsed.values);
4234
- const target = resolveGreenscreenTarget(parsed.values);
4235
- if (target === "local" && !localBackendAvailable())
4236
- throw new LocalModeUnavailableError();
4237
- if (target !== "local") {
4238
- guardBilled(ctx, {
4239
- label: "cloud greenscreen removal",
4240
- estimate: "billed at real compute ×1.2",
4241
- freeAlternative: "vidfarm remove-greenscreen --local (in-process sharp/ffmpeg, $0.00)"
4242
- });
4243
- }
4451
+ let target = resolveGreenscreenTarget(parsed.values);
4244
4452
  const sourceArg = parsed.values.source ?? parsed.positionals[0];
4245
4453
  if (!sourceArg) {
4246
- throw new Error("remove-greenscreen requires a source image or video: `vidfarm remove-greenscreen <image|video|url> [--preset green|blue|white|black|digital-green|magenta] [--key-color #00FF00] [--local] [--out cutout.png|.webm]`.");
4454
+ throw new Error("remove-greenscreen requires a source image or video: `vidfarm remove-greenscreen <image|video|url> [--preset green|blue|white|black|digital-green|magenta] [--key-color #00FF00] [--cloud] [--out cutout.png|.webm]`.");
4247
4455
  }
4248
- const sourceUrl = await resolveSingleMediaUrl(ctx, sourceArg);
4249
4456
  const presetRaw = parsed.values.preset?.trim().toLowerCase();
4250
- if (presetRaw && !GREENSCREEN_PRESET_NAMES.has(presetRaw)) {
4251
- throw new Error(`Unknown --preset "${presetRaw}". Choose one of: ${[...GREENSCREEN_PRESET_NAMES].join(", ")}, or pass --key-color <hex>.`);
4457
+ if (presetRaw && !(presetRaw in GREENSCREEN_PRESETS)) {
4458
+ throw new Error(`Unknown --preset "${presetRaw}". Choose one of: ${Object.keys(GREENSCREEN_PRESETS).join(", ")}, or pass --key-color <hex>.`);
4459
+ }
4460
+ // The local keyer is free but needs ffmpeg. If ffmpeg is missing, downgrade to
4461
+ // cloud when possible (or error clearly if the user explicitly demanded local).
4462
+ if (target === "local" && !(await localGreenscreenAvailable())) {
4463
+ const hasCloudKey = Boolean(parsed.values["api-key"] ?? process.env.VIDFARM_API_KEY);
4464
+ if (parsed.values.local) {
4465
+ throw new Error("Local greenscreen needs ffmpeg, which wasn't found. Install ffmpeg (or `npm i -g ffmpeg-static`), or run without --local to use the cloud keyer.");
4466
+ }
4467
+ if (!hasCloudKey) {
4468
+ throw new Error("Greenscreen removal needs either ffmpeg (for the free local keyer) or a cloud API key. Install ffmpeg, or set VIDFARM_API_KEY / pass --cloud --api-key.");
4469
+ }
4470
+ target = "cloud";
4471
+ if (!ctx.json)
4472
+ console.log(`${DIM}ffmpeg not found — using the cloud keyer instead.${RESET}`);
4473
+ }
4474
+ // ---- LOCAL: free, offline, bundled ffmpeg (cloud-parity chroma key) --------
4475
+ if (target === "local") {
4476
+ await runLocalGreenscreen(ctx, parsed.values, sourceArg, presetRaw);
4477
+ return;
4252
4478
  }
4479
+ // ---- CLOUD: billed primitive ----------------------------------------------
4480
+ guardBilled(ctx, {
4481
+ label: "cloud greenscreen removal",
4482
+ estimate: "billed at real compute ×1.2",
4483
+ freeAlternative: "vidfarm remove-greenscreen --local (bundled ffmpeg, $0.00)"
4484
+ });
4485
+ const sourceUrl = await resolveSingleMediaUrl(ctx, sourceArg);
4253
4486
  const mediaTypeArg = parsed.values["media-type"]?.trim().toLowerCase();
4254
4487
  const mediaType = mediaTypeArg === "image" || mediaTypeArg === "video"
4255
4488
  ? mediaTypeArg
@@ -4269,13 +4502,11 @@ async function runRemoveGreenscreenCommand(argv) {
4269
4502
  payload.output_format = parsed.values["output-format"];
4270
4503
  const tracer = parsed.values.tracer ?? `devcli-greenscreen-${Date.now().toString(36)}`;
4271
4504
  const route = "/api/v1/primitives/remove-greenscreen";
4272
- const submit = target === "local"
4273
- ? await localApiRequest({ method: "POST", path: route, auth: ctx.auth, body: { tracer, payload }, home: ctx.home })
4274
- : await apiRequest({ method: "POST", host: ctx.host, path: route, auth: ctx.auth, body: { tracer, payload } });
4505
+ const submit = await apiRequest({ method: "POST", host: ctx.host, path: route, auth: ctx.auth, body: { tracer, payload } });
4275
4506
  assertApiOk(submit, "remove-greenscreen");
4276
4507
  const jobId = submit.json?.job_id;
4277
4508
  if (!ctx.json)
4278
- console.log(`${DIM}${target === "local" ? "Local (free)" : "Cloud"} ${mediaType} greenscreen removal…${RESET}`);
4509
+ console.log(`${DIM}Cloud ${mediaType} greenscreen removal…${RESET}`);
4279
4510
  const wait = !parsed.values["no-wait"];
4280
4511
  if (!wait || !jobId) {
4281
4512
  if (!ctx.json && jobId)
@@ -4314,6 +4545,62 @@ async function runRemoveGreenscreenCommand(argv) {
4314
4545
  console.log(`${DIM}Place it with: vidfarm set-media <dir> --src "${mediaUrl}" --replace <layer_key> (keeps its transparency).${RESET}`);
4315
4546
  }
4316
4547
  }
4548
+ // Run the greenscreen key LOCALLY with bundled ffmpeg — free, offline, no
4549
+ // wallet, no in-process backend (so it works in the published cloud-only CLI,
4550
+ // exactly like `remove-background` rides the bundled ONNX engine). Produces a
4551
+ // transparent file on disk and prints its path, mirroring remove-background's
4552
+ // UX. Uses the SAME chromakey/despill filter chain as the cloud primitive.
4553
+ async function runLocalGreenscreen(ctx, values, sourceArg, presetRaw) {
4554
+ // Resolve the source to a real local file: a path on disk is used in place;
4555
+ // anything else (url / raw id / raws path) resolves to a URL and downloads.
4556
+ const directPath = path.resolve(process.cwd(), sourceArg);
4557
+ const isLocalFile = !/^https?:\/\//i.test(sourceArg) && existsSync(directPath);
4558
+ let sourcePath = directPath;
4559
+ let downloadDir = null;
4560
+ if (!isLocalFile) {
4561
+ const sourceUrl = await resolveSingleMediaUrl(ctx, sourceArg);
4562
+ downloadDir = mkdtempSync(path.join(tmpdir(), "vidfarm-greenscreen-dl-"));
4563
+ sourcePath = path.join(downloadDir, path.basename(new URL(sourceUrl).pathname) || "source.bin");
4564
+ if (!ctx.json)
4565
+ console.log(`${DIM}Downloading source…${RESET}`);
4566
+ await downloadUrlToFile(sourceUrl, sourcePath);
4567
+ }
4568
+ try {
4569
+ const mediaTypeArg = values["media-type"]?.trim().toLowerCase();
4570
+ const mediaType = mediaTypeArg === "image" || mediaTypeArg === "video"
4571
+ ? mediaTypeArg
4572
+ : (/\.(mp4|mov|webm|m4v|mkv)(\?|#|$)/i.test(sourcePath) ? "video" : "image");
4573
+ // Fold preset defaults, letting explicit flags win (mirrors the cloud path).
4574
+ const preset = presetRaw ? GREENSCREEN_PRESETS[presetRaw] : undefined;
4575
+ const keyColor = values["key-color"] ?? preset?.key_color;
4576
+ const tolerance = values.tolerance !== undefined ? Number(values.tolerance) : preset?.tolerance;
4577
+ const softness = values.softness !== undefined ? Number(values.softness) : preset?.softness;
4578
+ const despill = !values["no-despill"];
4579
+ const outputFormat = values["output-format"]?.trim().toLowerCase();
4580
+ const outPath = values.out
4581
+ ? path.resolve(process.cwd(), String(values.out))
4582
+ : defaultGreenscreenOutPath(sourcePath, mediaType, outputFormat);
4583
+ mkdirSync(path.dirname(outPath), { recursive: true });
4584
+ if (!ctx.json)
4585
+ console.log(`${DIM}Keying out ${presetRaw ?? keyColor ?? "green"} background locally with ffmpeg (free)…${RESET}`);
4586
+ const result = await removeGreenscreenLocal({ sourcePath, mediaType, outputPath: outPath, keyColor, tolerance, softness, despill });
4587
+ if (ctx.json) {
4588
+ printJson({ ok: true, target: "local", media_type: mediaType, format: result.format, out: result.outputPath, bytes: safeSize(result.outputPath), webm_alpha: result.webmAlpha });
4589
+ }
4590
+ else {
4591
+ const label = result.format === "webm" ? "Transparent WebM" : result.format === "mov" ? "Transparent ProRes 4444 .mov" : "Transparent cut-out";
4592
+ console.log(`${GREEN}${label} ready:${RESET} ${result.outputPath} ${DIM}(${formatBytes(safeSize(result.outputPath))})${RESET}`);
4593
+ if (mediaType === "video" && !result.webmAlpha) {
4594
+ console.log(`${YELLOW}Note:${RESET} ${DIM}your local ffmpeg can't encode transparent WebM (VP9 alpha), so a ProRes .mov alpha master was written instead — genuinely transparent and free, but not directly browser/editor-playable. For a browser-ready transparent WebM, re-run with ${RESET}--cloud${DIM}.${RESET}`);
4595
+ }
4596
+ console.log(`${DIM}Drop it on a composition with: vidfarm set-media <dir> --src "${result.outputPath}" --replace <layer_key> (keeps its transparency).${RESET}`);
4597
+ }
4598
+ }
4599
+ finally {
4600
+ if (downloadDir)
4601
+ rmSync(downloadDir, { recursive: true, force: true });
4602
+ }
4603
+ }
4317
4604
  // Poll a greenscreen job through the same backend it was submitted to (local
4318
4605
  // in-process app or cloud). Mirrors pollPrimitiveJob's terminal conditions.
4319
4606
  async function pollGreenscreenJob(ctx, target, jobId) {
@@ -6026,12 +6313,12 @@ async function runWhoamiCommand(argv) {
6026
6313
  console.log(`${GREEN}${BOLD}${who}${RESET} → ${ctx.host}`);
6027
6314
  console.log(` plan ${customer.isPaidPlan ? `${GREEN}paid${plan}${RESET}` : `${DIM}free${plan}${RESET}`}`);
6028
6315
  console.log(` credential ${DIM}${stored ? "persisted login (vidfarm login)" : "env/flag key — not persisted (run vidfarm login to persist)"}${RESET}`);
6029
- console.log(` ${costModeSummaryLine(ctx.costMode).replace("cost mode: ", "cost mode ")}${!ctx.costMode.isSet ? ` ${DIM}(set with: vidfarm cost-mode <minimize|hybrid|pure-ai>)${RESET}` : ""}`);
6316
+ console.log(` ${costModeSummaryLine(ctx.costMode).replace("cost mode: ", "cost mode ")}${!ctx.costMode.isSet ? ` ${DIM}(set with: vidfarm cost-mode <minimize|hybrid|rich-ai>)${RESET}` : ""}`);
6030
6317
  return;
6031
6318
  }
6032
6319
  emitResult(result, ctx.json);
6033
6320
  }
6034
- // `vidfarm cost-mode [minimize|hybrid|pure-ai]` — show or set the money-saving
6321
+ // `vidfarm cost-mode [minimize|hybrid|rich-ai]` — show or set the money-saving
6035
6322
  // preference every billed command respects. No arg = show current + explain the
6036
6323
  // three simply so an agent can relay them to the user (and be reminded to ask
6037
6324
  // about saving the choice into whatever agent memory it has). `--clear` forgets it.
@@ -6054,14 +6341,14 @@ async function runCostModeCommand(argv) {
6054
6341
  if (requested) {
6055
6342
  const mode = normalizeCostMode(requested);
6056
6343
  if (!mode) {
6057
- throw new Error(`Unknown cost mode "${requested}". Choose one of: ${COST_MODES.join(", ")}.`);
6344
+ throw new Error(`Unknown cost mode "${requested}". Choose one of: ${COST_MODE_DISPLAY_LIST.join(", ")}.`);
6058
6345
  }
6059
6346
  // Stamp time on the host so this stays deterministic-friendly for scripts.
6060
6347
  const savedAt = new Date().toISOString();
6061
6348
  const file = writeStoredCostMode(mode, savedAt, parsed.values.note ?? null, home);
6062
6349
  if (json)
6063
6350
  return printJson({ ok: true, cost_mode: mode, saved_at: savedAt, file });
6064
- console.log(`${GREEN}${BOLD}Cost mode: ${mode}${RESET}`);
6351
+ console.log(`${GREEN}${BOLD}Cost mode: ${costModeDisplayName(mode)}${RESET}`);
6065
6352
  console.log(`${DIM}${COST_MODE_BLURB[mode]}${RESET}`);
6066
6353
  console.log(`${DIM}Saved to ${file} (every billed command now respects it).${RESET}`);
6067
6354
  console.log(`${DIM}Tip: if the user wants this remembered across sessions, offer to also save it to your` +
@@ -6077,7 +6364,7 @@ async function runCostModeCommand(argv) {
6077
6364
  console.log("");
6078
6365
  console.log(costModeExplainer());
6079
6366
  console.log("");
6080
- console.log(`${DIM}Set it: ${BOLD}vidfarm cost-mode <minimize|hybrid|pure-ai>${RESET}`);
6367
+ console.log(`${DIM}Set it: ${BOLD}vidfarm cost-mode <minimize|hybrid|rich-ai>${RESET}`);
6081
6368
  console.log(`${DIM}Forget it: vidfarm cost-mode --clear · override per-run: --cost-mode <m> or VIDFARM_COST_MODE.${RESET}`);
6082
6369
  if (!resolved.isSet) {
6083
6370
  console.log(`${DIM}Nothing saved yet — ask the user which one they want before spending AI credits.${RESET}`);
@@ -7460,7 +7747,7 @@ async function runUpdateSkillCommand(argv) {
7460
7747
  }
7461
7748
  });
7462
7749
  const ctx = commonContext(parsed.values);
7463
- const skillNames = ["vidfarm-director", ...(parsed.values.platform ? ["vidfarm-platform"] : [])];
7750
+ const skillNames = ["vidfarm", ...(parsed.values.platform ? ["vidfarm-platform"] : [])];
7464
7751
  if (parsed.values.print) {
7465
7752
  for (const name of skillNames) {
7466
7753
  const { contents } = await fetchSkillContents(ctx.host, SKILL_TARGETS[name]);
@@ -7469,14 +7756,14 @@ async function runUpdateSkillCommand(argv) {
7469
7756
  return;
7470
7757
  }
7471
7758
  if (parsed.values.global || parsed.values.platform) {
7472
- throw new Error("update-skill --global/--platform are retired. Use `vidfarm skills add vidfarm-director --dir <project>` for the canonical multi-file pack; `update-skill --print` remains available for the generated copy-friendly rollup.");
7759
+ throw new Error("update-skill --global/--platform are retired. Use `vidfarm skills add vidfarm --dir <project>` for the canonical multi-file pack; `update-skill --print` remains available for the generated copy-friendly rollup.");
7473
7760
  }
7474
7761
  if (!ctx.json) {
7475
- console.warn(`Deprecated: update-skill now delegates to the canonical multi-file skill installer. Use ${BOLD}vidfarm skills add vidfarm-director${RESET}.`);
7762
+ console.warn(`Deprecated: update-skill now delegates to the canonical multi-file skill installer. Use ${BOLD}vidfarm skills add vidfarm${RESET}.`);
7476
7763
  }
7477
7764
  await runSkillsCommand([
7478
7765
  "add",
7479
- "vidfarm-director",
7766
+ "vidfarm",
7480
7767
  ...(parsed.values.dir ? ["--dir", String(parsed.values.dir)] : []),
7481
7768
  ...(parsed.values.host ? ["--host", String(parsed.values.host)] : []),
7482
7769
  ...(ctx.json ? ["--json"] : [])
@@ -10,7 +10,7 @@
10
10
  // - hybrid : free where it's free, spend on AI only where it clearly wins.
11
11
  // (default recommendation.) Billed ops run but print a cost line.
12
12
  // - pure-ai : best-quality; AI image/video/voice/music used freely. Billed
13
- // ops run; cost is still surfaced.
13
+ // ops run; cost is still surfaced. Shown to humans as "rich-ai".
14
14
  //
15
15
  // This module lives in the CLI's static import closure and is BACKEND-FREE (only
16
16
  // Node built-ins) so it ships in the public cloud-only package.
@@ -33,10 +33,25 @@ export function normalizeCostMode(raw) {
33
33
  return "minimize";
34
34
  if (["hybrid", "mixed", "balanced", "smart", "default"].includes(v))
35
35
  return "hybrid";
36
- if (["pure-ai", "pureai", "ai", "best", "best-quality", "premium", "max"].includes(v))
36
+ if (["pure-ai", "pureai", "rich-ai", "richai", "rich", "ai", "best", "best-quality", "premium", "max"].includes(v))
37
37
  return "pure-ai";
38
38
  return null;
39
39
  }
40
+ /**
41
+ * Human-facing NAME for a mode. The canonical stored slug for best-quality stays
42
+ * `pure-ai` (so saved cost-mode.json, --cost-mode flags, and VIDFARM_COST_MODE
43
+ * keep resolving), but users see and can type "rich-ai".
44
+ */
45
+ export const COST_MODE_DISPLAY = {
46
+ minimize: "minimize",
47
+ hybrid: "hybrid",
48
+ "pure-ai": "rich-ai"
49
+ };
50
+ /** The three modes as users see them, e.g. for "<minimize|hybrid|rich-ai>". */
51
+ export const COST_MODE_DISPLAY_LIST = COST_MODES.map((m) => COST_MODE_DISPLAY[m]);
52
+ export function costModeDisplayName(mode) {
53
+ return COST_MODE_DISPLAY[mode];
54
+ }
40
55
  /** Read the persisted cost mode, or null if absent/unparseable. */
41
56
  export function readStoredCostMode(home) {
42
57
  try {
@@ -96,13 +111,13 @@ export const COST_MODE_BLURB = {
96
111
  hybrid: "Hybrid (recommended) — free where it's free, spend AI credits only where they " +
97
112
  "clearly win (a hero shot, a voice you can't fake locally). Billed ops run but " +
98
113
  "each prints its cost so nothing is a surprise.",
99
- "pure-ai": "Pure AI — best quality; AI image/video/voice/music used freely. Billed ops run " +
114
+ "pure-ai": "Rich AI — best quality; AI image/video/voice/music used freely. Billed ops run " +
100
115
  "without gating; cost is still shown."
101
116
  };
102
117
  /** One short human line summarizing the active mode. */
103
118
  export function costModeSummaryLine(resolved) {
104
119
  const setNote = resolved.isSet ? `set via ${resolved.source}` : "NOT set — assuming hybrid";
105
- return `cost mode: ${resolved.mode} (${setNote})`;
120
+ return `cost mode: ${costModeDisplayName(resolved.mode)} (${setNote})`;
106
121
  }
107
122
  /** The 3-line "explain it simply" block an agent should relay to the user. */
108
123
  export function costModeExplainer() {
@@ -110,7 +125,7 @@ export function costModeExplainer() {
110
125
  "How much do you want Vidfarm to spend on AI credits?",
111
126
  " • minimize — cheapest: free local compute + free stock media, no surprise AI spend.",
112
127
  " • hybrid — recommended: free where free, pay AI only where it clearly wins.",
113
- " • pure-ai — best quality: use AI image/video/voice/music freely.",
128
+ " • rich-ai — best quality: use AI image/video/voice/music freely.",
114
129
  "Tip: before paying to generate music/SFX/images/video, try the free stock catalog —",
115
130
  ' vidfarm media search "<meaning>" --type bgm|sfx|image|vector|icon|video.',
116
131
  " Check the keyless sources first — Openverse (CC/CC0 music, SFX, images) and iconify (icons)",
@@ -150,9 +165,9 @@ export function assertBilledAllowed(input) {
150
165
  return;
151
166
  if (!resolved.isSet) {
152
167
  log(`[cost] No spend preference set — ${label}${cost} will bill the account. ` +
153
- `Ask the user minimize / hybrid / pure-ai and save it: vidfarm cost-mode <choice>.`);
168
+ `Ask the user minimize / hybrid / rich-ai and save it: vidfarm cost-mode <choice>.`);
154
169
  return;
155
170
  }
156
- log(`[cost] ${label}${cost} — billed (mode: ${resolved.mode}${yes ? ", confirmed" : ""}).`);
171
+ log(`[cost] ${label}${cost} — billed (mode: ${costModeDisplayName(resolved.mode)}${yes ? ", confirmed" : ""}).`);
157
172
  }
158
173
  //# sourceMappingURL=cost-mode.js.map