@mevdragon/vidfarm-devcli 0.19.1 → 0.20.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.agents/skills/music/SKILL.md +416 -0
- package/.agents/skills/music/references/api_reference.md +519 -0
- package/.agents/skills/music/references/installation.md +65 -0
- package/.agents/skills/text-to-speech/SKILL.md +226 -0
- package/.agents/skills/text-to-speech/references/installation.md +90 -0
- package/.agents/skills/text-to-speech/references/streaming.md +307 -0
- package/.agents/skills/text-to-speech/references/voice-settings.md +115 -0
- package/.agents/skills/vidfarm-media/SKILL.md +25 -11
- package/.agents/skills/vidfarm-media/references/tts.md +41 -3
- package/SKILL.director.md +31 -13
- package/SKILL.platform.md +2 -2
- package/demo/dist/app.css +1 -1
- package/demo/dist/app.js +75 -75
- package/dist/src/app.js +129 -41
- package/dist/src/cli.js +162 -5
- package/dist/src/config.js +12 -0
- package/dist/src/devcli/clips.js +7 -2
- package/dist/src/domain.js +3 -0
- package/dist/src/editor-chat.js +5 -1
- package/dist/src/frontend/file-directory.js +41 -18
- package/dist/src/primitive-context.js +14 -0
- package/dist/src/primitive-registry.js +140 -18
- package/dist/src/reskin/chat-page.js +1 -1
- package/dist/src/reskin/inpaint-clipper-page.js +446 -205
- package/dist/src/reskin/library-page.js +7 -1
- package/dist/src/reskin/portfolio-page.js +9 -9
- package/dist/src/reskin/settings-page.js +4 -4
- package/dist/src/reskin/theme.js +8 -4
- package/dist/src/services/billing.js +5 -0
- package/dist/src/services/clip-curation/ffmpeg.js +48 -0
- package/dist/src/services/clip-curation/hunt.js +2 -0
- package/dist/src/services/clip-curation/index.js +1 -1
- package/dist/src/services/clip-curation/scan.js +29 -16
- package/dist/src/services/elevenlabs.js +222 -0
- package/dist/src/services/providers.js +216 -2
- package/dist/src/services/swipe-customize.js +5 -2
- package/package.json +1 -1
- package/public/assets/file-directory-app.js +3 -2
package/dist/src/cli.js
CHANGED
|
@@ -285,6 +285,8 @@ Raws (the third library — mine long-form video into a reusable raws store):
|
|
|
285
285
|
--tracer <name> Group this session's clips (also the default folder)
|
|
286
286
|
--folder <name> Save the clip into /raws/<name>
|
|
287
287
|
--name <text> Human name for the clip
|
|
288
|
+
--audio-only Save just the audio of the subrange as an .m4a raw
|
|
289
|
+
(needs --start/--end; e.g. lift a copyright-free track)
|
|
288
290
|
(multi-clip: re-run with a new range + same --tracer)
|
|
289
291
|
|
|
290
292
|
Speech (TTS/STT) — LOCAL-FIRST on your own AI key; --cloud is the explicit backup:
|
|
@@ -300,7 +302,19 @@ Speech (TTS/STT) — LOCAL-FIRST on your own AI key; --cloud is the explicit bac
|
|
|
300
302
|
--engine local KEYLESS local Kokoro-82M engine (free, no account) — fixed voice
|
|
301
303
|
presets (af_heart, am_adam, bf_emma, …), no --style. When NO
|
|
302
304
|
provider key is found the default byok engine auto-falls back here.
|
|
303
|
-
--cloud BACKUP:
|
|
305
|
+
--cloud BACKUP: platform job → POST /api/v1/primitives/audio/speech (+ poll).
|
|
306
|
+
Defaults to ElevenLabs on the platform key (wallet-billed); --voice
|
|
307
|
+
takes an ElevenLabs voice_id (see: vidfarm voices).
|
|
308
|
+
--own-key (--cloud) use your own ElevenLabs / BYOK key instead of the wallet
|
|
309
|
+
--list-voices List ElevenLabs voices and exit (same as: vidfarm voices)
|
|
310
|
+
music "<prompt>" Prompt → music track (ElevenLabs). CLOUD-ONLY (no local engine)
|
|
311
|
+
--length <sec> | --length-ms <ms> Duration (default 30s, max 5 min)
|
|
312
|
+
--out <file> Output mp3 path (default music-<id>.mp3)
|
|
313
|
+
--own-key Use your own ElevenLabs key instead of the platform key + wallet
|
|
314
|
+
(default: platform ElevenLabs, wallet-billed) → POST /api/v1/primitives/music/generate
|
|
315
|
+
voices List ElevenLabs voices for TTS (voice_id + name + labels).
|
|
316
|
+
--own-key List voices on your own saved ElevenLabs key (default: platform account)
|
|
317
|
+
--limit <n> | --json Show more / raw JSON
|
|
304
318
|
stt <file|url> Video or audio → transcript (alias: transcribe). LOCAL: local ffmpeg demux +
|
|
305
319
|
Returns BOTH formats: the simple subtitle your provider key (gemini labels
|
|
306
320
|
version (plain text + timed SRT cues) and speakers; openai/openrouter give
|
|
@@ -315,8 +329,11 @@ Speech (TTS/STT) — LOCAL-FIRST on your own AI key; --cloud is the explicit bac
|
|
|
315
329
|
timestamps (words[] in the .json/--json output), no speaker labels.
|
|
316
330
|
When NO provider key is found the default byok engine auto-falls
|
|
317
331
|
back here. --model then picks the whisper model (tiny.en…large-v3).
|
|
318
|
-
--cloud BACKUP:
|
|
332
|
+
--cloud BACKUP: platform job → POST /api/v1/primitives/audio/transcribe (+ poll).
|
|
333
|
+
Defaults to ElevenLabs Scribe on the platform key (wallet-billed;
|
|
334
|
+
native diarization + real word timings).
|
|
319
335
|
(a local file uploads to your temp folder first — 30-day TTL)
|
|
336
|
+
--own-key (--cloud) use your own ElevenLabs / BYOK key instead of the wallet
|
|
320
337
|
|
|
321
338
|
Animated captions (word-by-word TikTok/CapCut styles on a pulled/served composition):
|
|
322
339
|
captions generate <dir> Transcribe the narration & lay down animated LOCAL: local ffmpeg demux +
|
|
@@ -586,6 +603,12 @@ async function main() {
|
|
|
586
603
|
case "tts":
|
|
587
604
|
await runTtsCommand(rest);
|
|
588
605
|
return;
|
|
606
|
+
case "music":
|
|
607
|
+
await runMusicCommand(rest);
|
|
608
|
+
return;
|
|
609
|
+
case "voices":
|
|
610
|
+
await runVoicesCommand(rest);
|
|
611
|
+
return;
|
|
589
612
|
case "stt":
|
|
590
613
|
case "transcribe":
|
|
591
614
|
await runSttCommand(rest);
|
|
@@ -3045,10 +3068,17 @@ async function runTtsCommand(argv) {
|
|
|
3045
3068
|
"gemini-key": { type: "string" },
|
|
3046
3069
|
"openai-key": { type: "string" },
|
|
3047
3070
|
"openrouter-key": { type: "string" },
|
|
3071
|
+
"own-key": { type: "boolean", default: false },
|
|
3072
|
+
"list-voices": { type: "boolean", default: false },
|
|
3048
3073
|
"no-wait": { type: "boolean", default: false },
|
|
3049
3074
|
tracer: { type: "string" }
|
|
3050
3075
|
}
|
|
3051
3076
|
});
|
|
3077
|
+
// --list-voices: browse the ElevenLabs voice catalog and exit (cloud call).
|
|
3078
|
+
if (parsed.values["list-voices"]) {
|
|
3079
|
+
await runVoicesCommand(parsed.values["own-key"] ? ["--own-key"] : []);
|
|
3080
|
+
return;
|
|
3081
|
+
}
|
|
3052
3082
|
const text = (parsed.values.text ?? parsed.positionals.join(" ")).trim();
|
|
3053
3083
|
if (!text)
|
|
3054
3084
|
throw new Error('tts requires text: vidfarm tts "Welcome back to the channel" --style "calm, warm narrator".');
|
|
@@ -3118,7 +3148,9 @@ async function runTtsCommand(argv) {
|
|
|
3118
3148
|
}
|
|
3119
3149
|
return;
|
|
3120
3150
|
}
|
|
3121
|
-
// BACKUP: the platform primitive route (
|
|
3151
|
+
// BACKUP: the platform primitive route (async job). Defaults to high-quality
|
|
3152
|
+
// ElevenLabs on the platform key, billed to the wallet (use_wallet_credits).
|
|
3153
|
+
// --own-key flips to your own saved ElevenLabs / BYOK provider key instead.
|
|
3122
3154
|
const ctx = commonContext(parsed.values);
|
|
3123
3155
|
const payload = { text, output_format: format };
|
|
3124
3156
|
if (voice)
|
|
@@ -3129,6 +3161,11 @@ async function runTtsCommand(argv) {
|
|
|
3129
3161
|
payload.provider = parsed.values.provider;
|
|
3130
3162
|
if (parsed.values.model)
|
|
3131
3163
|
payload.model = parsed.values.model;
|
|
3164
|
+
if (parsed.values["own-key"])
|
|
3165
|
+
payload.use_wallet_credits = false;
|
|
3166
|
+
if (!ctx.json && !parsed.values["own-key"]) {
|
|
3167
|
+
console.log(`${DIM}Using ElevenLabs on the platform key (wallet-billed). Browse voices: vidfarm voices — then pass --voice <voice_id>. Add --own-key to use your own key.${RESET}`);
|
|
3168
|
+
}
|
|
3132
3169
|
const tracer = parsed.values.tracer ?? `devcli-tts-${Date.now().toString(36)}`;
|
|
3133
3170
|
const submit = await apiRequest({ method: "POST", host: ctx.host, path: "/api/v1/primitives/audio/speech", auth: ctx.auth, body: { tracer, payload } });
|
|
3134
3171
|
assertApiOk(submit, "tts");
|
|
@@ -3290,6 +3327,112 @@ function emitSttResult(input) {
|
|
|
3290
3327
|
console.log(`${GREEN}Wrote ${dest}${RESET}`);
|
|
3291
3328
|
}
|
|
3292
3329
|
}
|
|
3330
|
+
// Music generation (ElevenLabs) — cloud-only (no local engine). Defaults to the
|
|
3331
|
+
// platform key billed to the wallet; --own-key uses your saved ElevenLabs key.
|
|
3332
|
+
async function runMusicCommand(argv) {
|
|
3333
|
+
const parsed = parseArgs({
|
|
3334
|
+
args: argv,
|
|
3335
|
+
allowPositionals: true,
|
|
3336
|
+
options: {
|
|
3337
|
+
...commonOptions(),
|
|
3338
|
+
prompt: { type: "string" },
|
|
3339
|
+
"length-ms": { type: "string" },
|
|
3340
|
+
length: { type: "string" },
|
|
3341
|
+
model: { type: "string" },
|
|
3342
|
+
out: { type: "string" },
|
|
3343
|
+
"own-key": { type: "boolean", default: false },
|
|
3344
|
+
"no-wait": { type: "boolean", default: false },
|
|
3345
|
+
tracer: { type: "string" }
|
|
3346
|
+
}
|
|
3347
|
+
});
|
|
3348
|
+
const prompt = (parsed.values.prompt ?? parsed.positionals.join(" ")).trim();
|
|
3349
|
+
if (!prompt)
|
|
3350
|
+
throw new Error('music requires a prompt: vidfarm music "upbeat lo-fi hip hop beat with jazzy piano" --length 30');
|
|
3351
|
+
const ctx = commonContext(parsed.values);
|
|
3352
|
+
let musicLengthMs;
|
|
3353
|
+
const lengthMs = parsed.values["length-ms"]?.trim();
|
|
3354
|
+
const lengthSec = parsed.values.length?.trim();
|
|
3355
|
+
if (lengthMs)
|
|
3356
|
+
musicLengthMs = Math.round(Number(lengthMs));
|
|
3357
|
+
else if (lengthSec)
|
|
3358
|
+
musicLengthMs = Math.round(Number(lengthSec) * 1000);
|
|
3359
|
+
const payload = { prompt };
|
|
3360
|
+
if (musicLengthMs && Number.isFinite(musicLengthMs))
|
|
3361
|
+
payload.music_length_ms = musicLengthMs;
|
|
3362
|
+
if (parsed.values.model)
|
|
3363
|
+
payload.model = parsed.values.model;
|
|
3364
|
+
if (parsed.values["own-key"])
|
|
3365
|
+
payload.use_wallet_credits = false;
|
|
3366
|
+
if (!ctx.json && !parsed.values["own-key"]) {
|
|
3367
|
+
console.log(`${DIM}Composing with ElevenLabs on the platform key (wallet-billed). Add --own-key to use your own ElevenLabs key.${RESET}`);
|
|
3368
|
+
}
|
|
3369
|
+
const tracer = parsed.values.tracer ?? `devcli-music-${Date.now().toString(36)}`;
|
|
3370
|
+
const submit = await apiRequest({ method: "POST", host: ctx.host, path: "/api/v1/primitives/music/generate", auth: ctx.auth, body: { tracer, payload } });
|
|
3371
|
+
assertApiOk(submit, "music");
|
|
3372
|
+
const jobId = submit.json?.job_id;
|
|
3373
|
+
if (parsed.values["no-wait"] || !jobId) {
|
|
3374
|
+
if (!ctx.json && jobId)
|
|
3375
|
+
console.log(`${DIM}Queued ${jobId} (tracer ${tracer}). Poll: vidfarm api GET /api/v1/user/me/jobs/${jobId} — or drop --no-wait to auto-poll.${RESET}`);
|
|
3376
|
+
emitResult(submit, ctx.json);
|
|
3377
|
+
return;
|
|
3378
|
+
}
|
|
3379
|
+
if (!ctx.json)
|
|
3380
|
+
console.log(`${DIM}Generating music (${jobId})… polling every 5s.${RESET}`);
|
|
3381
|
+
const job = await pollPrimitiveJob(ctx, jobId);
|
|
3382
|
+
const mediaUrl = resolveJobMediaUrl(job);
|
|
3383
|
+
if (!mediaUrl) {
|
|
3384
|
+
if (ctx.json) {
|
|
3385
|
+
printJson({ ok: false, job_id: jobId, status: String(job?.status ?? ""), job });
|
|
3386
|
+
}
|
|
3387
|
+
else {
|
|
3388
|
+
console.log(`${RED}Music ${String(job?.status ?? "did not finish")} — no audio URL.${RESET}`);
|
|
3389
|
+
printJson(job);
|
|
3390
|
+
}
|
|
3391
|
+
process.exitCode = 1;
|
|
3392
|
+
return;
|
|
3393
|
+
}
|
|
3394
|
+
const outPath = path.resolve(process.cwd(), parsed.values.out ?? `music-${jobId}.mp3`);
|
|
3395
|
+
await downloadUrlToFile(mediaUrl, outPath);
|
|
3396
|
+
if (ctx.json) {
|
|
3397
|
+
printJson({ ok: true, job_id: jobId, tracer, audio_url: mediaUrl, out: outPath });
|
|
3398
|
+
}
|
|
3399
|
+
else {
|
|
3400
|
+
console.log(`${GREEN}Wrote ${outPath}${RESET} ${DIM}(${mediaUrl})${RESET}`);
|
|
3401
|
+
console.log(`${DIM}Place it as its own <audio> layer under narration (~0.1–0.2 volume).${RESET}`);
|
|
3402
|
+
}
|
|
3403
|
+
}
|
|
3404
|
+
// List ElevenLabs voices for TTS. --own-key lists the voices on your own saved
|
|
3405
|
+
// ElevenLabs key; default lists the platform account's voices.
|
|
3406
|
+
async function runVoicesCommand(argv) {
|
|
3407
|
+
const parsed = parseArgs({
|
|
3408
|
+
args: argv,
|
|
3409
|
+
allowPositionals: true,
|
|
3410
|
+
options: {
|
|
3411
|
+
...commonOptions(),
|
|
3412
|
+
"own-key": { type: "boolean", default: false },
|
|
3413
|
+
limit: { type: "string" }
|
|
3414
|
+
}
|
|
3415
|
+
});
|
|
3416
|
+
const ctx = commonContext(parsed.values);
|
|
3417
|
+
const query = parsed.values["own-key"] ? "?use_wallet_credits=false" : "";
|
|
3418
|
+
const res = await apiRequest({ method: "GET", host: ctx.host, path: `/api/v1/primitives/audio/voices${query}`, auth: ctx.auth });
|
|
3419
|
+
assertApiOk(res, "voices");
|
|
3420
|
+
const body = (res.json ?? {});
|
|
3421
|
+
const voices = Array.isArray(body.voices) ? body.voices : [];
|
|
3422
|
+
if (ctx.json) {
|
|
3423
|
+
printJson(body);
|
|
3424
|
+
return;
|
|
3425
|
+
}
|
|
3426
|
+
console.log(`${DIM}${voices.length} ElevenLabs voices (${body.scope ?? "platform"} account). Browse more: ${body.voice_library_url ?? "https://elevenlabs.io/app/voice-library"}${RESET}`);
|
|
3427
|
+
const limit = Number(parsed.values.limit ?? "40") || 40;
|
|
3428
|
+
for (const voice of voices.slice(0, limit)) {
|
|
3429
|
+
const labels = voice.labels && typeof voice.labels === "object" ? Object.values(voice.labels).join(", ") : "";
|
|
3430
|
+
console.log(`${GREEN}${voice.voice_id}${RESET} ${voice.name}${labels ? ` ${DIM}(${labels})${RESET}` : ""}`);
|
|
3431
|
+
}
|
|
3432
|
+
if (voices.length > limit)
|
|
3433
|
+
console.log(`${DIM}… ${voices.length - limit} more (raise with --limit or use --json).${RESET}`);
|
|
3434
|
+
console.log(`${DIM}Use one: vidfarm tts "Hello" --cloud --voice <voice_id>${RESET}`);
|
|
3435
|
+
}
|
|
3293
3436
|
async function runSttCommand(argv) {
|
|
3294
3437
|
const parsed = parseArgs({
|
|
3295
3438
|
args: argv,
|
|
@@ -3307,6 +3450,7 @@ async function runSttCommand(argv) {
|
|
|
3307
3450
|
"gemini-key": { type: "string" },
|
|
3308
3451
|
"openai-key": { type: "string" },
|
|
3309
3452
|
"openrouter-key": { type: "string" },
|
|
3453
|
+
"own-key": { type: "boolean", default: false },
|
|
3310
3454
|
"no-wait": { type: "boolean", default: false },
|
|
3311
3455
|
tracer: { type: "string" },
|
|
3312
3456
|
folder: { type: "string" }
|
|
@@ -3405,6 +3549,11 @@ async function runSttCommand(argv) {
|
|
|
3405
3549
|
payload.provider = parsed.values.provider;
|
|
3406
3550
|
if (parsed.values.model)
|
|
3407
3551
|
payload.model = parsed.values.model;
|
|
3552
|
+
// Cloud STT defaults to ElevenLabs Scribe on the platform key (native
|
|
3553
|
+
// diarization + real word timings), wallet-billed. --own-key uses your own
|
|
3554
|
+
// ElevenLabs / BYOK key instead.
|
|
3555
|
+
if (parsed.values["own-key"])
|
|
3556
|
+
payload.use_wallet_credits = false;
|
|
3408
3557
|
const tracer = parsed.values.tracer ?? `devcli-stt-${Date.now().toString(36)}`;
|
|
3409
3558
|
const submit = await apiRequest({ method: "POST", host: ctx.host, path: "/api/v1/primitives/audio/transcribe", auth: ctx.auth, body: { tracer, payload } });
|
|
3410
3559
|
assertApiOk(submit, "stt");
|
|
@@ -3953,7 +4102,8 @@ async function runClipperCommand(argv) {
|
|
|
3953
4102
|
out: { type: "string" },
|
|
3954
4103
|
tracer: { type: "string" },
|
|
3955
4104
|
folder: { type: "string" },
|
|
3956
|
-
name: { type: "string" }
|
|
4105
|
+
name: { type: "string" },
|
|
4106
|
+
"audio-only": { type: "boolean", default: false }
|
|
3957
4107
|
}
|
|
3958
4108
|
});
|
|
3959
4109
|
const source = parsed.positionals[0];
|
|
@@ -3970,6 +4120,10 @@ async function runClipperCommand(argv) {
|
|
|
3970
4120
|
if (!wholeVideo && (!hasStart || !hasEnd)) {
|
|
3971
4121
|
throw new Error("clipper needs BOTH --start and --end for an exact subrange, or NEITHER to import the whole video.");
|
|
3972
4122
|
}
|
|
4123
|
+
const audioOnly = Boolean(parsed.values["audio-only"]);
|
|
4124
|
+
if (audioOnly && wholeVideo) {
|
|
4125
|
+
throw new Error("--audio-only needs an exact subrange (--start <t> --end <t>). Whole-video import keeps the video track.");
|
|
4126
|
+
}
|
|
3973
4127
|
const body = {};
|
|
3974
4128
|
if (parsed.values.tracer)
|
|
3975
4129
|
body.tracer = String(parsed.values.tracer);
|
|
@@ -3977,6 +4131,8 @@ async function runClipperCommand(argv) {
|
|
|
3977
4131
|
body.folder_path = String(parsed.values.folder);
|
|
3978
4132
|
if (parsed.values.name)
|
|
3979
4133
|
body.name = String(parsed.values.name);
|
|
4134
|
+
if (audioOnly)
|
|
4135
|
+
body.audio_only = true;
|
|
3980
4136
|
// Resolve the source into the body once: a URL rides as source_url; a local
|
|
3981
4137
|
// file is staged into the temp store and referenced by temp_file_id.
|
|
3982
4138
|
if (/^https?:\/\//i.test(source)) {
|
|
@@ -4037,7 +4193,8 @@ async function runClipperCommand(argv) {
|
|
|
4037
4193
|
}
|
|
4038
4194
|
const j = res.json;
|
|
4039
4195
|
const dest = j?.folder_path ? `/raws/${j.folder_path}` : "your raws library";
|
|
4040
|
-
|
|
4196
|
+
const kindWord = (j?.media_kind === "audio" || audioOnly) ? "audio" : "clip";
|
|
4197
|
+
console.log(`${GREEN}Clipped ${kindWord} ${(j?.duration_sec ?? (endSec - startSec)).toFixed?.(2) ?? j?.duration_sec}s → ${dest} (clip ${j?.clip_id ?? "?"}).${RESET}`);
|
|
4041
4198
|
if (j?.view_url)
|
|
4042
4199
|
console.log(`${DIM}Preview: ${new URL(j.view_url, ctx.host).toString()}${RESET}`);
|
|
4043
4200
|
console.log(`${DIM}Grab another range from the same source with the same --tracer/--folder.${RESET}`);
|
package/dist/src/config.js
CHANGED
|
@@ -66,6 +66,18 @@ const schema = z.object({
|
|
|
66
66
|
OPENROUTER_API_KEY: z.string().optional(),
|
|
67
67
|
GEMINI_API_KEY: z.string().optional(),
|
|
68
68
|
PERPLEXITY_API_KEY: z.string().optional(),
|
|
69
|
+
// Platform ElevenLabs key — the fallback used for music / TTS / STT primitives
|
|
70
|
+
// when the caller opts into `use_wallet_credits` (the default) or has no usable
|
|
71
|
+
// BYOK audio key. Usage on this key is billed to the customer's vidfarm wallet.
|
|
72
|
+
ELEVENLABS_API_KEY: z.string().optional(),
|
|
73
|
+
// Base provider costs for ElevenLabs primitives billed to the wallet (the
|
|
74
|
+
// billing lambda applies the standard markup on top). Defaults track published
|
|
75
|
+
// ElevenLabs list pricing: TTS multilingual v2/v3 $0.10 / 1K chars (flash/turbo
|
|
76
|
+
// is $0.05), Music $0.15 / minute, Scribe STT $0.22 / hour ≈ $0.003667 / min.
|
|
77
|
+
// Override per-environment as pricing shifts.
|
|
78
|
+
ELEVENLABS_MUSIC_USD_PER_MIN: z.coerce.number().min(0).default(0.15),
|
|
79
|
+
ELEVENLABS_TTS_USD_PER_1K_CHARS: z.coerce.number().min(0).default(0.10),
|
|
80
|
+
ELEVENLABS_STT_USD_PER_MIN: z.coerce.number().min(0).default(0.003667),
|
|
69
81
|
GHOSTCUT_KEY: z.string().optional(),
|
|
70
82
|
GHOSTCUT_SECRET: z.string().optional(),
|
|
71
83
|
GHOSTCUT_USD_PER_30S: z.coerce.number().min(0).default(0.10),
|
package/dist/src/devcli/clips.js
CHANGED
|
@@ -44,6 +44,7 @@ export const CLIPS_HELP = `vidfarm raws — build & search a local raws library
|
|
|
44
44
|
--yes Skip the >$1 confirmation prompt
|
|
45
45
|
--frames <n> Keyframes per scene for tagging (1-3, default 2)
|
|
46
46
|
--no-audio Skip per-scene audio (Gemini only; no transcripts; cheaper)
|
|
47
|
+
--audio-only Save each mined clip as an audio-only .m4a (no video); waveform thumb
|
|
47
48
|
--concurrency <n> Parallel scenes (default 3; agent default 2)
|
|
48
49
|
--threshold <0..1> Scene-cut sensitivity (default 0.3; lower = more cuts)
|
|
49
50
|
--min-scene <sec> Merge scenes shorter than this (default 0.6)
|
|
@@ -133,6 +134,7 @@ async function runScan(argv) {
|
|
|
133
134
|
"allow-text": { type: "boolean" },
|
|
134
135
|
"max-clips": { type: "string" },
|
|
135
136
|
"no-refine": { type: "boolean" },
|
|
137
|
+
"audio-only": { type: "boolean" },
|
|
136
138
|
cloud: { type: "boolean" },
|
|
137
139
|
url: { type: "string" },
|
|
138
140
|
tracer: { type: "string" },
|
|
@@ -197,7 +199,8 @@ async function runScan(argv) {
|
|
|
197
199
|
aspect,
|
|
198
200
|
cropFocus,
|
|
199
201
|
avoidText,
|
|
200
|
-
maxClips
|
|
202
|
+
maxClips,
|
|
203
|
+
audioOnly: Boolean(values["audio-only"])
|
|
201
204
|
});
|
|
202
205
|
}
|
|
203
206
|
const videoArg = positionals[0];
|
|
@@ -336,6 +339,7 @@ async function runScan(argv) {
|
|
|
336
339
|
guidancePrompt: guidance,
|
|
337
340
|
refineSegmentation: !values["no-refine"],
|
|
338
341
|
reencodeClips: true,
|
|
342
|
+
audioOnly: Boolean(values["audio-only"]),
|
|
339
343
|
crop: aspect && aspect !== "original" ? { aspect, focus: cropFocus } : undefined
|
|
340
344
|
};
|
|
341
345
|
console.log(`[raws] scanning ${scenes.length} scenes (${provider}/${activeClient.tagModelId}, ${concurrency}x) …`);
|
|
@@ -466,7 +470,8 @@ async function runScanCloud(input) {
|
|
|
466
470
|
// Send explicitly (incl. false) so --allow-text can override the
|
|
467
471
|
// server-side avoid-text default.
|
|
468
472
|
avoid_text: input.avoidText,
|
|
469
|
-
...(input.maxClips ? { max_clips: input.maxClips } : {})
|
|
473
|
+
...(input.maxClips ? { max_clips: input.maxClips } : {}),
|
|
474
|
+
...(input.audioOnly ? { audio_only: true } : {})
|
|
470
475
|
}
|
|
471
476
|
})
|
|
472
477
|
});
|
package/dist/src/domain.js
CHANGED
|
@@ -1,2 +1,5 @@
|
|
|
1
|
+
// Providers that count as a qualified AI key for text/image/video generation
|
|
2
|
+
// (everything except the audio-only elevenlabs key).
|
|
3
|
+
export const QUALIFIED_AI_PROVIDER_TYPES = ["openai", "gemini", "openrouter", "perplexity"];
|
|
1
4
|
export const CUSTOMER_PLAN_TIERS = ["client", "standalone", "agency"];
|
|
2
5
|
//# sourceMappingURL=domain.js.map
|
package/dist/src/editor-chat.js
CHANGED
|
@@ -21,6 +21,10 @@ export function defaultEditorChatModel(provider) {
|
|
|
21
21
|
return "qwen/qwen3.6-plus";
|
|
22
22
|
case "perplexity":
|
|
23
23
|
return "sonar";
|
|
24
|
+
default:
|
|
25
|
+
// Non-text providers (e.g. audio-only elevenlabs) never reach editor chat;
|
|
26
|
+
// return a safe text default rather than undefined.
|
|
27
|
+
return "gpt-5.4-mini";
|
|
24
28
|
}
|
|
25
29
|
}
|
|
26
30
|
export function buildTemplateEditorChatSystemPrompt(input) {
|
|
@@ -75,7 +79,7 @@ export function buildTemplateEditorChatSystemPrompt(input) {
|
|
|
75
79
|
"FINE TIMELINE CONTROL — you have trackpad-level verbs; use them instead of rewriting the whole document for small changes: (1) set_layer_keyframes authors a custom, script-free CSS @keyframes animation on ONE layer that previews AND renders — pass layer_key + keyframes:[{offset(0..1), opacity?, translate_x?, translate_y?, scale?, rotate?}, …] (>=2 stops), optional keyframe_easing (linear/ease/ease-in/ease-out/ease-in-out/cubic-bezier(...)) and keyframe_duration (seconds, default = clip duration). translate_x/translate_y are percentages of the layer's OWN box. Example fly-up-and-fade-in: keyframes=[{offset:0,opacity:0,translate_y:40},{offset:1,opacity:1,translate_y:0}]. This is the durable way to hand-author motion beyond the Ken Burns / transition / caption presets — the JS runtime adapters (anime.js/GSAP/Lottie) are devcli-only. (2) nudge_layers shifts one or more layers by a RELATIVE delta_start (seconds) and/or delta_track (lanes) — pass layer_key or layer_keys; grouped clips move together. (3) ripple_edit inserts (delta_start>0) or closes (delta_start<0) time at at_time and shifts every downstream clip so the rest of the timeline stays in sync — use it to make room before an insert or to close a gap you can see in timeline_gaps. (4) trim_layer moves ONE edge to a time: edge='start' (left; advances the in-point, and for video/audio pushes the media start so the visible frame is unchanged) or edge='end' (right; changes duration), with to_time in composition seconds. (5) set_layer_zindex restacks: z_order='front'|'back'|'forward'|'backward' (stacking == track index; higher draws on top) or an explicit track. All of these auto-resolve track collisions and note it in the summary; read the NEXT editor_context to confirm the new start/duration/track/animation landed.",
|
|
76
80
|
"If the user asks to export, render, publish, or produce the final MP4 for this composition (phrases like 'export it', 'render this', 'kick off the export', 'make the mp4'), call editor_action with action_type=export_composition and a brief explanation. This is the same action as the editor's Export button — it queues the composition on production AWS Lambda. Do NOT try to hit any /api/v1/compositions/:id/export route via http_request for this; use the editor_action tool. After calling export_composition, tell the user the export has started and that the finished MP4 URL will appear once the render succeeds; do not fabricate a URL or claim success until a later turn shows last_export_url populated in editor_context.",
|
|
77
81
|
"CONFIRM BEFORE RENDERING — DO NOT auto-render. Rendering the final MP4 is a slow, costly, screen-taking action, so by DEFAULT you must NOT call export_composition on your own initiative. After you finish an edit (a swap, a re-theme, a scene rebuild), STOP and let the user review the live preview, then ASK a short question like 'Happy with the preview? Want me to render the final MP4?' and wait for a yes. Only fire export_composition when EITHER (a) the user explicitly asks to render/export/'make the mp4' in that turn, or (b) the user has told you up front to just render without asking (e.g. 'edit and render it', 'no need to check, just export'). If in doubt, ask — never surprise the user with a render. Never render merely because you judged the edit 'done'.",
|
|
78
|
-
"MEDIA SWAP — TARGET A REAL LAYER KEY, NOT A SCENE NAME. To swap a clip's media with set_layer_media, the layer_key MUST be an EXACT key (or its slug) from editor_context.layers — that array lists every targetable layer with its key, slug, kind (video/image/audio), and current src. Read it and copy the real key of the video/image layer you mean; do NOT invent a semantic name (like 'growth_examples' or 'mistakes_reflection') unless that exact string appears as a layer key/slug in editor_context, and never target a scene label/group when you mean the video inside it. If set_layer_media returns 'No change applied' or 'Layer not found', the error now lists the actual media layers — re-read it and retry with one of those exact keys. NEVER report a swap as done
|
|
82
|
+
"MEDIA SWAP — TARGET A REAL LAYER KEY, NOT A SCENE NAME. To swap a clip's media with set_layer_media, the layer_key MUST be an EXACT key (or its slug) from editor_context.layers — that array lists every targetable layer with its key, slug, kind (video/image/audio), and current src. Read it and copy the real key of the video/image layer you mean; do NOT invent a semantic name (like 'growth_examples' or 'mistakes_reflection') unless that exact string appears as a layer key/slug in editor_context, and never target a scene label/group when you mean the video inside it. If set_layer_media returns 'No change applied' or 'Layer not found', the error now lists the actual media layers — re-read it and retry with one of those exact keys. This applies to EVERY set_layer_media field (src, object_fit, object_position, volume, muted, ken_burns, transitions), not just src: the layer_key you used to add a layer (e.g. add_layer layer_key='vf-fishing-snippet-1') becomes an 'element_'-prefixed key on the timeline, so target the real key/slug shown in editor_context, not the bare name you passed earlier. NEVER report a swap or fit/position change as done while any editor_action in this turn came back with a ⚠ / 'Layer not found' error — those calls did NOT apply; re-read the listed keys, retry against a real one, and only confirm success once the action returns ✓ (and, for src, the NEXT editor_context shows the layer changed). Do not claim you changed layers you never successfully targeted.",
|
|
79
83
|
"GETTING THE MEDIA URL IS EASY — USE browse_files. When the user says 'use my raws' / 'it's in my file directory' / 'the fishing clips', you do NOT need them to paste a URL. Call browse_files action=search (or list path='/raws') to find the clip, then action=read with its file_id — the read result's view_url IS the public, ready-to-use media URL. Drop that view_url straight into set_layer_media src (or add_layer src). Do not stall asking the user for a URL you can look up yourself, and never pass a placeholder like 'please-ignore' as src.",
|
|
80
84
|
"The editor_context block includes last_export_url, last_export_title, last_export_status, and last_export_render_id when a render has succeeded in this session. Treat last_export_url as the current 'finished Export MP4' for this composition. If the user asks to approve, package, share, or schedule the exported video without providing an explicit URL, use last_export_url as the primary MP4 media (kind=video, role=primary) for POST /api/v1/approved/posts. If last_export_url is null, tell the user to Export first (or offer to call editor_action with action_type=export_composition on their behalf) instead of guessing a URL.",
|
|
81
85
|
"Each layer in editor_context includes element_id (data-hf-id), optional slug (data-hf-slug), and optional note (data-hf-note). layer_key on any editor_action may be EITHER the element_id or the slug — prefer the slug when the user (or a viral DNA blurb) refers to a layer by its human name like 'the hero_image'. Use set_layer_identity to add/change a layer's slug or note. Slugs are the stable AI-friendly handle: when planning viral DNA notes, reference the element by its slug (e.g., 'raise the {{hero_image}} to full canvas at 4s') so future turns can resolve it deterministically.",
|
|
@@ -501,20 +501,23 @@ export function createDirectoryExplorer(mount, opts = {}) {
|
|
|
501
501
|
return;
|
|
502
502
|
selbar.innerHTML = "";
|
|
503
503
|
selbar.appendChild(el("span", "rk-dir-selcount", n + " selected"));
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
selbar.appendChild(att);
|
|
509
|
-
}
|
|
510
|
-
const cp = el("button", "rk-dir-selbtn", "Copy paths");
|
|
504
|
+
const picked = () => Array.from(selection.values());
|
|
505
|
+
// Primary action stays on the bar; everything else tucks into a "⋯" menu so
|
|
506
|
+
// the toolbar never overflows in the narrow editor dock.
|
|
507
|
+
const cp = el("button", "rk-dir-selbtn is-primary", "Copy paths");
|
|
511
508
|
cp.type = "button";
|
|
512
|
-
cp.addEventListener("click", () => copyPath(
|
|
513
|
-
const clr = el("button", "rk-dir-selbtn is-ghost", "Clear");
|
|
514
|
-
clr.type = "button";
|
|
515
|
-
clr.addEventListener("click", clearSelection);
|
|
509
|
+
cp.addEventListener("click", () => copyPath(picked().map((s) => s.path).join("\n")));
|
|
516
510
|
selbar.appendChild(cp);
|
|
517
|
-
|
|
511
|
+
const more = [];
|
|
512
|
+
if (mode === "attach")
|
|
513
|
+
more.push({ label: "Attach", run: () => { emitAttach(picked()); clearSelection(); } });
|
|
514
|
+
more.push({ label: "Copy paths", run: () => copyPath(picked().map((s) => s.path).join("\n")) });
|
|
515
|
+
more.push({ label: "Clear selection", run: clearSelection });
|
|
516
|
+
const menuBtn = el("button", "rk-dir-selbtn is-ghost rk-dir-selmore", "⋯");
|
|
517
|
+
menuBtn.type = "button";
|
|
518
|
+
menuBtn.setAttribute("aria-label", "More actions");
|
|
519
|
+
menuBtn.addEventListener("click", (e) => { e.stopPropagation(); openMenuAt(menuBtn, more); });
|
|
520
|
+
selbar.appendChild(menuBtn);
|
|
518
521
|
}
|
|
519
522
|
function checkbox(it) {
|
|
520
523
|
const box = el("input", "rk-dir-check");
|
|
@@ -719,15 +722,27 @@ export function createDirectoryExplorer(mount, opts = {}) {
|
|
|
719
722
|
return;
|
|
720
723
|
uploading = true;
|
|
721
724
|
const folder = currentFolder();
|
|
725
|
+
const root = currentRoot();
|
|
722
726
|
const arr = Array.from(list);
|
|
723
727
|
let done = 0, failed = 0;
|
|
728
|
+
// The server may re-home an upload (e.g. /temp files land in a dated
|
|
729
|
+
// subfolder). Track where the bytes actually landed so we can reveal them —
|
|
730
|
+
// otherwise the refresh shows the folder we uploaded FROM, which looks empty.
|
|
731
|
+
let landedFolder = null;
|
|
724
732
|
dropText.textContent = "Uploading " + arr.length + " file(s)…";
|
|
733
|
+
const finish = () => {
|
|
734
|
+
uploading = false;
|
|
735
|
+
if (failed)
|
|
736
|
+
toast(failed + " of " + arr.length + " failed");
|
|
737
|
+
const target = root && landedFolder != null ? buildDirectoryPath(root, landedFolder) : null;
|
|
738
|
+
if (target && target !== currentPath)
|
|
739
|
+
navigate(target); // reveal the uploads
|
|
740
|
+
else
|
|
741
|
+
load(true);
|
|
742
|
+
};
|
|
725
743
|
const next = (i) => {
|
|
726
744
|
if (i >= arr.length) {
|
|
727
|
-
|
|
728
|
-
if (failed)
|
|
729
|
-
toast(failed + " of " + arr.length + " failed");
|
|
730
|
-
load(true);
|
|
745
|
+
finish();
|
|
731
746
|
return;
|
|
732
747
|
}
|
|
733
748
|
const fd = new FormData();
|
|
@@ -735,7 +750,13 @@ export function createDirectoryExplorer(mount, opts = {}) {
|
|
|
735
750
|
fd.append("folder_path", folder);
|
|
736
751
|
fetch(endpoint, { method: "POST", credentials: "same-origin", body: fd })
|
|
737
752
|
.then((r) => { if (!r.ok)
|
|
738
|
-
throw new Error("http " + r.status);
|
|
753
|
+
throw new Error("http " + r.status); return r.json().catch(() => null); })
|
|
754
|
+
.then((j) => {
|
|
755
|
+
done++;
|
|
756
|
+
const fp = j && ((j.file && j.file.folderPath) ?? (j.attachment && j.attachment.folderPath));
|
|
757
|
+
if (typeof fp === "string" && landedFolder == null)
|
|
758
|
+
landedFolder = fp;
|
|
759
|
+
})
|
|
739
760
|
.catch(() => { failed++; })
|
|
740
761
|
.then(() => next(i + 1));
|
|
741
762
|
};
|
|
@@ -943,7 +964,9 @@ export function openDirectoryPicker(opts = {}) {
|
|
|
943
964
|
mode: "standalone",
|
|
944
965
|
showSearch: true,
|
|
945
966
|
showMultiSelect: false,
|
|
946
|
-
|
|
967
|
+
// Opt-in upload: when allowed, the explorer shows its drop zone so users can
|
|
968
|
+
// add a file to the currently-open folder (Files/Temp), then click it to pick.
|
|
969
|
+
showUpload: opts.allowUpload === true,
|
|
947
970
|
// In file mode a name-click resolves the pick; folders still navigate.
|
|
948
971
|
primaryClick: select === "file" ? "attach" : "copyPath",
|
|
949
972
|
onNavigate: (p) => syncFooter(p),
|
|
@@ -198,6 +198,20 @@ export function createPrimitiveJobContext(input) {
|
|
|
198
198
|
workerId: input.workerId,
|
|
199
199
|
...request
|
|
200
200
|
});
|
|
201
|
+
},
|
|
202
|
+
async generateMusic(request) {
|
|
203
|
+
return input.providers.generateMusic({
|
|
204
|
+
customerId: input.customer.id,
|
|
205
|
+
jobId: input.job.id,
|
|
206
|
+
workerId: input.workerId,
|
|
207
|
+
...request
|
|
208
|
+
});
|
|
209
|
+
},
|
|
210
|
+
async listElevenLabsVoices(request) {
|
|
211
|
+
return input.providers.listElevenLabsVoices({
|
|
212
|
+
customerId: input.customer.id,
|
|
213
|
+
...request
|
|
214
|
+
});
|
|
201
215
|
}
|
|
202
216
|
},
|
|
203
217
|
hyperframes: {
|