@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.
- package/.agents/skills/farmville-saas-ux/SKILL.md +156 -0
- package/.agents/skills/farmville-saas-ux/assets/starter.html +294 -0
- package/.agents/skills/farmville-saas-ux/references/components.md +340 -0
- package/.agents/skills/farmville-saas-ux/references/porting-guide.md +121 -0
- package/.agents/skills/farmville-saas-ux/references/tokens.md +271 -0
- package/.agents/skills/vidfarm-media/SKILL.md +4 -3
- package/.agents/skills/vidfarm-media/references/tts.md +20 -1
- package/SKILL.director.md +20 -20
- package/SKILL.platform.md +4 -4
- package/dist/src/account-pages-legacy.js +2 -2
- package/dist/src/app.js +721 -101
- package/dist/src/cli.js +11 -10
- package/dist/src/devcli/clips.js +64 -64
- package/dist/src/editor-chat.js +2 -2
- package/dist/src/frontend/homepage-client.js +162 -2
- package/dist/src/frontend/homepage-store.js +30 -1
- package/dist/src/frontend/homepage-view.js +111 -4
- package/dist/src/homepage.js +184 -1
- package/dist/src/landing-page.js +367 -0
- package/dist/src/primitive-registry.js +278 -2
- package/dist/src/reskin/agency-page.js +299 -0
- package/dist/src/reskin/calendar-page.js +567 -0
- package/dist/src/reskin/chat-page.js +607 -0
- package/dist/src/reskin/discover-page.js +1096 -0
- package/dist/src/reskin/document.js +663 -0
- package/dist/src/reskin/help-page.js +356 -0
- package/dist/src/reskin/index-page.js +62 -0
- package/dist/src/reskin/inpaint-page.js +541 -0
- package/dist/src/reskin/job-runs-page.js +477 -0
- package/dist/src/reskin/library-page.js +688 -0
- package/dist/src/reskin/login-page.js +262 -0
- package/dist/src/reskin/pricing-page.js +388 -0
- package/dist/src/reskin/settings-page.js +687 -0
- package/dist/src/reskin/theme.js +362 -0
- package/dist/src/services/serverless-records.js +54 -0
- package/dist/src/services/swipe-customize.js +434 -0
- package/package.json +1 -1
- 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
|
-
|
|
172
|
-
|
|
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 /
|
|
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
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
(run 'vidfarm
|
|
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 /
|
|
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 `
|
|
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
|
package/dist/src/devcli/clips.js
CHANGED
|
@@ -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
|
|
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,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
|
-
|
|
56
|
-
--source <id> Only
|
|
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
|
-
|
|
59
|
+
raws search "<natural language>" Hybrid structured + semantic search
|
|
60
60
|
--limit <n> | --json | --gemini-key <key>
|
|
61
61
|
|
|
62
|
-
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
73
|
-
|
|
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
|
|
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
|
|
110
|
-
console.log(` ${stats.clips}
|
|
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
|
|
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 `
|
|
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(`[
|
|
220
|
+
console.log(`[raws] probing ${path.basename(videoPath)} …`);
|
|
221
221
|
const probe = await probeVideo(videoPath);
|
|
222
|
-
console.log(`[
|
|
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(`[
|
|
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(`[
|
|
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(`[
|
|
234
|
+
console.log(`[raws] cropping clips to ${aspect} (${cropFocus}).`);
|
|
235
235
|
if (avoidText)
|
|
236
|
-
console.log(`[
|
|
237
|
-
console.log(`[
|
|
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(`[
|
|
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(`[
|
|
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("[
|
|
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("[
|
|
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(`[
|
|
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(`[
|
|
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(`[
|
|
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(`[
|
|
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[
|
|
369
|
-
console.log(`
|
|
370
|
-
console.log(` db
|
|
371
|
-
console.log(` try: vidfarm
|
|
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
|
|
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(`[
|
|
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(`[
|
|
432
|
-
const scanRes = await fetch(`${baseUrl}/
|
|
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(`[
|
|
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(`[
|
|
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}/
|
|
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[
|
|
468
|
-
console.log(` browse → ${baseUrl}/
|
|
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[
|
|
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
|
|
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}
|
|
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
|
|
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
|
|
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(`[
|
|
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(`[
|
|
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(`[
|
|
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
|
|
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(`[
|
|
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}
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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}
|
|
888
|
+
console.log(`\nExported ${exported.length} raws + clips.metadata.json → ${outDir}`);
|
|
889
889
|
}
|
|
890
890
|
// ── helpers ─────────────────────────────────────────────────────────────────
|
|
891
891
|
function normalizeProvider(value) {
|
package/dist/src/editor-chat.js
CHANGED
|
@@ -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)" },
|