@mevdragon/vidfarm-devcli 0.19.1 → 0.20.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/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/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 +4 -0
- 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 +1 -0
- 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
|
@@ -7,6 +7,7 @@ import { ServerlessProviderKeyService } from "./serverless-provider-keys.js";
|
|
|
7
7
|
import sharp from "sharp";
|
|
8
8
|
import { ProviderAuthError, ProviderKeyUnavailableError, ProviderLeaseTimeoutError, ProviderQuotaExceededError, ProviderRateLimitError, ProviderTransientError, ProviderWaitExceededError, SpeechVoiceMismatchError, conciseProviderDetails, parseRetryAfterSeconds, rateLimitOrQuotaError } from "./provider-errors.js";
|
|
9
9
|
import { SUPPORTED_STT_MODELS, SUPPORTED_TTS_MODELS, defaultSpeechModelFor, generateSpeechWithKey, inferSpeechContentType, speechVoiceFamilyFor, speechVoiceMismatch, transcribeSpeechWithKey } from "./speech.js";
|
|
10
|
+
import { DEFAULT_ELEVENLABS_STT_MODEL, DEFAULT_ELEVENLABS_TTS_MODEL, elevenLabsListVoices, elevenLabsMusicCompose, elevenLabsSpeechToText, elevenLabsTextToSpeech, validateElevenLabsKey } from "./elevenlabs.js";
|
|
10
11
|
export { ProviderAuthError, ProviderKeyUnavailableError, ProviderLeaseTimeoutError, ProviderQuotaExceededError, ProviderRateLimitError, ProviderTransientError, ProviderWaitExceededError } from "./provider-errors.js";
|
|
11
12
|
const SUPPORTED_TEXT_MODELS = {
|
|
12
13
|
openai: new Set(["gpt-5.4"]),
|
|
@@ -70,6 +71,8 @@ const OPENAI_IMAGE_SIZES = new Set([
|
|
|
70
71
|
"auto"
|
|
71
72
|
]);
|
|
72
73
|
const GEMINI_IMAGE_ASPECT_RATIOS = new Set(["1:1", "16:9", "9:16", "4:3", "3:4", "21:9", "9:21"]);
|
|
74
|
+
// elevenlabs is intentionally absent — it validates against its own /v1/user
|
|
75
|
+
// endpoint (see validateProviderKey), not a chat-completion probe.
|
|
73
76
|
const PROVIDER_KEY_VALIDATION_MODELS = {
|
|
74
77
|
openai: "gpt-4.1-mini",
|
|
75
78
|
gemini: "gemini-3.1-flash-lite-preview",
|
|
@@ -334,6 +337,20 @@ export class ProviderService {
|
|
|
334
337
|
return [...new Set(providers)];
|
|
335
338
|
}
|
|
336
339
|
async validateProviderKey(input) {
|
|
340
|
+
// ElevenLabs is audio-only: validate against its own account endpoint, not a
|
|
341
|
+
// chat-completion probe.
|
|
342
|
+
if (input.provider === "elevenlabs") {
|
|
343
|
+
if (config.mockProviders) {
|
|
344
|
+
return { provider: input.provider, model: "elevenlabs", mock: true };
|
|
345
|
+
}
|
|
346
|
+
try {
|
|
347
|
+
await validateElevenLabsKey(input.secret);
|
|
348
|
+
return { provider: input.provider, model: "elevenlabs", mock: false };
|
|
349
|
+
}
|
|
350
|
+
catch (error) {
|
|
351
|
+
throw new Error(toProviderValidationMessage(input.provider, error));
|
|
352
|
+
}
|
|
353
|
+
}
|
|
337
354
|
const model = PROVIDER_KEY_VALIDATION_MODELS[input.provider];
|
|
338
355
|
if (!model) {
|
|
339
356
|
throw new Error(`Provider ${input.provider} does not support validation.`);
|
|
@@ -541,7 +558,123 @@ export class ProviderService {
|
|
|
541
558
|
await this.releaseLease(lease);
|
|
542
559
|
}
|
|
543
560
|
}
|
|
561
|
+
// --- Audio (music / TTS / STT): platform-key + wallet vs BYOK routing ---
|
|
562
|
+
async resolveCustomerElevenLabsKey(customerId) {
|
|
563
|
+
const now = new Date().toISOString();
|
|
564
|
+
const secret = (await this.serverlessProviderKeys.listProviderKeysWithSecrets(customerId))
|
|
565
|
+
.filter((entry) => String(entry.provider) === "elevenlabs")
|
|
566
|
+
.filter((entry) => String(entry.status) === "active")
|
|
567
|
+
.filter((entry) => !entry.cooldown_until || String(entry.cooldown_until) <= now)
|
|
568
|
+
.map((entry) => readStoredProviderSecret(String(entry.secret ?? "")))
|
|
569
|
+
.find((value) => value.trim().length > 0);
|
|
570
|
+
return secret ?? null;
|
|
571
|
+
}
|
|
572
|
+
async customerHasByokAudioKey(customerId) {
|
|
573
|
+
const providers = await this.listAvailableProvidersAsync(customerId);
|
|
574
|
+
return providers.some((provider) => provider === "openai" || provider === "gemini" || provider === "openrouter");
|
|
575
|
+
}
|
|
576
|
+
/**
|
|
577
|
+
* Decide how an audio call is authorized. Default (`use_wallet_credits` unset
|
|
578
|
+
* or true): the platform ElevenLabs key, billed to the wallet. Explicit
|
|
579
|
+
* `false`: the customer's own ElevenLabs key, else a BYOK openai/gemini/
|
|
580
|
+
* openrouter key (TTS/STT only), else fall back to the platform key + wallet.
|
|
581
|
+
*/
|
|
582
|
+
async resolveAudioAuthPlan(input) {
|
|
583
|
+
// `undefined` = legacy BYOK-only. Internal callers (captions, voice-matched
|
|
584
|
+
// regenerate-speech) never pass the flag, so they keep their exact behavior
|
|
585
|
+
// and never silently draw on the platform key or the wallet. Only surfaces
|
|
586
|
+
// that explicitly opt in (the music/tts/stt primitives) reach ElevenLabs.
|
|
587
|
+
if (input.useWalletCredits === undefined) {
|
|
588
|
+
return { mode: "byok" };
|
|
589
|
+
}
|
|
590
|
+
const platformKey = config.ELEVENLABS_API_KEY?.trim() || null;
|
|
591
|
+
if (input.useWalletCredits) {
|
|
592
|
+
if (platformKey) {
|
|
593
|
+
return { mode: "elevenlabs", apiKey: platformKey, walletBilled: true, scope: "platform" };
|
|
594
|
+
}
|
|
595
|
+
// Operator hasn't wired a platform key — honor the intent with the user's
|
|
596
|
+
// own ElevenLabs key (no charge) or a BYOK audio key before failing.
|
|
597
|
+
const userKey = await this.resolveCustomerElevenLabsKey(input.customerId);
|
|
598
|
+
if (userKey) {
|
|
599
|
+
return { mode: "elevenlabs", apiKey: userKey, walletBilled: false, scope: "user" };
|
|
600
|
+
}
|
|
601
|
+
if (input.byokFallback && (await this.customerHasByokAudioKey(input.customerId))) {
|
|
602
|
+
return { mode: "byok" };
|
|
603
|
+
}
|
|
604
|
+
throw new ProviderKeyUnavailableError("use_wallet_credits is on but no platform ElevenLabs key is configured, and you have no usable audio key. Ask the operator to set ELEVENLABS_API_KEY or save your own key.");
|
|
605
|
+
}
|
|
606
|
+
// use_wallet_credits === false: prefer the user's OWN key, then a BYOK audio
|
|
607
|
+
// key (TTS/STT), and only then fall back to the platform key + wallet.
|
|
608
|
+
const userKey = await this.resolveCustomerElevenLabsKey(input.customerId);
|
|
609
|
+
if (userKey) {
|
|
610
|
+
return { mode: "elevenlabs", apiKey: userKey, walletBilled: false, scope: "user" };
|
|
611
|
+
}
|
|
612
|
+
if (input.byokFallback && (await this.customerHasByokAudioKey(input.customerId))) {
|
|
613
|
+
return { mode: "byok" };
|
|
614
|
+
}
|
|
615
|
+
if (platformKey) {
|
|
616
|
+
return { mode: "elevenlabs", apiKey: platformKey, walletBilled: true, scope: "platform" };
|
|
617
|
+
}
|
|
618
|
+
throw new ProviderKeyUnavailableError(input.capability === "music"
|
|
619
|
+
? "Music needs ElevenLabs: save an ElevenLabs key in Settings, or ask the operator to configure the platform ELEVENLABS_API_KEY (use_wallet_credits)."
|
|
620
|
+
: `No audio provider available for ${input.capability}. Enable use_wallet_credits (platform ElevenLabs key), save your own ElevenLabs key, or add an OpenAI/Gemini/OpenRouter key.`);
|
|
621
|
+
}
|
|
622
|
+
/** Voices available to whichever ElevenLabs account will serve this customer. */
|
|
623
|
+
async listElevenLabsVoices(input) {
|
|
624
|
+
const plan = await this.resolveAudioAuthPlan({
|
|
625
|
+
customerId: input.customerId,
|
|
626
|
+
useWalletCredits: input.useWalletCredits,
|
|
627
|
+
byokFallback: false,
|
|
628
|
+
capability: "tts"
|
|
629
|
+
});
|
|
630
|
+
if (plan.mode !== "elevenlabs") {
|
|
631
|
+
throw new ProviderKeyUnavailableError("No ElevenLabs account is available to list voices.");
|
|
632
|
+
}
|
|
633
|
+
return { scope: plan.scope, voices: await elevenLabsListVoices(plan.apiKey) };
|
|
634
|
+
}
|
|
544
635
|
async generateSpeech(input) {
|
|
636
|
+
const plan = await this.resolveAudioAuthPlan({
|
|
637
|
+
customerId: input.customerId,
|
|
638
|
+
useWalletCredits: input.useWalletCredits,
|
|
639
|
+
byokFallback: true,
|
|
640
|
+
capability: "tts"
|
|
641
|
+
});
|
|
642
|
+
if (plan.mode === "elevenlabs") {
|
|
643
|
+
const model = /^eleven/i.test(input.model || "") ? input.model : DEFAULT_ELEVENLABS_TTS_MODEL;
|
|
644
|
+
const generated = await elevenLabsTextToSpeech({
|
|
645
|
+
apiKey: plan.apiKey,
|
|
646
|
+
text: input.text,
|
|
647
|
+
voice: input.voice,
|
|
648
|
+
model,
|
|
649
|
+
responseFormat: input.responseFormat
|
|
650
|
+
});
|
|
651
|
+
const billing = plan.walletBilled
|
|
652
|
+
? {
|
|
653
|
+
costUsd: Number(((generated.characterCount / 1000) * config.ELEVENLABS_TTS_USD_PER_1K_CHARS).toFixed(4)),
|
|
654
|
+
costCenterSlug: "elevenlabs_tts",
|
|
655
|
+
metadata: {
|
|
656
|
+
operation: "tts",
|
|
657
|
+
provider: "elevenlabs",
|
|
658
|
+
model,
|
|
659
|
+
character_count: generated.characterCount,
|
|
660
|
+
wallet_billed: true,
|
|
661
|
+
scope: plan.scope
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
: null;
|
|
665
|
+
return {
|
|
666
|
+
bytes: generated.bytes,
|
|
667
|
+
contentType: generated.contentType,
|
|
668
|
+
usage: generated.usage,
|
|
669
|
+
effectiveProvider: "elevenlabs",
|
|
670
|
+
effectiveModel: model,
|
|
671
|
+
billing
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
const byok = await this.generateSpeechByok(input);
|
|
675
|
+
return { ...byok.result, effectiveProvider: byok.provider, effectiveModel: byok.model, billing: null };
|
|
676
|
+
}
|
|
677
|
+
async generateSpeechByok(input) {
|
|
545
678
|
// Provider fallback must stay voice-aware: a Gemini preset routed to an
|
|
546
679
|
// OpenAI key (or the reverse) would 400 opaquely at the provider.
|
|
547
680
|
const attempts = (await providerAttempts({
|
|
@@ -562,11 +695,12 @@ export class ProviderService {
|
|
|
562
695
|
let lastError = null;
|
|
563
696
|
for (const attempt of attempts) {
|
|
564
697
|
try {
|
|
565
|
-
|
|
698
|
+
const result = await this.generateSpeechWithProvider({
|
|
566
699
|
...input,
|
|
567
700
|
provider: attempt.provider,
|
|
568
701
|
model: attempt.model
|
|
569
702
|
});
|
|
703
|
+
return { result, provider: attempt.provider, model: attempt.model };
|
|
570
704
|
}
|
|
571
705
|
catch (error) {
|
|
572
706
|
lastError = error;
|
|
@@ -581,6 +715,42 @@ export class ProviderService {
|
|
|
581
715
|
throw lastError instanceof Error ? lastError : new ProviderLeaseTimeoutError("No eligible AI speech provider key became available in time.");
|
|
582
716
|
}
|
|
583
717
|
}
|
|
718
|
+
/** ElevenLabs music generation. Platform key + wallet by default; the
|
|
719
|
+
* customer's own ElevenLabs key when use_wallet_credits is false. */
|
|
720
|
+
async generateMusic(input) {
|
|
721
|
+
const plan = await this.resolveAudioAuthPlan({
|
|
722
|
+
customerId: input.customerId,
|
|
723
|
+
useWalletCredits: input.useWalletCredits,
|
|
724
|
+
byokFallback: false,
|
|
725
|
+
capability: "music"
|
|
726
|
+
});
|
|
727
|
+
if (plan.mode !== "elevenlabs") {
|
|
728
|
+
throw new ProviderKeyUnavailableError("Music generation requires an ElevenLabs key.");
|
|
729
|
+
}
|
|
730
|
+
const generated = await elevenLabsMusicCompose({
|
|
731
|
+
apiKey: plan.apiKey,
|
|
732
|
+
prompt: input.prompt,
|
|
733
|
+
musicLengthMs: input.musicLengthMs,
|
|
734
|
+
compositionPlan: input.compositionPlan,
|
|
735
|
+
model: input.model,
|
|
736
|
+
outputFormat: input.outputFormat
|
|
737
|
+
});
|
|
738
|
+
const minutes = Math.max(0, generated.musicLengthMs) / 60000;
|
|
739
|
+
const billing = plan.walletBilled
|
|
740
|
+
? {
|
|
741
|
+
costUsd: Number((minutes * config.ELEVENLABS_MUSIC_USD_PER_MIN).toFixed(4)),
|
|
742
|
+
costCenterSlug: "elevenlabs_music",
|
|
743
|
+
metadata: {
|
|
744
|
+
operation: "music",
|
|
745
|
+
provider: "elevenlabs",
|
|
746
|
+
music_length_ms: generated.musicLengthMs,
|
|
747
|
+
wallet_billed: true,
|
|
748
|
+
scope: plan.scope
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
: null;
|
|
752
|
+
return { ...generated, scope: plan.scope, billing };
|
|
753
|
+
}
|
|
584
754
|
async generateSpeechWithProvider(input) {
|
|
585
755
|
assertSupportedSpeechModel("tts", input.provider, input.model);
|
|
586
756
|
const lease = await this.leaseCustomerKey({
|
|
@@ -1475,6 +1645,45 @@ export class ProviderService {
|
|
|
1475
1645
|
}
|
|
1476
1646
|
}
|
|
1477
1647
|
async transcribeSpeech(input) {
|
|
1648
|
+
const plan = await this.resolveAudioAuthPlan({
|
|
1649
|
+
customerId: input.customerId,
|
|
1650
|
+
useWalletCredits: input.useWalletCredits,
|
|
1651
|
+
byokFallback: true,
|
|
1652
|
+
capability: "stt"
|
|
1653
|
+
});
|
|
1654
|
+
if (plan.mode === "elevenlabs") {
|
|
1655
|
+
const model = /^scribe/i.test(input.model || "") ? input.model : DEFAULT_ELEVENLABS_STT_MODEL;
|
|
1656
|
+
const transcription = await elevenLabsSpeechToText({
|
|
1657
|
+
apiKey: plan.apiKey,
|
|
1658
|
+
audio: input.audio,
|
|
1659
|
+
contentType: input.contentType,
|
|
1660
|
+
model,
|
|
1661
|
+
diarize: input.diarize,
|
|
1662
|
+
languageCode: input.language
|
|
1663
|
+
});
|
|
1664
|
+
const minutes = Math.max(0, transcription.audioDurationSec ?? 0) / 60;
|
|
1665
|
+
const billing = plan.walletBilled
|
|
1666
|
+
? {
|
|
1667
|
+
costUsd: Number((minutes * config.ELEVENLABS_STT_USD_PER_MIN).toFixed(4)),
|
|
1668
|
+
costCenterSlug: "elevenlabs_stt",
|
|
1669
|
+
metadata: {
|
|
1670
|
+
operation: "stt",
|
|
1671
|
+
provider: "elevenlabs",
|
|
1672
|
+
model,
|
|
1673
|
+
audio_duration_sec: transcription.audioDurationSec,
|
|
1674
|
+
wallet_billed: true,
|
|
1675
|
+
scope: plan.scope
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
: null;
|
|
1679
|
+
const { audioDurationSec, ...rest } = transcription;
|
|
1680
|
+
void audioDurationSec;
|
|
1681
|
+
return { ...rest, effectiveProvider: "elevenlabs", effectiveModel: model, billing };
|
|
1682
|
+
}
|
|
1683
|
+
const byok = await this.transcribeSpeechByok(input);
|
|
1684
|
+
return { ...byok.result, effectiveProvider: byok.provider, effectiveModel: byok.model, billing: null };
|
|
1685
|
+
}
|
|
1686
|
+
async transcribeSpeechByok(input) {
|
|
1478
1687
|
const attempts = await providerAttempts({
|
|
1479
1688
|
providerKeyService: this.serverlessProviderKeys,
|
|
1480
1689
|
customerId: input.customerId,
|
|
@@ -1489,11 +1698,12 @@ export class ProviderService {
|
|
|
1489
1698
|
let lastError = null;
|
|
1490
1699
|
for (const attempt of attempts) {
|
|
1491
1700
|
try {
|
|
1492
|
-
|
|
1701
|
+
const result = await this.transcribeSpeechWithProvider({
|
|
1493
1702
|
...input,
|
|
1494
1703
|
provider: attempt.provider,
|
|
1495
1704
|
model: attempt.model
|
|
1496
1705
|
});
|
|
1706
|
+
return { result, provider: attempt.provider, model: attempt.model };
|
|
1497
1707
|
}
|
|
1498
1708
|
catch (error) {
|
|
1499
1709
|
lastError = error;
|
|
@@ -2041,6 +2251,10 @@ function formatProviderLabel(provider) {
|
|
|
2041
2251
|
return "OpenRouter";
|
|
2042
2252
|
case "perplexity":
|
|
2043
2253
|
return "Perplexity";
|
|
2254
|
+
case "elevenlabs":
|
|
2255
|
+
return "ElevenLabs";
|
|
2256
|
+
default:
|
|
2257
|
+
return provider;
|
|
2044
2258
|
}
|
|
2045
2259
|
}
|
|
2046
2260
|
function readStringField(value, field) {
|
|
@@ -66,6 +66,7 @@ export async function reconcileSwipeRows(customerId, rows) {
|
|
|
66
66
|
}
|
|
67
67
|
return out;
|
|
68
68
|
}
|
|
69
|
+
// Text generation only — the audio-only elevenlabs provider never appears here.
|
|
69
70
|
const TEXT_MODEL_DEFAULTS = {
|
|
70
71
|
openai: "gpt-5.4-mini",
|
|
71
72
|
gemini: "gemini-2.5-flash",
|
|
@@ -246,7 +247,9 @@ function parseJsonLoose(text) {
|
|
|
246
247
|
}
|
|
247
248
|
async function generateRecaption(input) {
|
|
248
249
|
const available = await providers.listAvailableProvidersAsync(input.customerId);
|
|
249
|
-
|
|
250
|
+
// Text generation only — never fall back to a non-text provider (e.g. the
|
|
251
|
+
// audio-only elevenlabs key), which has no text model.
|
|
252
|
+
const provider = TEXT_PROVIDER_PRIORITY.find((p) => available.includes(p));
|
|
250
253
|
if (!provider)
|
|
251
254
|
throw new NoProviderKeyError();
|
|
252
255
|
const generated = await providers.generateText({
|
|
@@ -254,7 +257,7 @@ async function generateRecaption(input) {
|
|
|
254
257
|
jobId: input.jobId,
|
|
255
258
|
workerId: "swipe-customize",
|
|
256
259
|
provider,
|
|
257
|
-
model: TEXT_MODEL_DEFAULTS[provider],
|
|
260
|
+
model: TEXT_MODEL_DEFAULTS[provider] ?? "gpt-5.4-mini",
|
|
258
261
|
prompt: input.prompt,
|
|
259
262
|
temperature: 0.7,
|
|
260
263
|
responseFormat: "json"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mevdragon/vidfarm-devcli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.20.0",
|
|
4
4
|
"description": "Local bridge for the Vidfarm Trackpad Editor. `vidfarm serve <template_id>` boots the FULL editor on localhost (disk-backed records/storage, free in-process render); edit composition.html on disk (Claude Code, Codex, etc.) and the browser live-morphs it.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|