@mevdragon/vidfarm-devcli 0.20.5 → 0.20.8
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 +15 -1
- package/.agents/skills/vidfarm-director/SKILL.md +106 -0
- package/.agents/skills/vidfarm-director/recipes/find-and-fork-template.md +16 -0
- package/.agents/skills/vidfarm-director/recipes/local-edit-render-approve.md +13 -0
- package/.agents/skills/vidfarm-director/recipes/onboard-a-new-director.md +13 -0
- package/.agents/skills/vidfarm-director/recipes/retheme-template.md +17 -0
- package/.agents/skills/vidfarm-director/references/assets-and-sourcing.md +111 -0
- package/.agents/skills/vidfarm-director/references/automation-and-local-dev.md +243 -0
- package/.agents/skills/vidfarm-director/references/core-workflows.md +273 -0
- package/.agents/skills/vidfarm-director/references/editor-workflows.md +365 -0
- package/.agents/skills/vidfarm-director/references/onboarding.md +28 -0
- package/.agents/skills/vidfarm-director/references/primitives.md +265 -0
- package/SKILL.director.md +521 -261
- package/dist/src/cli.js +404 -96
- package/dist/src/devcli/clips.js +15 -10
- package/dist/src/devcli/doctor.js +2 -2
- package/dist/src/devcli/local-backend.js +46 -2
- package/dist/src/services/clip-curation/cost.js +5 -1
- package/dist/src/services/clip-curation/gemini.js +10 -0
- package/package.json +30 -10
- package/SKILL.platform.md +0 -432
- package/demo/README.md +0 -28
- package/demo/dist/app.css +0 -1
- package/demo/dist/app.js +0 -1850
- package/demo/dist/chunks/chunk-DXB73IDG.js +0 -1
- package/demo/dist/chunks/chunk-S7OWAJDS.js +0 -36
- package/demo/dist/chunks/chunk-VTIBZ6AN.js +0 -1
- package/demo/dist/chunks/dist-ADSJKBVE.js +0 -332
- package/demo/dist/chunks/domEditingLayers-VZMLL4AP-SGHWPND4.js +0 -1
- package/demo/dist/chunks/hyperframes-player-XB65TCD6.js +0 -425
- package/demo/dist/chunks/lib-XAQ37YOE.js +0 -1
- package/demo/dist/chunks/src-TJ2QYA4U.js +0 -207
- package/demo/dist/favicon.ico +0 -0
- package/demo/dist/icons/timeline/audio.svg +0 -7
- package/demo/dist/icons/timeline/captions.svg +0 -5
- package/demo/dist/icons/timeline/composition.svg +0 -12
- package/demo/dist/icons/timeline/image.svg +0 -18
- package/demo/dist/icons/timeline/music.svg +0 -10
- package/demo/dist/icons/timeline/text.svg +0 -3
- package/demo/dist/index.html +0 -15
- package/dist/src/account-pages-legacy.js +0 -9396
- package/dist/src/account-pages.js +0 -61
- package/dist/src/app.js +0 -21602
- package/dist/src/composition-runtime.js +0 -1053
- package/dist/src/config.js +0 -217
- package/dist/src/context.js +0 -447
- package/dist/src/dev-app-legacy.js +0 -739
- package/dist/src/dev-app.js +0 -6
- package/dist/src/devcli/migrate-local.js +0 -140
- package/dist/src/devcli/sync.js +0 -368
- package/dist/src/domain.js +0 -5
- package/dist/src/editor-chat-history.js +0 -82
- package/dist/src/editor-chat.js +0 -756
- package/dist/src/editor-dark-theme.js +0 -1128
- package/dist/src/frontend/debug.js +0 -71
- package/dist/src/frontend/discover-client.js +0 -130
- package/dist/src/frontend/discover-store.js +0 -23
- package/dist/src/frontend/file-directory.js +0 -1018
- package/dist/src/frontend/flockposter-cache-store.js +0 -124
- package/dist/src/frontend/homepage-client.js +0 -446
- package/dist/src/frontend/homepage-shared.js +0 -201
- package/dist/src/frontend/homepage-store.js +0 -66
- package/dist/src/frontend/homepage-view.js +0 -705
- package/dist/src/frontend/page-runtime-client.js +0 -132
- package/dist/src/frontend/page-runtime-store.js +0 -9
- package/dist/src/frontend/sentry.js +0 -42
- package/dist/src/frontend/template-editor-chat.js +0 -4181
- package/dist/src/help-page.js +0 -346
- package/dist/src/homepage.js +0 -1458
- package/dist/src/index.js +0 -16
- package/dist/src/instrument.js +0 -30
- package/dist/src/landing-page.js +0 -384
- package/dist/src/page-runtime.js +0 -2
- package/dist/src/page-shell.js +0 -1452
- package/dist/src/primitive-context.js +0 -416
- package/dist/src/primitive-registry.js +0 -3940
- package/dist/src/primitive-sdk.js +0 -4
- package/dist/src/primitives/hyperframes-media.js +0 -108
- package/dist/src/react-page-shell.js +0 -35
- package/dist/src/ready-post-schedule-component.js +0 -1540
- package/dist/src/registry.js +0 -296
- package/dist/src/reskin/agency-page.js +0 -299
- package/dist/src/reskin/calendar-page.js +0 -568
- package/dist/src/reskin/chat-page.js +0 -942
- package/dist/src/reskin/discover-page.js +0 -1788
- package/dist/src/reskin/document.js +0 -1587
- package/dist/src/reskin/help-page.js +0 -357
- package/dist/src/reskin/index-page.js +0 -62
- package/dist/src/reskin/inpaint-clipper-page.js +0 -890
- package/dist/src/reskin/inpaint-page.js +0 -2554
- package/dist/src/reskin/inpaint-video-page.js +0 -1339
- package/dist/src/reskin/job-runs-page.js +0 -477
- package/dist/src/reskin/library-page.js +0 -1634
- package/dist/src/reskin/login-page.js +0 -262
- package/dist/src/reskin/portfolio-page.js +0 -687
- package/dist/src/reskin/pricing-page.js +0 -390
- package/dist/src/reskin/settings-page.js +0 -732
- package/dist/src/reskin/theme.js +0 -711
- package/dist/src/runtime.js +0 -35
- package/dist/src/services/api-call-history.js +0 -249
- package/dist/src/services/auth.js +0 -152
- package/dist/src/services/billing-pricing.js +0 -39
- package/dist/src/services/billing.js +0 -241
- package/dist/src/services/cast.js +0 -127
- package/dist/src/services/chat-threads.js +0 -92
- package/dist/src/services/clip-records.js +0 -250
- package/dist/src/services/clip-search.js +0 -77
- package/dist/src/services/clip-vectors.js +0 -125
- package/dist/src/services/composition-sanitize.js +0 -124
- package/dist/src/services/composition-watch.js +0 -79
- package/dist/src/services/elevenlabs.js +0 -222
- package/dist/src/services/file-directory.js +0 -117
- package/dist/src/services/fork-access.js +0 -93
- package/dist/src/services/fork-manifest.js +0 -42
- package/dist/src/services/ghostcut.js +0 -179
- package/dist/src/services/hyperframes.js +0 -3517
- package/dist/src/services/job-capacity.js +0 -14
- package/dist/src/services/job-logs.js +0 -197
- package/dist/src/services/jobs.js +0 -136
- package/dist/src/services/local-dynamo.js +0 -0
- package/dist/src/services/media-processing.js +0 -766
- package/dist/src/services/primitive-media-lambda.js +0 -280
- package/dist/src/services/providers.js +0 -2748
- package/dist/src/services/rate-limits.js +0 -262
- package/dist/src/services/scene-annotations.js +0 -32
- package/dist/src/services/serverless-auth.js +0 -382
- package/dist/src/services/serverless-jobs.js +0 -1084
- package/dist/src/services/serverless-provider-keys.js +0 -409
- package/dist/src/services/serverless-records.js +0 -1515
- package/dist/src/services/serverless-template-configs.js +0 -75
- package/dist/src/services/storage.js +0 -461
- package/dist/src/services/swipe-customize.js +0 -437
- package/dist/src/services/template-certification.js +0 -413
- package/dist/src/services/template-loader.js +0 -99
- package/dist/src/services/template-runtime-bundles.js +0 -217
- package/dist/src/services/template-sources.js +0 -1017
- package/dist/src/services/upstream.js +0 -248
- package/dist/src/services/video-normalization.js +0 -2
- package/dist/src/services/webhooks.js +0 -62
- package/dist/src/template-editor-pages.js +0 -2576
- package/dist/src/template-editor-shell.js +0 -2893
- package/dist/src/template-sdk.js +0 -4
- package/dist/src/worker.js +0 -17
- package/public/assets/discover-client-app.js +0 -1
- package/public/assets/file-directory-app.js +0 -3
- package/public/assets/homepage-app.js +0 -54
- package/public/assets/homepage-client-app.js +0 -80
- package/public/assets/page-runtime-client-app.js +0 -94
- package/public/assets/placeholders/scene-placeholder.png +0 -0
- package/src/assets/SELLING_AWARENESS_STAGES.md +0 -579
- package/src/assets/SELLING_WITH_HOOKS.md +0 -377
- package/src/assets/SELLING_WITH_VSLS.md +0 -606
- package/src/assets/favicon.ico +0 -0
- package/src/assets/logo-vidfarm.png +0 -0
package/dist/src/cli.js
CHANGED
|
@@ -22,6 +22,7 @@ import { renderCompositionStills } from "./devcli/stills.js";
|
|
|
22
22
|
import { runDoctorCommand } from "./devcli/doctor.js";
|
|
23
23
|
import { runSkillsCommand } from "./devcli/skills.js";
|
|
24
24
|
import { initTelemetry, reportCliCrash } from "./devcli/telemetry.js";
|
|
25
|
+
import { resolveLocalDataDir, localBackendAvailable, LocalModeUnavailableError } from "./devcli/local-backend.js";
|
|
25
26
|
// vidfarm-devcli — command-line bridge for the Vidfarm video studio. The
|
|
26
27
|
// `serve` command boots the FULL editor locally (single origin, disk-backed
|
|
27
28
|
// records + storage) so power users edit compositions on disk while a browser
|
|
@@ -60,16 +61,16 @@ Local editor loop:
|
|
|
60
61
|
serve [template_id] [opts] Boot the FULL editor locally (single origin, disk-backed).
|
|
61
62
|
Records + storage run on disk; render runs in-process on
|
|
62
63
|
this box for FREE (no cloud, no wallet charge).
|
|
63
|
-
/discover and /library
|
|
64
|
-
opening a cloud template seeds
|
|
65
|
-
the editor
|
|
66
|
-
|
|
67
|
-
|
|
64
|
+
With a VALID cloud API key, /discover and /library can
|
|
65
|
+
mirror the CLOUD catalog, opening a cloud template seeds
|
|
66
|
+
it onto local disk, and the editor can hand a render off
|
|
67
|
+
to the cloud renderer. Without a valid key, serve falls
|
|
68
|
+
back to local browse mode and opens /tools/clipper.
|
|
68
69
|
With a template_id or --fork, pulls that composition
|
|
69
70
|
(or the template's default/shared decomposition) from
|
|
70
71
|
the cloud host onto local disk to edit offline.
|
|
71
72
|
--port <n> Server port (default: 3000)
|
|
72
|
-
--dir <path> Local data dir (default:
|
|
73
|
+
--dir <path> Local data dir (default: ~/.vidfarm/data or VIDFARM_HOME/data)
|
|
73
74
|
--key <api-key> Bootstrap/browser key (default: VIDFARM_API_KEY or a dev key)
|
|
74
75
|
--fork <id> Pull + open a specific cloud fork
|
|
75
76
|
--host <url> Cloud host to pull from (default: ${DEFAULT_HOST})
|
|
@@ -84,17 +85,27 @@ Discover & inspiration (browse the viral-video catalog, add your own source):
|
|
|
84
85
|
discover [query...] List/search TEMPLATES (best for an offer) → GET /discover/feed
|
|
85
86
|
--query <text> Keyword search (e.g. "weight loss app")
|
|
86
87
|
--limit <n> Max results
|
|
88
|
+
public-raws [query...] Browse PUBLIC RAWS (feed / categories) → GET /api/v1/public-raws
|
|
89
|
+
--query <text> Keyword/vector query over title + metadata
|
|
90
|
+
--category <key> Filter one public-raw category (e.g. emotion:confused)
|
|
91
|
+
--type <raw_type> Filter one raw family (greenscreen, b_roll, ...)
|
|
92
|
+
--freshness-days <n> Only raws newer than N days
|
|
93
|
+
--bookmark <raw_id> Save that public raw into /raws/public/* → POST /api/v1/public-raws/:rawId/bookmark
|
|
94
|
+
--limit <n> Max results
|
|
87
95
|
videos [query...] List/search source INSPIRATIONS → GET /api/v1/videos
|
|
88
96
|
--query <text> Keyword search over the catalog
|
|
89
97
|
--mine Only your own uploaded inspirations
|
|
90
98
|
--limit <n> Max results
|
|
91
99
|
inspiration-add <url|file> Ingest a TikTok/YT/IG/X URL — or upload a → POST /discover/templates
|
|
92
100
|
local MP4/MOV/WebM file — as a private (files: presign + PUT via
|
|
93
|
-
template.
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
101
|
+
template. A TikTok PHOTO/slideshow URL is /discover/templates/upload)
|
|
102
|
+
ingested as a slideshow (ordered images + its
|
|
103
|
+
audio) into an editable image timeline.
|
|
104
|
+
--title (uploads; defaults to the template
|
|
105
|
+
id), --tagline, --notes optional. --raw
|
|
106
|
+
(uploads) marks it as your own raw project
|
|
107
|
+
footage — the editor won't auto-prompt
|
|
108
|
+
Decompose on it.
|
|
98
109
|
inspiration-rm <id> Delete a private inspiration/template you own → DELETE /discover/templates/:id
|
|
99
110
|
inspiration-decompose <id> AI-decompose an inspiration into scenes → POST /api/v1/inspirations/:id/decompose
|
|
100
111
|
|
|
@@ -103,7 +114,9 @@ Make a video in one command (orchestrates ingest → decompose → fork):
|
|
|
103
114
|
the URL, AI-decompose/recreate its scenes,
|
|
104
115
|
and open an editable fork. --prompt "...and
|
|
105
116
|
make it about X" steers the recreation.
|
|
106
|
-
--title, --no-fork optional.
|
|
117
|
+
--title, --no-fork optional. A TikTok photo/
|
|
118
|
+
slideshow URL skips decompose (already an
|
|
119
|
+
editable image timeline) and forks directly.
|
|
107
120
|
create "<prompt>" Spin up a NEW video from a text prompt: AI- → generate + ingest + decompose + fork
|
|
108
121
|
generate a base video, wrap it into an
|
|
109
122
|
editable template, decompose + fork. --duration
|
|
@@ -260,9 +273,9 @@ Generate AI media and drop it on the timeline (for local coding agents):
|
|
|
260
273
|
--replace <layer_key> Swap the media into this existing scene/layer instead
|
|
261
274
|
--kind <video|image|audio> Media kind (inferred from the URL if omitted)
|
|
262
275
|
--folder <name> (raw) Save the import into /raws/<name> (also the tracer)
|
|
263
|
-
--clip [--prompt "..."] [--provider gemini|openai|openrouter]
|
|
276
|
+
--clip [--prompt "..."] [--provider gemini|openai|openrouter|nvidia]
|
|
264
277
|
(raw) AI-clip the video into matching moments instead of
|
|
265
|
-
importing the whole thing (BYOK)
|
|
278
|
+
importing the whole thing (BYOK; nvidia = lite clipping key)
|
|
266
279
|
|
|
267
280
|
Raws (the third library — mine long-form video into a reusable raws store):
|
|
268
281
|
raws scan <video-path> Hunt short clips out of a long video. LOCAL-FIRST: local ffmpeg +
|
|
@@ -281,9 +294,14 @@ Raws (the third library — mine long-form video into a reusable raws store):
|
|
|
281
294
|
raws preset list|run <name>|save <name> --from-query '<...>' → /raws/presets
|
|
282
295
|
raws export <ids...> --to <dir> Copy raw mp4s + metadata out
|
|
283
296
|
(run 'vidfarm raws' for the full raws help)
|
|
284
|
-
clipper <video-url|file> Import a source into raws (the /tools/clipper twin).
|
|
285
|
-
DEFAULT = the WHOLE video; --start/--end trims an
|
|
297
|
+
clipper <video-url|file> Import a source into raws (the /tools/clipper twin). LOCAL-FIRST by default;
|
|
298
|
+
DEFAULT = the WHOLE video; --start/--end trims an add --cloud for vidfarm.cc
|
|
286
299
|
EXACT subrange and saves one raw.
|
|
300
|
+
Local URL imports resolve direct media from the page itself
|
|
301
|
+
first (JSON-LD / og:video / player metadata), so you can
|
|
302
|
+
browse manually and paste a source page URL without RapidAPI.
|
|
303
|
+
For bulk ingestion, cloud catalog access, and reliable scale,
|
|
304
|
+
sign up for vidfarm.cc.
|
|
287
305
|
--start <t> --end <t> In/out point — seconds (12.5) or HH:MM:SS:NNNN. Omit
|
|
288
306
|
BOTH to import the whole video (entirety); pass BOTH for
|
|
289
307
|
a subrange (one without the other is an error).
|
|
@@ -312,7 +330,7 @@ Speech (TTS/STT) — LOCAL-FIRST on your own AI key; --cloud is the explicit bac
|
|
|
312
330
|
takes an ElevenLabs voice_id (see: vidfarm voices).
|
|
313
331
|
--own-key (--cloud) use your own ElevenLabs / BYOK key instead of the wallet
|
|
314
332
|
--list-voices List ElevenLabs voices and exit (same as: vidfarm voices)
|
|
315
|
-
music "<prompt>" Prompt → music track (ElevenLabs). CLOUD
|
|
333
|
+
music "<prompt>" Prompt → music track (ElevenLabs). CLOUD/BYOK (no free local engine)
|
|
316
334
|
--length <sec> | --length-ms <ms> Duration (default 30s, max 5 min)
|
|
317
335
|
--out <file> Output mp3 path (default music-<id>.mp3)
|
|
318
336
|
--own-key Use your own ElevenLabs key instead of the platform key + wallet
|
|
@@ -543,6 +561,12 @@ const GREEN = "\x1b[32m";
|
|
|
543
561
|
const RED = "\x1b[31m";
|
|
544
562
|
const RESET = "\x1b[0m";
|
|
545
563
|
void main().catch(async (error) => {
|
|
564
|
+
// Deliberate, expected user-facing conditions (e.g. local mode in the
|
|
565
|
+
// published cloud-only CLI) print just the message — no stack, no crash report.
|
|
566
|
+
if (error instanceof LocalModeUnavailableError) {
|
|
567
|
+
console.error(error.message);
|
|
568
|
+
process.exit(1);
|
|
569
|
+
}
|
|
546
570
|
console.error(error instanceof Error ? error.stack ?? error.message : String(error));
|
|
547
571
|
// Report only unexpected crashes (bugs), never the deliberate user-facing
|
|
548
572
|
// `throw new Error(...)` above; no-op unless a devcli DSN is configured.
|
|
@@ -565,6 +589,8 @@ async function main() {
|
|
|
565
589
|
// editor locally. The old cross-origin dev-serve/`?dev=` flow is gone.
|
|
566
590
|
case "serve":
|
|
567
591
|
case "edit":
|
|
592
|
+
if (!localBackendAvailable())
|
|
593
|
+
throw new LocalModeUnavailableError();
|
|
568
594
|
await runServeCommand(rest);
|
|
569
595
|
return;
|
|
570
596
|
case "publish":
|
|
@@ -577,6 +603,9 @@ async function main() {
|
|
|
577
603
|
case "discover":
|
|
578
604
|
await runDiscoverCommand(rest);
|
|
579
605
|
return;
|
|
606
|
+
case "public-raws":
|
|
607
|
+
await runPublicRawsCommand(rest);
|
|
608
|
+
return;
|
|
580
609
|
case "videos":
|
|
581
610
|
await runVideosCommand(rest);
|
|
582
611
|
return;
|
|
@@ -729,11 +758,15 @@ async function main() {
|
|
|
729
758
|
await runDirectoryCommand(rest);
|
|
730
759
|
return;
|
|
731
760
|
case "migrate-local": {
|
|
761
|
+
if (!localBackendAvailable())
|
|
762
|
+
throw new LocalModeUnavailableError();
|
|
732
763
|
const { runMigrateLocalCommand } = await import("./devcli/migrate-local.js");
|
|
733
764
|
await runMigrateLocalCommand(rest);
|
|
734
765
|
return;
|
|
735
766
|
}
|
|
736
767
|
case "sync": {
|
|
768
|
+
if (!localBackendAvailable())
|
|
769
|
+
throw new LocalModeUnavailableError();
|
|
737
770
|
const { runSyncCommand } = await import("./devcli/sync.js");
|
|
738
771
|
await runSyncCommand(rest);
|
|
739
772
|
return;
|
|
@@ -857,6 +890,30 @@ async function apiRequest(input) {
|
|
|
857
890
|
}
|
|
858
891
|
return { status: res.status, ok: res.ok, json, text };
|
|
859
892
|
}
|
|
893
|
+
async function detectCloudAuth(host, apiKey) {
|
|
894
|
+
const key = apiKey?.trim() || "";
|
|
895
|
+
if (!key) {
|
|
896
|
+
return { usable: false, reason: "missing", detail: "VIDFARM_API_KEY not set." };
|
|
897
|
+
}
|
|
898
|
+
try {
|
|
899
|
+
const res = await fetch(new URL("/api/v1/user/me", host), {
|
|
900
|
+
headers: { "vidfarm-api-key": key, accept: "application/json" }
|
|
901
|
+
});
|
|
902
|
+
if (res.ok)
|
|
903
|
+
return { usable: true, reason: "ok", detail: "authenticated" };
|
|
904
|
+
if (res.status === 401 || res.status === 403) {
|
|
905
|
+
return { usable: false, reason: "invalid", detail: `cloud auth rejected (${res.status})` };
|
|
906
|
+
}
|
|
907
|
+
return { usable: false, reason: "unreachable", detail: `cloud auth probe returned HTTP ${res.status}` };
|
|
908
|
+
}
|
|
909
|
+
catch (error) {
|
|
910
|
+
return {
|
|
911
|
+
usable: false,
|
|
912
|
+
reason: "unreachable",
|
|
913
|
+
detail: `cloud auth probe failed: ${error instanceof Error ? error.message : String(error)}`
|
|
914
|
+
};
|
|
915
|
+
}
|
|
916
|
+
}
|
|
860
917
|
// Named commands should fail loudly (unlike the read-tolerant edit fetch).
|
|
861
918
|
function assertApiOk(result, action) {
|
|
862
919
|
if (result.ok)
|
|
@@ -907,7 +964,10 @@ function resolveTarget(values) {
|
|
|
907
964
|
const envTarget = (process.env.VIDFARM_TARGET ?? "").trim().toLowerCase();
|
|
908
965
|
if (envTarget === "cloud" || envTarget === "both" || envTarget === "local")
|
|
909
966
|
return envTarget;
|
|
910
|
-
|
|
967
|
+
// Local-first by default — but only when the in-process backend is present
|
|
968
|
+
// (a full source checkout). The published cloud-only CLI has no local backend,
|
|
969
|
+
// so it defaults to cloud instead of failing on every bare command.
|
|
970
|
+
return localBackendAvailable() ? "local" : "cloud";
|
|
911
971
|
}
|
|
912
972
|
function commonContext(values) {
|
|
913
973
|
return {
|
|
@@ -964,6 +1024,9 @@ function editorFrontendUrl(host, templateId, forkId) {
|
|
|
964
1024
|
function discoverFrontendUrl(host) {
|
|
965
1025
|
return `${host}/discover`;
|
|
966
1026
|
}
|
|
1027
|
+
function publicRawsFrontendUrl(host) {
|
|
1028
|
+
return `${host}/discover/raws/feed`;
|
|
1029
|
+
}
|
|
967
1030
|
// The devcli twin of the studio editor's "Create New" → /tools/image hand-off:
|
|
968
1031
|
// a deep link that opens the Tools image editor seeded with a source image so
|
|
969
1032
|
// the user can author a replacement. Mirrors buildInpaintEditUrl in
|
|
@@ -1029,7 +1092,9 @@ async function runServeCommand(argv) {
|
|
|
1029
1092
|
if (!Number.isFinite(port) || port <= 0) {
|
|
1030
1093
|
throw new Error(`Invalid --port: ${parsed.values.port}`);
|
|
1031
1094
|
}
|
|
1032
|
-
const dataDir =
|
|
1095
|
+
const dataDir = parsed.values.dir
|
|
1096
|
+
? path.resolve(process.cwd(), parsed.values.dir)
|
|
1097
|
+
: resolveLocalDataDir();
|
|
1033
1098
|
mkdirSync(dataDir, { recursive: true });
|
|
1034
1099
|
// Capture the ambient cloud key BEFORE we overwrite VIDFARM_API_KEY with the
|
|
1035
1100
|
// local bootstrap key, so `--fork`/template seeding can still authenticate to
|
|
@@ -1043,6 +1108,10 @@ async function runServeCommand(argv) {
|
|
|
1043
1108
|
const host = trimTrailingSlash(parsed.values.host);
|
|
1044
1109
|
const templateId = parsed.positionals[0];
|
|
1045
1110
|
const forkFlag = parsed.values.fork;
|
|
1111
|
+
const requestedCloud = !parsed.values["no-cloud"];
|
|
1112
|
+
const cloudAuth = requestedCloud
|
|
1113
|
+
? await detectCloudAuth(host, cloudApiKey)
|
|
1114
|
+
: { usable: false, reason: "missing", detail: "--no-cloud set" };
|
|
1046
1115
|
// Env MUST be set before importing runtime — config.ts reads it at module
|
|
1047
1116
|
// load. Force the fully-local drivers.
|
|
1048
1117
|
process.env.RECORDS_DRIVER = "local";
|
|
@@ -1077,7 +1146,7 @@ async function runServeCommand(argv) {
|
|
|
1077
1146
|
// listing the upstream (cloud) catalog, templates seed onto disk on demand,
|
|
1078
1147
|
// and the editor's Render button can hand off to the cloud renderer. The
|
|
1079
1148
|
// upstream key is the AMBIENT cloud key (never the local bootstrap key).
|
|
1080
|
-
const cloudEnabled =
|
|
1149
|
+
const cloudEnabled = requestedCloud && cloudAuth.usable;
|
|
1081
1150
|
process.env.VIDFARM_UPSTREAM_HOST = cloudEnabled ? host : "";
|
|
1082
1151
|
process.env.VIDFARM_UPSTREAM_API_KEY = cloudEnabled ? (cloudApiKey ?? "") : "";
|
|
1083
1152
|
const { startRuntime } = await import("./runtime.js");
|
|
@@ -1088,7 +1157,7 @@ async function runServeCommand(argv) {
|
|
|
1088
1157
|
let openTemplateId = templateId;
|
|
1089
1158
|
let openForkId = forkFlag;
|
|
1090
1159
|
if ((templateId || forkFlag) && !cloudEnabled) {
|
|
1091
|
-
console.warn(
|
|
1160
|
+
console.warn(`[vidfarm] serve: cloud seed unavailable (${requestedCloud ? cloudAuth.detail : "--no-cloud set"}) — starting local-only.`);
|
|
1092
1161
|
}
|
|
1093
1162
|
else if (templateId || forkFlag) {
|
|
1094
1163
|
try {
|
|
@@ -1109,11 +1178,22 @@ async function runServeCommand(argv) {
|
|
|
1109
1178
|
}
|
|
1110
1179
|
}
|
|
1111
1180
|
const base = `http://localhost:${port}`;
|
|
1181
|
+
const browseMode = !cloudEnabled && !openTemplateId && !openForkId;
|
|
1112
1182
|
const editorPath = openTemplateId
|
|
1113
1183
|
? `/editor/${encodeURIComponent(openTemplateId)}${openForkId ? `/fork/${encodeURIComponent(openForkId)}` : ""}`
|
|
1114
|
-
:
|
|
1184
|
+
: browseMode
|
|
1185
|
+
? "/tools/clipper"
|
|
1186
|
+
: "/discover";
|
|
1115
1187
|
const openUrl = `${base}/auto-login?api_key=${encodeURIComponent(apiKey)}&redirect=${encodeURIComponent(editorPath)}`;
|
|
1116
|
-
printServeBanner({
|
|
1188
|
+
printServeBanner({
|
|
1189
|
+
base,
|
|
1190
|
+
dataDir,
|
|
1191
|
+
openUrl,
|
|
1192
|
+
editorPath,
|
|
1193
|
+
browseMode,
|
|
1194
|
+
cloud: cloudEnabled ? { host, hasKey: true, detail: cloudAuth.detail } : null,
|
|
1195
|
+
cloudDisabledReason: requestedCloud && !cloudEnabled ? cloudAuth.detail : null
|
|
1196
|
+
});
|
|
1117
1197
|
const shouldOpen = parsed.values.open && !parsed.values["no-open"];
|
|
1118
1198
|
if (shouldOpen)
|
|
1119
1199
|
openInBrowser(openUrl);
|
|
@@ -1147,14 +1227,21 @@ function printServeBanner(input) {
|
|
|
1147
1227
|
console.log(` data dir ${input.dataDir}`);
|
|
1148
1228
|
console.log(` render ${DIM}local (in-process HyperFrames render — free, no cloud charge)${RESET}`);
|
|
1149
1229
|
if (input.cloud) {
|
|
1150
|
-
console.log(` cloud ${input.cloud.host} ${DIM}(/discover + /library list the cloud catalog; templates seed locally on open
|
|
1230
|
+
console.log(` cloud ${input.cloud.host} ${DIM}(/discover + /library list the cloud catalog; templates seed locally on open; Render can hand off to the cloud renderer)${RESET}`);
|
|
1151
1231
|
}
|
|
1152
1232
|
else {
|
|
1153
|
-
console.log(` cloud ${DIM}off (--no-cloud)
|
|
1233
|
+
console.log(` cloud ${DIM}off${input.cloudDisabledReason ? ` — ${input.cloudDisabledReason}` : " (--no-cloud)"}${RESET}`);
|
|
1234
|
+
}
|
|
1235
|
+
if (input.browseMode) {
|
|
1236
|
+
console.log(` mode ${DIM}browse — local clipper opens instead of the cloud catalog${RESET}`);
|
|
1154
1237
|
}
|
|
1155
1238
|
console.log("");
|
|
1156
1239
|
console.log(` ${DIM}Agents: edit composition files under ${input.dataDir}/storage — the`);
|
|
1157
1240
|
console.log(` browser live-reloads on save. Multiple forks can be edited at once.${RESET}`);
|
|
1241
|
+
if (input.browseMode) {
|
|
1242
|
+
console.log(` ${DIM}Browse externally yourself, then paste/select source pages in /tools/clipper.`);
|
|
1243
|
+
console.log(` For bulk ingestion, curated fresh data, and cloud rendering at scale, use vidfarm.cc.${RESET}`);
|
|
1244
|
+
}
|
|
1158
1245
|
console.log("");
|
|
1159
1246
|
console.log(` ${BOLD}Open (pre-authed):${RESET}`);
|
|
1160
1247
|
console.log(` ${FRONTEND}${input.openUrl}${RESET}`);
|
|
@@ -1360,7 +1447,12 @@ async function fetchCompositionFiles(input) {
|
|
|
1360
1447
|
// "how to edit like this" companion to the video-context viral DNA (whose
|
|
1361
1448
|
// `emotional_punch` captures the joke/vibe/intonation and, being decomposed
|
|
1362
1449
|
// over the transcript too, the trending sound driving the emotion).
|
|
1363
|
-
|
|
1450
|
+
// replication-harness.json carries the TECHNICAL REPLICATION ANALYSIS (the
|
|
1451
|
+
// 3-paintbrush cheap vs best-quality build plans — which beats are raw_clip
|
|
1452
|
+
// vs hyperframes vs reusable_asset vs ai_gen, with reusable-asset guidance
|
|
1453
|
+
// disciplined by viral DNA and a free-tier open-tools note, all under
|
|
1454
|
+
// `.harness`) so an agent can rebuild the template WITHOUT burning AI credits.
|
|
1455
|
+
const files = ["composition.html", "composition.json", "manifest.json", "video-context.json", "cast.json", "scene-annotations.json", "editor-harness.json", "replication-harness.json"];
|
|
1364
1456
|
for (const filename of files) {
|
|
1365
1457
|
const target = path.join(input.dir, filename);
|
|
1366
1458
|
if (existsSync(target) && !input.refetch) {
|
|
@@ -1408,6 +1500,7 @@ function readJsonStringList(value, max = 8) {
|
|
|
1408
1500
|
function writeLocalAgentHarnessFiles(dir) {
|
|
1409
1501
|
const videoContext = readJsonFile(path.join(dir, "video-context.json"));
|
|
1410
1502
|
const editorHarness = readJsonFile(path.join(dir, "editor-harness.json"));
|
|
1503
|
+
const replicationHarness = readJsonFile(path.join(dir, "replication-harness.json"));
|
|
1411
1504
|
const sceneAnnotations = readJsonFile(path.join(dir, "scene-annotations.json"));
|
|
1412
1505
|
const cast = readJsonFile(path.join(dir, "cast.json"));
|
|
1413
1506
|
const viralDna = videoContext && readJsonString(videoContext.status) === "ready" && videoContext.viral_dna && typeof videoContext.viral_dna === "object"
|
|
@@ -1416,6 +1509,9 @@ function writeLocalAgentHarnessFiles(dir) {
|
|
|
1416
1509
|
const harness = editorHarness && readJsonString(editorHarness.status) === "ready" && editorHarness.harness && typeof editorHarness.harness === "object"
|
|
1417
1510
|
? editorHarness.harness
|
|
1418
1511
|
: null;
|
|
1512
|
+
const replication = replicationHarness && readJsonString(replicationHarness.status) === "ready" && replicationHarness.harness && typeof replicationHarness.harness === "object"
|
|
1513
|
+
? replicationHarness.harness
|
|
1514
|
+
: null;
|
|
1419
1515
|
const annotations = sceneAnnotations && readJsonString(sceneAnnotations.status) === "done" && Array.isArray(sceneAnnotations.annotations)
|
|
1420
1516
|
? sceneAnnotations.annotations
|
|
1421
1517
|
: [];
|
|
@@ -1429,7 +1525,9 @@ function writeLocalAgentHarnessFiles(dir) {
|
|
|
1429
1525
|
generated_at: new Date().toISOString(),
|
|
1430
1526
|
source_title: readJsonString(videoContext?.summary) || null,
|
|
1431
1527
|
composition_context: viralDna,
|
|
1528
|
+
static_vs_pivot: viralDna?.static_vs_pivot ?? null,
|
|
1432
1529
|
editor_harness: harness,
|
|
1530
|
+
replication_harness: replication,
|
|
1433
1531
|
scenes: Array.isArray(videoContext?.scenes) ? videoContext?.scenes : [],
|
|
1434
1532
|
scene_annotations: annotations,
|
|
1435
1533
|
cast: castMembers,
|
|
@@ -1444,12 +1542,56 @@ function writeLocalAgentHarnessFiles(dir) {
|
|
|
1444
1542
|
const doList = readJsonStringList(harness?.do, 5);
|
|
1445
1543
|
const dontList = readJsonStringList(harness?.dont, 5);
|
|
1446
1544
|
const preserve = readJsonStringList(emotionalPunch?.preserve, 5);
|
|
1545
|
+
const staticVsPivot = viralDna?.static_vs_pivot && typeof viralDna.static_vs_pivot === "object"
|
|
1546
|
+
? viralDna.static_vs_pivot
|
|
1547
|
+
: null;
|
|
1548
|
+
const sceneReplacement = staticVsPivot?.scene_replacement && typeof staticVsPivot.scene_replacement === "object"
|
|
1549
|
+
? staticVsPivot.scene_replacement
|
|
1550
|
+
: null;
|
|
1551
|
+
const narrationPivot = staticVsPivot?.narration && typeof staticVsPivot.narration === "object"
|
|
1552
|
+
? staticVsPivot.narration
|
|
1553
|
+
: null;
|
|
1554
|
+
const musicPivot = staticVsPivot?.music && typeof staticVsPivot.music === "object"
|
|
1555
|
+
? staticVsPivot.music
|
|
1556
|
+
: null;
|
|
1557
|
+
const captionsPivot = staticVsPivot?.captions && typeof staticVsPivot.captions === "object"
|
|
1558
|
+
? staticVsPivot.captions
|
|
1559
|
+
: null;
|
|
1560
|
+
const visualRebuild = staticVsPivot?.visual_rebuild && typeof staticVsPivot.visual_rebuild === "object"
|
|
1561
|
+
? staticVsPivot.visual_rebuild
|
|
1562
|
+
: null;
|
|
1563
|
+
const viralPivot = staticVsPivot?.viral_dna && typeof staticVsPivot.viral_dna === "object"
|
|
1564
|
+
? staticVsPivot.viral_dna
|
|
1565
|
+
: null;
|
|
1447
1566
|
const mustPreserve = annotations
|
|
1448
1567
|
.flatMap((entry) => readJsonStringList(entry.must_preserve, 3).map((item) => {
|
|
1449
1568
|
const slug = readJsonString(entry.scene_slug);
|
|
1450
1569
|
return slug ? `${slug}: ${item}` : item;
|
|
1451
1570
|
}))
|
|
1452
1571
|
.slice(0, 8);
|
|
1572
|
+
// Replication harness (3-paintbrush cheap vs best-quality plans). Surface the
|
|
1573
|
+
// RECOMMENDED plan's per-beat brush assignment concretely so a local agent can
|
|
1574
|
+
// execute it without opening the raw JSON.
|
|
1575
|
+
const recommendedStrategy = readJsonString(replication?.recommended_strategy) || "cheap_efficient";
|
|
1576
|
+
const cheapPlan = replication?.cheap && typeof replication.cheap === "object" ? replication.cheap : null;
|
|
1577
|
+
const qualityPlan = replication?.quality && typeof replication.quality === "object" ? replication.quality : null;
|
|
1578
|
+
const recommendedPlan = recommendedStrategy === "best_quality" ? qualityPlan : cheapPlan;
|
|
1579
|
+
const motionStyle = replication?.motion_style && typeof replication.motion_style === "object" ? replication.motion_style : null;
|
|
1580
|
+
const planBeats = (plan) => {
|
|
1581
|
+
const scenes = plan && Array.isArray(plan.scenes) ? plan.scenes : [];
|
|
1582
|
+
return scenes.slice(0, 8).map((s) => {
|
|
1583
|
+
const role = readJsonString(s.role) || "beat";
|
|
1584
|
+
const ts = readJsonString(s.timestamp);
|
|
1585
|
+
const method = readJsonString(s.method) || "?";
|
|
1586
|
+
const credits = readJsonString(s.est_credits);
|
|
1587
|
+
const technique = readJsonString(s.technique);
|
|
1588
|
+
return `${ts ? `${ts} ` : ""}${role} → ${method}${credits ? ` (${credits} credits)` : ""}${technique ? `: ${technique}` : ""}`;
|
|
1589
|
+
});
|
|
1590
|
+
};
|
|
1591
|
+
const recommendedBeats = planBeats(recommendedPlan);
|
|
1592
|
+
const hyperframesCandidates = readJsonStringList(motionStyle?.hyperframes_candidates, 6);
|
|
1593
|
+
const assetOpportunities = readJsonStringList(replication?.asset_opportunities, 6);
|
|
1594
|
+
const dnaGuardrails = readJsonStringList(replication?.viral_dna_guardrails, 6);
|
|
1453
1595
|
const agentMd = `# Vidfarm Local Harness
|
|
1454
1596
|
|
|
1455
1597
|
Read \`.harness/context.json\` before making substantial edits. This fork was pulled from Vidfarm with decompose context; do not freestyle the rewrite from \`composition.html\` alone.
|
|
@@ -1457,6 +1599,7 @@ Read \`.harness/context.json\` before making substantial edits. This fork was pu
|
|
|
1457
1599
|
Files to use:
|
|
1458
1600
|
- \`video-context.json\`: transcript, scene descriptions, and viral DNA.
|
|
1459
1601
|
- \`editor-harness.json\`: the technical editing brief under \`harness\` (pace, typography, audio, scene roles).
|
|
1602
|
+
- \`replication-harness.json\`: the technical replication analysis under \`harness\` — the 3-paintbrush cheap vs best-quality build plans (which beats are raw_clip / hyperframes / reusable_asset / ai_gen), reusable-asset guidance, and a free-tier note. See "Replication plan" below.
|
|
1460
1603
|
- \`scene-annotations.json\`: per-scene replacement and regeneration DNA.
|
|
1461
1604
|
- \`.harness/context.json\`: merged agent-facing snapshot generated by \`vidfarm pull\`.
|
|
1462
1605
|
- \`.harness/agent-guide.md\`: Vidfarm-specific local scripting guidance generated by \`vidfarm pull\`.
|
|
@@ -1465,8 +1608,21 @@ Rules:
|
|
|
1465
1608
|
- Treat the viral DNA and editor harness as hard defaults unless the user explicitly overrides them.
|
|
1466
1609
|
- If you rewrite text, make it fit the existing performance and joke mechanic. Do not replace a format-specific bit with generic product-benefit copy.
|
|
1467
1610
|
- Preserve the emotional punch first: tone, mechanism, contrast, audio/visual tension, delivery, and preserve beats.
|
|
1611
|
+
- If the user gives a lazy prompt like "recreate this for my business" with little detail, use static_vs_pivot as the first-pass execution plan instead of interrogating them for every choice.
|
|
1468
1612
|
- Protect any harness scene marked critical/must_keep and the must_preserve notes from scene annotations.
|
|
1469
1613
|
- When swapping visuals, match both the literal scene DNA and the narrative purpose of the beat.
|
|
1614
|
+
- For replacement graphics, screenshots, or still-like scenes, prefer AI image generation plus Ken Burns before paying for AI video unless static_vs_pivot says motion footage is load-bearing.
|
|
1615
|
+
- If narration must be customized, default to premium ElevenLabs first, then the user's own ElevenLabs path, then BYOK OpenAI/Gemini/OpenRouter. If captions or scenes were timed to the old VO, retime them to the new narration.
|
|
1616
|
+
|
|
1617
|
+
The three paintbrushes (Vidfarm is thrift-first — do NOT spend AI credits on every scene):
|
|
1618
|
+
- Paint each REPLACE beat with one of three brushes, cheapest first: (1) RAW CLIPS remixed from existing footage (\`vidfarm raws search\` the local/library first, then \`vidfarm raws scan <url|file>\` to hunt new ones — free local compute); (2) HTML/JS HYPERFRAMES — animate text/images/logos/stickers/charts with CSS or JS adapters (anime.js/GSAP/Lottie/Three) for titles, kinetic captions, data beats, brand cards; (3) PURE AI GENERATION (\`vidfarm generate image|video\`) — most expensive, AI video especially, last resort for beats no clip or hyperframe can cover.
|
|
1619
|
+
- A background video + a foreground video (greenscreen / picture-in-picture) covers most "video meme" formats with zero generation.
|
|
1620
|
+
- Reusable asset library (logos, stickers, reactions, b-roll, a-roll, brand media kit): have an opinion on when/where to reuse — but anchor every asset choice to the viral DNA + harness above. Reskinning past a load-bearing beat kills what made the format work. AI-generate a reusable element ONCE, then reuse it; don't regenerate per-scene.
|
|
1621
|
+
|
|
1622
|
+
Two replication harnesses — pick one and name it to the user (default A). When \`replication-harness.json\` is present it holds a CONCRETE, per-beat version of both plans (see "Replication plan" below) — prefer its beat assignments over freestyling; this prose is the fallback when it's absent:
|
|
1623
|
+
- (A) CHEAP & EFFICIENT (default = \`replication-harness.json\`.harness.cheap): recaption text; background+foreground video memes; animate HTML/image elements with hyperframes; reuse library media; AI-generate a reusable element once then reuse; greenscreen; raw-clip long-form and remix; lean on the memes/reactions/b-roll/a-roll library + brand media kit; only if genuinely needed, AI image then (last) AI video/voice/music.
|
|
1624
|
+
- (B) BEST QUALITY (\`.harness.quality\`): AI video generation by default; storyboard with AI IMAGE first (cheap stills lock composition/subject), generate motion from those references; then adversarially grade the render with a coding agent (Claude Code/Codex) against this harness and iterate.
|
|
1625
|
+
- This harness is open source: an everyday AI agent can follow it to recreate a template with NO Vidfarm wallet — clipping, hyperframes, and local \`vidfarm serve\` render all run on free compute. Keep the plan self-contained; don't gate the cheap path behind paid primitives. \`replication-harness.json\`.harness.free_tier_note names the open path for THIS template.
|
|
1470
1626
|
|
|
1471
1627
|
Current format snapshot:
|
|
1472
1628
|
- Trend tagline: ${readJsonString(viralDna?.trend_tagline) || "unknown"}
|
|
@@ -1480,9 +1636,46 @@ Current format snapshot:
|
|
|
1480
1636
|
- Delivery: ${readJsonString(emotionalPunch?.delivery) || "unknown"}
|
|
1481
1637
|
- Harness one-liner: ${readJsonString(harness?.one_liner) || "unknown"}
|
|
1482
1638
|
|
|
1639
|
+
Static vs pivot:
|
|
1640
|
+
- Summary: ${readJsonString(staticVsPivot?.summary) || "unknown"}
|
|
1641
|
+
- Scene replacement: ${readJsonString(sceneReplacement?.overall) || "unknown"}${readJsonString(sceneReplacement?.reason) ? ` — ${readJsonString(sceneReplacement?.reason)}` : ""}
|
|
1642
|
+
- Narration: ${readJsonString(narrationPivot?.overall) || "unknown"}${readJsonString(narrationPivot?.reason) ? ` — ${readJsonString(narrationPivot?.reason)}` : ""}
|
|
1643
|
+
- Music: ${readJsonString(musicPivot?.overall) || "unknown"}${readJsonString(musicPivot?.volume_guidance) ? ` — ${readJsonString(musicPivot?.volume_guidance)}` : ""}
|
|
1644
|
+
- Captions: ${readJsonString(captionsPivot?.overall) || "unknown"}${readJsonString(captionsPivot?.animation_dependency) ? ` — ${readJsonString(captionsPivot?.animation_dependency)}` : ""}
|
|
1645
|
+
- Visual rebuild: ${readJsonString(visualRebuild?.default_strategy) || "unknown"}${readJsonString(visualRebuild?.reason) ? ` — ${readJsonString(visualRebuild?.reason)}` : ""}
|
|
1646
|
+
|
|
1647
|
+
Replication plan (from replication-harness.json — the recommended path for THIS template):
|
|
1648
|
+
- Recommended strategy: ${recommendedStrategy}${readJsonString(replication?.recommendation_reason) ? ` — ${readJsonString(replication?.recommendation_reason)}` : ""}
|
|
1649
|
+
- Summary: ${readJsonString(replication?.summary) || "unknown (run `vidfarm decompose`)"}
|
|
1650
|
+
- Paintbrush rationale: ${readJsonString(replication?.paintbrush_rationale) || "unknown"}
|
|
1651
|
+
- Cheap plan mix: ${readJsonString(cheapPlan?.method_mix) || readJsonString(cheapPlan?.one_liner) || "unknown"}
|
|
1652
|
+
- Best-quality mix: ${readJsonString(qualityPlan?.method_mix) || readJsonString(qualityPlan?.one_liner) || "unknown"}
|
|
1653
|
+
- Free-tier path: ${readJsonString(replication?.free_tier_note) || "unknown"}
|
|
1654
|
+
|
|
1655
|
+
Recommended per-beat brush plan (${recommendedStrategy}):
|
|
1656
|
+
${recommendedBeats.length ? recommendedBeats.map((item) => `- ${item}`).join("\n") : "- none recorded (run `vidfarm decompose`)"}
|
|
1657
|
+
|
|
1658
|
+
Do these in HTML/hyperframes (NOT AI video — save credits):
|
|
1659
|
+
${hyperframesCandidates.length ? hyperframesCandidates.map((item) => `- ${item}`).join("\n") : "- all on-screen text + graphic overlays"}
|
|
1660
|
+
|
|
1661
|
+
Reusable-asset opportunities (logos/stickers/b-roll — generate once, reuse):
|
|
1662
|
+
${assetOpportunities.length ? assetOpportunities.map((item) => `- ${item}`).join("\n") : "- none recorded"}
|
|
1663
|
+
|
|
1664
|
+
Replication viral-DNA guardrails (never break these when swapping/reusing):
|
|
1665
|
+
${dnaGuardrails.length ? dnaGuardrails.map((item) => `- ${item}`).join("\n") : "- none recorded"}
|
|
1666
|
+
|
|
1483
1667
|
Priority preserve beats:
|
|
1484
1668
|
${preserve.length ? preserve.map((item) => `- ${item}`).join("\n") : "- none recorded"}
|
|
1485
1669
|
|
|
1670
|
+
Static-vs-pivot must-keep:
|
|
1671
|
+
${readJsonStringList(viralPivot?.must_keep, 6).length ? readJsonStringList(viralPivot?.must_keep, 6).map((item) => `- ${item}`).join("\n") : "- none recorded"}
|
|
1672
|
+
|
|
1673
|
+
Static-vs-pivot flexible:
|
|
1674
|
+
${readJsonStringList(viralPivot?.flexible, 6).length ? readJsonStringList(viralPivot?.flexible, 6).map((item) => `- ${item}`).join("\n") : "- none recorded"}
|
|
1675
|
+
|
|
1676
|
+
Static-vs-pivot load-bearing scenes:
|
|
1677
|
+
${readJsonStringList(sceneReplacement?.load_bearing_scenes, 6).length ? readJsonStringList(sceneReplacement?.load_bearing_scenes, 6).map((item) => `- ${item}`).join("\n") : "- none recorded"}
|
|
1678
|
+
|
|
1486
1679
|
Important scenes:
|
|
1487
1680
|
${importantScenes.length ? importantScenes.map((item) => `- ${item}`).join("\n") : "- none recorded"}
|
|
1488
1681
|
|
|
@@ -1962,6 +2155,50 @@ async function runDiscoverCommand(argv) {
|
|
|
1962
2155
|
assertApiOk(result, "discover");
|
|
1963
2156
|
emitResult(result, ctx.json, [["Browse ", discoverFrontendUrl(ctx.host)]]);
|
|
1964
2157
|
}
|
|
2158
|
+
async function runPublicRawsCommand(argv) {
|
|
2159
|
+
const parsed = parseArgs({
|
|
2160
|
+
args: argv,
|
|
2161
|
+
allowPositionals: true,
|
|
2162
|
+
options: {
|
|
2163
|
+
...commonOptions(),
|
|
2164
|
+
limit: { type: "string" },
|
|
2165
|
+
query: { type: "string" },
|
|
2166
|
+
category: { type: "string" },
|
|
2167
|
+
type: { type: "string" },
|
|
2168
|
+
"freshness-days": { type: "string" },
|
|
2169
|
+
bookmark: { type: "string" }
|
|
2170
|
+
}
|
|
2171
|
+
});
|
|
2172
|
+
const ctx = commonContext(parsed.values);
|
|
2173
|
+
const bookmark = parsed.values.bookmark?.trim();
|
|
2174
|
+
if (bookmark) {
|
|
2175
|
+
const result = await apiRequest({
|
|
2176
|
+
method: "POST",
|
|
2177
|
+
host: ctx.host,
|
|
2178
|
+
path: `/api/v1/public-raws/${encodeURIComponent(bookmark)}/bookmark`,
|
|
2179
|
+
auth: ctx.auth
|
|
2180
|
+
});
|
|
2181
|
+
assertApiOk(result, "public-raws bookmark");
|
|
2182
|
+
emitResult(result, ctx.json, [["Drive ", `${ctx.host}/library/files?route=raws/public`]]);
|
|
2183
|
+
return;
|
|
2184
|
+
}
|
|
2185
|
+
const query = (parsed.values.query ?? parsed.positionals.join(" ").trim()) || undefined;
|
|
2186
|
+
const result = await apiRequest({
|
|
2187
|
+
method: "GET",
|
|
2188
|
+
host: ctx.host,
|
|
2189
|
+
path: "/api/v1/public-raws",
|
|
2190
|
+
auth: ctx.auth,
|
|
2191
|
+
query: {
|
|
2192
|
+
limit: parsed.values.limit,
|
|
2193
|
+
q: query,
|
|
2194
|
+
category: parsed.values.category,
|
|
2195
|
+
raw_type: parsed.values.type,
|
|
2196
|
+
freshness_days: parsed.values["freshness-days"]
|
|
2197
|
+
}
|
|
2198
|
+
});
|
|
2199
|
+
assertApiOk(result, "public-raws");
|
|
2200
|
+
emitResult(result, ctx.json, [["Browse ", publicRawsFrontendUrl(ctx.host)]]);
|
|
2201
|
+
}
|
|
1965
2202
|
// `videos` lists source INSPIRATIONS (the raw viral videos), with the same
|
|
1966
2203
|
// keyword search and an optional --mine for the caller's own uploads.
|
|
1967
2204
|
async function runVideosCommand(argv) {
|
|
@@ -2190,12 +2427,24 @@ async function runReplicateCommand(argv) {
|
|
|
2190
2427
|
if (!templateId)
|
|
2191
2428
|
throw new Error("replicate: no template was minted from the source video.");
|
|
2192
2429
|
// 3. Decompose (AI scene analysis + recreation), steered by --prompt.
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2430
|
+
// Slideshows (TikTok photo/carousel posts) have no source video to slice —
|
|
2431
|
+
// they're already an editable image timeline — so skip decompose and fork the
|
|
2432
|
+
// ready slideshow template directly.
|
|
2433
|
+
const isSlideshow = String(ready.media_kind ?? "") === "slideshow";
|
|
2434
|
+
let defaultForkId;
|
|
2435
|
+
if (isSlideshow) {
|
|
2436
|
+
const slideCount = Array.isArray(ready.slide_image_urls) ? ready.slide_image_urls.length : 0;
|
|
2437
|
+
if (!ctx.json)
|
|
2438
|
+
console.log(`${DIM}Slideshow (${slideCount} slides) — already an editable image timeline, skipping decompose.${RESET}`);
|
|
2439
|
+
}
|
|
2440
|
+
else {
|
|
2441
|
+
if (!ctx.json)
|
|
2442
|
+
process.stdout.write(`${DIM}Analyzing + recreating scenes (can take a minute)… ${RESET}`);
|
|
2443
|
+
const decomposed = await decomposeInspiration(ctx, inspirationId, userPrompt);
|
|
2444
|
+
if (!ctx.json)
|
|
2445
|
+
console.log(`${GREEN}done${RESET}`);
|
|
2446
|
+
defaultForkId = decomposed.json?.default_fork_id;
|
|
2447
|
+
}
|
|
2199
2448
|
// 4. Fork a personal editable copy (unless --no-fork opens the shared base).
|
|
2200
2449
|
let forkId = defaultForkId;
|
|
2201
2450
|
let openTemplate = templateId;
|
|
@@ -3394,8 +3643,8 @@ function speechKeyHint(kind) {
|
|
|
3394
3643
|
: "OPENAI_API_KEY, GEMINI_API_KEY, or OPENROUTER_API_KEY";
|
|
3395
3644
|
return `No local AI key found. Set ${preference} (or pass --gemini-key/--openai-key/--openrouter-key), or run with --cloud to use your saved Vidfarm provider keys.`;
|
|
3396
3645
|
}
|
|
3397
|
-
async function downloadUrlToFile(url, dest) {
|
|
3398
|
-
const res = await fetch(url);
|
|
3646
|
+
async function downloadUrlToFile(url, dest, headers) {
|
|
3647
|
+
const res = await fetch(url, headers && Object.keys(headers).length ? { headers } : undefined);
|
|
3399
3648
|
if (!res.ok || !res.body)
|
|
3400
3649
|
throw new Error(`download failed (${res.status}) for ${url}`);
|
|
3401
3650
|
mkdirSync(path.dirname(dest), { recursive: true });
|
|
@@ -3473,7 +3722,7 @@ async function runTtsCommand(argv) {
|
|
|
3473
3722
|
// (speechKeyHint), speak through the local Kokoro engine so agents
|
|
3474
3723
|
// always get audio out of the box.
|
|
3475
3724
|
if (!json)
|
|
3476
|
-
console.log(`${DIM}No provider key found — using the local Kokoro engine
|
|
3725
|
+
console.log(`${DIM}No provider key found — using the local Kokoro engine. For better voices or higher-throughput runs, bring your own audio key or use vidfarm cloud. (${speechKeyHint("tts")})${RESET}`);
|
|
3477
3726
|
await runLocalKokoroTts({ text, voice, style, out: parsed.values.out, format: explicitFormat, json, fallback: true });
|
|
3478
3727
|
return;
|
|
3479
3728
|
}
|
|
@@ -3716,7 +3965,7 @@ async function runMusicCommand(argv) {
|
|
|
3716
3965
|
if (parsed.values["own-key"])
|
|
3717
3966
|
payload.use_wallet_credits = false;
|
|
3718
3967
|
if (!ctx.json && !parsed.values["own-key"]) {
|
|
3719
|
-
console.log(`${DIM}
|
|
3968
|
+
console.log(`${DIM}Music generation runs through Vidfarm cloud on ElevenLabs (wallet-billed). Add --own-key to use your own ElevenLabs key; if you need a completely free local path, keep a human in the loop and skip AI music generation.${RESET}`);
|
|
3720
3969
|
}
|
|
3721
3970
|
const tracer = parsed.values.tracer ?? `devcli-music-${Date.now().toString(36)}`;
|
|
3722
3971
|
const submit = await apiRequest({ method: "POST", host: ctx.host, path: "/api/v1/primitives/music/generate", auth: ctx.auth, body: { tracer, payload } });
|
|
@@ -3847,7 +4096,7 @@ async function runSttCommand(argv) {
|
|
|
3847
4096
|
// (speechKeyHint), transcribe with the local whisper.cpp engine (word
|
|
3848
4097
|
// timestamps, no speaker labels).
|
|
3849
4098
|
if (!json)
|
|
3850
|
-
console.log(`${DIM}No provider key found — using the local whisper.cpp engine
|
|
4099
|
+
console.log(`${DIM}No provider key found — using the local whisper.cpp engine. For better diarization or heavier workloads, bring your own audio key or use vidfarm cloud. (${speechKeyHint("stt")})${RESET}`);
|
|
3851
4100
|
await runLocalWhisperStt({ target, out: parsed.values.out, json, language, fallback: true });
|
|
3852
4101
|
return;
|
|
3853
4102
|
}
|
|
@@ -4442,6 +4691,62 @@ function parseClipTime(value, label) {
|
|
|
4442
4691
|
throw new Error(`--${label} "${raw}" isn't a valid number of seconds.`);
|
|
4443
4692
|
return n;
|
|
4444
4693
|
}
|
|
4694
|
+
function authHeadersForSourceUrl(sourceUrl, ctx) {
|
|
4695
|
+
try {
|
|
4696
|
+
const source = new URL(sourceUrl);
|
|
4697
|
+
const host = new URL(ctx.host);
|
|
4698
|
+
return source.origin === host.origin ? buildAuthHeaders(ctx.auth) : undefined;
|
|
4699
|
+
}
|
|
4700
|
+
catch {
|
|
4701
|
+
return undefined;
|
|
4702
|
+
}
|
|
4703
|
+
}
|
|
4704
|
+
async function uploadTemporaryFileToSpace(ctx, absPath, space) {
|
|
4705
|
+
const form = new FormData();
|
|
4706
|
+
form.append("file", new Blob([readFileSync(absPath)]), path.basename(absPath));
|
|
4707
|
+
if (space === "local") {
|
|
4708
|
+
const { withLocalBackend } = await import("./devcli/local-backend.js");
|
|
4709
|
+
const backend = await withLocalBackend({ home: ctx.home, apiKey: ctx.auth.apiKey });
|
|
4710
|
+
const res = await backend.app.request("/api/v1/user/me/temporary-files/upload", {
|
|
4711
|
+
method: "POST",
|
|
4712
|
+
headers: buildAuthHeaders({ apiKey: ctx.auth.apiKey || backend.apiKey, shareToken: ctx.auth.shareToken }),
|
|
4713
|
+
body: form
|
|
4714
|
+
});
|
|
4715
|
+
const text = await res.text();
|
|
4716
|
+
let json = null;
|
|
4717
|
+
try {
|
|
4718
|
+
json = text ? JSON.parse(text) : null;
|
|
4719
|
+
}
|
|
4720
|
+
catch {
|
|
4721
|
+
json = null;
|
|
4722
|
+
}
|
|
4723
|
+
if (!res.ok)
|
|
4724
|
+
throw new Error(`Upload failed (HTTP ${res.status}): ${(json && json.error) || text.slice(0, 200)}`);
|
|
4725
|
+
const tempFileId = json?.file?.id ?? json?.id ?? json?.file_id;
|
|
4726
|
+
if (!tempFileId)
|
|
4727
|
+
throw new Error("Upload did not return a temporary file id.");
|
|
4728
|
+
return String(tempFileId);
|
|
4729
|
+
}
|
|
4730
|
+
const res = await fetch(new URL("/api/v1/user/me/temporary-files/upload", ctx.host), {
|
|
4731
|
+
method: "POST",
|
|
4732
|
+
headers: buildAuthHeaders(ctx.auth),
|
|
4733
|
+
body: form
|
|
4734
|
+
});
|
|
4735
|
+
const text = await res.text();
|
|
4736
|
+
let json = null;
|
|
4737
|
+
try {
|
|
4738
|
+
json = text ? JSON.parse(text) : null;
|
|
4739
|
+
}
|
|
4740
|
+
catch {
|
|
4741
|
+
json = null;
|
|
4742
|
+
}
|
|
4743
|
+
if (!res.ok)
|
|
4744
|
+
throw new Error(`Upload failed (HTTP ${res.status}): ${(json && json.error) || text.slice(0, 200)}`);
|
|
4745
|
+
const tempFileId = json?.file?.id ?? json?.id ?? json?.file_id;
|
|
4746
|
+
if (!tempFileId)
|
|
4747
|
+
throw new Error("Upload did not return a temporary file id.");
|
|
4748
|
+
return String(tempFileId);
|
|
4749
|
+
}
|
|
4445
4750
|
async function runClipperCommand(argv) {
|
|
4446
4751
|
const parsed = parseArgs({
|
|
4447
4752
|
args: argv,
|
|
@@ -4463,6 +4768,9 @@ async function runClipperCommand(argv) {
|
|
|
4463
4768
|
throw new Error("clipper requires a source: `vidfarm clipper <video-url|file> [--start <t> --end <t>] [--tracer NAME] [--folder NAME] [--name NAME]`. Omit --start/--end to import the WHOLE video.");
|
|
4464
4769
|
}
|
|
4465
4770
|
const ctx = commonContext(parsed.values);
|
|
4771
|
+
if (ctx.target === "both")
|
|
4772
|
+
throw new Error("clipper only supports one backend at a time. Omit --cloud for the local-first path, or pass --cloud for vidfarm.cc.");
|
|
4773
|
+
const target = ctx.target === "cloud" ? "cloud" : "local";
|
|
4466
4774
|
const rawStart = (parsed.values.start ?? parsed.values.in);
|
|
4467
4775
|
const rawEnd = (parsed.values.end ?? parsed.values.out);
|
|
4468
4776
|
const hasStart = rawStart !== undefined && String(rawStart).trim() !== "";
|
|
@@ -4485,71 +4793,69 @@ async function runClipperCommand(argv) {
|
|
|
4485
4793
|
body.name = String(parsed.values.name);
|
|
4486
4794
|
if (audioOnly)
|
|
4487
4795
|
body.audio_only = true;
|
|
4488
|
-
//
|
|
4489
|
-
//
|
|
4490
|
-
|
|
4491
|
-
|
|
4492
|
-
|
|
4493
|
-
|
|
4494
|
-
|
|
4495
|
-
if (
|
|
4496
|
-
|
|
4497
|
-
const form = new FormData();
|
|
4498
|
-
form.append("file", new Blob([readFileSync(abs)]), path.basename(abs));
|
|
4499
|
-
const up = await fetch(new URL("/api/v1/user/me/temporary-files/upload", ctx.host), { method: "POST", headers: buildAuthHeaders(ctx.auth), body: form });
|
|
4500
|
-
const upText = await up.text();
|
|
4501
|
-
let upJson = null;
|
|
4502
|
-
try {
|
|
4503
|
-
upJson = upText ? JSON.parse(upText) : null;
|
|
4796
|
+
// Local-first: local URL clipping hands the source page URL straight to the
|
|
4797
|
+
// local backend, which resolves direct media from page metadata/JSON-LD (and
|
|
4798
|
+
// only falls back to RapidAPI when configured). Cloud mode still hands the
|
|
4799
|
+
// URL to vidfarm.cc; local file paths are staged into the chosen backend.
|
|
4800
|
+
const sourceIsUrl = /^https?:\/\//i.test(source);
|
|
4801
|
+
let stagedDir = null;
|
|
4802
|
+
try {
|
|
4803
|
+
if (sourceIsUrl) {
|
|
4804
|
+
body.source_url = source;
|
|
4504
4805
|
}
|
|
4505
|
-
|
|
4506
|
-
|
|
4806
|
+
else {
|
|
4807
|
+
const abs = path.resolve(process.cwd(), source.replace(/^@/, ""));
|
|
4808
|
+
if (!existsSync(abs))
|
|
4809
|
+
throw new Error(`No such file or URL: ${source}`);
|
|
4810
|
+
body.temp_file_id = await uploadTemporaryFileToSpace(ctx, abs, target);
|
|
4811
|
+
if (!body.name)
|
|
4812
|
+
body.name = path.basename(abs);
|
|
4813
|
+
}
|
|
4814
|
+
if (wholeVideo) {
|
|
4815
|
+
// Entirety → import the whole source as ONE raw (no trim/re-encode), the same
|
|
4816
|
+
// import_only path as `upload-media --from raw` and the /tools/clipper page's
|
|
4817
|
+
// Whole-video button.
|
|
4818
|
+
body.import_only = true;
|
|
4819
|
+
const res = target === "local"
|
|
4820
|
+
? await dispatch(ctx, { method: "POST", path: "/raws/scan", body }, "local")
|
|
4821
|
+
: await apiRequest({ method: "POST", host: ctx.host, path: "/raws/scan", auth: ctx.auth, body });
|
|
4822
|
+
assertApiOk(res, "clipper (whole-video import)");
|
|
4823
|
+
if (ctx.json) {
|
|
4824
|
+
printJson({ ok: true, mode: "whole", target, ...res.json });
|
|
4825
|
+
return;
|
|
4826
|
+
}
|
|
4827
|
+
const j = res.json;
|
|
4828
|
+
const dest = parsed.values.folder ? `/raws/${String(parsed.values.folder)}` : "your raws library";
|
|
4829
|
+
console.log(`${GREEN}Imported the whole video → ${dest} (${target}, scan ${j?.scan_id ?? "?"}, status ${j?.status ?? "?"}).${RESET}`);
|
|
4830
|
+
console.log(`${DIM}Grab an exact subrange instead with --start <t> --end <t>.${RESET}`);
|
|
4831
|
+
return;
|
|
4507
4832
|
}
|
|
4508
|
-
|
|
4509
|
-
|
|
4510
|
-
|
|
4511
|
-
|
|
4512
|
-
|
|
4513
|
-
body.
|
|
4514
|
-
|
|
4515
|
-
body
|
|
4516
|
-
|
|
4517
|
-
|
|
4518
|
-
// Entirety → import the whole source as ONE raw (no trim/re-encode), the same
|
|
4519
|
-
// import_only path as `upload-media --from raw` and the /tools/clipper page's
|
|
4520
|
-
// Whole-video button.
|
|
4521
|
-
body.import_only = true;
|
|
4522
|
-
const res = await apiRequest({ method: "POST", host: ctx.host, path: "/raws/scan", auth: ctx.auth, body });
|
|
4523
|
-
assertApiOk(res, "clipper (whole-video import)");
|
|
4833
|
+
const startSec = parseClipTime(rawStart, "start");
|
|
4834
|
+
const endSec = parseClipTime(rawEnd, "end");
|
|
4835
|
+
if (endSec <= startSec)
|
|
4836
|
+
throw new Error(`--end (${endSec}s) must be greater than --start (${startSec}s).`);
|
|
4837
|
+
body.start_sec = Number(startSec.toFixed(3));
|
|
4838
|
+
body.end_sec = Number(endSec.toFixed(3));
|
|
4839
|
+
const res = target === "local"
|
|
4840
|
+
? await dispatch(ctx, { method: "POST", path: "/raws/clip-range", body }, "local")
|
|
4841
|
+
: await apiRequest({ method: "POST", host: ctx.host, path: "/raws/clip-range", auth: ctx.auth, body });
|
|
4842
|
+
assertApiOk(res, "clipper");
|
|
4524
4843
|
if (ctx.json) {
|
|
4525
|
-
printJson({ ok: true, mode: "
|
|
4844
|
+
printJson({ ok: true, mode: "range", target, ...res.json });
|
|
4526
4845
|
return;
|
|
4527
4846
|
}
|
|
4528
4847
|
const j = res.json;
|
|
4529
|
-
const dest =
|
|
4530
|
-
|
|
4531
|
-
console.log(`${
|
|
4532
|
-
|
|
4848
|
+
const dest = j?.folder_path ? `/raws/${j.folder_path}` : "your raws library";
|
|
4849
|
+
const kindWord = (j?.media_kind === "audio" || audioOnly) ? "audio" : "clip";
|
|
4850
|
+
console.log(`${GREEN}Clipped ${kindWord} ${(j?.duration_sec ?? (endSec - startSec)).toFixed?.(2) ?? j?.duration_sec}s → ${dest} (${target}, clip ${j?.clip_id ?? "?"}).${RESET}`);
|
|
4851
|
+
if (j?.view_url)
|
|
4852
|
+
console.log(`${DIM}Preview: ${new URL(j.view_url, ctx.host).toString()}${RESET}`);
|
|
4853
|
+
console.log(`${DIM}Grab another range from the same source with the same --tracer/--folder.${RESET}`);
|
|
4533
4854
|
}
|
|
4534
|
-
|
|
4535
|
-
|
|
4536
|
-
|
|
4537
|
-
throw new Error(`--end (${endSec}s) must be greater than --start (${startSec}s).`);
|
|
4538
|
-
body.start_sec = Number(startSec.toFixed(3));
|
|
4539
|
-
body.end_sec = Number(endSec.toFixed(3));
|
|
4540
|
-
const res = await apiRequest({ method: "POST", host: ctx.host, path: "/raws/clip-range", auth: ctx.auth, body });
|
|
4541
|
-
assertApiOk(res, "clipper");
|
|
4542
|
-
if (ctx.json) {
|
|
4543
|
-
printJson({ ok: true, mode: "range", ...res.json });
|
|
4544
|
-
return;
|
|
4855
|
+
finally {
|
|
4856
|
+
if (stagedDir)
|
|
4857
|
+
rmSync(stagedDir, { recursive: true, force: true });
|
|
4545
4858
|
}
|
|
4546
|
-
const j = res.json;
|
|
4547
|
-
const dest = j?.folder_path ? `/raws/${j.folder_path}` : "your raws library";
|
|
4548
|
-
const kindWord = (j?.media_kind === "audio" || audioOnly) ? "audio" : "clip";
|
|
4549
|
-
console.log(`${GREEN}Clipped ${kindWord} ${(j?.duration_sec ?? (endSec - startSec)).toFixed?.(2) ?? j?.duration_sec}s → ${dest} (clip ${j?.clip_id ?? "?"}).${RESET}`);
|
|
4550
|
-
if (j?.view_url)
|
|
4551
|
-
console.log(`${DIM}Preview: ${new URL(j.view_url, ctx.host).toString()}${RESET}`);
|
|
4552
|
-
console.log(`${DIM}Grab another range from the same source with the same --tracer/--folder.${RESET}`);
|
|
4553
4859
|
}
|
|
4554
4860
|
async function runPullCommand(argv) {
|
|
4555
4861
|
const parsed = parseArgs({ args: argv, allowPositionals: true, options: { ...commonOptions(), dir: { type: "string" }, refetch: { type: "boolean", default: false } } });
|
|
@@ -4569,6 +4875,7 @@ async function runPullCommand(argv) {
|
|
|
4569
4875
|
const hasCast = existsSync(path.join(dir, "cast.json"));
|
|
4570
4876
|
const hasAnnotations = existsSync(path.join(dir, "scene-annotations.json"));
|
|
4571
4877
|
const hasHarness = existsSync(path.join(dir, "editor-harness.json"));
|
|
4878
|
+
const hasReplication = existsSync(path.join(dir, "replication-harness.json"));
|
|
4572
4879
|
if (ctx.json) {
|
|
4573
4880
|
printJson({
|
|
4574
4881
|
ok: true,
|
|
@@ -4578,6 +4885,7 @@ async function runPullCommand(argv) {
|
|
|
4578
4885
|
has_cast: hasCast,
|
|
4579
4886
|
has_scene_annotations: hasAnnotations,
|
|
4580
4887
|
has_editor_harness: hasHarness,
|
|
4888
|
+
has_replication_harness: hasReplication,
|
|
4581
4889
|
harness_context_path: harnessFiles.contextPath,
|
|
4582
4890
|
harness_instructions_path: harnessFiles.instructionsPath,
|
|
4583
4891
|
...info
|
|
@@ -4592,7 +4900,7 @@ async function runPullCommand(argv) {
|
|
|
4592
4900
|
const flags = [layer.is_timeline_proxy ? "proxy" : null, layer.is_full_canvas ? "full-canvas" : null].filter(Boolean).join(",");
|
|
4593
4901
|
console.log(` ${layer.key} ${DIM}${layer.kind ?? "?"} ${layer.start}-${(layer.start + layer.duration).toFixed(2)}s track${layer.track}${flags ? ` [${flags}]` : ""}${layer.slug ? ` slug=${layer.slug}` : ""}${RESET}`);
|
|
4594
4902
|
}
|
|
4595
|
-
console.log(`${DIM}Grounding: ${hasContext ? "video-context.json ✓" : "video-context.json ✗ (run `vidfarm decompose`)"}, ${hasCast ? "cast.json ✓" : "cast.json ✗"}, ${hasAnnotations ? "scene-annotations.json ✓" : "scene-annotations.json ✗"}, ${hasHarness ? "editor-harness.json ✓ (style brief under .harness)" : "editor-harness.json ✗"}.${RESET}`);
|
|
4903
|
+
console.log(`${DIM}Grounding: ${hasContext ? "video-context.json ✓" : "video-context.json ✗ (run `vidfarm decompose`)"}, ${hasCast ? "cast.json ✓" : "cast.json ✗"}, ${hasAnnotations ? "scene-annotations.json ✓" : "scene-annotations.json ✗"}, ${hasHarness ? "editor-harness.json ✓ (style brief under .harness)" : "editor-harness.json ✗"}, ${hasReplication ? "replication-harness.json ✓ (3-paintbrush build plan under .harness)" : "replication-harness.json ✗"}.${RESET}`);
|
|
4596
4904
|
console.log(`${DIM}Agent brief: ${path.relative(dir, harnessFiles.instructionsPath) || path.basename(harnessFiles.instructionsPath)}; merged context: ${path.relative(dir, harnessFiles.contextPath)}.${RESET}`);
|
|
4597
4905
|
console.log(`${DIM}Next: vidfarm generate video --prompt "..." --aspect-ratio ${info.aspect_ratio ?? "9:16"} --place ${dir} --at <gap start>|--replace <layer_key>${RESET}`);
|
|
4598
4906
|
}
|
|
@@ -4806,7 +5114,7 @@ async function runAddProviderKeyCommand(argv) {
|
|
|
4806
5114
|
const provider = parsed.positionals[0];
|
|
4807
5115
|
const secret = parsed.positionals[1];
|
|
4808
5116
|
if (!provider || !secret)
|
|
4809
|
-
throw new Error("add-provider-key requires <provider> <secret> (provider: openai|gemini|openrouter|perplexity).");
|
|
5117
|
+
throw new Error("add-provider-key requires <provider> <secret> (provider: openai|gemini|openrouter|perplexity|nvidia|elevenlabs).");
|
|
4810
5118
|
const ctx = commonContext(parsed.values);
|
|
4811
5119
|
const result = await apiRequest({ method: "POST", host: ctx.host, path: "/api/v1/user/me/provider-keys", auth: ctx.auth, body: { provider, secret, label: parsed.values.label } });
|
|
4812
5120
|
assertApiOk(result, "add-provider-key");
|