@mevdragon/vidfarm-devcli 0.16.0 → 0.18.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 (38) hide show
  1. package/.agents/skills/farmville-saas-ux/SKILL.md +156 -0
  2. package/.agents/skills/farmville-saas-ux/assets/starter.html +294 -0
  3. package/.agents/skills/farmville-saas-ux/references/components.md +340 -0
  4. package/.agents/skills/farmville-saas-ux/references/porting-guide.md +121 -0
  5. package/.agents/skills/farmville-saas-ux/references/tokens.md +271 -0
  6. package/.agents/skills/vidfarm-media/SKILL.md +4 -3
  7. package/.agents/skills/vidfarm-media/references/tts.md +20 -1
  8. package/SKILL.director.md +20 -20
  9. package/SKILL.platform.md +4 -4
  10. package/dist/src/account-pages-legacy.js +2 -2
  11. package/dist/src/app.js +721 -101
  12. package/dist/src/cli.js +11 -10
  13. package/dist/src/devcli/clips.js +64 -64
  14. package/dist/src/editor-chat.js +2 -2
  15. package/dist/src/frontend/homepage-client.js +162 -2
  16. package/dist/src/frontend/homepage-store.js +30 -1
  17. package/dist/src/frontend/homepage-view.js +111 -4
  18. package/dist/src/homepage.js +184 -1
  19. package/dist/src/landing-page.js +367 -0
  20. package/dist/src/primitive-registry.js +278 -2
  21. package/dist/src/reskin/agency-page.js +299 -0
  22. package/dist/src/reskin/calendar-page.js +567 -0
  23. package/dist/src/reskin/chat-page.js +607 -0
  24. package/dist/src/reskin/discover-page.js +1096 -0
  25. package/dist/src/reskin/document.js +663 -0
  26. package/dist/src/reskin/help-page.js +356 -0
  27. package/dist/src/reskin/index-page.js +62 -0
  28. package/dist/src/reskin/inpaint-page.js +541 -0
  29. package/dist/src/reskin/job-runs-page.js +477 -0
  30. package/dist/src/reskin/library-page.js +688 -0
  31. package/dist/src/reskin/login-page.js +262 -0
  32. package/dist/src/reskin/pricing-page.js +388 -0
  33. package/dist/src/reskin/settings-page.js +687 -0
  34. package/dist/src/reskin/theme.js +362 -0
  35. package/dist/src/services/serverless-records.js +54 -0
  36. package/dist/src/services/swipe-customize.js +434 -0
  37. package/package.json +1 -1
  38. package/public/assets/homepage-client-app.js +22 -22
package/dist/src/cli.js CHANGED
@@ -168,22 +168,22 @@ Generate AI media and drop it on the timeline (for local coding agents):
168
168
  original vs decomposed (caption-free),
169
169
  non-billing (alias: ghostcut)
170
170
 
171
- Clip hunting (the third library — mine long-form video into a reusable clip store):
172
- clips scan <video-path> Hunt short clips out of a long video. LOCAL-FIRST: local ffmpeg +
171
+ Raws (the third library — mine long-form video into a reusable raws store):
172
+ raws scan <video-path> Hunt short clips out of a long video. LOCAL-FIRST: local ffmpeg +
173
173
  Detect → tag/transcribe → embed → persist your local claude/codex CLI
174
174
  to ~/.vidfarm/clips.db. (keys are fallback)
175
175
  --range "MM:SS-MM:SS" Only hunt these source windows (repeatable; big cost saver)
176
176
  --duration <sec> Target clip length as a SOFT band (10→5-20s, 30→20-40s)
177
177
  --aspect 9:16|16:9|4:3|1:1 Crop clips (also: vertical/horizontal/square) [--crop-focus <f>]
178
178
  --no-text Prefer scenes WITHOUT captions/on-screen text (selection only)
179
- --cloud [--url <url>] BACKUP: run on the deployed pipeline → POST /clips/scan (+ poll)
179
+ --cloud [--url <url>] BACKUP: run on the deployed pipeline → POST /raws/scan (+ poll)
180
180
  (uploads to your temp folder — 30-day TTL — bills AWS compute only)
181
181
  --dry-run Show the estimated cost and stop (over $1 needs --yes/confirm)
182
- clips list List the local clip library → GET /clips
183
- clips search "<text>" Hybrid structured + semantic search → POST /clips/search
184
- clips preset list|run <name>|save <name> --from-query '<...>' → /clips/presets
185
- clips export <ids...> --to <dir> Copy clip mp4s + metadata out
186
- (run 'vidfarm clips' for the full clips help)
182
+ raws list List the local raws library → GET /raws
183
+ raws search "<text>" Hybrid structured + semantic search → POST /raws/search
184
+ raws preset list|run <name>|save <name> --from-query '<...>' → /raws/presets
185
+ raws export <ids...> --to <dir> Copy raw mp4s + metadata out
186
+ (run 'vidfarm raws' for the full raws help)
187
187
 
188
188
  Speech (TTS/STT) — LOCAL-FIRST on your own AI key; --cloud is the explicit backup:
189
189
  tts "<text>" Text → narration audio file. LOCAL: your OPENAI/GEMINI/
