@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.
- package/.agents/skills/editor-capabilities/SKILL.md +3 -3
- package/.agents/skills/{vidfarm-director → vidfarm}/SKILL.md +40 -7
- package/.agents/skills/{vidfarm-director → vidfarm}/references/assets-and-sourcing.md +6 -0
- package/.agents/skills/{vidfarm-director → vidfarm}/references/automation-and-local-dev.md +38 -6
- package/.agents/skills/{vidfarm-director → vidfarm}/references/core-workflows.md +2 -2
- package/.agents/skills/{vidfarm-director → vidfarm}/references/editor-workflows.md +29 -2
- package/.agents/skills/{vidfarm-director → vidfarm}/references/rest-api.md +1 -1
- package/SKILL.director.md +115 -17
- package/SKILL.md +10 -5
- package/demo/dist/app.js +207 -207
- package/dist/src/cli.js +333 -46
- package/dist/src/devcli/cost-mode.js +22 -7
- package/dist/src/devcli/doctor.js +12 -2
- package/dist/src/devcli/greenscreen-local.js +191 -0
- package/dist/src/services/clip-curation/hunt.js +5 -4
- package/dist/src/services/clip-curation/media-select.js +76 -30
- package/package.json +2 -1
- package/public/serve-shells/editor.html +17 -7
- package/public/serve-shells/library-files.html +89 -5
- package/public/serve-shells/library-raws.html +224 -18
- package/public/serve-shells/tools-clipper.html +75 -6
- package/public/serve-shells/tools-image.html +174 -7
- package/public/serve-shells/tools-video.html +104 -6
- /package/.agents/skills/{vidfarm-director → vidfarm}/recipes/find-and-fork-template.md +0 -0
- /package/.agents/skills/{vidfarm-director → vidfarm}/recipes/local-edit-render-approve.md +0 -0
- /package/.agents/skills/{vidfarm-director → vidfarm}/recipes/onboard-a-new-director.md +0 -0
- /package/.agents/skills/{vidfarm-director → vidfarm}/recipes/retheme-template.md +0 -0
- /package/.agents/skills/{vidfarm-director → vidfarm}/references/onboarding.md +0 -0
- /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 {
|
|
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
|
|
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 |
|
|
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
|
|
214
|
-
|
|
215
|
-
--
|
|
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
|
|
543
|
-
Use \`vidfarm skills add vidfarm
|
|
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|
|
|
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/
|
|
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
|
|
4196
|
-
//
|
|
4197
|
-
//
|
|
4198
|
-
|
|
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
|
-
|
|
4208
|
-
if
|
|
4209
|
-
|
|
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
|
-
|
|
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] [--
|
|
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 && !
|
|
4251
|
-
throw new Error(`Unknown --preset "${presetRaw}". Choose one of: ${
|
|
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 =
|
|
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}
|
|
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|
|
|
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|
|
|
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: ${
|
|
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|
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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": "
|
|
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
|
-
" •
|
|
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 /
|
|
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
|