@mevdragon/vidfarm-devcli 0.18.0 → 0.19.0
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 +166 -0
- package/.agents/skills/editor-capabilities/references/re-theme-walkthrough.md +58 -0
- package/.agents/skills/vidfarm-media/SKILL.md +43 -4
- package/SKILL.director.md +190 -18
- package/SKILL.platform.md +8 -4
- package/demo/dist/app.css +1 -1
- package/demo/dist/app.js +77 -75
- package/demo/dist/favicon.ico +0 -0
- package/dist/src/app.js +3031 -300
- package/dist/src/cli.js +1549 -69
- package/dist/src/config.js +7 -0
- package/dist/src/devcli/clip-store.js +29 -2
- package/dist/src/devcli/clips.js +55 -9
- package/dist/src/devcli/composition-edit.js +658 -8
- package/dist/src/devcli/local-backend.js +123 -0
- package/dist/src/devcli/migrate-local.js +140 -0
- package/dist/src/devcli/sync.js +311 -0
- package/dist/src/devcli/timeline-edit.js +490 -0
- package/dist/src/editor-chat.js +56 -17
- 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 +995 -0
- package/dist/src/frontend/homepage-view.js +3 -3
- package/dist/src/frontend/template-editor-chat.js +28 -22
- package/dist/src/landing-page.js +24 -7
- package/dist/src/page-shell.js +26 -2
- package/dist/src/primitive-registry.js +409 -4
- package/dist/src/reskin/agency-page.js +1 -1
- package/dist/src/reskin/calendar-page.js +2 -1
- package/dist/src/reskin/chat-page.js +420 -85
- package/dist/src/reskin/discover-page.js +731 -39
- package/dist/src/reskin/document.js +1311 -387
- package/dist/src/reskin/help-page.js +1 -0
- package/dist/src/reskin/inpaint-clipper-page.js +649 -0
- package/dist/src/reskin/inpaint-page.js +2459 -446
- package/dist/src/reskin/inpaint-video-page.js +1339 -0
- package/dist/src/reskin/library-page.js +1168 -228
- package/dist/src/reskin/login-page.js +6 -6
- package/dist/src/reskin/pricing-page.js +2 -0
- package/dist/src/reskin/settings-page.js +55 -10
- package/dist/src/reskin/theme.js +365 -20
- package/dist/src/services/billing.js +4 -0
- package/dist/src/services/clip-curation/gemini.js +5 -0
- package/dist/src/services/clip-curation/hunt.js +81 -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 +42 -1
- package/dist/src/services/clip-search.js +43 -13
- package/dist/src/services/file-directory.js +117 -0
- package/dist/src/services/hyperframes.js +283 -3
- package/dist/src/services/serverless-records.js +43 -0
- package/dist/src/services/storage.js +47 -2
- 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/public/assets/placeholders/scene-placeholder.png +0 -0
- package/src/assets/favicon.ico +0 -0
- package/src/assets/logo-vidfarm.png +0 -0
package/dist/src/config.js
CHANGED
|
@@ -101,6 +101,13 @@ const schema = z.object({
|
|
|
101
101
|
RAPIDAPI_REMOVE_BACKGROUND_URL: z.string().url().default("https://remove-background18.p.rapidapi.com/public/remove-background/url"),
|
|
102
102
|
RAPIDAPI_REMOVE_BACKGROUND_HOST: z.string().default("remove-background18.p.rapidapi.com"),
|
|
103
103
|
RAPIDAPI_REMOVE_BACKGROUND_COST_USD: z.coerce.number().min(0).default(0.005),
|
|
104
|
+
// Local sharp chroma-key background removal (no third-party API) — a small
|
|
105
|
+
// compute/convenience wallet fee, far cheaper than the RapidAPI matting above.
|
|
106
|
+
GREENSCREEN_CHROMA_KEY_COST_USD: z.coerce.number().min(0).default(0.002),
|
|
107
|
+
// Two-step "create media overlay": BYOK AI image gen (provider cost rides the
|
|
108
|
+
// caller's own key) + local chroma key. This fee bills only the platform
|
|
109
|
+
// convenience/compute leg of the combined primitive.
|
|
110
|
+
MEDIA_OVERLAY_COST_USD: z.coerce.number().min(0).default(0.008),
|
|
104
111
|
SUPERAGENCY_KEY: z.string().optional(),
|
|
105
112
|
ARTIFACT_SIGNING_SECRET: z.string().optional(),
|
|
106
113
|
HYPERFRAMES_LAMBDA_STACK_NAME: z.string().default("hyperframes-vidfarm-prod"),
|
|
@@ -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: [],
|
package/dist/src/devcli/clips.js
CHANGED
|
@@ -12,7 +12,7 @@ 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
17
|
export const CLIPS_HELP = `vidfarm raws — build & search a local raws library (local ffmpeg + local agent by default)
|
|
18
18
|
|
|
@@ -54,9 +54,14 @@ export const CLIPS_HELP = `vidfarm raws — build & search a local raws library
|
|
|
54
54
|
|
|
55
55
|
raws list List raws in the library
|
|
56
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)
|
|
@@ -125,10 +130,13 @@ 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" },
|
|
131
138
|
tracer: { type: "string" },
|
|
139
|
+
folder: { type: "string" },
|
|
132
140
|
"api-url": { type: "string" },
|
|
133
141
|
"api-key": { type: "string" },
|
|
134
142
|
"dry-run": { type: "boolean" },
|
|
@@ -153,16 +161,22 @@ async function runScan(argv) {
|
|
|
153
161
|
const rawWindows = values.range?.length
|
|
154
162
|
? parseTimeRanges(values.range)
|
|
155
163
|
: parsedPrompt.windows;
|
|
156
|
-
|
|
164
|
+
// Explicit flags / prompt hints win; otherwise fall back to the same
|
|
165
|
+
// unspecified-prompt product defaults (5–15s clips, vertical 9:16, avoid text,
|
|
166
|
+
// ~10 clips / 10 min). Opt out with `--aspect original` / `--allow-text`.
|
|
167
|
+
const explicitBand = values.duration
|
|
157
168
|
? resolveDurationBand(Number(values.duration))
|
|
158
169
|
: parsedPrompt.target_duration_sec != null
|
|
159
170
|
? resolveDurationBand(parsedPrompt.target_duration_sec)
|
|
160
171
|
: null;
|
|
161
|
-
const
|
|
162
|
-
|
|
172
|
+
const durationBand = explicitBand ?? { ...DEFAULT_CLIP_DURATION_BAND };
|
|
173
|
+
const explicitAspect = values.aspect
|
|
174
|
+
? (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
175
|
: parsedPrompt.aspect;
|
|
176
|
+
const aspect = explicitAspect ?? DEFAULT_CLIP_ASPECT;
|
|
164
177
|
const cropFocus = normalizeCropFocus(values["crop-focus"]);
|
|
165
|
-
const avoidText = Boolean(values["no-text"]) || parsedPrompt.avoid_text;
|
|
178
|
+
const avoidText = values["allow-text"] ? false : (Boolean(values["no-text"]) || parsedPrompt.avoid_text || true);
|
|
179
|
+
const maxClips = values["max-clips"] ? Math.max(1, Math.round(Number(values["max-clips"]))) : parsedPrompt.max_clips ?? undefined;
|
|
166
180
|
const guidance = buildEffectiveGuidance({ contentPrompt: guidancePromptRaw, avoidText }) || undefined;
|
|
167
181
|
// ── BACKUP path: run the hunt on the deployed pipeline (--cloud) ──────────
|
|
168
182
|
if (values.cloud) {
|
|
@@ -175,13 +189,15 @@ async function runScan(argv) {
|
|
|
175
189
|
apiUrl: values["api-url"],
|
|
176
190
|
apiKey: values["api-key"],
|
|
177
191
|
tracer: values.tracer,
|
|
192
|
+
folder: values.folder,
|
|
178
193
|
prompt: guidancePromptRaw,
|
|
179
194
|
provider: apiProvider,
|
|
180
195
|
windows: rawWindows,
|
|
181
196
|
durationBand,
|
|
182
197
|
aspect,
|
|
183
198
|
cropFocus,
|
|
184
|
-
avoidText
|
|
199
|
+
avoidText,
|
|
200
|
+
maxClips
|
|
185
201
|
});
|
|
186
202
|
}
|
|
187
203
|
const videoArg = positionals[0];
|
|
@@ -224,6 +240,8 @@ async function runScan(argv) {
|
|
|
224
240
|
if (rawWindows.length && !windows.length) {
|
|
225
241
|
throw new Error("--range produced no usable window inside the video's duration.");
|
|
226
242
|
}
|
|
243
|
+
// Density/uniqueness target: ~10 clips / 10 min unless --max-clips / "N clips".
|
|
244
|
+
const targetClipCount = resolveTargetClipCount(effectiveHuntDurationSec(probe.duration_sec, windows), { max_clips: maxClips });
|
|
227
245
|
if (windows.length) {
|
|
228
246
|
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
247
|
}
|
|
@@ -234,6 +252,7 @@ async function runScan(argv) {
|
|
|
234
252
|
console.log(`[raws] cropping clips to ${aspect} (${cropFocus}).`);
|
|
235
253
|
if (avoidText)
|
|
236
254
|
console.log(`[raws] preferring scenes without on-screen text/captions (selection only — GhostCut is never run on the source).`);
|
|
255
|
+
console.log(`[raws] aiming for ~${targetClipCount} distinct raw${targetClipCount === 1 ? "" : "s"}${maxClips ? " (--max-clips)" : " (~10 / 10 min)"}.`);
|
|
237
256
|
console.log(`[raws] detecting scenes …`);
|
|
238
257
|
const detected = await detectScenes(videoPath, { durationSec: probe.duration_sec, windows, ...sceneOptions });
|
|
239
258
|
const scenes = fitScenesToDurationBand(detected, durationBand);
|
|
@@ -329,6 +348,7 @@ async function runScan(argv) {
|
|
|
329
348
|
probe,
|
|
330
349
|
windows,
|
|
331
350
|
durationBand,
|
|
351
|
+
targetClipCount,
|
|
332
352
|
sourceFilename: path.basename(videoPath),
|
|
333
353
|
concurrency,
|
|
334
354
|
onRefine: (r) => {
|
|
@@ -437,12 +457,16 @@ async function runScanCloud(input) {
|
|
|
437
457
|
prompt: input.prompt ?? "",
|
|
438
458
|
...(input.provider ? { provider: input.provider } : {}),
|
|
439
459
|
...(input.tracer ? { tracer: input.tracer } : {}),
|
|
460
|
+
...(input.folder ? { folder_path: input.folder } : {}),
|
|
440
461
|
hunt_spec: {
|
|
441
462
|
...(input.windows.length ? { windows: input.windows } : {}),
|
|
442
463
|
...(input.durationBand ? { duration_band: input.durationBand } : {}),
|
|
443
464
|
...(input.aspect ? { aspect: input.aspect } : {}),
|
|
444
465
|
crop_focus: input.cropFocus,
|
|
445
|
-
|
|
466
|
+
// Send explicitly (incl. false) so --allow-text can override the
|
|
467
|
+
// server-side avoid-text default.
|
|
468
|
+
avoid_text: input.avoidText,
|
|
469
|
+
...(input.maxClips ? { max_clips: input.maxClips } : {})
|
|
446
470
|
}
|
|
447
471
|
})
|
|
448
472
|
});
|
|
@@ -510,13 +534,20 @@ async function runList(argv) {
|
|
|
510
534
|
args: argv,
|
|
511
535
|
options: {
|
|
512
536
|
source: { type: "string" },
|
|
537
|
+
"content-type": { type: "string" },
|
|
538
|
+
offset: { type: "string" },
|
|
513
539
|
limit: { type: "string" },
|
|
514
540
|
json: { type: "boolean" },
|
|
515
541
|
home: { type: "string" }
|
|
516
542
|
}
|
|
517
543
|
});
|
|
518
544
|
const store = new ClipStore(values.home);
|
|
519
|
-
const clips = store.listClips({
|
|
545
|
+
const clips = store.listClips({
|
|
546
|
+
sourceVideoId: values.source,
|
|
547
|
+
contentTypes: parseContentTypes(values["content-type"]),
|
|
548
|
+
offset: values.offset ? clampInt(values.offset, 0, 0, 1_000_000) : undefined,
|
|
549
|
+
limit: clampInt(values.limit, 50, 1, 5000)
|
|
550
|
+
});
|
|
520
551
|
store.close();
|
|
521
552
|
if (values.json) {
|
|
522
553
|
console.log(JSON.stringify(clips, null, 2));
|
|
@@ -538,6 +569,7 @@ async function runSearch(argv) {
|
|
|
538
569
|
allowPositionals: true,
|
|
539
570
|
options: {
|
|
540
571
|
provider: { type: "string" },
|
|
572
|
+
"content-type": { type: "string" },
|
|
541
573
|
limit: { type: "string" },
|
|
542
574
|
json: { type: "boolean" },
|
|
543
575
|
"gemini-key": { type: "string" },
|
|
@@ -549,6 +581,7 @@ async function runSearch(argv) {
|
|
|
549
581
|
const query = positionals.join(" ").trim();
|
|
550
582
|
if (!query)
|
|
551
583
|
throw new Error('Usage: vidfarm raws search "<natural language>"');
|
|
584
|
+
const contentTypes = parseContentTypes(values["content-type"]);
|
|
552
585
|
const limit = clampInt(values.limit, 10, 1, 200);
|
|
553
586
|
const store = new ClipStore(values.home);
|
|
554
587
|
if (store.stats().clips === 0) {
|
|
@@ -582,7 +615,13 @@ async function runSearch(argv) {
|
|
|
582
615
|
console.warn(`[raws] no ${provider} key or local agent CLI — structured/keyword search only.`);
|
|
583
616
|
}
|
|
584
617
|
const t0 = timestamp();
|
|
585
|
-
|
|
618
|
+
// Over-fetch when a content-type filter is on so the post-filter still fills
|
|
619
|
+
// the requested limit (the tag lives in tags_json, not the search index).
|
|
620
|
+
const searchLimit = contentTypes.length ? Math.min(1000, limit * 5) : limit;
|
|
621
|
+
let hits = store.search({ criteria, queryEmbedding, limit: searchLimit });
|
|
622
|
+
if (contentTypes.length) {
|
|
623
|
+
hits = hits.filter((h) => (h.clip.tags.content_type ?? []).some((t) => contentTypes.includes(String(t).toLowerCase()))).slice(0, limit);
|
|
624
|
+
}
|
|
586
625
|
const ms = elapsedMs(t0);
|
|
587
626
|
store.close();
|
|
588
627
|
if (values.json) {
|
|
@@ -895,6 +934,13 @@ function normalizeProvider(value) {
|
|
|
895
934
|
return value;
|
|
896
935
|
throw new Error(`--provider must be gemini | openai | openrouter (got "${value}")`);
|
|
897
936
|
}
|
|
937
|
+
// Parse a comma-separated --content-type flag into lowercase shot-KIND tags for
|
|
938
|
+
// the tags.content_type filter (talking_head, b_roll, product_shot, …).
|
|
939
|
+
function parseContentTypes(value) {
|
|
940
|
+
if (!value)
|
|
941
|
+
return [];
|
|
942
|
+
return value.split(",").map((s) => s.trim().toLowerCase()).filter(Boolean);
|
|
943
|
+
}
|
|
898
944
|
function normalizeTier(value) {
|
|
899
945
|
if (value === "flash")
|
|
900
946
|
return "flash";
|