@mevdragon/vidfarm-devcli 0.17.0 → 0.18.1
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/vidfarm-media/SKILL.md +43 -6
- package/SKILL.director.md +37 -23
- package/SKILL.platform.md +6 -6
- package/demo/dist/app.js +69 -69
- package/demo/dist/favicon.ico +0 -0
- package/dist/src/app.js +1832 -213
- package/dist/src/cli.js +238 -17
- package/dist/src/devcli/clip-store.js +29 -2
- package/dist/src/devcli/clips.js +116 -73
- package/dist/src/devcli/composition-edit.js +262 -0
- package/dist/src/devcli/timeline-edit.js +283 -0
- package/dist/src/editor-chat.js +29 -6
- package/dist/src/frontend/discover-client.js +130 -0
- package/dist/src/frontend/discover-store.js +23 -0
- package/dist/src/frontend/file-directory.js +744 -0
- package/dist/src/frontend/template-editor-chat.js +22 -19
- package/dist/src/landing-page.js +24 -7
- package/dist/src/page-shell.js +25 -1
- package/dist/src/reskin/agency-page.js +214 -170
- package/dist/src/reskin/calendar-page.js +503 -187
- package/dist/src/reskin/chat-page.js +739 -299
- package/dist/src/reskin/discover-page.js +1450 -279
- package/dist/src/reskin/document.js +1392 -16
- package/dist/src/reskin/help-page.js +139 -100
- package/dist/src/reskin/index-page.js +1 -1
- package/dist/src/reskin/inpaint-page.js +547 -0
- package/dist/src/reskin/job-runs-page.js +355 -127
- package/dist/src/reskin/library-page.js +1188 -317
- package/dist/src/reskin/login-page.js +124 -87
- package/dist/src/reskin/pricing-page.js +197 -166
- package/dist/src/reskin/settings-page.js +566 -187
- package/dist/src/reskin/theme.js +434 -17
- package/dist/src/services/clip-curation/gemini.js +5 -0
- package/dist/src/services/clip-curation/hunt.js +79 -1
- package/dist/src/services/clip-curation/index.js +2 -1
- package/dist/src/services/clip-curation/local-agent.js +4 -3
- package/dist/src/services/clip-curation/media-select.js +85 -0
- package/dist/src/services/clip-curation/query.js +5 -1
- package/dist/src/services/clip-curation/refine.js +50 -20
- package/dist/src/services/clip-curation/scan.js +10 -3
- package/dist/src/services/clip-curation/taxonomy.js +3 -1
- package/dist/src/services/clip-curation/taxonomy.v1.json +13 -1
- package/dist/src/services/clip-records.js +14 -1
- package/dist/src/services/clip-search.js +43 -13
- package/dist/src/services/file-directory.js +114 -0
- package/dist/src/services/storage.js +24 -1
- package/dist/src/services/upstream.js +5 -5
- package/dist/src/template-editor-shell.js +16 -2
- package/package.json +1 -1
- package/public/assets/discover-client-app.js +1 -0
- package/public/assets/file-directory-app.js +2 -0
- package/public/assets/homepage-client-app.js +12 -12
- package/public/assets/page-runtime-client-app.js +24 -24
- package/src/assets/favicon.ico +0 -0
- package/src/assets/logo-vidfarm.png +0 -0
package/dist/src/cli.js
CHANGED
|
@@ -12,6 +12,7 @@ import { computeCompositionGaps, insertMediaLayer, inspectComposition, replaceLa
|
|
|
12
12
|
import { runCaptionsCommand } from "./devcli/captions.js";
|
|
13
13
|
import { runClipsCommand } from "./devcli/clips.js";
|
|
14
14
|
import { runTransitionsCommand } from "./devcli/transitions.js";
|
|
15
|
+
import { runKeyframesCommand, runMoveCommand, runRestackCommand, runRippleCommand, runTrimCommand } from "./devcli/timeline-edit.js";
|
|
15
16
|
import { MAX_LOCAL_TRANSCRIBE_AUDIO_BYTES, loadSpeechAudioLocally, localGenerateSpeech, localTranscribeSpeech, resolveLocalSpeechAuth } from "./devcli/speech.js";
|
|
16
17
|
import { buildSrtFromSegments, inferSpeechProviderForVoice } from "./services/speech.js";
|
|
17
18
|
import { formatCompositionLintIssues, lintCompositionHtml } from "./services/composition-lint.js";
|
|
@@ -168,22 +169,22 @@ Generate AI media and drop it on the timeline (for local coding agents):
|
|
|
168
169
|
original vs decomposed (caption-free),
|
|
169
170
|
non-billing (alias: ghostcut)
|
|
170
171
|
|
|
171
|
-
|
|
172
|
-
|
|
172
|
+
Raws (the third library — mine long-form video into a reusable raws store):
|
|
173
|
+
raws scan <video-path> Hunt short clips out of a long video. LOCAL-FIRST: local ffmpeg +
|
|
173
174
|
Detect → tag/transcribe → embed → persist your local claude/codex CLI
|
|
174
175
|
to ~/.vidfarm/clips.db. (keys are fallback)
|
|
175
176
|
--range "MM:SS-MM:SS" Only hunt these source windows (repeatable; big cost saver)
|
|
176
177
|
--duration <sec> Target clip length as a SOFT band (10→5-20s, 30→20-40s)
|
|
177
178
|
--aspect 9:16|16:9|4:3|1:1 Crop clips (also: vertical/horizontal/square) [--crop-focus <f>]
|
|
178
179
|
--no-text Prefer scenes WITHOUT captions/on-screen text (selection only)
|
|
179
|
-
--cloud [--url <url>] BACKUP: run on the deployed pipeline → POST /
|
|
180
|
+
--cloud [--url <url>] BACKUP: run on the deployed pipeline → POST /raws/scan (+ poll)
|
|
180
181
|
(uploads to your temp folder — 30-day TTL — bills AWS compute only)
|
|
181
182
|
--dry-run Show the estimated cost and stop (over $1 needs --yes/confirm)
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
(run 'vidfarm
|
|
183
|
+
raws list List the local raws library → GET /raws
|
|
184
|
+
raws search "<text>" Hybrid structured + semantic search → POST /raws/search
|
|
185
|
+
raws preset list|run <name>|save <name> --from-query '<...>' → /raws/presets
|
|
186
|
+
raws export <ids...> --to <dir> Copy raw mp4s + metadata out
|
|
187
|
+
(run 'vidfarm raws' for the full raws help)
|
|
187
188
|
|
|
188
189
|
Speech (TTS/STT) — LOCAL-FIRST on your own AI key; --cloud is the explicit backup:
|
|
189
190
|
tts "<text>" Text → narration audio file. LOCAL: your OPENAI/GEMINI/
|
|
@@ -236,6 +237,20 @@ Scene transitions (between clips on a pulled/served composition — local file w
|
|
|
236
237
|
transitions list <dir> Show transitions on every scene clip [--json]
|
|
237
238
|
transitions clear <dir> Remove all transitions
|
|
238
239
|
(run 'vidfarm transitions' for presets & all flags)
|
|
240
|
+
|
|
241
|
+
Fine timeline control (trackpad-level verbs on a pulled/served composition — local file write):
|
|
242
|
+
keyframes <dir> --layer <key> --preset <name>|--keyframes '<json>' Script-free CSS
|
|
243
|
+
@keyframes animation on ONE layer (opacity/translate/
|
|
244
|
+
scale/rotate); previews AND renders. --easing --duration
|
|
245
|
+
move <dir> --layer <key> [--layer …] Shift layers by --delta-start (sec) and/or
|
|
246
|
+
--delta-track (lanes); group-aware. (alias: nudge)
|
|
247
|
+
ripple <dir> --at <sec> --delta <sec> Insert (+) or close (-) time at --at and
|
|
248
|
+
shift every downstream clip so the rest stays in sync
|
|
249
|
+
trim <dir> --layer <key> --edge start|end --to <sec> Move one edge to a time
|
|
250
|
+
(left trim couples the media in-point on video/audio)
|
|
251
|
+
restack <dir> --layer <key> --order front|back|forward|backward|--track <n> Change
|
|
252
|
+
stacking (== track index; higher = on top). (alias: zindex)
|
|
253
|
+
(run 'vidfarm keyframes|move|ripple|trim|restack help' for all flags)
|
|
239
254
|
versions <forkId> List immutable version snapshots → GET .../compositions/:forkId/versions
|
|
240
255
|
snapshot <forkId> Save the working state as a new version → POST .../compositions/:forkId/versions
|
|
241
256
|
render <forkId> Render the fork to MP4 (--wait to poll) → POST .../compositions/:forkId/render
|
|
@@ -319,10 +334,19 @@ Files (multi-step flows the devcli handles for you):
|
|
|
319
334
|
--folder <path> Only files under this folder (e.g. a product/offer)
|
|
320
335
|
--search <query> Find files by MEANING (keyword + vector over → POST /api/v1/user/me/attachments/search
|
|
321
336
|
name/folder/notes) instead of listing everything
|
|
337
|
+
--content-type <a,b> With --search: filter the /raws branch by shot-KIND tag
|
|
338
|
+
(talking_head,b_roll,product_shot,screen_recording,demo,
|
|
339
|
+
reaction,interview,establishing,lifestyle,text_graphic)
|
|
322
340
|
get-file <id> [dest] Resolve a My Files id to its URL and download it
|
|
323
341
|
--print Print text contents (md/txt/csv/json) instead of saving
|
|
324
342
|
annotate-file <id|name> Set metadata notes on one My Files entry → PATCH /api/v1/user/me/attachments/:id
|
|
325
343
|
--notes <text> The notes to attach (empty string clears); embedded for vector search
|
|
344
|
+
directory ls [path] Browse the unified file tree (/files·/temp·/raws) → GET /api/v1/user/me/directory
|
|
345
|
+
--limit <n> --offset <n> Paginate (folders + files under a path; default "/")
|
|
346
|
+
directory search <query> Ranked search across the whole tree → POST /api/v1/user/me/directory/search
|
|
347
|
+
--path <scope> Scope to a subtree · --mode auto|semantic|substring|path · --limit <n>
|
|
348
|
+
--content-type <a,b> Filter the /raws branch by shot-KIND tag (talking_head, b_roll, …)
|
|
349
|
+
(alias: dir)
|
|
326
350
|
put-file / get-file / files / annotate-file are the My Files (persistent) set;
|
|
327
351
|
upload is the throwaway temp store for dropping media into a composition.
|
|
328
352
|
Annotate assets you'll want back — esp. character refs (character_sprite_card.png
|
|
@@ -528,6 +552,11 @@ async function main() {
|
|
|
528
552
|
case "annotate-file":
|
|
529
553
|
await runAnnotateFileCommand(rest);
|
|
530
554
|
return;
|
|
555
|
+
case "directory":
|
|
556
|
+
case "dir":
|
|
557
|
+
await runDirectoryCommand(rest);
|
|
558
|
+
return;
|
|
559
|
+
case "raws":
|
|
531
560
|
case "clips":
|
|
532
561
|
await runClipsCommand(rest);
|
|
533
562
|
return;
|
|
@@ -537,6 +566,24 @@ async function main() {
|
|
|
537
566
|
case "transitions":
|
|
538
567
|
await runTransitionsCommand(rest);
|
|
539
568
|
return;
|
|
569
|
+
// Fine timeline control — local-file twins of the editor's trackpad verbs.
|
|
570
|
+
case "keyframes":
|
|
571
|
+
await runKeyframesCommand(rest);
|
|
572
|
+
return;
|
|
573
|
+
case "move":
|
|
574
|
+
case "nudge":
|
|
575
|
+
await runMoveCommand(rest);
|
|
576
|
+
return;
|
|
577
|
+
case "ripple":
|
|
578
|
+
await runRippleCommand(rest);
|
|
579
|
+
return;
|
|
580
|
+
case "trim":
|
|
581
|
+
await runTrimCommand(rest);
|
|
582
|
+
return;
|
|
583
|
+
case "restack":
|
|
584
|
+
case "zindex":
|
|
585
|
+
await runRestackCommand(rest);
|
|
586
|
+
return;
|
|
540
587
|
// Local media engines + toolchain (bundled HyperFrames local commands —
|
|
541
588
|
// free, no account — plus vidfarm-native lint/stills/doctor/skills).
|
|
542
589
|
case "remove-background":
|
|
@@ -653,7 +700,7 @@ function emitResult(result, json, frontend) {
|
|
|
653
700
|
// Frontend URL builders — these are the pages a director opens in a browser.
|
|
654
701
|
function editorFrontendUrl(host, templateId, forkId) {
|
|
655
702
|
const base = `${host}/editor/${encodeURIComponent(templateId)}`;
|
|
656
|
-
return forkId ? `${base}
|
|
703
|
+
return forkId ? `${base}/fork/${encodeURIComponent(forkId)}` : base;
|
|
657
704
|
}
|
|
658
705
|
function discoverFrontendUrl(host) {
|
|
659
706
|
return `${host}/discover`;
|
|
@@ -722,7 +769,7 @@ async function runServeCommand(argv) {
|
|
|
722
769
|
// render_target="cloud" upstream handoff (the editor's "Render in Cloud").
|
|
723
770
|
process.env.VIDFARM_JOB_STATE_MACHINE_ARN = "";
|
|
724
771
|
// Same trap for clip hunts: a dev-shell .env carrying the staging clip-scan
|
|
725
|
-
// ARN would route this box's /
|
|
772
|
+
// ARN would route this box's /raws/scan to the cloud Step Functions. serve
|
|
726
773
|
// hunts are LOCAL-FIRST (local agent CLI / BYOK keys); the upstream cloud is
|
|
727
774
|
// the explicit backup inside the scan route itself.
|
|
728
775
|
process.env.VIDFARM_CLIP_SCAN_STATE_MACHINE_ARN = "";
|
|
@@ -778,7 +825,7 @@ async function runServeCommand(argv) {
|
|
|
778
825
|
}
|
|
779
826
|
const base = `http://localhost:${port}`;
|
|
780
827
|
const editorPath = openTemplateId
|
|
781
|
-
? `/editor/${encodeURIComponent(openTemplateId)}${openForkId ?
|
|
828
|
+
? `/editor/${encodeURIComponent(openTemplateId)}${openForkId ? `/fork/${encodeURIComponent(openForkId)}` : ""}`
|
|
782
829
|
: "/discover";
|
|
783
830
|
const openUrl = `${base}/auto-login?api_key=${encodeURIComponent(apiKey)}&redirect=${encodeURIComponent(editorPath)}`;
|
|
784
831
|
printServeBanner({ base, dataDir, openUrl, editorPath, cloud: cloudEnabled ? { host, hasKey: Boolean(cloudApiKey) } : null });
|
|
@@ -981,9 +1028,9 @@ function printPublishBanner(input) {
|
|
|
981
1028
|
console.log("");
|
|
982
1029
|
}
|
|
983
1030
|
async function resolveForkId(input) {
|
|
984
|
-
// /editor/<template_id> issues a 302 → /editor/<template_id
|
|
1031
|
+
// /editor/<template_id> issues a 302 → /editor/<template_id>/fork/<id>
|
|
985
1032
|
// whenever the caller (or the template's default fork) has one. Follow the
|
|
986
|
-
// redirect once by hand so we can read the fork id off the
|
|
1033
|
+
// redirect once by hand so we can read the fork id off the path.
|
|
987
1034
|
const url = `${input.host}/editor/${encodeURIComponent(input.templateId)}`;
|
|
988
1035
|
const res = await fetch(url, {
|
|
989
1036
|
method: "GET",
|
|
@@ -995,7 +1042,7 @@ async function resolveForkId(input) {
|
|
|
995
1042
|
if (location) {
|
|
996
1043
|
try {
|
|
997
1044
|
const target = new URL(location, input.host);
|
|
998
|
-
const fork = target.
|
|
1045
|
+
const fork = parseForkFromEditorPath(target.pathname);
|
|
999
1046
|
if (fork)
|
|
1000
1047
|
return fork;
|
|
1001
1048
|
}
|
|
@@ -1006,6 +1053,12 @@ async function resolveForkId(input) {
|
|
|
1006
1053
|
}
|
|
1007
1054
|
return null;
|
|
1008
1055
|
}
|
|
1056
|
+
// Parse the fork id out of a canonical editor path
|
|
1057
|
+
// /editor/<template_id>/fork/<fork_id> → "<fork_id>" (else null).
|
|
1058
|
+
function parseForkFromEditorPath(pathname) {
|
|
1059
|
+
const match = pathname.match(/^\/editor\/[^/]+\/fork\/([^/]+)\/?$/);
|
|
1060
|
+
return match ? decodeURIComponent(match[1]) : null;
|
|
1061
|
+
}
|
|
1009
1062
|
async function fetchCompositionFiles(input) {
|
|
1010
1063
|
// video-context.json carries the source video's audio transcript and
|
|
1011
1064
|
// per-scene visual descriptions so desktop agents (Claude Code, Codex)
|
|
@@ -2001,6 +2054,13 @@ async function resolveReferenceUrls(ctx, refs) {
|
|
|
2001
2054
|
function inferMediaKindFromUrl(url) {
|
|
2002
2055
|
return /\.(mp4|mov|webm|m4v|mkv)(\?|#|$)/i.test(url) ? "video" : "image";
|
|
2003
2056
|
}
|
|
2057
|
+
// Comma-separated shot-KIND filter (talking_head, b_roll, product_shot, …) →
|
|
2058
|
+
// the content_type array the /raws branch of the search endpoints honors.
|
|
2059
|
+
function parseContentTypeFlag(value) {
|
|
2060
|
+
if (typeof value !== "string" || !value.trim())
|
|
2061
|
+
return [];
|
|
2062
|
+
return value.split(",").map((s) => s.trim().toLowerCase()).filter(Boolean);
|
|
2063
|
+
}
|
|
2004
2064
|
// Resolve a composition.html on disk from a file path or a directory holding one.
|
|
2005
2065
|
function resolveCompositionHtmlPath(target) {
|
|
2006
2066
|
const abs = path.resolve(process.cwd(), target);
|
|
@@ -2256,7 +2316,7 @@ async function runGenerateCommand(argv) {
|
|
|
2256
2316
|
}
|
|
2257
2317
|
}
|
|
2258
2318
|
// ── Speech: tts / stt ────────────────────────────────────────────────────────
|
|
2259
|
-
// LOCAL-FIRST like `
|
|
2319
|
+
// LOCAL-FIRST like `raws scan`: both commands default to running on the
|
|
2260
2320
|
// user's OWN AI key (GEMINI_API_KEY / OPENAI_API_KEY / OPENROUTER_API_KEY or
|
|
2261
2321
|
// the --*-key flags) via src/services/speech.ts — local ffmpeg demux, direct
|
|
2262
2322
|
// provider call, no cloud job, no wallet. The REST primitive routes
|
|
@@ -3213,18 +3273,20 @@ async function runDownloadCommand(argv) {
|
|
|
3213
3273
|
// This is the same filesystem the /editor AI agent browses via the browse_files
|
|
3214
3274
|
// tool, so an agent CLI (Claude Code / Codex) can find assets the same way.
|
|
3215
3275
|
async function runFilesCommand(argv) {
|
|
3216
|
-
const parsed = parseArgs({ args: argv, allowPositionals: true, options: { ...commonOptions(), folder: { type: "string" }, search: { type: "string" } } });
|
|
3276
|
+
const parsed = parseArgs({ args: argv, allowPositionals: true, options: { ...commonOptions(), folder: { type: "string" }, search: { type: "string" }, "content-type": { type: "string" } } });
|
|
3217
3277
|
const ctx = commonContext(parsed.values);
|
|
3218
3278
|
const folderFilter = parsed.values.folder ? String(parsed.values.folder).replace(/^\/+|\/+$/g, "") : undefined;
|
|
3279
|
+
const contentTypes = parseContentTypeFlag(parsed.values["content-type"]);
|
|
3219
3280
|
if (parsed.values.search) {
|
|
3220
3281
|
// Meaning-first lookup: keyword + vector search over every file's
|
|
3221
3282
|
// name/folder/notes (BYOK embeddings; keyword-only without a saved key).
|
|
3283
|
+
// content_type filters the /raws branch by shot-KIND tag (server-honored).
|
|
3222
3284
|
const result = await apiRequest({
|
|
3223
3285
|
method: "POST",
|
|
3224
3286
|
host: ctx.host,
|
|
3225
3287
|
path: "/api/v1/user/me/attachments/search",
|
|
3226
3288
|
auth: ctx.auth,
|
|
3227
|
-
body: { query: String(parsed.values.search), ...(folderFilter ? { folder_path: folderFilter } : {}) }
|
|
3289
|
+
body: { query: String(parsed.values.search), ...(folderFilter ? { folder_path: folderFilter } : {}), ...(contentTypes.length ? { content_type: contentTypes } : {}) }
|
|
3228
3290
|
});
|
|
3229
3291
|
assertApiOk(result, "files --search");
|
|
3230
3292
|
emitResult(result, ctx.json);
|
|
@@ -3240,6 +3302,165 @@ async function runFilesCommand(argv) {
|
|
|
3240
3302
|
}
|
|
3241
3303
|
emitResult(result, ctx.json);
|
|
3242
3304
|
}
|
|
3305
|
+
// ── directory (unified virtual file tree) ────────────────────────────────────
|
|
3306
|
+
// Browse + search the three file roots (/files, /temp, /raws) as one tree via
|
|
3307
|
+
// the cloud REST directory endpoints. Same cloud auth (--api-key/VIDFARM_API_KEY,
|
|
3308
|
+
// --host, --json) as the rest of the wrapper commands.
|
|
3309
|
+
const DIRECTORY_HELP = `vidfarm directory — browse the unified file tree (/files · /temp · /raws)
|
|
3310
|
+
|
|
3311
|
+
directory ls [path] List folders + files under a path (default "/") → GET /api/v1/user/me/directory
|
|
3312
|
+
--limit <n> --offset <n> Paginate (server returns next_offset when more)
|
|
3313
|
+
--json Raw JSON response
|
|
3314
|
+
|
|
3315
|
+
directory search <query> Ranked search across the tree → POST /api/v1/user/me/directory/search
|
|
3316
|
+
--path <scope> Scope the search to a subtree (e.g. /raws/demos)
|
|
3317
|
+
--mode auto|semantic|substring|path Signal(s) to use (default: auto)
|
|
3318
|
+
--content-type <a,b> Filter the /raws branch by shot-KIND tag (comma-separated:
|
|
3319
|
+
talking_head, b_roll, product_shot, screen_recording, demo,
|
|
3320
|
+
reaction, interview, establishing, lifestyle, text_graphic)
|
|
3321
|
+
--limit <n> --json
|
|
3322
|
+
|
|
3323
|
+
Aliases: directory | dir · Auth: --api-key <key> or VIDFARM_API_KEY, --host <url>
|
|
3324
|
+
Move a raw between folders with: vidfarm api PATCH /raws/<clipId> --data '{"folder_path":"…"}'`;
|
|
3325
|
+
async function runDirectoryCommand(argv) {
|
|
3326
|
+
const sub = argv[0];
|
|
3327
|
+
const rest = argv.slice(1);
|
|
3328
|
+
switch (sub) {
|
|
3329
|
+
case "ls":
|
|
3330
|
+
case "list":
|
|
3331
|
+
return runDirectoryLs(rest);
|
|
3332
|
+
case "search":
|
|
3333
|
+
case "find":
|
|
3334
|
+
return runDirectorySearch(rest);
|
|
3335
|
+
case undefined:
|
|
3336
|
+
case "help":
|
|
3337
|
+
case "--help":
|
|
3338
|
+
case "-h":
|
|
3339
|
+
console.log(DIRECTORY_HELP);
|
|
3340
|
+
return;
|
|
3341
|
+
default:
|
|
3342
|
+
console.error(`Unknown directory subcommand: ${sub}\n`);
|
|
3343
|
+
console.log(DIRECTORY_HELP);
|
|
3344
|
+
process.exitCode = 1;
|
|
3345
|
+
}
|
|
3346
|
+
}
|
|
3347
|
+
async function runDirectoryLs(argv) {
|
|
3348
|
+
const parsed = parseArgs({
|
|
3349
|
+
args: argv,
|
|
3350
|
+
allowPositionals: true,
|
|
3351
|
+
options: { ...commonOptions(), limit: { type: "string" }, offset: { type: "string" } }
|
|
3352
|
+
});
|
|
3353
|
+
const ctx = commonContext(parsed.values);
|
|
3354
|
+
const dirPath = parsed.positionals[0] ?? "/";
|
|
3355
|
+
const result = await apiRequest({
|
|
3356
|
+
method: "GET",
|
|
3357
|
+
host: ctx.host,
|
|
3358
|
+
path: "/api/v1/user/me/directory",
|
|
3359
|
+
auth: ctx.auth,
|
|
3360
|
+
query: {
|
|
3361
|
+
path: dirPath,
|
|
3362
|
+
limit: parsed.values.limit,
|
|
3363
|
+
offset: parsed.values.offset
|
|
3364
|
+
}
|
|
3365
|
+
});
|
|
3366
|
+
assertApiOk(result, "directory ls");
|
|
3367
|
+
if (ctx.json) {
|
|
3368
|
+
printJson(result.json ?? result.text);
|
|
3369
|
+
return;
|
|
3370
|
+
}
|
|
3371
|
+
printDirectoryListing(result.json, dirPath);
|
|
3372
|
+
}
|
|
3373
|
+
async function runDirectorySearch(argv) {
|
|
3374
|
+
const parsed = parseArgs({
|
|
3375
|
+
args: argv,
|
|
3376
|
+
allowPositionals: true,
|
|
3377
|
+
options: { ...commonOptions(), path: { type: "string" }, mode: { type: "string" }, "content-type": { type: "string" }, limit: { type: "string" } }
|
|
3378
|
+
});
|
|
3379
|
+
const ctx = commonContext(parsed.values);
|
|
3380
|
+
const query = parsed.positionals.join(" ").trim();
|
|
3381
|
+
if (!query) {
|
|
3382
|
+
throw new Error('directory search requires a query: vidfarm directory search "<query>" [--path /raws] [--mode auto|semantic|substring|path] [--content-type talking_head,b_roll] [--limit N]');
|
|
3383
|
+
}
|
|
3384
|
+
const mode = parsed.values.mode;
|
|
3385
|
+
if (mode && !["auto", "semantic", "substring", "path"].includes(mode)) {
|
|
3386
|
+
throw new Error(`--mode must be auto | semantic | substring | path (got "${mode}")`);
|
|
3387
|
+
}
|
|
3388
|
+
const contentTypes = parseContentTypeFlag(parsed.values["content-type"]);
|
|
3389
|
+
const limit = parsed.values.limit != null ? Number(parsed.values.limit) : undefined;
|
|
3390
|
+
const result = await apiRequest({
|
|
3391
|
+
method: "POST",
|
|
3392
|
+
host: ctx.host,
|
|
3393
|
+
path: "/api/v1/user/me/directory/search",
|
|
3394
|
+
auth: ctx.auth,
|
|
3395
|
+
body: {
|
|
3396
|
+
query,
|
|
3397
|
+
...(parsed.values.path ? { path: String(parsed.values.path) } : {}),
|
|
3398
|
+
...(mode ? { mode } : {}),
|
|
3399
|
+
...(contentTypes.length ? { content_type: contentTypes } : {}),
|
|
3400
|
+
...(limit != null && Number.isFinite(limit) ? { limit } : {})
|
|
3401
|
+
}
|
|
3402
|
+
});
|
|
3403
|
+
assertApiOk(result, "directory search");
|
|
3404
|
+
if (ctx.json) {
|
|
3405
|
+
printJson(result.json ?? result.text);
|
|
3406
|
+
return;
|
|
3407
|
+
}
|
|
3408
|
+
printDirectorySearch(result.json, query);
|
|
3409
|
+
}
|
|
3410
|
+
// Compact "12.3s · 4.2 MB · video/mp4" line for a directory file item.
|
|
3411
|
+
function directoryFileMeta(item) {
|
|
3412
|
+
const parts = [];
|
|
3413
|
+
if (typeof item?.durationSec === "number" && item.durationSec > 0)
|
|
3414
|
+
parts.push(`${item.durationSec.toFixed(1)}s`);
|
|
3415
|
+
if (typeof item?.sizeBytes === "number" && item.sizeBytes > 0)
|
|
3416
|
+
parts.push(formatBytes(item.sizeBytes));
|
|
3417
|
+
if (typeof item?.contentType === "string" && item.contentType)
|
|
3418
|
+
parts.push(item.contentType);
|
|
3419
|
+
return parts.join(" · ");
|
|
3420
|
+
}
|
|
3421
|
+
function printDirectoryListing(data, requestedPath) {
|
|
3422
|
+
const listedPath = data && typeof data.path === "string" ? data.path : requestedPath;
|
|
3423
|
+
const root = data?.root ? ` ${DIM}[${data.root}]${RESET}` : "";
|
|
3424
|
+
console.log(`${BOLD}${listedPath}${RESET}${root}`);
|
|
3425
|
+
const folders = Array.isArray(data?.folders) ? data.folders : [];
|
|
3426
|
+
const files = Array.isArray(data?.files) ? data.files : [];
|
|
3427
|
+
if (folders.length === 0 && files.length === 0) {
|
|
3428
|
+
console.log(` ${DIM}(empty)${RESET}`);
|
|
3429
|
+
}
|
|
3430
|
+
for (const f of folders) {
|
|
3431
|
+
console.log(` ${DIM}dir ${RESET} ${f?.name ?? ""}/ ${DIM}${f?.path ?? ""}${RESET}`);
|
|
3432
|
+
}
|
|
3433
|
+
for (const f of files) {
|
|
3434
|
+
const meta = directoryFileMeta(f);
|
|
3435
|
+
console.log(` file ${f?.name ?? ""}${meta ? ` ${DIM}${meta}${RESET}` : ""} ${DIM}${f?.path ?? ""}${RESET}`);
|
|
3436
|
+
if (f?.viewUrl)
|
|
3437
|
+
console.log(` ${FRONTEND}${f.viewUrl}${RESET}`);
|
|
3438
|
+
}
|
|
3439
|
+
const total = folders.length + files.length;
|
|
3440
|
+
const more = data?.next_offset != null ? ` (more: --offset ${data.next_offset})` : "";
|
|
3441
|
+
console.log(`\n ${total} item${total === 1 ? "" : "s"} (${folders.length} folder${folders.length === 1 ? "" : "s"}, ${files.length} file${files.length === 1 ? "" : "s"})${more}`);
|
|
3442
|
+
}
|
|
3443
|
+
function printDirectorySearch(data, query) {
|
|
3444
|
+
const results = Array.isArray(data?.results) ? data.results : [];
|
|
3445
|
+
const scope = data?.scope ? ` in ${data.scope}` : "";
|
|
3446
|
+
const modeLabel = data?.mode ? ` · mode=${data.mode}` : "";
|
|
3447
|
+
const semantic = data?.semantic === true ? " · semantic" : data?.semantic === false ? " · keyword-only" : "";
|
|
3448
|
+
console.log(`${BOLD}"${query}"${RESET}${scope}${modeLabel}${semantic} → ${results.length} result${results.length === 1 ? "" : "s"}\n`);
|
|
3449
|
+
if (results.length === 0) {
|
|
3450
|
+
console.log(` ${DIM}(no matches)${RESET}`);
|
|
3451
|
+
return;
|
|
3452
|
+
}
|
|
3453
|
+
results.forEach((r, i) => {
|
|
3454
|
+
const score = typeof r?.score === "number" ? ` ${DIM}score=${r.score.toFixed(3)}${RESET}` : "";
|
|
3455
|
+
const kind = r?.kind === "folder" ? "dir " : "file";
|
|
3456
|
+
console.log(` ${String(i + 1).padStart(2)}. ${kind} ${r?.path ?? r?.name ?? ""}${score}`);
|
|
3457
|
+
const meta = directoryFileMeta(r);
|
|
3458
|
+
if (meta)
|
|
3459
|
+
console.log(` ${DIM}${meta}${RESET}`);
|
|
3460
|
+
if (r?.viewUrl)
|
|
3461
|
+
console.log(` ${FRONTEND}${r.viewUrl}${RESET}`);
|
|
3462
|
+
});
|
|
3463
|
+
}
|
|
3243
3464
|
// Attach metadata notes to one My Files entry (by id, or file name as a
|
|
3244
3465
|
// fallback). Notes describe what the file IS and when to use it; the server
|
|
3245
3466
|
// vector-embeds them so `files --search` finds the asset by meaning later —
|
|
@@ -130,7 +130,12 @@ export class ClipStore {
|
|
|
130
130
|
this.attachEmbeddings([clip]);
|
|
131
131
|
return clip;
|
|
132
132
|
}
|
|
133
|
-
/**
|
|
133
|
+
/**
|
|
134
|
+
* All clips (metadata only), newest first, optionally scoped to a source
|
|
135
|
+
* video. `contentTypes` filters by the shot-KIND tag (tags.content_type,
|
|
136
|
+
* exact match, case-insensitive); when present the limit/offset are applied
|
|
137
|
+
* AFTER the JS filter (the tag lives in tags_json, not a column).
|
|
138
|
+
*/
|
|
134
139
|
listClips(opts = {}) {
|
|
135
140
|
let sql = "SELECT * FROM clips";
|
|
136
141
|
const args = [];
|
|
@@ -139,9 +144,30 @@ export class ClipStore {
|
|
|
139
144
|
args.push(opts.sourceVideoId);
|
|
140
145
|
}
|
|
141
146
|
sql += " ORDER BY created_at DESC";
|
|
142
|
-
|
|
147
|
+
const wanted = (opts.contentTypes ?? []).map((s) => s.trim().toLowerCase()).filter(Boolean);
|
|
148
|
+
if (wanted.length) {
|
|
149
|
+
// Content-type filter can't ride SQL (tag is in JSON), so filter every
|
|
150
|
+
// matching row in JS, then window with offset/limit.
|
|
151
|
+
let clips = this.db.prepare(sql).all(...args)
|
|
152
|
+
.map(rowToClip)
|
|
153
|
+
.filter((c) => (c.tags.content_type ?? []).some((t) => wanted.includes(String(t).toLowerCase())));
|
|
154
|
+
if (opts.offset)
|
|
155
|
+
clips = clips.slice(Math.max(0, Math.floor(opts.offset)));
|
|
156
|
+
if (opts.limit)
|
|
157
|
+
clips = clips.slice(0, opts.limit);
|
|
158
|
+
return clips;
|
|
159
|
+
}
|
|
160
|
+
if (opts.limit != null) {
|
|
143
161
|
sql += " LIMIT ?";
|
|
144
162
|
args.push(opts.limit);
|
|
163
|
+
if (opts.offset) {
|
|
164
|
+
sql += " OFFSET ?";
|
|
165
|
+
args.push(Math.max(0, Math.floor(opts.offset)));
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
else if (opts.offset) {
|
|
169
|
+
sql += " LIMIT -1 OFFSET ?";
|
|
170
|
+
args.push(Math.max(0, Math.floor(opts.offset)));
|
|
145
171
|
}
|
|
146
172
|
return this.db.prepare(sql).all(...args).map(rowToClip);
|
|
147
173
|
}
|
|
@@ -313,6 +339,7 @@ function rowToClip(row) {
|
|
|
313
339
|
}
|
|
314
340
|
function emptyTags() {
|
|
315
341
|
return {
|
|
342
|
+
content_type: [],
|
|
316
343
|
subject: [],
|
|
317
344
|
action: [],
|
|
318
345
|
emotion: [],
|