@@ -528,6 +528,7 @@ async function main() {
528
528
  case "annotate-file":
529
529
  await runAnnotateFileCommand(rest);
530
530
  return;
531
+ case "raws":
531
532
  case "clips":
532
533
  await runClipsCommand(rest);
533
534
  return;
@@ -722,7 +723,7 @@ async function runServeCommand(argv) {
722
723
  // render_target="cloud" upstream handoff (the editor's "Render in Cloud").
723
724
  process.env.VIDFARM_JOB_STATE_MACHINE_ARN = "";
724
725
  // Same trap for clip hunts: a dev-shell .env carrying the staging clip-scan
725
- // ARN would route this box's /clips/scan to the cloud Step Functions. serve
726
+ // ARN would route this box's /raws/scan to the cloud Step Functions. serve
726
727
  // hunts are LOCAL-FIRST (local agent CLI / BYOK keys); the upstream cloud is
727
728
  // the explicit backup inside the scan route itself.
728
729
  process.env.VIDFARM_CLIP_SCAN_STATE_MACHINE_ARN = "";
@@ -2256,7 +2257,7 @@ async function runGenerateCommand(argv) {
2256
2257
  }
2257
2258
  }
2258
2259
  // ── Speech: tts / stt ────────────────────────────────────────────────────────
2259
- // LOCAL-FIRST like `clips scan`: both commands default to running on the
2260
+ // LOCAL-FIRST like `raws scan`: both commands default to running on the
2260
2261
  // user's OWN AI key (GEMINI_API_KEY / OPENAI_API_KEY / OPENROUTER_API_KEY or
2261
2262
  // the --*-key flags) via src/services/speech.ts — local ffmpeg demux, direct
2262
2263
  // provider call, no cloud job, no wallet. The REST primitive routes
@@ -14,9 +14,9 @@ import { createInterface } from "node:readline/promises";
14
14
  import { createIdV7 } from "../lib/ids.js";
15
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";
16
16
  import { ClipStore } from "./clip-store.js";
17
- export const CLIPS_HELP = `vidfarm clips — build & search a local clip library (local ffmpeg + local agent by default)
17
+ export const CLIPS_HELP = `vidfarm raws — build & search a local raws library (local ffmpeg + local agent by default)
18
18
 
19
- clips scan <video-path> Mine a long-form video into tagged, searchable clips
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 clip length as a SOFT band (10→5-20s, 30→20-40s, …)
31
- --aspect <a> Crop clips to 9:16 | 16:9 | 4:3 | 1:1 (or vertical/horizontal/square)
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,25 @@ 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
- clips list List clips in the library
56
- --source <id> Only clips from one source video
55
+ raws list List raws in the library
56
+ --source <id> Only raws from one source video
57
57
  --limit <n> | --json
58
58
 
59
- clips search "<natural language>" Hybrid structured + semantic search
59
+ raws search "<natural language>" Hybrid structured + semantic search
60
60
  --limit <n> | --json | --gemini-key <key>
61
61
 
62
- clips match "<text>" Top library clips to REPLACE a scene (vector search)
62
+ raws match "<text>" Top library raws to REPLACE a scene (vector search)
63
63
  --criteria '<json>' Match a structured SplicingCriteria instead of text
64
64
  --scenes <annotations.json> Batch: match every scene of a decomposed video
65
65
  (array of scene annotations, or {annotations|scenes:[…]})
66
66
  --limit <n> | --json
67
67
 
68
- clips preset list List built-in + saved presets
69
- clips preset run <name> Run a preset query [--limit <n>] [--json]
70
- clips preset save <name> --from-query '<json | natural language>'
68
+ raws preset list List built-in + saved presets
69
+ raws preset run <name> Run a preset query [--limit <n>] [--json]
70
+ raws preset save <name> --from-query '<json | natural language>'
71
71
 
72
- clips sources List scanned source videos
73
- clips export <clip-ids...> --to <dir> Copy clip mp4s (+ metadata.json) out
72
+ raws sources List scanned source videos
73
+ raws export <raw-ids...> --to <dir> Copy raw mp4s (+ metadata.json) out
74
74
  `;
75
75
  export async function runClipsCommand(argv) {
76
76
  const sub = argv[0];
@@ -96,7 +96,7 @@ export async function runClipsCommand(argv) {
96
96
  case "-h":
97
97
  return runOverview();
98
98
  default:
99
- console.error(`Unknown clips subcommand: ${sub}\n`);
99
+ console.error(`Unknown raws subcommand: ${sub}\n`);
100
100
  console.log(CLIPS_HELP);
101
101
  process.exitCode = 1;
102
102
  }
@@ -106,8 +106,8 @@ function runOverview() {
106
106
  const store = new ClipStore();
107
107
  const stats = store.stats();
108
108
  store.close();
109
- console.log(`vidfarm clips — library at ${resolveHome()}`);
110
- console.log(` ${stats.clips} clips · ${stats.sources} source videos · ${stats.presets} presets\n`);
109
+ console.log(`vidfarm raws — library at ${resolveHome()}`);
110
+ console.log(` ${stats.clips} raws · ${stats.sources} source videos · ${stats.presets} presets\n`);
111
111
  console.log(CLIPS_HELP);
112
112
  }
113
113
  // ── clips scan ──────────────────────────────────────────────────────────────
@@ -186,12 +186,12 @@ async function runScan(argv) {
186
186
  }
187
187
  const videoArg = positionals[0];
188
188
  if (!videoArg)
189
- throw new Error("Usage: vidfarm clips scan <video-path> [--range MM:SS-MM:SS] [--duration 30] [--aspect 9:16] [--dry-run]");
189
+ throw new Error("Usage: vidfarm raws scan <video-path> [--range MM:SS-MM:SS] [--duration 30] [--aspect 9:16] [--dry-run]");
190
190
  const videoPath = path.resolve(process.cwd(), videoArg);
191
191
  if (!existsSync(videoPath))
192
192
  throw new Error(`No such video: ${videoPath}`);
193
193
  if (!(await hasFfmpeg())) {
194
- throw new Error("ffmpeg is required for `clips scan` but was not found (bundled ffmpeg-static missing and none on PATH).");
194
+ throw new Error("ffmpeg is required for `raws scan` but was not found (bundled ffmpeg-static missing and none on PATH).");
195
195
  }
196
196
  // ── Model resolution: LOCAL-FIRST ─────────────────────────────────────────
197
197
  // 1. --provider agent (or no --provider + a local claude/codex CLI installed)
@@ -217,24 +217,24 @@ async function runScan(argv) {
217
217
  const embedding = provider === "agent"
218
218
  ? resolveAgentEmbeddingConfig(values)
219
219
  : apiKey ? resolveEmbeddingConfig(provider, apiKey, values) : null;
220
- console.log(`[clips] probing ${path.basename(videoPath)} …`);
220
+ console.log(`[raws] probing ${path.basename(videoPath)} …`);
221
221
  const probe = await probeVideo(videoPath);
222
- console.log(`[clips] ${fmtDuration(probe.duration_sec)} · ${probe.width ?? "?"}x${probe.height ?? "?"} · ${probe.codec ?? "?"}`);
222
+ console.log(`[raws] ${fmtDuration(probe.duration_sec)} · ${probe.width ?? "?"}x${probe.height ?? "?"} · ${probe.codec ?? "?"}`);
223
223
  const windows = normalizeWindows(rawWindows, probe.duration_sec);
224
224
  if (rawWindows.length && !windows.length) {
225
225
  throw new Error("--range produced no usable window inside the video's duration.");
226
226
  }
227
227
  if (windows.length) {
228
- console.log(`[clips] 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)}).`);
228
+ 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
229
  }
230
230
  if (durationBand) {
231
- console.log(`[clips] target clip length ~${durationBand.target_sec}s (soft range ${durationBand.min_sec}–${durationBand.max_sec}s).`);
231
+ console.log(`[raws] target clip length ~${durationBand.target_sec}s (soft range ${durationBand.min_sec}–${durationBand.max_sec}s).`);
232
232
  }
233
233
  if (aspect && aspect !== "original")
234
- console.log(`[clips] cropping clips to ${aspect} (${cropFocus}).`);
234
+ console.log(`[raws] cropping clips to ${aspect} (${cropFocus}).`);
235
235
  if (avoidText)
236
- console.log(`[clips] preferring scenes without on-screen text/captions (selection only — GhostCut is never run on the source).`);
237
- console.log(`[clips] detecting scenes …`);
236
+ console.log(`[raws] preferring scenes without on-screen text/captions (selection only — GhostCut is never run on the source).`);
237
+ console.log(`[raws] detecting scenes …`);
238
238
  const detected = await detectScenes(videoPath, { durationSec: probe.duration_sec, windows, ...sceneOptions });
239
239
  const scenes = fitScenesToDurationBand(detected, durationBand);
240
240
  if (scenes.length === 0)
@@ -247,13 +247,13 @@ async function runScan(argv) {
247
247
  embedding,
248
248
  tier
249
249
  });
250
- console.log(`[clips] evaluating scenes with your local ${client.tagModelId} subscription (no API cost).`);
250
+ console.log(`[raws] evaluating scenes with your local ${client.tagModelId} subscription (no API cost).`);
251
251
  }
252
252
  else if (apiKey) {
253
253
  client = new ClipModelClient({ provider, apiKey, tier, embedding });
254
254
  }
255
255
  if (client && !client.canEmbed) {
256
- console.warn(`[clips] no embedding provider for "${provider}" — clips will be structured-searchable only.`);
256
+ console.warn(`[raws] no embedding provider for "${provider}" — raws will be structured-searchable only.`);
257
257
  console.warn(` Add --embed-provider gemini|openai --embed-key <key> for semantic search.`);
258
258
  }
259
259
  // Cost estimate: local-agent scans ride the user's subscription, so only the
@@ -279,7 +279,7 @@ async function runScan(argv) {
279
279
  console.log(` Guidance: "${guidance.replace(/\n/g, " · ")}"`);
280
280
  console.log("");
281
281
  if (values["dry-run"]) {
282
- console.log("[clips] --dry-run: not scanning. Re-run without --dry-run to build the library.");
282
+ console.log("[raws] --dry-run: not scanning. Re-run without --dry-run to build the library.");
283
283
  return;
284
284
  }
285
285
  // Actually scanning now — an evaluator is required.
@@ -293,7 +293,7 @@ async function runScan(argv) {
293
293
  if (estimate && estimate.usd > 1 && !values.yes) {
294
294
  const ok = await confirm(`This scan is estimated at $${estimate.usd.toFixed(2)}. Proceed? [y/N] `);
295
295
  if (!ok) {
296
- console.log("[clips] aborted.");
296
+ console.log("[raws] aborted.");
297
297
  return;
298
298
  }
299
299
  }
@@ -301,7 +301,7 @@ async function runScan(argv) {
301
301
  if (activeClient.embeddingModelId) {
302
302
  const existing = store.getLibraryEmbeddingModel();
303
303
  if (existing && existing !== activeClient.embeddingModelId) {
304
- console.warn(`[clips] WARNING: library was built with "${existing}" embeddings but this scan uses "${activeClient.embeddingModelId}". Semantic search across them will be unreliable.`);
304
+ console.warn(`[raws] WARNING: library was built with "${existing}" embeddings but this scan uses "${activeClient.embeddingModelId}". Semantic search across them will be unreliable.`);
305
305
  }
306
306
  else {
307
307
  store.rememberEmbeddingModel(activeClient.embeddingModelId);
@@ -319,7 +319,7 @@ async function runScan(argv) {
319
319
  reencodeClips: true,
320
320
  crop: aspect && aspect !== "original" ? { aspect, focus: cropFocus } : undefined
321
321
  };
322
- console.log(`[clips] scanning ${scenes.length} scenes (${provider}/${activeClient.tagModelId}, ${concurrency}x) …`);
322
+ console.log(`[raws] scanning ${scenes.length} scenes (${provider}/${activeClient.tagModelId}, ${concurrency}x) …`);
323
323
  let errors = 0;
324
324
  const started = timestamp();
325
325
  const result = await scanVideo({
@@ -333,10 +333,10 @@ async function runScan(argv) {
333
333
  concurrency,
334
334
  onRefine: (r) => {
335
335
  if (r.refined) {
336
- console.log(`[clips] guided segmentation: ${scenes.length} → ${r.kept} clips (dropped ${r.dropped}, merged ${r.merged}).`);
336
+ console.log(`[raws] guided segmentation: ${scenes.length} → ${r.kept} raws (dropped ${r.dropped}, merged ${r.merged}).`);
337
337
  }
338
338
  else if (r.note) {
339
- console.log(`[clips] guided segmentation skipped: ${r.note}.`);
339
+ console.log(`[raws] guided segmentation skipped: ${r.note}.`);
340
340
  }
341
341
  },
342
342
  onClip: (clip, i, total) => {
@@ -365,10 +365,10 @@ async function runScan(argv) {
365
365
  /* ignore */
366
366
  }
367
367
  store.close();
368
- console.log(`\n[clips] done in ${elapsed(started)}: ${result.clips.length} clips${errors ? `, ${errors} scene(s) failed` : ""}.`);
369
- console.log(` clips → ${store.paths.clipsDir}`);
370
- console.log(` db → ${store.paths.db}`);
371
- console.log(` try: vidfarm clips search "…" | vidfarm clips preset run "funny reaction gifs"`);
368
+ console.log(`\n[raws] done in ${elapsed(started)}: ${result.clips.length} raws${errors ? `, ${errors} scene(s) failed` : ""}.`);
369
+ console.log(` raws → ${store.paths.clipsDir}`);
370
+ console.log(` db → ${store.paths.db}`);
371
+ console.log(` try: vidfarm raws search "…" | vidfarm raws preset run "funny reaction gifs"`);
372
372
  }
373
373
  // ── clips scan --cloud (explicit backup: the deployed pipeline) ─────────────
374
374
  // Uploads the local file into the user's temp folder (30-day TTL, auto-removed)
@@ -384,13 +384,13 @@ async function runScanCloud(input) {
384
384
  let fileName;
385
385
  if (!input.sourceUrl) {
386
386
  if (!input.videoArg)
387
- throw new Error("Usage: vidfarm clips scan --cloud <video-path> | --cloud --url <video-url>");
387
+ throw new Error("Usage: vidfarm raws scan --cloud <video-path> | --cloud --url <video-url>");
388
388
  const videoPath = path.resolve(process.cwd(), input.videoArg);
389
389
  if (!existsSync(videoPath))
390
390
  throw new Error(`No such video: ${videoPath}`);
391
391
  fileName = path.basename(videoPath);
392
392
  const sizeBytes = statSync(videoPath).size;
393
- console.log(`[clips] uploading ${fileName} (${(sizeBytes / (1024 * 1024)).toFixed(1)} MB) to your temp folder (auto-deletes in 30 days) …`);
393
+ console.log(`[raws] uploading ${fileName} (${(sizeBytes / (1024 * 1024)).toFixed(1)} MB) to your temp folder (auto-deletes in 30 days) …`);
394
394
  const presignRes = await fetch(`${baseUrl}/api/v1/user/me/temporary-files/presign`, {
395
395
  method: "POST",
396
396
  headers,
@@ -428,8 +428,8 @@ async function runScanCloud(input) {
428
428
  }
429
429
  tempFileId = presign.file_id;
430
430
  }
431
- console.log(`[clips] starting cloud hunt …`);
432
- const scanRes = await fetch(`${baseUrl}/clips/scan`, {
431
+ console.log(`[raws] starting cloud hunt …`);
432
+ const scanRes = await fetch(`${baseUrl}/raws/scan`, {
433
433
  method: "POST",
434
434
  headers,
435
435
  body: JSON.stringify({
@@ -450,28 +450,28 @@ async function runScanCloud(input) {
450
450
  if (!scanRes.ok || !scan.scan_id) {
451
451
  throw new Error(`Cloud scan failed to start (${scanRes.status}): ${scan.error ?? "unexpected response"}`);
452
452
  }
453
- console.log(`[clips] scan ${scan.scan_id} ${scan.status ?? "queued"}${scan.tracer ? ` (tracer ${scan.tracer})` : ""}.`);
453
+ console.log(`[raws] scan ${scan.scan_id} ${scan.status ?? "queued"}${scan.tracer ? ` (tracer ${scan.tracer})` : ""}.`);
454
454
  if (scan.compute_estimate?.estimated_charge_usd != null) {
455
- console.log(`[clips] estimated AWS compute charge ~$${scan.compute_estimate.estimated_charge_usd.toFixed(4)} (AI runs on your saved provider keys).`);
455
+ console.log(`[raws] estimated AWS compute charge ~$${scan.compute_estimate.estimated_charge_usd.toFixed(4)} (AI runs on your saved provider keys).`);
456
456
  }
457
457
  // Async by design — poll until the pipeline finishes.
458
458
  const started = timestamp();
459
459
  for (;;) {
460
460
  await new Promise((r) => setTimeout(r, 5000));
461
- const pollRes = await fetch(`${baseUrl}/clips/scan/${encodeURIComponent(scan.scan_id)}`, { headers });
461
+ const pollRes = await fetch(`${baseUrl}/raws/scan/${encodeURIComponent(scan.scan_id)}`, { headers });
462
462
  if (!pollRes.ok)
463
463
  continue;
464
464
  const poll = await pollRes.json();
465
465
  const status = poll.source?.status ?? poll.scan?.status ?? "running";
466
466
  if (status === "complete") {
467
- console.log(`\n[clips] cloud hunt done in ${elapsed(started)}: ${poll.source?.clip_count ?? 0} clips.`);
468
- console.log(` browse → ${baseUrl}/clips`);
467
+ console.log(`\n[raws] cloud hunt done in ${elapsed(started)}: ${poll.source?.clip_count ?? 0} raws.`);
468
+ console.log(` browse → ${baseUrl}/library/raws`);
469
469
  return;
470
470
  }
471
471
  if (status === "failed") {
472
472
  throw new Error(`Cloud hunt failed: ${poll.scan?.error ?? "unknown error"}`);
473
473
  }
474
- process.stdout.write(`\r[clips] ${status} … ${elapsed(started)} `);
474
+ process.stdout.write(`\r[raws] ${status} … ${elapsed(started)} `);
475
475
  }
476
476
  }
477
477
  /**
@@ -523,13 +523,13 @@ async function runList(argv) {
523
523
  return;
524
524
  }
525
525
  if (clips.length === 0) {
526
- console.log("No clips yet. Run: vidfarm clips scan <video-path>");
526
+ console.log("No raws yet. Run: vidfarm raws scan <video-path>");
527
527
  return;
528
528
  }
529
529
  for (const clip of clips) {
530
530
  console.log(`${clip.clip_id.slice(0, 14)} ${fmtRange(clip)} ${clip.source_filename} ${summarizeTags(clip)}`);
531
531
  }
532
- console.log(`\n${clips.length} clips.`);
532
+ console.log(`\n${clips.length} raws.`);
533
533
  }
534
534
  // ── clips search ────────────────────────────────────────────────────────────
535
535
  async function runSearch(argv) {
@@ -548,12 +548,12 @@ async function runSearch(argv) {
548
548
  });
549
549
  const query = positionals.join(" ").trim();
550
550
  if (!query)
551
- throw new Error('Usage: vidfarm clips search "<natural language>"');
551
+ throw new Error('Usage: vidfarm raws search "<natural language>"');
552
552
  const limit = clampInt(values.limit, 10, 1, 200);
553
553
  const store = new ClipStore(values.home);
554
554
  if (store.stats().clips === 0) {
555
555
  store.close();
556
- console.log("Library is empty — scan a video first: vidfarm clips scan <video-path>");
556
+ console.log("Library is empty — scan a video first: vidfarm raws scan <video-path>");
557
557
  return;
558
558
  }
559
559
  const provider = normalizeProvider(values.provider);
@@ -566,7 +566,7 @@ async function runSearch(argv) {
566
566
  if (client.canEmbed) {
567
567
  const libModel = store.getLibraryEmbeddingModel();
568
568
  if (libModel && client.embeddingModelId && libModel !== client.embeddingModelId) {
569
- console.warn(`[clips] note: library uses "${libModel}" embeddings but you're searching with "${client.embeddingModelId}" — semantic re-rank may be off; pass --provider to match.`);
569
+ 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
570
  }
