@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.
Files changed (68) hide show
  1. package/.agents/skills/editor-capabilities/SKILL.md +166 -0
  2. package/.agents/skills/editor-capabilities/references/re-theme-walkthrough.md +58 -0
  3. package/.agents/skills/vidfarm-media/SKILL.md +43 -4
  4. package/SKILL.director.md +190 -18
  5. package/SKILL.platform.md +8 -4
  6. package/demo/dist/app.css +1 -1
  7. package/demo/dist/app.js +77 -75
  8. package/demo/dist/favicon.ico +0 -0
  9. package/dist/src/app.js +3031 -300
  10. package/dist/src/cli.js +1549 -69
  11. package/dist/src/config.js +7 -0
  12. package/dist/src/devcli/clip-store.js +29 -2
  13. package/dist/src/devcli/clips.js +55 -9
  14. package/dist/src/devcli/composition-edit.js +658 -8
  15. package/dist/src/devcli/local-backend.js +123 -0
  16. package/dist/src/devcli/migrate-local.js +140 -0
  17. package/dist/src/devcli/sync.js +311 -0
  18. package/dist/src/devcli/timeline-edit.js +490 -0
  19. package/dist/src/editor-chat.js +56 -17
  20. package/dist/src/frontend/discover-client.js +130 -0
  21. package/dist/src/frontend/discover-store.js +23 -0
  22. package/dist/src/frontend/file-directory.js +995 -0
  23. package/dist/src/frontend/homepage-view.js +3 -3
  24. package/dist/src/frontend/template-editor-chat.js +28 -22
  25. package/dist/src/landing-page.js +24 -7
  26. package/dist/src/page-shell.js +26 -2
  27. package/dist/src/primitive-registry.js +409 -4
  28. package/dist/src/reskin/agency-page.js +1 -1
  29. package/dist/src/reskin/calendar-page.js +2 -1
  30. package/dist/src/reskin/chat-page.js +420 -85
  31. package/dist/src/reskin/discover-page.js +731 -39
  32. package/dist/src/reskin/document.js +1311 -387
  33. package/dist/src/reskin/help-page.js +1 -0
  34. package/dist/src/reskin/inpaint-clipper-page.js +649 -0
  35. package/dist/src/reskin/inpaint-page.js +2459 -446
  36. package/dist/src/reskin/inpaint-video-page.js +1339 -0
  37. package/dist/src/reskin/library-page.js +1168 -228
  38. package/dist/src/reskin/login-page.js +6 -6
  39. package/dist/src/reskin/pricing-page.js +2 -0
  40. package/dist/src/reskin/settings-page.js +55 -10
  41. package/dist/src/reskin/theme.js +365 -20
  42. package/dist/src/services/billing.js +4 -0
  43. package/dist/src/services/clip-curation/gemini.js +5 -0
  44. package/dist/src/services/clip-curation/hunt.js +81 -1
  45. package/dist/src/services/clip-curation/index.js +2 -1
  46. package/dist/src/services/clip-curation/local-agent.js +4 -3
  47. package/dist/src/services/clip-curation/media-select.js +85 -0
  48. package/dist/src/services/clip-curation/query.js +5 -1
  49. package/dist/src/services/clip-curation/refine.js +50 -20
  50. package/dist/src/services/clip-curation/scan.js +10 -3
  51. package/dist/src/services/clip-curation/taxonomy.js +3 -1
  52. package/dist/src/services/clip-curation/taxonomy.v1.json +13 -1
  53. package/dist/src/services/clip-records.js +42 -1
  54. package/dist/src/services/clip-search.js +43 -13
  55. package/dist/src/services/file-directory.js +117 -0
  56. package/dist/src/services/hyperframes.js +283 -3
  57. package/dist/src/services/serverless-records.js +43 -0
  58. package/dist/src/services/storage.js +47 -2
  59. package/dist/src/services/upstream.js +5 -5
  60. package/dist/src/template-editor-shell.js +16 -2
  61. package/package.json +1 -1
  62. package/public/assets/discover-client-app.js +1 -0
  63. package/public/assets/file-directory-app.js +2 -0
  64. package/public/assets/homepage-client-app.js +12 -12
  65. package/public/assets/page-runtime-client-app.js +24 -24
  66. package/public/assets/placeholders/scene-placeholder.png +0 -0
  67. package/src/assets/favicon.ico +0 -0
  68. package/src/assets/logo-vidfarm.png +0 -0
@@ -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
- /** All clips (metadata only), newest first, optionally scoped to a source video. */
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
- if (opts.limit) {
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: [],
@@ -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
- const durationBand = values.duration
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 aspect = values.aspect
162
- ? (normalizeAspect(values.aspect) ?? (() => { throw new Error(`--aspect must be 9:16 | 16:9 | 4:3 | 1:1 (or vertical/horizontal/square), got "${values.aspect}"`); })())
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
- ...(input.avoidText ? { avoid_text: true } : {})
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({ sourceVideoId: values.source, limit: clampInt(values.limit, 50, 1, 5000) });
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
- const hits = store.search({ criteria, queryEmbedding, limit });
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";