@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/devcli/clips.js
CHANGED
|
@@ -12,11 +12,11 @@ import { homedir } from "node:os";
|
|
|
12
12
|
import { parseArgs } from "node:util";
|
|
13
13
|
import { createInterface } from "node:readline/promises";
|
|
14
14
|
import { createIdV7 } from "../lib/ids.js";
|
|
15
|
-
import { buildEffectiveGuidance, ClipModelClient, detectLocalAgent, detectScenes, estimateScanCostFromScenes, fitScenesToDurationBand, formatCostEstimate, hasFfmpeg, LocalAgentClipClient, normalizeAspect, normalizeCropFocus, normalizeWindows, parseClipHuntPrompt, parseTimeRanges, probeVideo, resolveDurationBand, scanVideo, searchClips } from "../services/clip-curation/index.js";
|
|
15
|
+
import { buildEffectiveGuidance, ClipModelClient, DEFAULT_CLIP_ASPECT, DEFAULT_CLIP_DURATION_BAND, detectLocalAgent, detectScenes, effectiveHuntDurationSec, estimateScanCostFromScenes, fitScenesToDurationBand, formatCostEstimate, hasFfmpeg, LocalAgentClipClient, normalizeAspect, normalizeCropFocus, normalizeWindows, parseClipHuntPrompt, parseTimeRanges, probeVideo, resolveDurationBand, resolveTargetClipCount, scanVideo, searchClips } from "../services/clip-curation/index.js";
|
|
16
16
|
import { ClipStore } from "./clip-store.js";
|
|
17
|
-
export const CLIPS_HELP = `vidfarm
|
|
17
|
+
export const CLIPS_HELP = `vidfarm raws — build & search a local raws library (local ffmpeg + local agent by default)
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
raws scan <video-path> Mine a long-form video into tagged, searchable raws
|
|
20
20
|
--provider <p> agent | gemini | openai | openrouter
|
|
21
21
|
Default: agent when a local claude/codex CLI is installed
|
|
22
22
|
(your subscription, no API key), else gemini.
|
|
@@ -27,8 +27,8 @@ export const CLIPS_HELP = `vidfarm clips — build & search a local clip library
|
|
|
27
27
|
Inline hints are honored too: "between 12:30 and 15:45",
|
|
28
28
|
"30 sec clips", "vertical", "no captions".
|
|
29
29
|
--range "MM:SS-MM:SS" Only hunt inside this source range (repeatable / comma-separated)
|
|
30
|
-
--duration <sec> Target
|
|
31
|
-
--aspect <a> Crop
|
|
30
|
+
--duration <sec> Target raw length as a SOFT band (10→5-20s, 30→20-40s, …)
|
|
31
|
+
--aspect <a> Crop raws to 9:16 | 16:9 | 4:3 | 1:1 (or vertical/horizontal/square)
|
|
32
32
|
--crop-focus <f> center | top | bottom | left | right (default: center)
|
|
33
33
|
--no-text Prefer scenes WITHOUT burned-in captions/on-screen text
|
|
34
34
|
(scene selection — NEVER GhostCut on the long-form source)
|
|
@@ -52,25 +52,30 @@ export const CLIPS_HELP = `vidfarm clips — build & search a local clip library
|
|
|
52
52
|
--embed-provider gemini|openai --embed-key <key> Embeddings for agent/openrouter scans
|
|
53
53
|
--home <dir> Library dir (default: ~/.vidfarm or VIDFARM_HOME)
|
|
54
54
|
|
|
55
|
-
|
|
56
|
-
--source <id> Only
|
|
55
|
+
raws list List raws in the library
|
|
56
|
+
--source <id> Only raws from one source video
|
|
57
|
+
--content-type <a,b> Only raws whose shot-KIND tag matches (comma-separated,
|
|
58
|
+
exact): talking_head, b_roll, product_shot, screen_recording,
|
|
59
|
+
demo, reaction, interview, establishing, lifestyle, text_graphic
|
|
60
|
+
--offset <n> Skip the first N (paginate with --limit)
|
|
57
61
|
--limit <n> | --json
|
|
58
62
|
|
|
59
|
-
|
|
63
|
+
raws search "<natural language>" Hybrid structured + semantic search
|
|
64
|
+
--content-type <a,b> Post-filter hits by shot-KIND tag (see \`raws list\`)
|
|
60
65
|
--limit <n> | --json | --gemini-key <key>
|
|
61
66
|
|
|
62
|
-
|
|
67
|
+
raws match "<text>" Top library raws to REPLACE a scene (vector search)
|
|
63
68
|
--criteria '<json>' Match a structured SplicingCriteria instead of text
|
|
64
69
|
--scenes <annotations.json> Batch: match every scene of a decomposed video
|
|
65
70
|
(array of scene annotations, or {annotations|scenes:[…]})
|
|
66
71
|
--limit <n> | --json
|
|
67
72
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
73
|
+
raws preset list List built-in + saved presets
|
|
74
|
+
raws preset run <name> Run a preset query [--limit <n>] [--json]
|
|
75
|
+
raws preset save <name> --from-query '<json | natural language>'
|
|
71
76
|
|
|
72
|
-
|
|
73
|
-
|
|
77
|
+
raws sources List scanned source videos
|
|
78
|
+
raws export <raw-ids...> --to <dir> Copy raw mp4s (+ metadata.json) out
|
|
74
79
|
`;
|
|
75
80
|
export async function runClipsCommand(argv) {
|
|
76
81
|
const sub = argv[0];
|
|
@@ -96,7 +101,7 @@ export async function runClipsCommand(argv) {
|
|
|
96
101
|
case "-h":
|
|
97
102
|
return runOverview();
|
|
98
103
|
default:
|
|
99
|
-
console.error(`Unknown
|
|
104
|
+
console.error(`Unknown raws subcommand: ${sub}\n`);
|
|
100
105
|
console.log(CLIPS_HELP);
|
|
101
106
|
process.exitCode = 1;
|
|
102
107
|
}
|
|
@@ -106,8 +111,8 @@ function runOverview() {
|
|
|
106
111
|
const store = new ClipStore();
|
|
107
112
|
const stats = store.stats();
|
|
108
113
|
store.close();
|
|
109
|
-
console.log(`vidfarm
|
|
110
|
-
console.log(` ${stats.clips}
|
|
114
|
+
console.log(`vidfarm raws — library at ${resolveHome()}`);
|
|
115
|
+
console.log(` ${stats.clips} raws · ${stats.sources} source videos · ${stats.presets} presets\n`);
|
|
111
116
|
console.log(CLIPS_HELP);
|
|
112
117
|
}
|
|
113
118
|
// ── clips scan ──────────────────────────────────────────────────────────────
|
|
@@ -125,6 +130,8 @@ async function runScan(argv) {
|
|
|
125
130
|
aspect: { type: "string" },
|
|
126
131
|
"crop-focus": { type: "string" },
|
|
127
132
|
"no-text": { type: "boolean" },
|
|
133
|
+
"allow-text": { type: "boolean" },
|
|
134
|
+
"max-clips": { type: "string" },
|
|
128
135
|
"no-refine": { type: "boolean" },
|
|
129
136
|
cloud: { type: "boolean" },
|
|
130
137
|
url: { type: "string" },
|
|
@@ -153,16 +160,22 @@ async function runScan(argv) {
|
|
|
153
160
|
const rawWindows = values.range?.length
|
|
154
161
|
? parseTimeRanges(values.range)
|
|
155
162
|
: parsedPrompt.windows;
|
|
156
|
-
|
|
163
|
+
// Explicit flags / prompt hints win; otherwise fall back to the same
|
|
164
|
+
// unspecified-prompt product defaults (5–15s clips, vertical 9:16, avoid text,
|
|
165
|
+
// ~10 clips / 10 min). Opt out with `--aspect original` / `--allow-text`.
|
|
166
|
+
const explicitBand = values.duration
|
|
157
167
|
? resolveDurationBand(Number(values.duration))
|
|
158
168
|
: parsedPrompt.target_duration_sec != null
|
|
159
169
|
? resolveDurationBand(parsedPrompt.target_duration_sec)
|
|
160
170
|
: null;
|
|
161
|
-
const
|
|
162
|
-
|
|
171
|
+
const durationBand = explicitBand ?? { ...DEFAULT_CLIP_DURATION_BAND };
|
|
172
|
+
const explicitAspect = values.aspect
|
|
173
|
+
? (normalizeAspect(values.aspect) ?? (() => { throw new Error(`--aspect must be 9:16 | 16:9 | 4:3 | 1:1 | original (or vertical/horizontal/square), got "${values.aspect}"`); })())
|
|
163
174
|
: parsedPrompt.aspect;
|
|
175
|
+
const aspect = explicitAspect ?? DEFAULT_CLIP_ASPECT;
|
|
164
176
|
const cropFocus = normalizeCropFocus(values["crop-focus"]);
|
|
165
|
-
const avoidText = Boolean(values["no-text"]) || parsedPrompt.avoid_text;
|
|
177
|
+
const avoidText = values["allow-text"] ? false : (Boolean(values["no-text"]) || parsedPrompt.avoid_text || true);
|
|
178
|
+
const maxClips = values["max-clips"] ? Math.max(1, Math.round(Number(values["max-clips"]))) : parsedPrompt.max_clips ?? undefined;
|
|
166
179
|
const guidance = buildEffectiveGuidance({ contentPrompt: guidancePromptRaw, avoidText }) || undefined;
|
|
167
180
|
// ── BACKUP path: run the hunt on the deployed pipeline (--cloud) ──────────
|
|
168
181
|
if (values.cloud) {
|
|
@@ -181,17 +194,18 @@ async function runScan(argv) {
|
|
|
181
194
|
durationBand,
|
|
182
195
|
aspect,
|
|
183
196
|
cropFocus,
|
|
184
|
-
avoidText
|
|
197
|
+
avoidText,
|
|
198
|
+
maxClips
|
|
185
199
|
});
|
|
186
200
|
}
|
|
187
201
|
const videoArg = positionals[0];
|
|
188
202
|
if (!videoArg)
|
|
189
|
-
throw new Error("Usage: vidfarm
|
|
203
|
+
throw new Error("Usage: vidfarm raws scan <video-path> [--range MM:SS-MM:SS] [--duration 30] [--aspect 9:16] [--dry-run]");
|
|
190
204
|
const videoPath = path.resolve(process.cwd(), videoArg);
|
|
191
205
|
if (!existsSync(videoPath))
|
|
192
206
|
throw new Error(`No such video: ${videoPath}`);
|
|
193
207
|
if (!(await hasFfmpeg())) {
|
|
194
|
-
throw new Error("ffmpeg is required for `
|
|
208
|
+
throw new Error("ffmpeg is required for `raws scan` but was not found (bundled ffmpeg-static missing and none on PATH).");
|
|
195
209
|
}
|
|
196
210
|
// ── Model resolution: LOCAL-FIRST ─────────────────────────────────────────
|
|
197
211
|
// 1. --provider agent (or no --provider + a local claude/codex CLI installed)
|
|
@@ -217,24 +231,27 @@ async function runScan(argv) {
|
|
|
217
231
|
const embedding = provider === "agent"
|
|
218
232
|
? resolveAgentEmbeddingConfig(values)
|
|
219
233
|
: apiKey ? resolveEmbeddingConfig(provider, apiKey, values) : null;
|
|
220
|
-
console.log(`[
|
|
234
|
+
console.log(`[raws] probing ${path.basename(videoPath)} …`);
|
|
221
235
|
const probe = await probeVideo(videoPath);
|
|
222
|
-
console.log(`[
|
|
236
|
+
console.log(`[raws] ${fmtDuration(probe.duration_sec)} · ${probe.width ?? "?"}x${probe.height ?? "?"} · ${probe.codec ?? "?"}`);
|
|
223
237
|
const windows = normalizeWindows(rawWindows, probe.duration_sec);
|
|
224
238
|
if (rawWindows.length && !windows.length) {
|
|
225
239
|
throw new Error("--range produced no usable window inside the video's duration.");
|
|
226
240
|
}
|
|
241
|
+
// Density/uniqueness target: ~10 clips / 10 min unless --max-clips / "N clips".
|
|
242
|
+
const targetClipCount = resolveTargetClipCount(effectiveHuntDurationSec(probe.duration_sec, windows), { max_clips: maxClips });
|
|
227
243
|
if (windows.length) {
|
|
228
|
-
console.log(`[
|
|
244
|
+
console.log(`[raws] hunting only ${windows.map((w) => `${fmtClock(w.start_sec)}–${fmtClock(w.end_sec)}`).join(", ")} (${fmtDuration(windows.reduce((a, w) => a + w.end_sec - w.start_sec, 0))} of ${fmtDuration(probe.duration_sec)}).`);
|
|
229
245
|
}
|
|
230
246
|
if (durationBand) {
|
|
231
|
-
console.log(`[
|
|
247
|
+
console.log(`[raws] target clip length ~${durationBand.target_sec}s (soft range ${durationBand.min_sec}–${durationBand.max_sec}s).`);
|
|
232
248
|
}
|
|
233
249
|
if (aspect && aspect !== "original")
|
|
234
|
-
console.log(`[
|
|
250
|
+
console.log(`[raws] cropping clips to ${aspect} (${cropFocus}).`);
|
|
235
251
|
if (avoidText)
|
|
236
|
-
console.log(`[
|
|
237
|
-
console.log(`[
|
|
252
|
+
console.log(`[raws] preferring scenes without on-screen text/captions (selection only — GhostCut is never run on the source).`);
|
|
253
|
+
console.log(`[raws] aiming for ~${targetClipCount} distinct raw${targetClipCount === 1 ? "" : "s"}${maxClips ? " (--max-clips)" : " (~10 / 10 min)"}.`);
|
|
254
|
+
console.log(`[raws] detecting scenes …`);
|
|
238
255
|
const detected = await detectScenes(videoPath, { durationSec: probe.duration_sec, windows, ...sceneOptions });
|
|
239
256
|
const scenes = fitScenesToDurationBand(detected, durationBand);
|
|
240
257
|
if (scenes.length === 0)
|
|
@@ -247,13 +264,13 @@ async function runScan(argv) {
|
|
|
247
264
|
embedding,
|
|
248
265
|
tier
|
|
249
266
|
});
|
|
250
|
-
console.log(`[
|
|
267
|
+
console.log(`[raws] evaluating scenes with your local ${client.tagModelId} subscription (no API cost).`);
|
|
251
268
|
}
|
|
252
269
|
else if (apiKey) {
|
|
253
270
|
client = new ClipModelClient({ provider, apiKey, tier, embedding });
|
|
254
271
|
}
|
|
255
272
|
if (client && !client.canEmbed) {
|
|
256
|
-
console.warn(`[
|
|
273
|
+
console.warn(`[raws] no embedding provider for "${provider}" — raws will be structured-searchable only.`);
|
|
257
274
|
console.warn(` Add --embed-provider gemini|openai --embed-key <key> for semantic search.`);
|
|
258
275
|
}
|
|
259
276
|
// Cost estimate: local-agent scans ride the user's subscription, so only the
|
|
@@ -279,7 +296,7 @@ async function runScan(argv) {
|
|
|
279
296
|
console.log(` Guidance: "${guidance.replace(/\n/g, " · ")}"`);
|
|
280
297
|
console.log("");
|
|
281
298
|
if (values["dry-run"]) {
|
|
282
|
-
console.log("[
|
|
299
|
+
console.log("[raws] --dry-run: not scanning. Re-run without --dry-run to build the library.");
|
|
283
300
|
return;
|
|
284
301
|
}
|
|
285
302
|
// Actually scanning now — an evaluator is required.
|
|
@@ -293,7 +310,7 @@ async function runScan(argv) {
|
|
|
293
310
|
if (estimate && estimate.usd > 1 && !values.yes) {
|
|
294
311
|
const ok = await confirm(`This scan is estimated at $${estimate.usd.toFixed(2)}. Proceed? [y/N] `);
|
|
295
312
|
if (!ok) {
|
|
296
|
-
console.log("[
|
|
313
|
+
console.log("[raws] aborted.");
|
|
297
314
|
return;
|
|
298
315
|
}
|
|
299
316
|
}
|
|
@@ -301,7 +318,7 @@ async function runScan(argv) {
|
|
|
301
318
|
if (activeClient.embeddingModelId) {
|
|
302
319
|
const existing = store.getLibraryEmbeddingModel();
|
|
303
320
|
if (existing && existing !== activeClient.embeddingModelId) {
|
|
304
|
-
console.warn(`[
|
|
321
|
+
console.warn(`[raws] WARNING: library was built with "${existing}" embeddings but this scan uses "${activeClient.embeddingModelId}". Semantic search across them will be unreliable.`);
|
|
305
322
|
}
|
|
306
323
|
else {
|
|
307
324
|
store.rememberEmbeddingModel(activeClient.embeddingModelId);
|
|
@@ -319,7 +336,7 @@ async function runScan(argv) {
|
|
|
319
336
|
reencodeClips: true,
|
|
320
337
|
crop: aspect && aspect !== "original" ? { aspect, focus: cropFocus } : undefined
|
|
321
338
|
};
|
|
322
|
-
console.log(`[
|
|
339
|
+
console.log(`[raws] scanning ${scenes.length} scenes (${provider}/${activeClient.tagModelId}, ${concurrency}x) …`);
|
|
323
340
|
let errors = 0;
|
|
324
341
|
const started = timestamp();
|
|
325
342
|
const result = await scanVideo({
|
|
@@ -329,14 +346,15 @@ async function runScan(argv) {
|
|
|
329
346
|
probe,
|
|
330
347
|
windows,
|
|
331
348
|
durationBand,
|
|
349
|
+
targetClipCount,
|
|
332
350
|
sourceFilename: path.basename(videoPath),
|
|
333
351
|
concurrency,
|
|
334
352
|
onRefine: (r) => {
|
|
335
353
|
if (r.refined) {
|
|
336
|
-
console.log(`[
|
|
354
|
+
console.log(`[raws] guided segmentation: ${scenes.length} → ${r.kept} raws (dropped ${r.dropped}, merged ${r.merged}).`);
|
|
337
355
|
}
|
|
338
356
|
else if (r.note) {
|
|
339
|
-
console.log(`[
|
|
357
|
+
console.log(`[raws] guided segmentation skipped: ${r.note}.`);
|
|
340
358
|
}
|
|
341
359
|
},
|
|
342
360
|
onClip: (clip, i, total) => {
|
|
@@ -365,10 +383,10 @@ async function runScan(argv) {
|
|
|
365
383
|
/* ignore */
|
|
366
384
|
}
|
|
367
385
|
store.close();
|
|
368
|
-
console.log(`\n[
|
|
369
|
-
console.log(`
|
|
370
|
-
console.log(` db
|
|
371
|
-
console.log(` try: vidfarm
|
|
386
|
+
console.log(`\n[raws] done in ${elapsed(started)}: ${result.clips.length} raws${errors ? `, ${errors} scene(s) failed` : ""}.`);
|
|
387
|
+
console.log(` raws → ${store.paths.clipsDir}`);
|
|
388
|
+
console.log(` db → ${store.paths.db}`);
|
|
389
|
+
console.log(` try: vidfarm raws search "…" | vidfarm raws preset run "funny reaction gifs"`);
|
|
372
390
|
}
|
|
373
391
|
// ── clips scan --cloud (explicit backup: the deployed pipeline) ─────────────
|
|
374
392
|
// Uploads the local file into the user's temp folder (30-day TTL, auto-removed)
|
|
@@ -384,13 +402,13 @@ async function runScanCloud(input) {
|
|
|
384
402
|
let fileName;
|
|
385
403
|
if (!input.sourceUrl) {
|
|
386
404
|
if (!input.videoArg)
|
|
387
|
-
throw new Error("Usage: vidfarm
|
|
405
|
+
throw new Error("Usage: vidfarm raws scan --cloud <video-path> | --cloud --url <video-url>");
|
|
388
406
|
const videoPath = path.resolve(process.cwd(), input.videoArg);
|
|
389
407
|
if (!existsSync(videoPath))
|
|
390
408
|
throw new Error(`No such video: ${videoPath}`);
|
|
391
409
|
fileName = path.basename(videoPath);
|
|
392
410
|
const sizeBytes = statSync(videoPath).size;
|
|
393
|
-
console.log(`[
|
|
411
|
+
console.log(`[raws] uploading ${fileName} (${(sizeBytes / (1024 * 1024)).toFixed(1)} MB) to your temp folder (auto-deletes in 30 days) …`);
|
|
394
412
|
const presignRes = await fetch(`${baseUrl}/api/v1/user/me/temporary-files/presign`, {
|
|
395
413
|
method: "POST",
|
|
396
414
|
headers,
|
|
@@ -428,8 +446,8 @@ async function runScanCloud(input) {
|
|
|
428
446
|
}
|
|
429
447
|
tempFileId = presign.file_id;
|
|
430
448
|
}
|
|
431
|
-
console.log(`[
|
|
432
|
-
const scanRes = await fetch(`${baseUrl}/
|
|
449
|
+
console.log(`[raws] starting cloud hunt …`);
|
|
450
|
+
const scanRes = await fetch(`${baseUrl}/raws/scan`, {
|
|
433
451
|
method: "POST",
|
|
434
452
|
headers,
|
|
435
453
|
body: JSON.stringify({
|
|
@@ -442,7 +460,10 @@ async function runScanCloud(input) {
|
|
|
442
460
|
...(input.durationBand ? { duration_band: input.durationBand } : {}),
|
|
443
461
|
...(input.aspect ? { aspect: input.aspect } : {}),
|
|
444
462
|
crop_focus: input.cropFocus,
|
|
445
|
-
|
|
463
|
+
// Send explicitly (incl. false) so --allow-text can override the
|
|
464
|
+
// server-side avoid-text default.
|
|
465
|
+
avoid_text: input.avoidText,
|
|
466
|
+
...(input.maxClips ? { max_clips: input.maxClips } : {})
|
|
446
467
|
}
|
|
447
468
|
})
|
|
448
469
|
});
|
|
@@ -450,28 +471,28 @@ async function runScanCloud(input) {
|
|
|
450
471
|
if (!scanRes.ok || !scan.scan_id) {
|
|
451
472
|
throw new Error(`Cloud scan failed to start (${scanRes.status}): ${scan.error ?? "unexpected response"}`);
|
|
452
473
|
}
|
|
453
|
-
console.log(`[
|
|
474
|
+
console.log(`[raws] scan ${scan.scan_id} ${scan.status ?? "queued"}${scan.tracer ? ` (tracer ${scan.tracer})` : ""}.`);
|
|
454
475
|
if (scan.compute_estimate?.estimated_charge_usd != null) {
|
|
455
|
-
console.log(`[
|
|
476
|
+
console.log(`[raws] estimated AWS compute charge ~$${scan.compute_estimate.estimated_charge_usd.toFixed(4)} (AI runs on your saved provider keys).`);
|
|
456
477
|
}
|
|
457
478
|
// Async by design — poll until the pipeline finishes.
|
|
458
479
|
const started = timestamp();
|
|
459
480
|
for (;;) {
|
|
460
481
|
await new Promise((r) => setTimeout(r, 5000));
|
|
461
|
-
const pollRes = await fetch(`${baseUrl}/
|
|
482
|
+
const pollRes = await fetch(`${baseUrl}/raws/scan/${encodeURIComponent(scan.scan_id)}`, { headers });
|
|
462
483
|
if (!pollRes.ok)
|
|
463
484
|
continue;
|
|
464
485
|
const poll = await pollRes.json();
|
|
465
486
|
const status = poll.source?.status ?? poll.scan?.status ?? "running";
|
|
466
487
|
if (status === "complete") {
|
|
467
|
-
console.log(`\n[
|
|
468
|
-
console.log(` browse → ${baseUrl}/
|
|
488
|
+
console.log(`\n[raws] cloud hunt done in ${elapsed(started)}: ${poll.source?.clip_count ?? 0} raws.`);
|
|
489
|
+
console.log(` browse → ${baseUrl}/library/raws`);
|
|
469
490
|
return;
|
|
470
491
|
}
|
|
471
492
|
if (status === "failed") {
|
|
472
493
|
throw new Error(`Cloud hunt failed: ${poll.scan?.error ?? "unknown error"}`);
|
|
473
494
|
}
|
|
474
|
-
process.stdout.write(`\r[
|
|
495
|
+
process.stdout.write(`\r[raws] ${status} … ${elapsed(started)} `);
|
|
475
496
|
}
|
|
476
497
|
}
|
|
477
498
|
/**
|
|
@@ -510,26 +531,33 @@ async function runList(argv) {
|
|
|
510
531
|
args: argv,
|
|
511
532
|
options: {
|
|
512
533
|
source: { type: "string" },
|
|
534
|
+
"content-type": { type: "string" },
|
|
535
|
+
offset: { type: "string" },
|
|
513
536
|
limit: { type: "string" },
|
|
514
537
|
json: { type: "boolean" },
|
|
515
538
|
home: { type: "string" }
|
|
516
539
|
}
|
|
517
540
|
});
|
|
518
541
|
const store = new ClipStore(values.home);
|
|
519
|
-
const clips = store.listClips({
|
|
542
|
+
const clips = store.listClips({
|
|
543
|
+
sourceVideoId: values.source,
|
|
544
|
+
contentTypes: parseContentTypes(values["content-type"]),
|
|
545
|
+
offset: values.offset ? clampInt(values.offset, 0, 0, 1_000_000) : undefined,
|
|
546
|
+
limit: clampInt(values.limit, 50, 1, 5000)
|
|
547
|
+
});
|
|
520
548
|
store.close();
|
|
521
549
|
if (values.json) {
|
|
522
550
|
console.log(JSON.stringify(clips, null, 2));
|
|
523
551
|
return;
|
|
524
552
|
}
|
|
525
553
|
if (clips.length === 0) {
|
|
526
|
-
console.log("No
|
|
554
|
+
console.log("No raws yet. Run: vidfarm raws scan <video-path>");
|
|
527
555
|
return;
|
|
528
556
|
}
|
|
529
557
|
for (const clip of clips) {
|
|
530
558
|
console.log(`${clip.clip_id.slice(0, 14)} ${fmtRange(clip)} ${clip.source_filename} ${summarizeTags(clip)}`);
|
|
531
559
|
}
|
|
532
|
-
console.log(`\n${clips.length}
|
|
560
|
+
console.log(`\n${clips.length} raws.`);
|
|
533
561
|
}
|
|
534
562
|
// ── clips search ────────────────────────────────────────────────────────────
|
|
535
563
|
async function runSearch(argv) {
|
|
@@ -538,6 +566,7 @@ async function runSearch(argv) {
|
|
|
538
566
|
allowPositionals: true,
|
|
539
567
|
options: {
|
|
540
568
|
provider: { type: "string" },
|
|
569
|
+
"content-type": { type: "string" },
|
|
541
570
|
limit: { type: "string" },
|
|
542
571
|
json: { type: "boolean" },
|
|
543
572
|
"gemini-key": { type: "string" },
|
|
@@ -548,12 +577,13 @@ async function runSearch(argv) {
|
|
|
548
577
|
});
|
|
549
578
|
const query = positionals.join(" ").trim();
|
|
550
579
|
if (!query)
|
|
551
|
-
throw new Error('Usage: vidfarm
|
|
580
|
+
throw new Error('Usage: vidfarm raws search "<natural language>"');
|
|
581
|
+
const contentTypes = parseContentTypes(values["content-type"]);
|
|
552
582
|
const limit = clampInt(values.limit, 10, 1, 200);
|
|
553
583
|
const store = new ClipStore(values.home);
|
|
554
584
|
if (store.stats().clips === 0) {
|
|
555
585
|
store.close();
|
|
556
|
-
console.log("Library is empty — scan a video first: vidfarm
|
|
586
|
+
console.log("Library is empty — scan a video first: vidfarm raws scan <video-path>");
|
|
557
587
|
return;
|
|
558
588
|
}
|
|
559
589
|
const provider = normalizeProvider(values.provider);
|
|
@@ -566,7 +596,7 @@ async function runSearch(argv) {
|
|
|
566
596
|
if (client.canEmbed) {
|
|
567
597
|
const libModel = store.getLibraryEmbeddingModel();
|
|
568
598
|
if (libModel && client.embeddingModelId && libModel !== client.embeddingModelId) {
|
|
569
|
-
console.warn(`[
|
|
599
|
+
console.warn(`[raws] note: library uses "${libModel}" embeddings but you're searching with "${client.embeddingModelId}" — semantic re-rank may be off; pass --provider to match.`);
|
|
570
600
|
}
|
|
571
601
|
queryEmbedding = await client.embedQuery(criteria.semantic_text ?? query);
|
|
572
602
|
}
|
|
@@ -575,14 +605,20 @@ async function runSearch(argv) {
|
|
|
575
605
|
// Local-first: no key, but a local agent CLI can still turn the natural-
|
|
576
606
|
// language query into a structured filter (vector re-rank needs a key).
|
|
577
607
|
const agent = new LocalAgentClipClient({});
|
|
578
|
-
console.log(`[
|
|
608
|
+
console.log(`[raws] structuring query with your local ${agent.tagModelId} …`);
|
|
579
609
|
criteria = await agent.naturalLanguageToCriteria(query);
|
|
580
610
|
}
|
|
581
611
|
else {
|
|
582
|
-
console.warn(`[
|
|
612
|
+
console.warn(`[raws] no ${provider} key or local agent CLI — structured/keyword search only.`);
|
|
583
613
|
}
|
|
584
614
|
const t0 = timestamp();
|
|
585
|
-
|
|
615
|
+
// Over-fetch when a content-type filter is on so the post-filter still fills
|
|
616
|
+
// the requested limit (the tag lives in tags_json, not the search index).
|
|
617
|
+
const searchLimit = contentTypes.length ? Math.min(1000, limit * 5) : limit;
|
|
618
|
+
let hits = store.search({ criteria, queryEmbedding, limit: searchLimit });
|
|
619
|
+
if (contentTypes.length) {
|
|
620
|
+
hits = hits.filter((h) => (h.clip.tags.content_type ?? []).some((t) => contentTypes.includes(String(t).toLowerCase()))).slice(0, limit);
|
|
621
|
+
}
|
|
586
622
|
const ms = elapsedMs(t0);
|
|
587
623
|
store.close();
|
|
588
624
|
if (values.json) {
|
|
@@ -627,7 +663,7 @@ async function runMatch(argv) {
|
|
|
627
663
|
const store = new ClipStore(values.home);
|
|
628
664
|
if (store.stats().clips === 0) {
|
|
629
665
|
store.close();
|
|
630
|
-
console.log("Library is empty — scan a video first: vidfarm
|
|
666
|
+
console.log("Library is empty — scan a video first: vidfarm raws scan <video-path>");
|
|
631
667
|
return;
|
|
632
668
|
}
|
|
633
669
|
// Embed the match query in the SAME space the library was built with.
|
|
@@ -636,7 +672,7 @@ async function runMatch(argv) {
|
|
|
636
672
|
const embedKey = resolveProviderKey(embedProvider, values, { optional: true });
|
|
637
673
|
const client = embedKey ? new ClipModelClient({ provider: embedProvider, apiKey: embedKey }) : null;
|
|
638
674
|
if (!client)
|
|
639
|
-
console.warn(`[
|
|
675
|
+
console.warn(`[raws] no ${embedProvider} key — structured match only (set the key for vector re-rank).`);
|
|
640
676
|
if (values.scenes) {
|
|
641
677
|
const filePath = path.resolve(process.cwd(), values.scenes);
|
|
642
678
|
if (!existsSync(filePath))
|
|
@@ -670,7 +706,7 @@ async function runMatch(argv) {
|
|
|
670
706
|
console.log(JSON.stringify(output, null, 2));
|
|
671
707
|
return;
|
|
672
708
|
}
|
|
673
|
-
console.log(`Matched ${scenes.length} scenes against ${all.length}
|
|
709
|
+
console.log(`Matched ${scenes.length} scenes against ${all.length} raws:`);
|
|
674
710
|
for (const o of output) {
|
|
675
711
|
const rep = o.replaceability ? ` [replaceability: ${o.replaceability}]` : "";
|
|
676
712
|
const dur = typeof o.duration === "number" ? ` (${o.duration.toFixed(1)}s)` : "";
|
|
@@ -699,7 +735,7 @@ async function runMatch(argv) {
|
|
|
699
735
|
text = positionals.join(" ").trim();
|
|
700
736
|
if (!text) {
|
|
701
737
|
store.close();
|
|
702
|
-
throw new Error('Usage: vidfarm
|
|
738
|
+
throw new Error('Usage: vidfarm raws match "<text>" | --criteria \'<json>\' | --scenes <annotations.json>');
|
|
703
739
|
}
|
|
704
740
|
criteria = { semantic_text: text };
|
|
705
741
|
}
|
|
@@ -724,7 +760,7 @@ async function runPreset(argv) {
|
|
|
724
760
|
return runPresetRun(rest);
|
|
725
761
|
if (action === "save")
|
|
726
762
|
return runPresetSave(rest);
|
|
727
|
-
throw new Error("Usage: vidfarm
|
|
763
|
+
throw new Error("Usage: vidfarm raws preset <list|run <name>|save <name> --from-query '<...>'>");
|
|
728
764
|
}
|
|
729
765
|
async function runPresetList(argv) {
|
|
730
766
|
const { values } = parseArgs({ args: argv, options: { json: { type: "boolean" }, home: { type: "string" } } });
|
|
@@ -756,12 +792,12 @@ async function runPresetRun(argv) {
|
|
|
756
792
|
});
|
|
757
793
|
const name = positionals.join(" ").trim();
|
|
758
794
|
if (!name)
|
|
759
|
-
throw new Error("Usage: vidfarm
|
|
795
|
+
throw new Error("Usage: vidfarm raws preset run <name>");
|
|
760
796
|
const store = new ClipStore(values.home);
|
|
761
797
|
const preset = store.findPreset(name);
|
|
762
798
|
if (!preset) {
|
|
763
799
|
store.close();
|
|
764
|
-
throw new Error(`No preset "${name}". Try: vidfarm
|
|
800
|
+
throw new Error(`No preset "${name}". Try: vidfarm raws preset list`);
|
|
765
801
|
}
|
|
766
802
|
const limit = clampInt(values.limit, 10, 1, 200);
|
|
767
803
|
// Presets carry semantic_text; embed it if we have a key for the re-rank.
|
|
@@ -800,7 +836,7 @@ async function runPresetSave(argv) {
|
|
|
800
836
|
const name = positionals.join(" ").trim();
|
|
801
837
|
const fromQuery = values["from-query"];
|
|
802
838
|
if (!name || !fromQuery) {
|
|
803
|
-
throw new Error("Usage: vidfarm
|
|
839
|
+
throw new Error("Usage: vidfarm raws preset save <name> --from-query '<json | natural language>'");
|
|
804
840
|
}
|
|
805
841
|
let criteria;
|
|
806
842
|
const asJson = tryParseJson(fromQuery);
|
|
@@ -857,7 +893,7 @@ async function runExport(argv) {
|
|
|
857
893
|
const ids = positionals;
|
|
858
894
|
const to = values.to;
|
|
859
895
|
if (ids.length === 0 || !to) {
|
|
860
|
-
throw new Error("Usage: vidfarm
|
|
896
|
+
throw new Error("Usage: vidfarm raws export <raw-ids...> --to <dir>");
|
|
861
897
|
}
|
|
862
898
|
const outDir = path.resolve(process.cwd(), to);
|
|
863
899
|
mkdirSync(outDir, { recursive: true });
|
|
@@ -885,7 +921,7 @@ async function runExport(argv) {
|
|
|
885
921
|
store.close();
|
|
886
922
|
const { writeFileSync } = await import("node:fs");
|
|
887
923
|
writeFileSync(path.join(outDir, "clips.metadata.json"), JSON.stringify(exported, null, 2));
|
|
888
|
-
console.log(`\nExported ${exported.length}
|
|
924
|
+
console.log(`\nExported ${exported.length} raws + clips.metadata.json → ${outDir}`);
|
|
889
925
|
}
|
|
890
926
|
// ── helpers ─────────────────────────────────────────────────────────────────
|
|
891
927
|
function normalizeProvider(value) {
|
|
@@ -895,6 +931,13 @@ function normalizeProvider(value) {
|
|
|
895
931
|
return value;
|
|
896
932
|
throw new Error(`--provider must be gemini | openai | openrouter (got "${value}")`);
|
|
897
933
|
}
|
|
934
|
+
// Parse a comma-separated --content-type flag into lowercase shot-KIND tags for
|
|
935
|
+
// the tags.content_type filter (talking_head, b_roll, product_shot, …).
|
|
936
|
+
function parseContentTypes(value) {
|
|
937
|
+
if (!value)
|
|
938
|
+
return [];
|
|
939
|
+
return value.split(",").map((s) => s.trim().toLowerCase()).filter(Boolean);
|
|
940
|
+
}
|
|
898
941
|
function normalizeTier(value) {
|
|
899
942
|
if (value === "flash")
|
|
900
943
|
return "flash";
|