571
571
  queryEmbedding = await client.embedQuery(criteria.semantic_text ?? query);
572
572
  }
@@ -575,11 +575,11 @@ async function runSearch(argv) {
575
575
  // Local-first: no key, but a local agent CLI can still turn the natural-
576
576
  // language query into a structured filter (vector re-rank needs a key).
577
577
  const agent = new LocalAgentClipClient({});
578
- console.log(`[clips] structuring query with your local ${agent.tagModelId} …`);
578
+ console.log(`[raws] structuring query with your local ${agent.tagModelId} …`);
579
579
  criteria = await agent.naturalLanguageToCriteria(query);
580
580
  }
581
581
  else {
582
- console.warn(`[clips] no ${provider} key or local agent CLI — structured/keyword search only.`);
582
+ console.warn(`[raws] no ${provider} key or local agent CLI — structured/keyword search only.`);
583
583
  }
584
584
  const t0 = timestamp();
585
585
  const hits = store.search({ criteria, queryEmbedding, limit });
@@ -627,7 +627,7 @@ async function runMatch(argv) {
627
627
  const store = new ClipStore(values.home);
628
628
  if (store.stats().clips === 0) {
629
629
  store.close();
630
- console.log("Library is empty — scan a video first: vidfarm clips scan <video-path>");
630
+ console.log("Library is empty — scan a video first: vidfarm raws scan <video-path>");
631
631
  return;
632
632
  }
633
633
  // Embed the match query in the SAME space the library was built with.
@@ -636,7 +636,7 @@ async function runMatch(argv) {
636
636
  const embedKey = resolveProviderKey(embedProvider, values, { optional: true });
637
637
  const client = embedKey ? new ClipModelClient({ provider: embedProvider, apiKey: embedKey }) : null;
638
638
  if (!client)
639
- console.warn(`[clips] no ${embedProvider} key — structured match only (set the key for vector re-rank).`);
639
+ console.warn(`[raws] no ${embedProvider} key — structured match only (set the key for vector re-rank).`);
640
640
  if (values.scenes) {
641
641
  const filePath = path.resolve(process.cwd(), values.scenes);
642
642
  if (!existsSync(filePath))
@@ -670,7 +670,7 @@ async function runMatch(argv) {
670
670
  console.log(JSON.stringify(output, null, 2));
671
671
  return;
672
672
  }
673
- console.log(`Matched ${scenes.length} scenes against ${all.length} clips:`);
673
+ console.log(`Matched ${scenes.length} scenes against ${all.length} raws:`);
674
674
  for (const o of output) {
675
675
  const rep = o.replaceability ? ` [replaceability: ${o.replaceability}]` : "";
676
676
  const dur = typeof o.duration === "number" ? ` (${o.duration.toFixed(1)}s)` : "";
@@ -699,7 +699,7 @@ async function runMatch(argv) {
699
699
  text = positionals.join(" ").trim();
700
700
  if (!text) {
701
701
  store.close();
702
- throw new Error('Usage: vidfarm clips match "<text>" | --criteria \'<json>\' | --scenes <annotations.json>');
702
+ throw new Error('Usage: vidfarm raws match "<text>" | --criteria \'<json>\' | --scenes <annotations.json>');
703
703
  }
704
704
  criteria = { semantic_text: text };
705
705
  }
@@ -724,7 +724,7 @@ async function runPreset(argv) {
724
724
  return runPresetRun(rest);
725
725
  if (action === "save")
726
726
  return runPresetSave(rest);
727
- throw new Error("Usage: vidfarm clips preset <list|run <name>|save <name> --from-query '<...>'>");
727
+ throw new Error("Usage: vidfarm raws preset <list|run <name>|save <name> --from-query '<...>'>");
728
728
  }
729
729
  async function runPresetList(argv) {
730
730
  const { values } = parseArgs({ args: argv, options: { json: { type: "boolean" }, home: { type: "string" } } });
@@ -756,12 +756,12 @@ async function runPresetRun(argv) {
756
756
  });
757
757
  const name = positionals.join(" ").trim();
758
758
  if (!name)
759
- throw new Error("Usage: vidfarm clips preset run <name>");
759
+ throw new Error("Usage: vidfarm raws preset run <name>");
760
760
  const store = new ClipStore(values.home);
761
761
  const preset = store.findPreset(name);
762
762
  if (!preset) {
763
763
  store.close();
764
- throw new Error(`No preset "${name}". Try: vidfarm clips preset list`);
764
+ throw new Error(`No preset "${name}". Try: vidfarm raws preset list`);
765
765
  }
766
766
  const limit = clampInt(values.limit, 10, 1, 200);
767
767
  // Presets carry semantic_text; embed it if we have a key for the re-rank.
@@ -800,7 +800,7 @@ async function runPresetSave(argv) {
800
800
  const name = positionals.join(" ").trim();
801
801
  const fromQuery = values["from-query"];
802
802
  if (!name || !fromQuery) {
803
- throw new Error("Usage: vidfarm clips preset save <name> --from-query '<json | natural language>'");
803
+ throw new Error("Usage: vidfarm raws preset save <name> --from-query '<json | natural language>'");
804
804
  }
805
805
  let criteria;
806
806
  const asJson = tryParseJson(fromQuery);
@@ -857,7 +857,7 @@ async function runExport(argv) {
857
857
  const ids = positionals;
858
858
  const to = values.to;
859
859
  if (ids.length === 0 || !to) {
860
- throw new Error("Usage: vidfarm clips export <clip-ids...> --to <dir>");
860
+ throw new Error("Usage: vidfarm raws export <raw-ids...> --to <dir>");
861
861
  }
862
862
  const outDir = path.resolve(process.cwd(), to);
863
863
  mkdirSync(outDir, { recursive: true });
@@ -885,7 +885,7 @@ async function runExport(argv) {
885
885
  store.close();
886
886
  const { writeFileSync } = await import("node:fs");
887
887
  writeFileSync(path.join(outDir, "clips.metadata.json"), JSON.stringify(exported, null, 2));
888
- console.log(`\nExported ${exported.length} clips + clips.metadata.json → ${outDir}`);
888
+ console.log(`\nExported ${exported.length} raws + clips.metadata.json → ${outDir}`);
889
889
  }
890
890
  // ── helpers ─────────────────────────────────────────────────────────────────
891
891
  function normalizeProvider(value) {
@@ -45,7 +45,7 @@ export function buildTemplateEditorChatSystemPrompt(input) {
45
45
  "If the user asks to modify an attached image and the message includes an attachment URL, call POST /api/v1/primitives/images/edit with source_image_url inside payload set to that exact URL and instruction inside payload set to the requested edit.",
46
46
  "If the user asks to generate a new AI video from text or reference images, call POST /api/v1/primitives/videos/generate with body { tracer, payload: { prompt, input_references?, duration? } }. AI video duration is seconds; use duration: 4 for a 4-second video and never use duration_ms on /videos/generate. input_references must be direct image asset URLs, not webpage or social post URLs. Use frame_images for exact first/last-frame image-to-video.",
47
47
  "If the user asks to render HTML/design into an image, call POST /api/v1/primitives/images/render-html. If the user asks to turn existing media URLs into a slideshow/video, call POST /api/v1/primitives/videos/render-slides. If the user asks to dedupe, camouflage, or apply small zoom/tilt/rotate/filter/tint transforms to an image or video for reuse, call POST /api/v1/primitives/media/dedupe with body { tracer, payload: { source_media_url, media_type?, effects?: { zoom?, tilt?, rotate?, saturation?, speed?, horizontal_flip?, contrast?, brightness?, hue_rotate?, blur? }, tint_color?, tint_opacity?, width?, height?, duration_ms?, object_fit?, background_color?, output_format? }, webhook_url? }. Primitive media utility routes are also allowed directly here, including /videos/trim, /videos/remove-captions, /videos/extract-audio, /videos/probe, /videos/extract-frame, /videos/mute, /videos/replace-audio, /videos/concat, /audio/trim, /audio/probe, /audio/concat, and /audio/normalize. If the user asks to remove burned-in captions, subtitles, or on-screen text from a video, call POST /api/v1/primitives/videos/remove-captions with body { tracer, payload: { source_video_url } } — an async job that returns a durable caption-free MP4 and bills about $0.10 per 30 seconds of source video. Caption removal is capped at ~15 minutes of source and must NEVER be used on long-form video: when the user wants short clips out of a long video (a podcast, stream VOD, webinar, or a YouTube/TikTok/IG/X URL), call POST /clips/scan instead with body { source_url | temp_file_id | s3_key, prompt, ranges?: [\"MM:SS-MM:SS\"], target_duration_sec?, aspect?: \"9:16\"|\"16:9\"|\"4:3\"|\"1:1\", avoid_text?, tracer? } — target_duration_sec is a SOFT range (30 → 20-40s), ranges restrict the hunt to those source windows (big cost saver), aspect crops every clip, and avoid_text prefers text-free scenes via scene SELECTION (never caption removal on the source). The hunt is async (202 + scan_id): poll GET /clips/scan/:scanId, then browse GET /clips/feed?source=<source_video_id> or POST /clips/search. Hunts bill AWS compute only — the AI tagging runs on the user's own saved provider keys. To erase captions from a finished HUNTED clip, pass that short mp4 to /videos/remove-captions afterwards.",
48
- "SPEECH PRIMITIVES (TTS/STT) are first-class here. VOICEOVER / NARRATION: when the user wants a script, hook, caption, or dub read aloud as AI audio, call POST /api/v1/primitives/audio/speech with body { tracer, payload: { text, voice?, instructions?, provider?, model?, output_format? } } — instructions is a free-form voice-STYLE prompt (tone, pacing, accent, emotion, persona; e.g. 'calm warm bedtime narrator' or 'excited sports announcer, fast paced'), and voice picks a provider preset (OpenAI: alloy/ash/coral/...; Gemini: Kore/Puck/Charon/...). When sending a voice you may omit provider — it is inferred from the voice preset; a voice paired with the WRONG provider (e.g. voice 'Kore' with provider 'openai') is rejected with a 400, so never mix families. The finished job returns a durable audio URL — place it on the timeline with editor_action add_layer (kind=audio, src=that URL), typically starting where the narration should begin. TRANSCRIPTION: when the user wants a transcript, subtitles/captions from speech, a translation source, or to know who says what, call POST /api/v1/primitives/audio/transcribe with body { tracer, payload: { source_url, diarize?, language?, prompt? } } — source_url may be a VIDEO or AUDIO URL (video audio is demuxed automatically; pick the fork's source via GET /api/v1/compositions/:forkId/remove-video-captions first when transcribing the current composition). One job returns BOTH formats: a simple subtitle version (plain transcript text + timed SRT cues, stored as transcript.txt/transcript.srt) and an advanced multi-speaker version (speaker-attributed timed segments in output.segments and transcript.json) for multi-narration sources like podcasts and interviews. diarize defaults to true and speaker attribution needs a Gemini key — openai/openrouter keys degrade to a plain single-speaker transcript. For transcript-grounded caption work on THIS fork, the read-only video_context tool may already contain the decompose-time transcript; prefer it before running a new billable-key transcription, and use /audio/transcribe for other URLs, multi-speaker attribution, or SRT output.",
48
+ "SPEECH PRIMITIVES (TTS/STT) are first-class here. VOICEOVER / NARRATION: when the user wants a script, hook, caption, or dub read aloud as AI audio, call POST /api/v1/primitives/audio/speech with body { tracer, payload: { text, voice?, instructions?, provider?, model?, output_format? } } — instructions is a free-form voice-STYLE prompt (tone, pacing, accent, emotion, persona; e.g. 'calm warm bedtime narrator' or 'excited sports announcer, fast paced'), and voice picks a provider preset (OpenAI: alloy/ash/coral/...; Gemini: Kore/Puck/Charon/...). When sending a voice you may omit provider — it is inferred from the voice preset; a voice paired with the WRONG provider (e.g. voice 'Kore' with provider 'openai') is rejected with a 400, so never mix families. The finished job returns a durable audio URL — place it on the timeline with editor_action add_layer (kind=audio, src=that URL), typically starting where the narration should begin. TRANSCRIPTION: when the user wants a transcript, subtitles/captions from speech, a translation source, or to know who says what, call POST /api/v1/primitives/audio/transcribe with body { tracer, payload: { source_url, diarize?, language?, prompt? } } — source_url may be a VIDEO or AUDIO URL (video audio is demuxed automatically; pick the fork's source via GET /api/v1/compositions/:forkId/remove-video-captions first when transcribing the current composition). One job returns BOTH formats: a simple subtitle version (plain transcript text + timed SRT cues, stored as transcript.txt/transcript.srt) and an advanced multi-speaker version (speaker-attributed timed segments in output.segments and transcript.json) for multi-narration sources like podcasts and interviews. diarize defaults to true and speaker attribution needs a Gemini key — openai/openrouter keys degrade to a plain single-speaker transcript. For transcript-grounded caption work on THIS fork, the read-only video_context tool may already contain the decompose-time transcript; prefer it before running a new billable-key transcription, and use /audio/transcribe for other URLs, multi-speaker attribution, or SRT output. REGENERATE / REVOICE: when the user wants the SAME speaker to say DIFFERENT words (reword the narration, fix a line, change an offer or CTA, redub in the original voice), call POST /api/v1/primitives/audio/regenerate-speech with body { tracer, payload: { source_url (video or audio; video audio is demuxed), rewrite_instruction (how to reword the original transcript) OR text (the exact new script — one of the two is required), voice?, instructions?, provider?, model?, language?, output_format? } }. One job listens to the source, profiles the speaker's voice, rewords the script, and returns a durable audio URL spoken by the CLOSEST PRESET voice with a style prompt matched to the speaker (result fields: audioUrl, script, original_transcript, voice_profile, voice_match). Voice profiling needs a saved Gemini key — with only openai/openrouter keys the job degrades to transcript-only rewording with a default or explicitly passed voice and voice_match 'none'. IMPORTANT HONESTY RULE: OpenAI and Gemini APIs cannot clone voice identity (preset voices only), so always tell the user the regenerated audio APPROXIMATES the original voice rather than clones it. To swap the regenerated audio into the composition, mute the original span (/videos/mute or volume/muted on the layer) and place the new audio with editor_action add_layer (kind=audio, src=audioUrl).",
49
49
  "Brainstorm primitives are also available directly here. If the user explicitly asks for hooks, angles, awareness-stage advice, a cold-start strategy questionnaire, or product-placement opportunities in a video, call the matching brainstorm primitive immediately instead of brainstorming in chat from memory. Use POST /api/v1/primitives/brainstorm/coldstart with body { tracer, payload: { user_message }, webhook_url? } when the user is clueless and needs foundational strategy questions. Use /brainstorm/awareness_stages with body { tracer, payload: { offer_description }, webhook_url? } when they are unsure what type of ads to make. Use /brainstorm/angles with body { tracer, payload: { offer_description, problem_awareness, solution_awareness }, webhook_url? } when they want persuasive ad angles; problem_awareness must be problem_unaware or problem_aware, and solution_awareness must be solution_unaware or solution_aware. Use /brainstorm/hooks with body { tracer, payload: { offer_description }, webhook_url? } when they are stuck on openings and want many hooks.",
50
50
  "Product placement is a first-class brainstorm primitive, just like angles and hooks. When the user asks to identify product-placement opportunities in a video, or to repurpose a video into a native ad for their product, prefer POST /api/v1/primitives/brainstorm/product_placement with body { tracer, payload: { source_video_url, offer_description }, webhook_url? }. source_video_url is the exact video to analyze — for the current composition, first call GET /api/v1/compositions/:forkId/remove-video-captions to pick the correct source (the ORIGINAL uploaded video vs the DECOMPOSED, caption-free video), then pass that URL. offer_description is the user's product/offer. This primitive watches the video and returns timestamped, native placement opportunities. It prefers a Gemini key for video understanding.",
51
51
  "BEFORE calling /brainstorm/product_placement for the CURRENT composition, first call the product_placement_context tool (or GET /api/v1/compositions/:forkId/product-placement.json). Decompose already scouted product-AGNOSTIC placement surfaces for this fork — real, grounded moments (timestamp, surface_type, surface_note, best_for, why_it_works). Use those surfaces to ground your answer and to seed the primitive: fold the strongest surfaces into offer_description (for example append 'Known native placement surfaces: 0:04 empty desk prop; 0:12 phone screen swap') so the product-specific pass builds on grounded moments instead of re-watching blind. If the user just wants a quick conversational take on where their product could go, the surfaces from product_placement_context are often enough to answer directly without the billable primitive.",
@@ -469,7 +469,7 @@ export const EDITOR_CHAT_SKILL_PACKS = [
469
469
  { name: "hyperframes-animation", summary: "motion rules catalog, scene blueprints, transition doctrine, runtime adapters" },
470
470
  { name: "hyperframes-keyframes", summary: "seek-safe keyframe/CSS animation patterns" },
471
471
  { name: "hyperframes-creative", summary: "design specs, palettes, typography, house style, narration craft" },
472
- { name: "vidfarm-media", summary: "narration TTS/STT, BGM/SFX/media resolution through vidfarm primitives" },
472
+ { name: "vidfarm-media", summary: "narration TTS/STT, voice-matched speech regeneration (same speaker, new words), BGM/SFX/media resolution through vidfarm primitives" },
473
473
  { name: "embedded-captions", summary: "caption identity catalog + subject-occlusion caption pipeline" },
474
474
  { name: "talking-head-recut", summary: "timed graphic overlay cards on existing footage" },
475
475
  { name: "product-launch-video", summary: "product/launch promo workflow (time-coded shot sequences)" },