@mevdragon/vidfarm-devcli 0.20.6 → 0.20.9
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 +117 -0
- package/.agents/skills/vidfarm-director/references/automation-and-local-dev.md +245 -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 +308 -0
- package/SKILL.director.md +517 -250
- package/dist/src/cli.js +563 -99
- package/dist/src/devcli/clips.js +29 -12
- 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 +14 -2
- package/dist/src/services/clip-curation/hunt.js +56 -0
- package/dist/src/services/clip-curation/index.js +1 -1
- package/dist/src/services/clip-curation/scan.js +5 -3
- 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 -21603
- 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 -828
- 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 -3654
- 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,13 @@ 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)
|
|
279
|
+
Note: raw mode needs a VIDEO URL. For a LOCAL FILE from
|
|
280
|
+
your computer, use 'vidfarm clipper ./file.mp4 --cloud'
|
|
281
|
+
to upload it and import the whole video into /raws, or
|
|
282
|
+
add --start/--end to save one exact subrange.
|
|
266
283
|
|
|
267
284
|
Raws (the third library — mine long-form video into a reusable raws store):
|
|
268
285
|
raws scan <video-path> Hunt short clips out of a long video. LOCAL-FIRST: local ffmpeg +
|
|
@@ -272,6 +289,9 @@ Raws (the third library — mine long-form video into a reusable raws store):
|
|
|
272
289
|
--duration <sec> Target clip length as a SOFT band (10→5-20s, 30→20-40s)
|
|
273
290
|
--aspect 9:16|16:9|4:3|1:1 Crop clips (also: vertical/horizontal/square) [--crop-focus <f>]
|
|
274
291
|
--no-text Prefer scenes WITHOUT captions/on-screen text (selection only)
|
|
292
|
+
--notes "<private notes>" Store private owner notes as user_notes on derived raws and
|
|
293
|
+
bias decomposition/categorization with them. Explicit private
|
|
294
|
+
shot-kind hints like "content_type: demo" stay owner-only.
|
|
275
295
|
--cloud [--url <url>] BACKUP: run on the deployed pipeline → POST /raws/scan (+ poll)
|
|
276
296
|
(uploads to your temp folder — 30-day TTL — bills AWS compute only)
|
|
277
297
|
--folder <name> (with --cloud) Save the mined clips into /raws/<name>
|
|
@@ -281,18 +301,32 @@ Raws (the third library — mine long-form video into a reusable raws store):
|
|
|
281
301
|
raws preset list|run <name>|save <name> --from-query '<...>' → /raws/presets
|
|
282
302
|
raws export <ids...> --to <dir> Copy raw mp4s + metadata out
|
|
283
303
|
(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
|
|
304
|
+
clipper <video-url|file> Import a source into raws (the /tools/clipper twin). LOCAL-FIRST by default;
|
|
305
|
+
DEFAULT = the WHOLE video; --start/--end trims an add --cloud for vidfarm.cc
|
|
286
306
|
EXACT subrange and saves one raw.
|
|
307
|
+
Local URL imports resolve direct media from the page itself
|
|
308
|
+
first (JSON-LD / og:video / player metadata), so you can
|
|
309
|
+
browse manually and paste a source page URL without RapidAPI.
|
|
310
|
+
For bulk ingestion, cloud catalog access, and reliable scale,
|
|
311
|
+
sign up for vidfarm.cc.
|
|
287
312
|
--start <t> --end <t> In/out point — seconds (12.5) or HH:MM:SS:NNNN. Omit
|
|
288
313
|
BOTH to import the whole video (entirety); pass BOTH for
|
|
289
314
|
a subrange (one without the other is an error).
|
|
290
315
|
--tracer <name> Group this session's clips (also the default folder)
|
|
291
316
|
--folder <name> Save the clip into /raws/<name>
|
|
292
317
|
--name <text> Human name for the clip
|
|
318
|
+
--notes "<private notes>" Store private owner notes as user_notes on the imported raw.
|
|
319
|
+
The notes also bias categorization/decomposition, and an
|
|
320
|
+
explicit private shot-kind hint like 'content_type: demo'
|
|
321
|
+
or 'category: talking_head' is honored only for that owner.
|
|
293
322
|
--audio-only Save just the audio of the subrange as an .m4a raw
|
|
294
323
|
(needs --start/--end; e.g. lift a copyright-free track)
|
|
295
324
|
(multi-clip: re-run with a new range + same --tracer)
|
|
325
|
+
Direct local-file upload to CLOUD raws is supported:
|
|
326
|
+
'vidfarm clipper ./source.mp4 --cloud --folder campaign'
|
|
327
|
+
imports the whole local file as one raw; add
|
|
328
|
+
'--start ... --end ...' to upload the local file and
|
|
329
|
+
save only that exact clipped range.
|
|
296
330
|
|
|
297
331
|
Speech (TTS/STT) — LOCAL-FIRST on your own AI key; --cloud is the explicit backup:
|
|
298
332
|
tts "<text>" Text → narration audio file. LOCAL: your OPENAI/GEMINI/
|
|
@@ -312,7 +346,7 @@ Speech (TTS/STT) — LOCAL-FIRST on your own AI key; --cloud is the explicit bac
|
|
|
312
346
|
takes an ElevenLabs voice_id (see: vidfarm voices).
|
|
313
347
|
--own-key (--cloud) use your own ElevenLabs / BYOK key instead of the wallet
|
|
314
348
|
--list-voices List ElevenLabs voices and exit (same as: vidfarm voices)
|
|
315
|
-
music "<prompt>" Prompt → music track (ElevenLabs). CLOUD
|
|
349
|
+
music "<prompt>" Prompt → music track (ElevenLabs). CLOUD/BYOK (no free local engine)
|
|
316
350
|
--length <sec> | --length-ms <ms> Duration (default 30s, max 5 min)
|
|
317
351
|
--out <file> Output mp3 path (default music-<id>.mp3)
|
|
318
352
|
--own-key Use your own ElevenLabs key instead of the platform key + wallet
|
|
@@ -432,6 +466,8 @@ Approve & schedule (publish a finished MP4 as a shareable post):
|
|
|
432
466
|
--media <url|file> Extra media (repeatable; local files auto-upload to temp/)
|
|
433
467
|
--folder <path> Temp-store folder for uploaded local files (default: temp)
|
|
434
468
|
--caption <text> Post caption (required)
|
|
469
|
+
Direct local-file upload is supported here:
|
|
470
|
+
'vidfarm approve --video ./final.mp4 --caption "..."'
|
|
435
471
|
posts List your approved posts → GET /api/v1/approved/posts
|
|
436
472
|
post <postId> Read one approved post (prints share URL) → GET /api/v1/approved/posts/:postId
|
|
437
473
|
schedule <postId> Schedule an approved post to a channel → POST /api/v1/approved/posts/:postId/schedules
|
|
@@ -464,6 +500,11 @@ Files (multi-step flows the devcli handles for you):
|
|
|
464
500
|
e.g. vidfarm put-file About.md --folder acme-skincare
|
|
465
501
|
echo "..." | vidfarm put-file --stdin --as About.md --folder acme
|
|
466
502
|
download <url> [dest] Stream any Vidfarm/media URL to disk
|
|
503
|
+
download-post <url> Download a social/media post into Vidfarm media → POST /api/v1/primitives/videos/download
|
|
504
|
+
--quality <q> best | hd | full_hd (default: best)
|
|
505
|
+
--no-wait Return the queued job immediately instead of polling
|
|
506
|
+
download-post-audio <url> Download a social/media post's audio into Vidfarm → POST /api/v1/primitives/audio/download
|
|
507
|
+
--no-wait Return the queued job immediately instead of polling
|
|
467
508
|
files List My Files assets + folders → GET /api/v1/user/me/attachments
|
|
468
509
|
--folder <path> Only files under this folder (e.g. a product/offer)
|
|
469
510
|
--search <query> Find files by MEANING (keyword + vector over → POST /api/v1/user/me/attachments/search
|
|
@@ -482,6 +523,10 @@ Files (multi-step flows the devcli handles for you):
|
|
|
482
523
|
--content-type <a,b> Filter the /raws branch by shot-KIND tag (talking_head, b_roll, …)
|
|
483
524
|
directory rename <path> <new-name> Rename a /files or /temp file/folder → POST /api/v1/user/me/directory/rename
|
|
484
525
|
--file-id <id> Rename a FILE (display name); omit to rename the folder at <path>
|
|
526
|
+
directory move <path> <to-folder> Move a file/folder into another folder → POST /api/v1/user/me/directory/move
|
|
527
|
+
--file-id <id> Move a FILE; omit to move the folder (same root, e.g. /raws/demos → /raws/archive)
|
|
528
|
+
directory copy <path> [<to-folder>] Duplicate a file/folder (shared S3 obj) → POST /api/v1/user/me/directory/copy
|
|
529
|
+
--file-id <id> --as <name> Copy a FILE; omit --file-id to copy the folder; --as renames the copy
|
|
485
530
|
directory save-url <url> Save a durable media URL INTO My Files at a folder → POST /api/v1/user/me/attachments/from-url
|
|
486
531
|
--folder <path> Destination folder under /files (e.g. inpaints, promos)
|
|
487
532
|
--as <name> Name the saved file · --notes <text> vector-embedded notes
|
|
@@ -543,6 +588,12 @@ const GREEN = "\x1b[32m";
|
|
|
543
588
|
const RED = "\x1b[31m";
|
|
544
589
|
const RESET = "\x1b[0m";
|
|
545
590
|
void main().catch(async (error) => {
|
|
591
|
+
// Deliberate, expected user-facing conditions (e.g. local mode in the
|
|
592
|
+
// published cloud-only CLI) print just the message — no stack, no crash report.
|
|
593
|
+
if (error instanceof LocalModeUnavailableError) {
|
|
594
|
+
console.error(error.message);
|
|
595
|
+
process.exit(1);
|
|
596
|
+
}
|
|
546
597
|
console.error(error instanceof Error ? error.stack ?? error.message : String(error));
|
|
547
598
|
// Report only unexpected crashes (bugs), never the deliberate user-facing
|
|
548
599
|
// `throw new Error(...)` above; no-op unless a devcli DSN is configured.
|
|
@@ -565,6 +616,8 @@ async function main() {
|
|
|
565
616
|
// editor locally. The old cross-origin dev-serve/`?dev=` flow is gone.
|
|
566
617
|
case "serve":
|
|
567
618
|
case "edit":
|
|
619
|
+
if (!localBackendAvailable())
|
|
620
|
+
throw new LocalModeUnavailableError();
|
|
568
621
|
await runServeCommand(rest);
|
|
569
622
|
return;
|
|
570
623
|
case "publish":
|
|
@@ -577,6 +630,9 @@ async function main() {
|
|
|
577
630
|
case "discover":
|
|
578
631
|
await runDiscoverCommand(rest);
|
|
579
632
|
return;
|
|
633
|
+
case "public-raws":
|
|
634
|
+
await runPublicRawsCommand(rest);
|
|
635
|
+
return;
|
|
580
636
|
case "videos":
|
|
581
637
|
await runVideosCommand(rest);
|
|
582
638
|
return;
|
|
@@ -711,6 +767,12 @@ async function main() {
|
|
|
711
767
|
case "download":
|
|
712
768
|
await runDownloadCommand(rest);
|
|
713
769
|
return;
|
|
770
|
+
case "download-post":
|
|
771
|
+
await runDownloadPostCommand(rest);
|
|
772
|
+
return;
|
|
773
|
+
case "download-post-audio":
|
|
774
|
+
await runDownloadPostAudioCommand(rest);
|
|
775
|
+
return;
|
|
714
776
|
case "files":
|
|
715
777
|
await runFilesCommand(rest);
|
|
716
778
|
return;
|
|
@@ -729,11 +791,15 @@ async function main() {
|
|
|
729
791
|
await runDirectoryCommand(rest);
|
|
730
792
|
return;
|
|
731
793
|
case "migrate-local": {
|
|
794
|
+
if (!localBackendAvailable())
|
|
795
|
+
throw new LocalModeUnavailableError();
|
|
732
796
|
const { runMigrateLocalCommand } = await import("./devcli/migrate-local.js");
|
|
733
797
|
await runMigrateLocalCommand(rest);
|
|
734
798
|
return;
|
|
735
799
|
}
|
|
736
800
|
case "sync": {
|
|
801
|
+
if (!localBackendAvailable())
|
|
802
|
+
throw new LocalModeUnavailableError();
|
|
737
803
|
const { runSyncCommand } = await import("./devcli/sync.js");
|
|
738
804
|
await runSyncCommand(rest);
|
|
739
805
|
return;
|
|
@@ -857,6 +923,30 @@ async function apiRequest(input) {
|
|
|
857
923
|
}
|
|
858
924
|
return { status: res.status, ok: res.ok, json, text };
|
|
859
925
|
}
|
|
926
|
+
async function detectCloudAuth(host, apiKey) {
|
|
927
|
+
const key = apiKey?.trim() || "";
|
|
928
|
+
if (!key) {
|
|
929
|
+
return { usable: false, reason: "missing", detail: "VIDFARM_API_KEY not set." };
|
|
930
|
+
}
|
|
931
|
+
try {
|
|
932
|
+
const res = await fetch(new URL("/api/v1/user/me", host), {
|
|
933
|
+
headers: { "vidfarm-api-key": key, accept: "application/json" }
|
|
934
|
+
});
|
|
935
|
+
if (res.ok)
|
|
936
|
+
return { usable: true, reason: "ok", detail: "authenticated" };
|
|
937
|
+
if (res.status === 401 || res.status === 403) {
|
|
938
|
+
return { usable: false, reason: "invalid", detail: `cloud auth rejected (${res.status})` };
|
|
939
|
+
}
|
|
940
|
+
return { usable: false, reason: "unreachable", detail: `cloud auth probe returned HTTP ${res.status}` };
|
|
941
|
+
}
|
|
942
|
+
catch (error) {
|
|
943
|
+
return {
|
|
944
|
+
usable: false,
|
|
945
|
+
reason: "unreachable",
|
|
946
|
+
detail: `cloud auth probe failed: ${error instanceof Error ? error.message : String(error)}`
|
|
947
|
+
};
|
|
948
|
+
}
|
|
949
|
+
}
|
|
860
950
|
// Named commands should fail loudly (unlike the read-tolerant edit fetch).
|
|
861
951
|
function assertApiOk(result, action) {
|
|
862
952
|
if (result.ok)
|
|
@@ -907,7 +997,10 @@ function resolveTarget(values) {
|
|
|
907
997
|
const envTarget = (process.env.VIDFARM_TARGET ?? "").trim().toLowerCase();
|
|
908
998
|
if (envTarget === "cloud" || envTarget === "both" || envTarget === "local")
|
|
909
999
|
return envTarget;
|
|
910
|
-
|
|
1000
|
+
// Local-first by default — but only when the in-process backend is present
|
|
1001
|
+
// (a full source checkout). The published cloud-only CLI has no local backend,
|
|
1002
|
+
// so it defaults to cloud instead of failing on every bare command.
|
|
1003
|
+
return localBackendAvailable() ? "local" : "cloud";
|
|
911
1004
|
}
|
|
912
1005
|
function commonContext(values) {
|
|
913
1006
|
return {
|
|
@@ -964,6 +1057,9 @@ function editorFrontendUrl(host, templateId, forkId) {
|
|
|
964
1057
|
function discoverFrontendUrl(host) {
|
|
965
1058
|
return `${host}/discover`;
|
|
966
1059
|
}
|
|
1060
|
+
function publicRawsFrontendUrl(host) {
|
|
1061
|
+
return `${host}/discover/raws/feed`;
|
|
1062
|
+
}
|
|
967
1063
|
// The devcli twin of the studio editor's "Create New" → /tools/image hand-off:
|
|
968
1064
|
// a deep link that opens the Tools image editor seeded with a source image so
|
|
969
1065
|
// the user can author a replacement. Mirrors buildInpaintEditUrl in
|
|
@@ -1029,7 +1125,9 @@ async function runServeCommand(argv) {
|
|
|
1029
1125
|
if (!Number.isFinite(port) || port <= 0) {
|
|
1030
1126
|
throw new Error(`Invalid --port: ${parsed.values.port}`);
|
|
1031
1127
|
}
|
|
1032
|
-
const dataDir =
|
|
1128
|
+
const dataDir = parsed.values.dir
|
|
1129
|
+
? path.resolve(process.cwd(), parsed.values.dir)
|
|
1130
|
+
: resolveLocalDataDir();
|
|
1033
1131
|
mkdirSync(dataDir, { recursive: true });
|
|
1034
1132
|
// Capture the ambient cloud key BEFORE we overwrite VIDFARM_API_KEY with the
|
|
1035
1133
|
// local bootstrap key, so `--fork`/template seeding can still authenticate to
|
|
@@ -1043,6 +1141,10 @@ async function runServeCommand(argv) {
|
|
|
1043
1141
|
const host = trimTrailingSlash(parsed.values.host);
|
|
1044
1142
|
const templateId = parsed.positionals[0];
|
|
1045
1143
|
const forkFlag = parsed.values.fork;
|
|
1144
|
+
const requestedCloud = !parsed.values["no-cloud"];
|
|
1145
|
+
const cloudAuth = requestedCloud
|
|
1146
|
+
? await detectCloudAuth(host, cloudApiKey)
|
|
1147
|
+
: { usable: false, reason: "missing", detail: "--no-cloud set" };
|
|
1046
1148
|
// Env MUST be set before importing runtime — config.ts reads it at module
|
|
1047
1149
|
// load. Force the fully-local drivers.
|
|
1048
1150
|
process.env.RECORDS_DRIVER = "local";
|
|
@@ -1077,7 +1179,7 @@ async function runServeCommand(argv) {
|
|
|
1077
1179
|
// listing the upstream (cloud) catalog, templates seed onto disk on demand,
|
|
1078
1180
|
// and the editor's Render button can hand off to the cloud renderer. The
|
|
1079
1181
|
// upstream key is the AMBIENT cloud key (never the local bootstrap key).
|
|
1080
|
-
const cloudEnabled =
|
|
1182
|
+
const cloudEnabled = requestedCloud && cloudAuth.usable;
|
|
1081
1183
|
process.env.VIDFARM_UPSTREAM_HOST = cloudEnabled ? host : "";
|
|
1082
1184
|
process.env.VIDFARM_UPSTREAM_API_KEY = cloudEnabled ? (cloudApiKey ?? "") : "";
|
|
1083
1185
|
const { startRuntime } = await import("./runtime.js");
|
|
@@ -1088,7 +1190,7 @@ async function runServeCommand(argv) {
|
|
|
1088
1190
|
let openTemplateId = templateId;
|
|
1089
1191
|
let openForkId = forkFlag;
|
|
1090
1192
|
if ((templateId || forkFlag) && !cloudEnabled) {
|
|
1091
|
-
console.warn(
|
|
1193
|
+
console.warn(`[vidfarm] serve: cloud seed unavailable (${requestedCloud ? cloudAuth.detail : "--no-cloud set"}) — starting local-only.`);
|
|
1092
1194
|
}
|
|
1093
1195
|
else if (templateId || forkFlag) {
|
|
1094
1196
|
try {
|
|
@@ -1109,11 +1211,22 @@ async function runServeCommand(argv) {
|
|
|
1109
1211
|
}
|
|
1110
1212
|
}
|
|
1111
1213
|
const base = `http://localhost:${port}`;
|
|
1214
|
+
const browseMode = !cloudEnabled && !openTemplateId && !openForkId;
|
|
1112
1215
|
const editorPath = openTemplateId
|
|
1113
1216
|
? `/editor/${encodeURIComponent(openTemplateId)}${openForkId ? `/fork/${encodeURIComponent(openForkId)}` : ""}`
|
|
1114
|
-
:
|
|
1217
|
+
: browseMode
|
|
1218
|
+
? "/tools/clipper"
|
|
1219
|
+
: "/discover";
|
|
1115
1220
|
const openUrl = `${base}/auto-login?api_key=${encodeURIComponent(apiKey)}&redirect=${encodeURIComponent(editorPath)}`;
|
|
1116
|
-
printServeBanner({
|
|
1221
|
+
printServeBanner({
|
|
1222
|
+
base,
|
|
1223
|
+
dataDir,
|
|
1224
|
+
openUrl,
|
|
1225
|
+
editorPath,
|
|
1226
|
+
browseMode,
|
|
1227
|
+
cloud: cloudEnabled ? { host, hasKey: true, detail: cloudAuth.detail } : null,
|
|
1228
|
+
cloudDisabledReason: requestedCloud && !cloudEnabled ? cloudAuth.detail : null
|
|
1229
|
+
});
|
|
1117
1230
|
const shouldOpen = parsed.values.open && !parsed.values["no-open"];
|
|
1118
1231
|
if (shouldOpen)
|
|
1119
1232
|
openInBrowser(openUrl);
|
|
@@ -1147,14 +1260,21 @@ function printServeBanner(input) {
|
|
|
1147
1260
|
console.log(` data dir ${input.dataDir}`);
|
|
1148
1261
|
console.log(` render ${DIM}local (in-process HyperFrames render — free, no cloud charge)${RESET}`);
|
|
1149
1262
|
if (input.cloud) {
|
|
1150
|
-
console.log(` cloud ${input.cloud.host} ${DIM}(/discover + /library list the cloud catalog; templates seed locally on open
|
|
1263
|
+
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
1264
|
}
|
|
1152
1265
|
else {
|
|
1153
|
-
console.log(` cloud ${DIM}off (--no-cloud)
|
|
1266
|
+
console.log(` cloud ${DIM}off${input.cloudDisabledReason ? ` — ${input.cloudDisabledReason}` : " (--no-cloud)"}${RESET}`);
|
|
1267
|
+
}
|
|
1268
|
+
if (input.browseMode) {
|
|
1269
|
+
console.log(` mode ${DIM}browse — local clipper opens instead of the cloud catalog${RESET}`);
|
|
1154
1270
|
}
|
|
1155
1271
|
console.log("");
|
|
1156
1272
|
console.log(` ${DIM}Agents: edit composition files under ${input.dataDir}/storage — the`);
|
|
1157
1273
|
console.log(` browser live-reloads on save. Multiple forks can be edited at once.${RESET}`);
|
|
1274
|
+
if (input.browseMode) {
|
|
1275
|
+
console.log(` ${DIM}Browse externally yourself, then paste/select source pages in /tools/clipper.`);
|
|
1276
|
+
console.log(` For bulk ingestion, curated fresh data, and cloud rendering at scale, use vidfarm.cc.${RESET}`);
|
|
1277
|
+
}
|
|
1158
1278
|
console.log("");
|
|
1159
1279
|
console.log(` ${BOLD}Open (pre-authed):${RESET}`);
|
|
1160
1280
|
console.log(` ${FRONTEND}${input.openUrl}${RESET}`);
|
|
@@ -1360,7 +1480,12 @@ async function fetchCompositionFiles(input) {
|
|
|
1360
1480
|
// "how to edit like this" companion to the video-context viral DNA (whose
|
|
1361
1481
|
// `emotional_punch` captures the joke/vibe/intonation and, being decomposed
|
|
1362
1482
|
// over the transcript too, the trending sound driving the emotion).
|
|
1363
|
-
|
|
1483
|
+
// replication-harness.json carries the TECHNICAL REPLICATION ANALYSIS (the
|
|
1484
|
+
// 3-paintbrush cheap vs best-quality build plans — which beats are raw_clip
|
|
1485
|
+
// vs hyperframes vs reusable_asset vs ai_gen, with reusable-asset guidance
|
|
1486
|
+
// disciplined by viral DNA and a free-tier open-tools note, all under
|
|
1487
|
+
// `.harness`) so an agent can rebuild the template WITHOUT burning AI credits.
|
|
1488
|
+
const files = ["composition.html", "composition.json", "manifest.json", "video-context.json", "cast.json", "scene-annotations.json", "editor-harness.json", "replication-harness.json"];
|
|
1364
1489
|
for (const filename of files) {
|
|
1365
1490
|
const target = path.join(input.dir, filename);
|
|
1366
1491
|
if (existsSync(target) && !input.refetch) {
|
|
@@ -1408,6 +1533,7 @@ function readJsonStringList(value, max = 8) {
|
|
|
1408
1533
|
function writeLocalAgentHarnessFiles(dir) {
|
|
1409
1534
|
const videoContext = readJsonFile(path.join(dir, "video-context.json"));
|
|
1410
1535
|
const editorHarness = readJsonFile(path.join(dir, "editor-harness.json"));
|
|
1536
|
+
const replicationHarness = readJsonFile(path.join(dir, "replication-harness.json"));
|
|
1411
1537
|
const sceneAnnotations = readJsonFile(path.join(dir, "scene-annotations.json"));
|
|
1412
1538
|
const cast = readJsonFile(path.join(dir, "cast.json"));
|
|
1413
1539
|
const viralDna = videoContext && readJsonString(videoContext.status) === "ready" && videoContext.viral_dna && typeof videoContext.viral_dna === "object"
|
|
@@ -1416,6 +1542,9 @@ function writeLocalAgentHarnessFiles(dir) {
|
|
|
1416
1542
|
const harness = editorHarness && readJsonString(editorHarness.status) === "ready" && editorHarness.harness && typeof editorHarness.harness === "object"
|
|
1417
1543
|
? editorHarness.harness
|
|
1418
1544
|
: null;
|
|
1545
|
+
const replication = replicationHarness && readJsonString(replicationHarness.status) === "ready" && replicationHarness.harness && typeof replicationHarness.harness === "object"
|
|
1546
|
+
? replicationHarness.harness
|
|
1547
|
+
: null;
|
|
1419
1548
|
const annotations = sceneAnnotations && readJsonString(sceneAnnotations.status) === "done" && Array.isArray(sceneAnnotations.annotations)
|
|
1420
1549
|
? sceneAnnotations.annotations
|
|
1421
1550
|
: [];
|
|
@@ -1431,6 +1560,7 @@ function writeLocalAgentHarnessFiles(dir) {
|
|
|
1431
1560
|
composition_context: viralDna,
|
|
1432
1561
|
static_vs_pivot: viralDna?.static_vs_pivot ?? null,
|
|
1433
1562
|
editor_harness: harness,
|
|
1563
|
+
replication_harness: replication,
|
|
1434
1564
|
scenes: Array.isArray(videoContext?.scenes) ? videoContext?.scenes : [],
|
|
1435
1565
|
scene_annotations: annotations,
|
|
1436
1566
|
cast: castMembers,
|
|
@@ -1472,6 +1602,29 @@ function writeLocalAgentHarnessFiles(dir) {
|
|
|
1472
1602
|
return slug ? `${slug}: ${item}` : item;
|
|
1473
1603
|
}))
|
|
1474
1604
|
.slice(0, 8);
|
|
1605
|
+
// Replication harness (3-paintbrush cheap vs best-quality plans). Surface the
|
|
1606
|
+
// RECOMMENDED plan's per-beat brush assignment concretely so a local agent can
|
|
1607
|
+
// execute it without opening the raw JSON.
|
|
1608
|
+
const recommendedStrategy = readJsonString(replication?.recommended_strategy) || "cheap_efficient";
|
|
1609
|
+
const cheapPlan = replication?.cheap && typeof replication.cheap === "object" ? replication.cheap : null;
|
|
1610
|
+
const qualityPlan = replication?.quality && typeof replication.quality === "object" ? replication.quality : null;
|
|
1611
|
+
const recommendedPlan = recommendedStrategy === "best_quality" ? qualityPlan : cheapPlan;
|
|
1612
|
+
const motionStyle = replication?.motion_style && typeof replication.motion_style === "object" ? replication.motion_style : null;
|
|
1613
|
+
const planBeats = (plan) => {
|
|
1614
|
+
const scenes = plan && Array.isArray(plan.scenes) ? plan.scenes : [];
|
|
1615
|
+
return scenes.slice(0, 8).map((s) => {
|
|
1616
|
+
const role = readJsonString(s.role) || "beat";
|
|
1617
|
+
const ts = readJsonString(s.timestamp);
|
|
1618
|
+
const method = readJsonString(s.method) || "?";
|
|
1619
|
+
const credits = readJsonString(s.est_credits);
|
|
1620
|
+
const technique = readJsonString(s.technique);
|
|
1621
|
+
return `${ts ? `${ts} ` : ""}${role} → ${method}${credits ? ` (${credits} credits)` : ""}${technique ? `: ${technique}` : ""}`;
|
|
1622
|
+
});
|
|
1623
|
+
};
|
|
1624
|
+
const recommendedBeats = planBeats(recommendedPlan);
|
|
1625
|
+
const hyperframesCandidates = readJsonStringList(motionStyle?.hyperframes_candidates, 6);
|
|
1626
|
+
const assetOpportunities = readJsonStringList(replication?.asset_opportunities, 6);
|
|
1627
|
+
const dnaGuardrails = readJsonStringList(replication?.viral_dna_guardrails, 6);
|
|
1475
1628
|
const agentMd = `# Vidfarm Local Harness
|
|
1476
1629
|
|
|
1477
1630
|
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.
|
|
@@ -1479,6 +1632,7 @@ Read \`.harness/context.json\` before making substantial edits. This fork was pu
|
|
|
1479
1632
|
Files to use:
|
|
1480
1633
|
- \`video-context.json\`: transcript, scene descriptions, and viral DNA.
|
|
1481
1634
|
- \`editor-harness.json\`: the technical editing brief under \`harness\` (pace, typography, audio, scene roles).
|
|
1635
|
+
- \`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.
|
|
1482
1636
|
- \`scene-annotations.json\`: per-scene replacement and regeneration DNA.
|
|
1483
1637
|
- \`.harness/context.json\`: merged agent-facing snapshot generated by \`vidfarm pull\`.
|
|
1484
1638
|
- \`.harness/agent-guide.md\`: Vidfarm-specific local scripting guidance generated by \`vidfarm pull\`.
|
|
@@ -1493,6 +1647,16 @@ Rules:
|
|
|
1493
1647
|
- 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.
|
|
1494
1648
|
- 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.
|
|
1495
1649
|
|
|
1650
|
+
The three paintbrushes (Vidfarm is thrift-first — do NOT spend AI credits on every scene):
|
|
1651
|
+
- 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.
|
|
1652
|
+
- A background video + a foreground video (greenscreen / picture-in-picture) covers most "video meme" formats with zero generation.
|
|
1653
|
+
- 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.
|
|
1654
|
+
|
|
1655
|
+
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:
|
|
1656
|
+
- (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.
|
|
1657
|
+
- (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.
|
|
1658
|
+
- 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.
|
|
1659
|
+
|
|
1496
1660
|
Current format snapshot:
|
|
1497
1661
|
- Trend tagline: ${readJsonString(viralDna?.trend_tagline) || "unknown"}
|
|
1498
1662
|
- Hook: ${readJsonString(viralDna?.hook) || "unknown"}
|
|
@@ -1513,6 +1677,26 @@ Static vs pivot:
|
|
|
1513
1677
|
- Captions: ${readJsonString(captionsPivot?.overall) || "unknown"}${readJsonString(captionsPivot?.animation_dependency) ? ` — ${readJsonString(captionsPivot?.animation_dependency)}` : ""}
|
|
1514
1678
|
- Visual rebuild: ${readJsonString(visualRebuild?.default_strategy) || "unknown"}${readJsonString(visualRebuild?.reason) ? ` — ${readJsonString(visualRebuild?.reason)}` : ""}
|
|
1515
1679
|
|
|
1680
|
+
Replication plan (from replication-harness.json — the recommended path for THIS template):
|
|
1681
|
+
- Recommended strategy: ${recommendedStrategy}${readJsonString(replication?.recommendation_reason) ? ` — ${readJsonString(replication?.recommendation_reason)}` : ""}
|
|
1682
|
+
- Summary: ${readJsonString(replication?.summary) || "unknown (run `vidfarm decompose`)"}
|
|
1683
|
+
- Paintbrush rationale: ${readJsonString(replication?.paintbrush_rationale) || "unknown"}
|
|
1684
|
+
- Cheap plan mix: ${readJsonString(cheapPlan?.method_mix) || readJsonString(cheapPlan?.one_liner) || "unknown"}
|
|
1685
|
+
- Best-quality mix: ${readJsonString(qualityPlan?.method_mix) || readJsonString(qualityPlan?.one_liner) || "unknown"}
|
|
1686
|
+
- Free-tier path: ${readJsonString(replication?.free_tier_note) || "unknown"}
|
|
1687
|
+
|
|
1688
|
+
Recommended per-beat brush plan (${recommendedStrategy}):
|
|
1689
|
+
${recommendedBeats.length ? recommendedBeats.map((item) => `- ${item}`).join("\n") : "- none recorded (run `vidfarm decompose`)"}
|
|
1690
|
+
|
|
1691
|
+
Do these in HTML/hyperframes (NOT AI video — save credits):
|
|
1692
|
+
${hyperframesCandidates.length ? hyperframesCandidates.map((item) => `- ${item}`).join("\n") : "- all on-screen text + graphic overlays"}
|
|
1693
|
+
|
|
1694
|
+
Reusable-asset opportunities (logos/stickers/b-roll — generate once, reuse):
|
|
1695
|
+
${assetOpportunities.length ? assetOpportunities.map((item) => `- ${item}`).join("\n") : "- none recorded"}
|
|
1696
|
+
|
|
1697
|
+
Replication viral-DNA guardrails (never break these when swapping/reusing):
|
|
1698
|
+
${dnaGuardrails.length ? dnaGuardrails.map((item) => `- ${item}`).join("\n") : "- none recorded"}
|
|
1699
|
+
|
|
1516
1700
|
Priority preserve beats:
|
|
1517
1701
|
${preserve.length ? preserve.map((item) => `- ${item}`).join("\n") : "- none recorded"}
|
|
1518
1702
|
|
|
@@ -2004,6 +2188,50 @@ async function runDiscoverCommand(argv) {
|
|
|
2004
2188
|
assertApiOk(result, "discover");
|
|
2005
2189
|
emitResult(result, ctx.json, [["Browse ", discoverFrontendUrl(ctx.host)]]);
|
|
2006
2190
|
}
|
|
2191
|
+
async function runPublicRawsCommand(argv) {
|
|
2192
|
+
const parsed = parseArgs({
|
|
2193
|
+
args: argv,
|
|
2194
|
+
allowPositionals: true,
|
|
2195
|
+
options: {
|
|
2196
|
+
...commonOptions(),
|
|
2197
|
+
limit: { type: "string" },
|
|
2198
|
+
query: { type: "string" },
|
|
2199
|
+
category: { type: "string" },
|
|
2200
|
+
type: { type: "string" },
|
|
2201
|
+
"freshness-days": { type: "string" },
|
|
2202
|
+
bookmark: { type: "string" }
|
|
2203
|
+
}
|
|
2204
|
+
});
|
|
2205
|
+
const ctx = commonContext(parsed.values);
|
|
2206
|
+
const bookmark = parsed.values.bookmark?.trim();
|
|
2207
|
+
if (bookmark) {
|
|
2208
|
+
const result = await apiRequest({
|
|
2209
|
+
method: "POST",
|
|
2210
|
+
host: ctx.host,
|
|
2211
|
+
path: `/api/v1/public-raws/${encodeURIComponent(bookmark)}/bookmark`,
|
|
2212
|
+
auth: ctx.auth
|
|
2213
|
+
});
|
|
2214
|
+
assertApiOk(result, "public-raws bookmark");
|
|
2215
|
+
emitResult(result, ctx.json, [["Drive ", `${ctx.host}/library/files?route=raws/public`]]);
|
|
2216
|
+
return;
|
|
2217
|
+
}
|
|
2218
|
+
const query = (parsed.values.query ?? parsed.positionals.join(" ").trim()) || undefined;
|
|
2219
|
+
const result = await apiRequest({
|
|
2220
|
+
method: "GET",
|
|
2221
|
+
host: ctx.host,
|
|
2222
|
+
path: "/api/v1/public-raws",
|
|
2223
|
+
auth: ctx.auth,
|
|
2224
|
+
query: {
|
|
2225
|
+
limit: parsed.values.limit,
|
|
2226
|
+
q: query,
|
|
2227
|
+
category: parsed.values.category,
|
|
2228
|
+
raw_type: parsed.values.type,
|
|
2229
|
+
freshness_days: parsed.values["freshness-days"]
|
|
2230
|
+
}
|
|
2231
|
+
});
|
|
2232
|
+
assertApiOk(result, "public-raws");
|
|
2233
|
+
emitResult(result, ctx.json, [["Browse ", publicRawsFrontendUrl(ctx.host)]]);
|
|
2234
|
+
}
|
|
2007
2235
|
// `videos` lists source INSPIRATIONS (the raw viral videos), with the same
|
|
2008
2236
|
// keyword search and an optional --mine for the caller's own uploads.
|
|
2009
2237
|
async function runVideosCommand(argv) {
|
|
@@ -2232,12 +2460,24 @@ async function runReplicateCommand(argv) {
|
|
|
2232
2460
|
if (!templateId)
|
|
2233
2461
|
throw new Error("replicate: no template was minted from the source video.");
|
|
2234
2462
|
// 3. Decompose (AI scene analysis + recreation), steered by --prompt.
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2463
|
+
// Slideshows (TikTok photo/carousel posts) have no source video to slice —
|
|
2464
|
+
// they're already an editable image timeline — so skip decompose and fork the
|
|
2465
|
+
// ready slideshow template directly.
|
|
2466
|
+
const isSlideshow = String(ready.media_kind ?? "") === "slideshow";
|
|
2467
|
+
let defaultForkId;
|
|
2468
|
+
if (isSlideshow) {
|
|
2469
|
+
const slideCount = Array.isArray(ready.slide_image_urls) ? ready.slide_image_urls.length : 0;
|
|
2470
|
+
if (!ctx.json)
|
|
2471
|
+
console.log(`${DIM}Slideshow (${slideCount} slides) — already an editable image timeline, skipping decompose.${RESET}`);
|
|
2472
|
+
}
|
|
2473
|
+
else {
|
|
2474
|
+
if (!ctx.json)
|
|
2475
|
+
process.stdout.write(`${DIM}Analyzing + recreating scenes (can take a minute)… ${RESET}`);
|
|
2476
|
+
const decomposed = await decomposeInspiration(ctx, inspirationId, userPrompt);
|
|
2477
|
+
if (!ctx.json)
|
|
2478
|
+
console.log(`${GREEN}done${RESET}`);
|
|
2479
|
+
defaultForkId = decomposed.json?.default_fork_id;
|
|
2480
|
+
}
|
|
2241
2481
|
// 4. Fork a personal editable copy (unless --no-fork opens the shared base).
|
|
2242
2482
|
let forkId = defaultForkId;
|
|
2243
2483
|
let openTemplate = templateId;
|
|
@@ -2642,6 +2882,7 @@ function resolveJobMediaUrl(job) {
|
|
|
2642
2882
|
const output = (result.output && typeof result.output === "object") ? result.output : result;
|
|
2643
2883
|
const direct = readStr(output.primary_file_url)
|
|
2644
2884
|
?? readStr(output.video?.file_url)
|
|
2885
|
+
?? readStr(output.audio?.file_url)
|
|
2645
2886
|
?? readStr(output.image?.file_url)
|
|
2646
2887
|
?? readStr(output.render?.output_url);
|
|
2647
2888
|
if (direct)
|
|
@@ -3436,8 +3677,8 @@ function speechKeyHint(kind) {
|
|
|
3436
3677
|
: "OPENAI_API_KEY, GEMINI_API_KEY, or OPENROUTER_API_KEY";
|
|
3437
3678
|
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.`;
|
|
3438
3679
|
}
|
|
3439
|
-
async function downloadUrlToFile(url, dest) {
|
|
3440
|
-
const res = await fetch(url);
|
|
3680
|
+
async function downloadUrlToFile(url, dest, headers) {
|
|
3681
|
+
const res = await fetch(url, headers && Object.keys(headers).length ? { headers } : undefined);
|
|
3441
3682
|
if (!res.ok || !res.body)
|
|
3442
3683
|
throw new Error(`download failed (${res.status}) for ${url}`);
|
|
3443
3684
|
mkdirSync(path.dirname(dest), { recursive: true });
|
|
@@ -3515,7 +3756,7 @@ async function runTtsCommand(argv) {
|
|
|
3515
3756
|
// (speechKeyHint), speak through the local Kokoro engine so agents
|
|
3516
3757
|
// always get audio out of the box.
|
|
3517
3758
|
if (!json)
|
|
3518
|
-
console.log(`${DIM}No provider key found — using the local Kokoro engine
|
|
3759
|
+
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}`);
|
|
3519
3760
|
await runLocalKokoroTts({ text, voice, style, out: parsed.values.out, format: explicitFormat, json, fallback: true });
|
|
3520
3761
|
return;
|
|
3521
3762
|
}
|
|
@@ -3758,7 +3999,7 @@ async function runMusicCommand(argv) {
|
|
|
3758
3999
|
if (parsed.values["own-key"])
|
|
3759
4000
|
payload.use_wallet_credits = false;
|
|
3760
4001
|
if (!ctx.json && !parsed.values["own-key"]) {
|
|
3761
|
-
console.log(`${DIM}
|
|
4002
|
+
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}`);
|
|
3762
4003
|
}
|
|
3763
4004
|
const tracer = parsed.values.tracer ?? `devcli-music-${Date.now().toString(36)}`;
|
|
3764
4005
|
const submit = await apiRequest({ method: "POST", host: ctx.host, path: "/api/v1/primitives/music/generate", auth: ctx.auth, body: { tracer, payload } });
|
|
@@ -3889,7 +4130,7 @@ async function runSttCommand(argv) {
|
|
|
3889
4130
|
// (speechKeyHint), transcribe with the local whisper.cpp engine (word
|
|
3890
4131
|
// timestamps, no speaker labels).
|
|
3891
4132
|
if (!json)
|
|
3892
|
-
console.log(`${DIM}No provider key found — using the local whisper.cpp engine
|
|
4133
|
+
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}`);
|
|
3893
4134
|
await runLocalWhisperStt({ target, out: parsed.values.out, json, language, fallback: true });
|
|
3894
4135
|
return;
|
|
3895
4136
|
}
|
|
@@ -4484,6 +4725,62 @@ function parseClipTime(value, label) {
|
|
|
4484
4725
|
throw new Error(`--${label} "${raw}" isn't a valid number of seconds.`);
|
|
4485
4726
|
return n;
|
|
4486
4727
|
}
|
|
4728
|
+
function authHeadersForSourceUrl(sourceUrl, ctx) {
|
|
4729
|
+
try {
|
|
4730
|
+
const source = new URL(sourceUrl);
|
|
4731
|
+
const host = new URL(ctx.host);
|
|
4732
|
+
return source.origin === host.origin ? buildAuthHeaders(ctx.auth) : undefined;
|
|
4733
|
+
}
|
|
4734
|
+
catch {
|
|
4735
|
+
return undefined;
|
|
4736
|
+
}
|
|
4737
|
+
}
|
|
4738
|
+
async function uploadTemporaryFileToSpace(ctx, absPath, space) {
|
|
4739
|
+
const form = new FormData();
|
|
4740
|
+
form.append("file", new Blob([readFileSync(absPath)]), path.basename(absPath));
|
|
4741
|
+
if (space === "local") {
|
|
4742
|
+
const { withLocalBackend } = await import("./devcli/local-backend.js");
|
|
4743
|
+
const backend = await withLocalBackend({ home: ctx.home, apiKey: ctx.auth.apiKey });
|
|
4744
|
+
const res = await backend.app.request("/api/v1/user/me/temporary-files/upload", {
|
|
4745
|
+
method: "POST",
|
|
4746
|
+
headers: buildAuthHeaders({ apiKey: ctx.auth.apiKey || backend.apiKey, shareToken: ctx.auth.shareToken }),
|
|
4747
|
+
body: form
|
|
4748
|
+
});
|
|
4749
|
+
const text = await res.text();
|
|
4750
|
+
let json = null;
|
|
4751
|
+
try {
|
|
4752
|
+
json = text ? JSON.parse(text) : null;
|
|
4753
|
+
}
|
|
4754
|
+
catch {
|
|
4755
|
+
json = null;
|
|
4756
|
+
}
|
|
4757
|
+
if (!res.ok)
|
|
4758
|
+
throw new Error(`Upload failed (HTTP ${res.status}): ${(json && json.error) || text.slice(0, 200)}`);
|
|
4759
|
+
const tempFileId = json?.file?.id ?? json?.id ?? json?.file_id;
|
|
4760
|
+
if (!tempFileId)
|
|
4761
|
+
throw new Error("Upload did not return a temporary file id.");
|
|
4762
|
+
return String(tempFileId);
|
|
4763
|
+
}
|
|
4764
|
+
const res = await fetch(new URL("/api/v1/user/me/temporary-files/upload", ctx.host), {
|
|
4765
|
+
method: "POST",
|
|
4766
|
+
headers: buildAuthHeaders(ctx.auth),
|
|
4767
|
+
body: form
|
|
4768
|
+
});
|
|
4769
|
+
const text = await res.text();
|
|
4770
|
+
let json = null;
|
|
4771
|
+
try {
|
|
4772
|
+
json = text ? JSON.parse(text) : null;
|
|
4773
|
+
}
|
|
4774
|
+
catch {
|
|
4775
|
+
json = null;
|
|
4776
|
+
}
|
|
4777
|
+
if (!res.ok)
|
|
4778
|
+
throw new Error(`Upload failed (HTTP ${res.status}): ${(json && json.error) || text.slice(0, 200)}`);
|
|
4779
|
+
const tempFileId = json?.file?.id ?? json?.id ?? json?.file_id;
|
|
4780
|
+
if (!tempFileId)
|
|
4781
|
+
throw new Error("Upload did not return a temporary file id.");
|
|
4782
|
+
return String(tempFileId);
|
|
4783
|
+
}
|
|
4487
4784
|
async function runClipperCommand(argv) {
|
|
4488
4785
|
const parsed = parseArgs({
|
|
4489
4786
|
args: argv,
|
|
@@ -4497,6 +4794,7 @@ async function runClipperCommand(argv) {
|
|
|
4497
4794
|
tracer: { type: "string" },
|
|
4498
4795
|
folder: { type: "string" },
|
|
4499
4796
|
name: { type: "string" },
|
|
4797
|
+
notes: { type: "string" },
|
|
4500
4798
|
"audio-only": { type: "boolean", default: false }
|
|
4501
4799
|
}
|
|
4502
4800
|
});
|
|
@@ -4505,6 +4803,9 @@ async function runClipperCommand(argv) {
|
|
|
4505
4803
|
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.");
|
|
4506
4804
|
}
|
|
4507
4805
|
const ctx = commonContext(parsed.values);
|
|
4806
|
+
if (ctx.target === "both")
|
|
4807
|
+
throw new Error("clipper only supports one backend at a time. Omit --cloud for the local-first path, or pass --cloud for vidfarm.cc.");
|
|
4808
|
+
const target = ctx.target === "cloud" ? "cloud" : "local";
|
|
4508
4809
|
const rawStart = (parsed.values.start ?? parsed.values.in);
|
|
4509
4810
|
const rawEnd = (parsed.values.end ?? parsed.values.out);
|
|
4510
4811
|
const hasStart = rawStart !== undefined && String(rawStart).trim() !== "";
|
|
@@ -4525,73 +4826,73 @@ async function runClipperCommand(argv) {
|
|
|
4525
4826
|
body.folder_path = String(parsed.values.folder);
|
|
4526
4827
|
if (parsed.values.name)
|
|
4527
4828
|
body.name = String(parsed.values.name);
|
|
4829
|
+
if (parsed.values.notes)
|
|
4830
|
+
body.user_notes = String(parsed.values.notes);
|
|
4528
4831
|
if (audioOnly)
|
|
4529
4832
|
body.audio_only = true;
|
|
4530
|
-
//
|
|
4531
|
-
//
|
|
4532
|
-
|
|
4533
|
-
|
|
4534
|
-
|
|
4535
|
-
|
|
4536
|
-
|
|
4537
|
-
if (
|
|
4538
|
-
|
|
4539
|
-
const form = new FormData();
|
|
4540
|
-
form.append("file", new Blob([readFileSync(abs)]), path.basename(abs));
|
|
4541
|
-
const up = await fetch(new URL("/api/v1/user/me/temporary-files/upload", ctx.host), { method: "POST", headers: buildAuthHeaders(ctx.auth), body: form });
|
|
4542
|
-
const upText = await up.text();
|
|
4543
|
-
let upJson = null;
|
|
4544
|
-
try {
|
|
4545
|
-
upJson = upText ? JSON.parse(upText) : null;
|
|
4833
|
+
// Local-first: local URL clipping hands the source page URL straight to the
|
|
4834
|
+
// local backend, which resolves direct media from page metadata/JSON-LD (and
|
|
4835
|
+
// only falls back to RapidAPI when configured). Cloud mode still hands the
|
|
4836
|
+
// URL to vidfarm.cc; local file paths are staged into the chosen backend.
|
|
4837
|
+
const sourceIsUrl = /^https?:\/\//i.test(source);
|
|
4838
|
+
let stagedDir = null;
|
|
4839
|
+
try {
|
|
4840
|
+
if (sourceIsUrl) {
|
|
4841
|
+
body.source_url = source;
|
|
4546
4842
|
}
|
|
4547
|
-
|
|
4548
|
-
|
|
4843
|
+
else {
|
|
4844
|
+
const abs = path.resolve(process.cwd(), source.replace(/^@/, ""));
|
|
4845
|
+
if (!existsSync(abs))
|
|
4846
|
+
throw new Error(`No such file or URL: ${source}`);
|
|
4847
|
+
body.temp_file_id = await uploadTemporaryFileToSpace(ctx, abs, target);
|
|
4848
|
+
if (!body.name)
|
|
4849
|
+
body.name = path.basename(abs);
|
|
4850
|
+
}
|
|
4851
|
+
if (wholeVideo) {
|
|
4852
|
+
// Entirety → import the whole source as ONE raw (no trim/re-encode), the same
|
|
4853
|
+
// import_only path as `upload-media --from raw` and the /tools/clipper page's
|
|
4854
|
+
// Whole-video button.
|
|
4855
|
+
body.import_only = true;
|
|
4856
|
+
const res = target === "local"
|
|
4857
|
+
? await dispatch(ctx, { method: "POST", path: "/raws/scan", body }, "local")
|
|
4858
|
+
: await apiRequest({ method: "POST", host: ctx.host, path: "/raws/scan", auth: ctx.auth, body });
|
|
4859
|
+
assertApiOk(res, "clipper (whole-video import)");
|
|
4860
|
+
if (ctx.json) {
|
|
4861
|
+
printJson({ ok: true, mode: "whole", target, ...res.json });
|
|
4862
|
+
return;
|
|
4863
|
+
}
|
|
4864
|
+
const j = res.json;
|
|
4865
|
+
const dest = parsed.values.folder ? `/raws/${String(parsed.values.folder)}` : "your raws library";
|
|
4866
|
+
console.log(`${GREEN}Imported the whole video → ${dest} (${target}, scan ${j?.scan_id ?? "?"}, status ${j?.status ?? "?"}).${RESET}`);
|
|
4867
|
+
console.log(`${DIM}Grab an exact subrange instead with --start <t> --end <t>.${RESET}`);
|
|
4868
|
+
return;
|
|
4549
4869
|
}
|
|
4550
|
-
|
|
4551
|
-
|
|
4552
|
-
|
|
4553
|
-
|
|
4554
|
-
|
|
4555
|
-
body.
|
|
4556
|
-
|
|
4557
|
-
body
|
|
4558
|
-
|
|
4559
|
-
|
|
4560
|
-
// Entirety → import the whole source as ONE raw (no trim/re-encode), the same
|
|
4561
|
-
// import_only path as `upload-media --from raw` and the /tools/clipper page's
|
|
4562
|
-
// Whole-video button.
|
|
4563
|
-
body.import_only = true;
|
|
4564
|
-
const res = await apiRequest({ method: "POST", host: ctx.host, path: "/raws/scan", auth: ctx.auth, body });
|
|
4565
|
-
assertApiOk(res, "clipper (whole-video import)");
|
|
4870
|
+
const startSec = parseClipTime(rawStart, "start");
|
|
4871
|
+
const endSec = parseClipTime(rawEnd, "end");
|
|
4872
|
+
if (endSec <= startSec)
|
|
4873
|
+
throw new Error(`--end (${endSec}s) must be greater than --start (${startSec}s).`);
|
|
4874
|
+
body.start_sec = Number(startSec.toFixed(3));
|
|
4875
|
+
body.end_sec = Number(endSec.toFixed(3));
|
|
4876
|
+
const res = target === "local"
|
|
4877
|
+
? await dispatch(ctx, { method: "POST", path: "/raws/clip-range", body }, "local")
|
|
4878
|
+
: await apiRequest({ method: "POST", host: ctx.host, path: "/raws/clip-range", auth: ctx.auth, body });
|
|
4879
|
+
assertApiOk(res, "clipper");
|
|
4566
4880
|
if (ctx.json) {
|
|
4567
|
-
printJson({ ok: true, mode: "
|
|
4881
|
+
printJson({ ok: true, mode: "range", target, ...res.json });
|
|
4568
4882
|
return;
|
|
4569
4883
|
}
|
|
4570
4884
|
const j = res.json;
|
|
4571
|
-
const dest =
|
|
4572
|
-
|
|
4573
|
-
console.log(`${
|
|
4574
|
-
|
|
4885
|
+
const dest = j?.folder_path ? `/raws/${j.folder_path}` : "your raws library";
|
|
4886
|
+
const kindWord = (j?.media_kind === "audio" || audioOnly) ? "audio" : "clip";
|
|
4887
|
+
console.log(`${GREEN}Clipped ${kindWord} ${(j?.duration_sec ?? (endSec - startSec)).toFixed?.(2) ?? j?.duration_sec}s → ${dest} (${target}, clip ${j?.clip_id ?? "?"}).${RESET}`);
|
|
4888
|
+
if (j?.view_url)
|
|
4889
|
+
console.log(`${DIM}Preview: ${new URL(j.view_url, ctx.host).toString()}${RESET}`);
|
|
4890
|
+
console.log(`${DIM}Grab another range from the same source with the same --tracer/--folder.${RESET}`);
|
|
4575
4891
|
}
|
|
4576
|
-
|
|
4577
|
-
|
|
4578
|
-
|
|
4579
|
-
throw new Error(`--end (${endSec}s) must be greater than --start (${startSec}s).`);
|
|
4580
|
-
body.start_sec = Number(startSec.toFixed(3));
|
|
4581
|
-
body.end_sec = Number(endSec.toFixed(3));
|
|
4582
|
-
const res = await apiRequest({ method: "POST", host: ctx.host, path: "/raws/clip-range", auth: ctx.auth, body });
|
|
4583
|
-
assertApiOk(res, "clipper");
|
|
4584
|
-
if (ctx.json) {
|
|
4585
|
-
printJson({ ok: true, mode: "range", ...res.json });
|
|
4586
|
-
return;
|
|
4892
|
+
finally {
|
|
4893
|
+
if (stagedDir)
|
|
4894
|
+
rmSync(stagedDir, { recursive: true, force: true });
|
|
4587
4895
|
}
|
|
4588
|
-
const j = res.json;
|
|
4589
|
-
const dest = j?.folder_path ? `/raws/${j.folder_path}` : "your raws library";
|
|
4590
|
-
const kindWord = (j?.media_kind === "audio" || audioOnly) ? "audio" : "clip";
|
|
4591
|
-
console.log(`${GREEN}Clipped ${kindWord} ${(j?.duration_sec ?? (endSec - startSec)).toFixed?.(2) ?? j?.duration_sec}s → ${dest} (clip ${j?.clip_id ?? "?"}).${RESET}`);
|
|
4592
|
-
if (j?.view_url)
|
|
4593
|
-
console.log(`${DIM}Preview: ${new URL(j.view_url, ctx.host).toString()}${RESET}`);
|
|
4594
|
-
console.log(`${DIM}Grab another range from the same source with the same --tracer/--folder.${RESET}`);
|
|
4595
4896
|
}
|
|
4596
4897
|
async function runPullCommand(argv) {
|
|
4597
4898
|
const parsed = parseArgs({ args: argv, allowPositionals: true, options: { ...commonOptions(), dir: { type: "string" }, refetch: { type: "boolean", default: false } } });
|
|
@@ -4611,6 +4912,7 @@ async function runPullCommand(argv) {
|
|
|
4611
4912
|
const hasCast = existsSync(path.join(dir, "cast.json"));
|
|
4612
4913
|
const hasAnnotations = existsSync(path.join(dir, "scene-annotations.json"));
|
|
4613
4914
|
const hasHarness = existsSync(path.join(dir, "editor-harness.json"));
|
|
4915
|
+
const hasReplication = existsSync(path.join(dir, "replication-harness.json"));
|
|
4614
4916
|
if (ctx.json) {
|
|
4615
4917
|
printJson({
|
|
4616
4918
|
ok: true,
|
|
@@ -4620,6 +4922,7 @@ async function runPullCommand(argv) {
|
|
|
4620
4922
|
has_cast: hasCast,
|
|
4621
4923
|
has_scene_annotations: hasAnnotations,
|
|
4622
4924
|
has_editor_harness: hasHarness,
|
|
4925
|
+
has_replication_harness: hasReplication,
|
|
4623
4926
|
harness_context_path: harnessFiles.contextPath,
|
|
4624
4927
|
harness_instructions_path: harnessFiles.instructionsPath,
|
|
4625
4928
|
...info
|
|
@@ -4634,7 +4937,7 @@ async function runPullCommand(argv) {
|
|
|
4634
4937
|
const flags = [layer.is_timeline_proxy ? "proxy" : null, layer.is_full_canvas ? "full-canvas" : null].filter(Boolean).join(",");
|
|
4635
4938
|
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}`);
|
|
4636
4939
|
}
|
|
4637
|
-
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}`);
|
|
4940
|
+
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}`);
|
|
4638
4941
|
console.log(`${DIM}Agent brief: ${path.relative(dir, harnessFiles.instructionsPath) || path.basename(harnessFiles.instructionsPath)}; merged context: ${path.relative(dir, harnessFiles.contextPath)}.${RESET}`);
|
|
4639
4942
|
console.log(`${DIM}Next: vidfarm generate video --prompt "..." --aspect-ratio ${info.aspect_ratio ?? "9:16"} --place ${dir} --at <gap start>|--replace <layer_key>${RESET}`);
|
|
4640
4943
|
}
|
|
@@ -4848,7 +5151,7 @@ async function runAddProviderKeyCommand(argv) {
|
|
|
4848
5151
|
const provider = parsed.positionals[0];
|
|
4849
5152
|
const secret = parsed.positionals[1];
|
|
4850
5153
|
if (!provider || !secret)
|
|
4851
|
-
throw new Error("add-provider-key requires <provider> <secret> (provider: openai|gemini|openrouter|perplexity).");
|
|
5154
|
+
throw new Error("add-provider-key requires <provider> <secret> (provider: openai|gemini|openrouter|perplexity|nvidia|elevenlabs).");
|
|
4852
5155
|
const ctx = commonContext(parsed.values);
|
|
4853
5156
|
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 } });
|
|
4854
5157
|
assertApiOk(result, "add-provider-key");
|
|
@@ -4910,6 +5213,88 @@ async function runDownloadCommand(argv) {
|
|
|
4910
5213
|
console.log(`${GREEN}Downloaded ${formatBytes(bytes)} → ${dest}${RESET}`);
|
|
4911
5214
|
}
|
|
4912
5215
|
}
|
|
5216
|
+
async function runDownloadPostCommand(argv) {
|
|
5217
|
+
const parsed = parseArgs({
|
|
5218
|
+
args: argv,
|
|
5219
|
+
allowPositionals: true,
|
|
5220
|
+
options: { ...commonOptions(), quality: { type: "string", default: "best" }, "no-wait": { type: "boolean", default: false }, tracer: { type: "string" } }
|
|
5221
|
+
});
|
|
5222
|
+
const url = parsed.positionals[0];
|
|
5223
|
+
if (!url)
|
|
5224
|
+
throw new Error("download-post requires a source URL.");
|
|
5225
|
+
const quality = String(parsed.values.quality ?? "best");
|
|
5226
|
+
if (!["best", "hd", "full_hd"].includes(quality)) {
|
|
5227
|
+
throw new Error("download-post --quality must be one of: best, hd, full_hd.");
|
|
5228
|
+
}
|
|
5229
|
+
const ctx = commonContext(parsed.values);
|
|
5230
|
+
const tracer = String(parsed.values.tracer ?? `devcli-download-post-${Date.now().toString(36)}`);
|
|
5231
|
+
const submit = await apiRequest({
|
|
5232
|
+
method: "POST",
|
|
5233
|
+
host: ctx.host,
|
|
5234
|
+
path: "/api/v1/primitives/videos/download",
|
|
5235
|
+
auth: ctx.auth,
|
|
5236
|
+
body: { tracer, payload: { source_url: url, quality } }
|
|
5237
|
+
});
|
|
5238
|
+
assertApiOk(submit, "download-post");
|
|
5239
|
+
const jobId = submit.json?.job_id;
|
|
5240
|
+
if (!jobId || parsed.values["no-wait"]) {
|
|
5241
|
+
emitResult(submit, ctx.json);
|
|
5242
|
+
return;
|
|
5243
|
+
}
|
|
5244
|
+
if (!ctx.json)
|
|
5245
|
+
console.log(`${DIM}Downloading social post (${jobId})… polling every 5s.${RESET}`);
|
|
5246
|
+
const final = await pollPrimitiveJob(ctx, jobId);
|
|
5247
|
+
emitPrimitiveJobResult(ctx, final, "download-post");
|
|
5248
|
+
}
|
|
5249
|
+
async function runDownloadPostAudioCommand(argv) {
|
|
5250
|
+
const parsed = parseArgs({
|
|
5251
|
+
args: argv,
|
|
5252
|
+
allowPositionals: true,
|
|
5253
|
+
options: { ...commonOptions(), "no-wait": { type: "boolean", default: false }, tracer: { type: "string" } }
|
|
5254
|
+
});
|
|
5255
|
+
const url = parsed.positionals[0];
|
|
5256
|
+
if (!url)
|
|
5257
|
+
throw new Error("download-post-audio requires a source URL.");
|
|
5258
|
+
const ctx = commonContext(parsed.values);
|
|
5259
|
+
const tracer = String(parsed.values.tracer ?? `devcli-download-post-audio-${Date.now().toString(36)}`);
|
|
5260
|
+
const submit = await apiRequest({
|
|
5261
|
+
method: "POST",
|
|
5262
|
+
host: ctx.host,
|
|
5263
|
+
path: "/api/v1/primitives/audio/download",
|
|
5264
|
+
auth: ctx.auth,
|
|
5265
|
+
body: { tracer, payload: { source_url: url } }
|
|
5266
|
+
});
|
|
5267
|
+
assertApiOk(submit, "download-post-audio");
|
|
5268
|
+
const jobId = submit.json?.job_id;
|
|
5269
|
+
if (!jobId || parsed.values["no-wait"]) {
|
|
5270
|
+
emitResult(submit, ctx.json);
|
|
5271
|
+
return;
|
|
5272
|
+
}
|
|
5273
|
+
if (!ctx.json)
|
|
5274
|
+
console.log(`${DIM}Downloading social post audio (${jobId})… polling every 5s.${RESET}`);
|
|
5275
|
+
const final = await pollPrimitiveJob(ctx, jobId);
|
|
5276
|
+
emitPrimitiveJobResult(ctx, final, "download-post-audio");
|
|
5277
|
+
}
|
|
5278
|
+
function emitPrimitiveJobResult(ctx, job, action) {
|
|
5279
|
+
const mediaUrl = resolveJobMediaUrl(job);
|
|
5280
|
+
if (ctx.json) {
|
|
5281
|
+
printJson(job);
|
|
5282
|
+
return;
|
|
5283
|
+
}
|
|
5284
|
+
const status = String(job?.status ?? "");
|
|
5285
|
+
if (status === "failed" || status === "cancelled") {
|
|
5286
|
+
throw new Error(`${action} ${status}: ${String(job?.error ?? "unknown error")}`);
|
|
5287
|
+
}
|
|
5288
|
+
if (status && status !== "succeeded") {
|
|
5289
|
+
console.log(`${DIM}${action} status: ${status}.${RESET}`);
|
|
5290
|
+
}
|
|
5291
|
+
if (mediaUrl) {
|
|
5292
|
+
console.log(`${GREEN}Done${RESET} ${mediaUrl}`);
|
|
5293
|
+
}
|
|
5294
|
+
else {
|
|
5295
|
+
printJson(job);
|
|
5296
|
+
}
|
|
5297
|
+
}
|
|
4913
5298
|
// Browse the user's My Files library — the persistent per-user asset store
|
|
4914
5299
|
// (videos, images, audio, docs) organized into virtual folders. `files` lists
|
|
4915
5300
|
// them (optionally scoped to a folder); `get-file` resolves one attachment id to
|
|
@@ -4979,6 +5364,21 @@ const DIRECTORY_HELP = `vidfarm directory — browse the unified file tree (/fil
|
|
|
4979
5364
|
e.g. vidfarm directory rename /files/characters/zara zara-fox
|
|
4980
5365
|
vidfarm directory rename /files/characters/zara/notes.md character_about.md --file-id att_123
|
|
4981
5366
|
|
|
5367
|
+
directory move <path> <to-folder> Move a file/folder into another folder → POST /api/v1/user/me/directory/move
|
|
5368
|
+
--file-id <id> Move a FILE into <to-folder>; omit to move the folder at <path> (nests under <to-folder>)
|
|
5369
|
+
--json Raw JSON response
|
|
5370
|
+
Same root only (/files·/temp·/raws·/approved). Metadata-only — S3 objects untouched.
|
|
5371
|
+
e.g. vidfarm directory move /raws/demos /raws/archive
|
|
5372
|
+
vidfarm directory move /files/inbox/logo.png /files/brand --file-id att_123
|
|
5373
|
+
|
|
5374
|
+
directory copy <path> [<to-folder>] Duplicate a file/folder (shared S3 object) → POST /api/v1/user/me/directory/copy
|
|
5375
|
+
--file-id <id> Copy a FILE; omit to copy the folder at <path>
|
|
5376
|
+
--as <name> Rename the copied file/folder
|
|
5377
|
+
--json Raw JSON response
|
|
5378
|
+
Same root only (/files·/temp·/raws). Omit <to-folder> to duplicate in place. Not for /approved or /projects.
|
|
5379
|
+
e.g. vidfarm directory copy /raws/demos /raws/archive
|
|
5380
|
+
vidfarm directory copy /files/brand/logo.png /files/inbox --file-id att_123 --as logo-copy.png
|
|
5381
|
+
|
|
4982
5382
|
directory save-url <url> Save a durable media URL INTO My Files at a folder → POST /api/v1/user/me/attachments/from-url
|
|
4983
5383
|
--folder <path> Destination folder under /files (e.g. inpaints, promos)
|
|
4984
5384
|
--as <name> Name the saved file (else derived from the URL)
|
|
@@ -4987,8 +5387,7 @@ const DIRECTORY_HELP = `vidfarm directory — browse the unified file tree (/fil
|
|
|
4987
5387
|
e.g. vidfarm directory save-url "https://…/inpaint-out.png" --folder inpaints --as hero.png
|
|
4988
5388
|
(server fetches the URL — handles cross-origin S3; the CLI twin of the pop-panel "Save to Files")
|
|
4989
5389
|
|
|
4990
|
-
Aliases: directory | dir · Auth: --api-key <key> or VIDFARM_API_KEY, --host <url
|
|
4991
|
-
Move a raw between folders with: vidfarm api PATCH /raws/<clipId> --data '{"folder_path":"…"}'`;
|
|
5390
|
+
Aliases: directory | dir (move|mv · copy|cp) · Auth: --api-key <key> or VIDFARM_API_KEY, --host <url>`;
|
|
4992
5391
|
async function runDirectoryCommand(argv) {
|
|
4993
5392
|
const sub = argv[0];
|
|
4994
5393
|
const rest = argv.slice(1);
|
|
@@ -5000,8 +5399,13 @@ async function runDirectoryCommand(argv) {
|
|
|
5000
5399
|
case "find":
|
|
5001
5400
|
return runDirectorySearch(rest);
|
|
5002
5401
|
case "rename":
|
|
5003
|
-
case "mv":
|
|
5004
5402
|
return runDirectoryRename(rest);
|
|
5403
|
+
case "move":
|
|
5404
|
+
case "mv":
|
|
5405
|
+
return runDirectoryMove(rest);
|
|
5406
|
+
case "copy":
|
|
5407
|
+
case "cp":
|
|
5408
|
+
return runDirectoryCopy(rest);
|
|
5005
5409
|
case "save-url":
|
|
5006
5410
|
case "from-url":
|
|
5007
5411
|
case "import-url":
|
|
@@ -5119,6 +5523,66 @@ async function runDirectoryRename(argv) {
|
|
|
5119
5523
|
const newPath = result.json?.path ?? "";
|
|
5120
5524
|
console.log(`${GREEN}✓${RESET} Renamed to ${BOLD}${newName}${RESET} ${DIM}[${space}]${RESET}${newPath ? ` ${DIM}${newPath}${RESET}` : ""}`);
|
|
5121
5525
|
}
|
|
5526
|
+
// Move a file or folder into another folder (same root) → POST /me/directory/move.
|
|
5527
|
+
async function runDirectoryMove(argv) {
|
|
5528
|
+
const parsed = parseArgs({
|
|
5529
|
+
args: argv,
|
|
5530
|
+
allowPositionals: true,
|
|
5531
|
+
options: { ...commonOptions(), "file-id": { type: "string" }, to: { type: "string" } }
|
|
5532
|
+
});
|
|
5533
|
+
const ctx = commonContext(parsed.values);
|
|
5534
|
+
const sourcePath = parsed.positionals[0];
|
|
5535
|
+
const to = parsed.values.to ?? parsed.positionals[1];
|
|
5536
|
+
if (!sourcePath || !to) {
|
|
5537
|
+
throw new Error('directory move requires a source and a destination folder: vidfarm directory move <path> <to-folder> [--file-id <id>]\n e.g. vidfarm directory move /raws/demos /raws/archive\n vidfarm directory move /files/inbox/logo.png /files/brand --file-id att_123');
|
|
5538
|
+
}
|
|
5539
|
+
const fileId = parsed.values["file-id"];
|
|
5540
|
+
const space = targetSpaces(ctx.target)[0];
|
|
5541
|
+
const result = await dispatch(ctx, { method: "POST", path: "/api/v1/user/me/directory/move", body: { path: sourcePath, to, ...(fileId ? { file_id: fileId } : {}) } }, space);
|
|
5542
|
+
assertApiOk(result, "directory move");
|
|
5543
|
+
if (ctx.json) {
|
|
5544
|
+
printJson(result.json ?? result.text);
|
|
5545
|
+
return;
|
|
5546
|
+
}
|
|
5547
|
+
const newPath = result.json?.path ?? to;
|
|
5548
|
+
console.log(`${GREEN}✓${RESET} Moved to ${BOLD}${newPath}${RESET} ${DIM}[${space}]${RESET}`);
|
|
5549
|
+
}
|
|
5550
|
+
// Duplicate a file or folder into another folder (same root) → POST /me/directory/copy.
|
|
5551
|
+
async function runDirectoryCopy(argv) {
|
|
5552
|
+
const parsed = parseArgs({
|
|
5553
|
+
args: argv,
|
|
5554
|
+
allowPositionals: true,
|
|
5555
|
+
options: { ...commonOptions(), "file-id": { type: "string" }, to: { type: "string" }, as: { type: "string" } }
|
|
5556
|
+
});
|
|
5557
|
+
const ctx = commonContext(parsed.values);
|
|
5558
|
+
const sourcePath = parsed.positionals[0];
|
|
5559
|
+
const to = parsed.values.to ?? parsed.positionals[1];
|
|
5560
|
+
if (!sourcePath) {
|
|
5561
|
+
throw new Error('directory copy requires a source: vidfarm directory copy <path> [<to-folder>] [--file-id <id>] [--as <new-name>]\n e.g. vidfarm directory copy /raws/demos /raws/archive\n vidfarm directory copy /files/brand/logo.png /files/inbox --file-id att_123 --as logo-copy.png');
|
|
5562
|
+
}
|
|
5563
|
+
const fileId = parsed.values["file-id"];
|
|
5564
|
+
const newName = parsed.values.as;
|
|
5565
|
+
const space = targetSpaces(ctx.target)[0];
|
|
5566
|
+
const result = await dispatch(ctx, {
|
|
5567
|
+
method: "POST",
|
|
5568
|
+
path: "/api/v1/user/me/directory/copy",
|
|
5569
|
+
body: {
|
|
5570
|
+
path: sourcePath,
|
|
5571
|
+
...(to ? { to } : {}),
|
|
5572
|
+
...(fileId ? { file_id: fileId } : {}),
|
|
5573
|
+
...(newName ? { new_name: newName } : {})
|
|
5574
|
+
}
|
|
5575
|
+
}, space);
|
|
5576
|
+
assertApiOk(result, "directory copy");
|
|
5577
|
+
if (ctx.json) {
|
|
5578
|
+
printJson(result.json ?? result.text);
|
|
5579
|
+
return;
|
|
5580
|
+
}
|
|
5581
|
+
const newPath = result.json?.path ?? to ?? "";
|
|
5582
|
+
const copied = result.json?.copied;
|
|
5583
|
+
const suffix = typeof copied === "number" ? ` ${DIM}(${copied} file${copied === 1 ? "" : "s"})${RESET}` : "";
|
|
5584
|
+
console.log(`${GREEN}✓${RESET} Copied to ${BOLD}${newPath}${RESET}${suffix} ${DIM}[${space}]${RESET}`);
|
|
5585
|
+
}
|
|
5122
5586
|
// Save a durable media URL (e.g. a finished job's output) INTO My Files at a
|
|
5123
5587
|
// folder — the CLI twin of the pop-panel "Save to Files" picker. Server fetches
|
|
5124
5588
|
// the URL (handles cross-origin S3), so no local download round-trip needed.
|