@mevdragon/vidfarm-devcli 0.2.7 → 0.2.8
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/GETTING_STARTED.developers.md +14 -18
- package/README.md +19 -5
- package/SKILL.developer.md +140 -11
- package/dist/src/account-pages.js +1 -1
- package/dist/src/app.js +305 -9
- package/dist/src/cli.js +142 -6
- package/dist/src/config.js +2 -5
- package/dist/src/context.js +16 -0
- package/dist/src/db.js +33 -2
- package/dist/src/homepage.js +33 -1
- package/dist/src/lib/template-paths.js +10 -4
- package/dist/src/registry.js +9 -2
- package/dist/src/services/auth.js +2 -6
- package/dist/src/services/job-logs.js +4 -4
- package/dist/src/services/providers.js +470 -0
- package/dist/src/services/storage.js +13 -0
- package/dist/src/services/template-certification.js +16 -2
- package/dist/src/services/template-loader.js +17 -1
- package/dist/src/services/template-sources.js +249 -5
- package/{templates/template_0000 → dist/templates/vidfarm_template_0000}/src/remotion/Root.js +1 -0
- package/dist/templates/{template_0000 → vidfarm_template_0000}/src/template.js +9 -3
- package/package.json +4 -3
- package/templates/{template_0000 → vidfarm_template_0000}/README.md +23 -0
- package/templates/{template_0000 → vidfarm_template_0000}/package.json +0 -1
- package/templates/{template_0000 → vidfarm_template_0000}/src/sdk.ts +18 -0
- package/templates/{template_0000 → vidfarm_template_0000}/src/template.js +9 -3
- package/templates/{template_0000 → vidfarm_template_0000}/src/template.ts +10 -3
- package/templates/vidfarm_template_0000/tmp/solobacterium-moorei-slideshow.request.json +31 -0
- /package/dist/templates/{template_0000 → vidfarm_template_0000}/src/lib/images.js +0 -0
- /package/dist/templates/{template_0000 → vidfarm_template_0000}/src/remotion/index.js +0 -0
- /package/dist/templates/{template_0000 → vidfarm_template_0000}/src/sdk.js +0 -0
- /package/dist/templates/{template_0000 → vidfarm_template_0000}/src/style-options.js +0 -0
- /package/dist/templates/{template_0000 → vidfarm_template_0000}/src/template-dna.js +0 -0
- /package/templates/{template_0000 → vidfarm_template_0000}/SKILL.md +0 -0
- /package/templates/{template_0000 → vidfarm_template_0000}/assets/Abel-Regular.ttf +0 -0
- /package/templates/{template_0000 → vidfarm_template_0000}/assets/DMSerifDisplay-Regular.ttf +0 -0
- /package/templates/{template_0000 → vidfarm_template_0000}/assets/Montserrat[wght].ttf +0 -0
- /package/templates/{template_0000 → vidfarm_template_0000}/assets/SourceCodePro[wght].ttf +0 -0
- /package/templates/{template_0000 → vidfarm_template_0000}/assets/TikTokSans-SemiBold.ttf +0 -0
- /package/templates/{template_0000 → vidfarm_template_0000}/assets/Yesteryear-Regular.ttf +0 -0
- /package/templates/{template_0000 → vidfarm_template_0000}/composition.json +0 -0
- /package/templates/{template_0000 → vidfarm_template_0000}/research/preview/.gitkeep +0 -0
- /package/templates/{template_0000 → vidfarm_template_0000}/research/source_notes.md +0 -0
- /package/templates/{template_0000 → vidfarm_template_0000}/src/lib/images.js +0 -0
- /package/templates/{template_0000 → vidfarm_template_0000}/src/lib/images.ts +0 -0
- /package/{dist/templates/template_0000 → templates/vidfarm_template_0000}/src/remotion/Root.js +0 -0
- /package/templates/{template_0000 → vidfarm_template_0000}/src/remotion/Root.tsx +0 -0
- /package/templates/{template_0000 → vidfarm_template_0000}/src/remotion/index.js +0 -0
- /package/templates/{template_0000 → vidfarm_template_0000}/src/remotion/index.tsx +0 -0
- /package/templates/{template_0000 → vidfarm_template_0000}/src/sdk.js +0 -0
- /package/templates/{template_0000 → vidfarm_template_0000}/src/style-options.js +0 -0
- /package/templates/{template_0000 → vidfarm_template_0000}/src/style-options.ts +0 -0
- /package/templates/{template_0000 → vidfarm_template_0000}/src/template-dna.js +0 -0
- /package/templates/{template_0000 → vidfarm_template_0000}/src/template-dna.ts +0 -0
- /package/templates/{template_0000 → vidfarm_template_0000}/template.config.json +0 -0
- /package/templates/{template_0000 → vidfarm_template_0000}/tsconfig.json +0 -0
|
@@ -15,6 +15,16 @@ export class ProviderRateLimitError extends Error {
|
|
|
15
15
|
this.retryAfterSeconds = retryAfterSeconds;
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
|
+
const SUPPORTED_TTS_MODELS = {
|
|
19
|
+
openrouter: new Set(["google/gemini-3.1-flash-tts-preview"]),
|
|
20
|
+
gemini: new Set(["gemini-3.1-flash-tts-preview"]),
|
|
21
|
+
openai: new Set(["gpt-4o-mini-tts-2025-12-15"])
|
|
22
|
+
};
|
|
23
|
+
const SUPPORTED_STT_MODELS = {
|
|
24
|
+
openrouter: new Set(["openai/gpt-4o-mini-transcribe"]),
|
|
25
|
+
gemini: new Set(["gemini-3.1-flash-lite-preview", "gemini-2.5-flash-lite"]),
|
|
26
|
+
openai: new Set(["gpt-4o-mini-transcribe-2025-12-15"])
|
|
27
|
+
};
|
|
18
28
|
function readStoredProviderSecret(value) {
|
|
19
29
|
try {
|
|
20
30
|
return decryptString(value, config.ENCRYPTION_SECRET);
|
|
@@ -129,6 +139,103 @@ export class ProviderService {
|
|
|
129
139
|
await this.releaseLease(lease);
|
|
130
140
|
}
|
|
131
141
|
}
|
|
142
|
+
async generateSpeech(input) {
|
|
143
|
+
assertSupportedSpeechModel("tts", input.provider, input.model);
|
|
144
|
+
const lease = await this.leaseCustomerKey({
|
|
145
|
+
customerId: input.customerId,
|
|
146
|
+
provider: input.provider,
|
|
147
|
+
jobId: input.jobId,
|
|
148
|
+
workerId: input.workerId
|
|
149
|
+
});
|
|
150
|
+
try {
|
|
151
|
+
if (config.mockProviders) {
|
|
152
|
+
const contentType = inferSpeechContentType(input.responseFormat ?? "mp3");
|
|
153
|
+
database.recordProviderKeyUsage({
|
|
154
|
+
id: createId("usage"),
|
|
155
|
+
keyId: lease.keyId,
|
|
156
|
+
jobId: input.jobId,
|
|
157
|
+
provider: input.provider,
|
|
158
|
+
model: input.model,
|
|
159
|
+
eventType: "success",
|
|
160
|
+
inputTokens: 0,
|
|
161
|
+
outputTokens: 0,
|
|
162
|
+
costUsd: 0.01,
|
|
163
|
+
metadata: { mock: true, type: "speech_generation" }
|
|
164
|
+
});
|
|
165
|
+
database.touchProviderKey(lease.keyId);
|
|
166
|
+
return {
|
|
167
|
+
bytes: Buffer.from(`mock speech:${input.text.slice(0, 120)}`, "utf8"),
|
|
168
|
+
contentType,
|
|
169
|
+
usage: {
|
|
170
|
+
inputTokens: 0,
|
|
171
|
+
outputTokens: 0,
|
|
172
|
+
costUsd: 0.01
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
const result = input.provider === "gemini"
|
|
177
|
+
? await this.callGeminiSpeechGeneration({
|
|
178
|
+
model: input.model,
|
|
179
|
+
text: input.text,
|
|
180
|
+
voice: input.voice,
|
|
181
|
+
instructions: input.instructions,
|
|
182
|
+
apiKey: lease.secret
|
|
183
|
+
})
|
|
184
|
+
: await this.callOpenAICompatibleSpeechGeneration({
|
|
185
|
+
provider: input.provider === "openrouter" ? "openrouter" : "openai",
|
|
186
|
+
model: input.model,
|
|
187
|
+
text: input.text,
|
|
188
|
+
voice: input.voice,
|
|
189
|
+
instructions: input.instructions,
|
|
190
|
+
responseFormat: input.responseFormat,
|
|
191
|
+
apiKey: lease.secret
|
|
192
|
+
});
|
|
193
|
+
database.recordProviderKeyUsage({
|
|
194
|
+
id: createId("usage"),
|
|
195
|
+
keyId: lease.keyId,
|
|
196
|
+
jobId: input.jobId,
|
|
197
|
+
provider: input.provider,
|
|
198
|
+
model: input.model,
|
|
199
|
+
eventType: "success",
|
|
200
|
+
inputTokens: result.usage.inputTokens,
|
|
201
|
+
outputTokens: result.usage.outputTokens,
|
|
202
|
+
costUsd: result.usage.costUsd,
|
|
203
|
+
metadata: { type: "speech_generation" }
|
|
204
|
+
});
|
|
205
|
+
database.touchProviderKey(lease.keyId);
|
|
206
|
+
return result;
|
|
207
|
+
}
|
|
208
|
+
catch (error) {
|
|
209
|
+
if (error instanceof ProviderRateLimitError) {
|
|
210
|
+
database.recordProviderKeyUsage({
|
|
211
|
+
id: createId("usage"),
|
|
212
|
+
keyId: lease.keyId,
|
|
213
|
+
jobId: input.jobId,
|
|
214
|
+
provider: input.provider,
|
|
215
|
+
model: input.model,
|
|
216
|
+
eventType: "rate_limit",
|
|
217
|
+
metadata: { retryAfterSeconds: error.retryAfterSeconds, type: "speech_generation" }
|
|
218
|
+
});
|
|
219
|
+
database.setProviderKeyCooldown(lease.keyId, addSeconds(new Date(), error.retryAfterSeconds));
|
|
220
|
+
}
|
|
221
|
+
else if (error instanceof ProviderAuthError) {
|
|
222
|
+
database.recordProviderKeyUsage({
|
|
223
|
+
id: createId("usage"),
|
|
224
|
+
keyId: lease.keyId,
|
|
225
|
+
jobId: input.jobId,
|
|
226
|
+
provider: input.provider,
|
|
227
|
+
model: input.model,
|
|
228
|
+
eventType: "auth_error",
|
|
229
|
+
metadata: { type: "speech_generation" }
|
|
230
|
+
});
|
|
231
|
+
database.setProviderKeyCooldown(lease.keyId, null, "invalid", "auth_error");
|
|
232
|
+
}
|
|
233
|
+
throw error;
|
|
234
|
+
}
|
|
235
|
+
finally {
|
|
236
|
+
await this.releaseLease(lease);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
132
239
|
async generateImage(input) {
|
|
133
240
|
const lease = await this.leaseCustomerKey({
|
|
134
241
|
customerId: input.customerId,
|
|
@@ -265,6 +372,110 @@ export class ProviderService {
|
|
|
265
372
|
await this.releaseLease(lease);
|
|
266
373
|
}
|
|
267
374
|
}
|
|
375
|
+
async transcribeSpeech(input) {
|
|
376
|
+
assertSupportedSpeechModel("stt", input.provider, input.model);
|
|
377
|
+
const lease = await this.leaseCustomerKey({
|
|
378
|
+
customerId: input.customerId,
|
|
379
|
+
provider: input.provider,
|
|
380
|
+
jobId: input.jobId,
|
|
381
|
+
workerId: input.workerId
|
|
382
|
+
});
|
|
383
|
+
try {
|
|
384
|
+
if (config.mockProviders) {
|
|
385
|
+
database.recordProviderKeyUsage({
|
|
386
|
+
id: createId("usage"),
|
|
387
|
+
keyId: lease.keyId,
|
|
388
|
+
jobId: input.jobId,
|
|
389
|
+
provider: input.provider,
|
|
390
|
+
model: input.model,
|
|
391
|
+
eventType: "success",
|
|
392
|
+
inputTokens: 0,
|
|
393
|
+
outputTokens: 0,
|
|
394
|
+
costUsd: 0.01,
|
|
395
|
+
metadata: { mock: true, type: "speech_transcription" }
|
|
396
|
+
});
|
|
397
|
+
database.touchProviderKey(lease.keyId);
|
|
398
|
+
return {
|
|
399
|
+
text: "Mock transcription output.",
|
|
400
|
+
usage: {
|
|
401
|
+
inputTokens: 0,
|
|
402
|
+
outputTokens: 0,
|
|
403
|
+
costUsd: 0.01
|
|
404
|
+
}
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
const result = input.provider === "gemini"
|
|
408
|
+
? await this.callGeminiSpeechTranscription({
|
|
409
|
+
model: input.model,
|
|
410
|
+
audio: input.audio,
|
|
411
|
+
contentType: input.contentType,
|
|
412
|
+
prompt: input.prompt,
|
|
413
|
+
language: input.language,
|
|
414
|
+
apiKey: lease.secret
|
|
415
|
+
})
|
|
416
|
+
: input.provider === "openrouter"
|
|
417
|
+
? await this.callOpenRouterSpeechTranscription({
|
|
418
|
+
model: input.model,
|
|
419
|
+
audio: input.audio,
|
|
420
|
+
contentType: input.contentType,
|
|
421
|
+
prompt: input.prompt,
|
|
422
|
+
language: input.language,
|
|
423
|
+
apiKey: lease.secret
|
|
424
|
+
})
|
|
425
|
+
: await this.callOpenAISpeechTranscription({
|
|
426
|
+
model: input.model,
|
|
427
|
+
audio: input.audio,
|
|
428
|
+
contentType: input.contentType,
|
|
429
|
+
prompt: input.prompt,
|
|
430
|
+
language: input.language,
|
|
431
|
+
apiKey: lease.secret
|
|
432
|
+
});
|
|
433
|
+
database.recordProviderKeyUsage({
|
|
434
|
+
id: createId("usage"),
|
|
435
|
+
keyId: lease.keyId,
|
|
436
|
+
jobId: input.jobId,
|
|
437
|
+
provider: input.provider,
|
|
438
|
+
model: input.model,
|
|
439
|
+
eventType: "success",
|
|
440
|
+
inputTokens: result.usage.inputTokens,
|
|
441
|
+
outputTokens: result.usage.outputTokens,
|
|
442
|
+
costUsd: result.usage.costUsd,
|
|
443
|
+
metadata: { type: "speech_transcription" }
|
|
444
|
+
});
|
|
445
|
+
database.touchProviderKey(lease.keyId);
|
|
446
|
+
return result;
|
|
447
|
+
}
|
|
448
|
+
catch (error) {
|
|
449
|
+
if (error instanceof ProviderRateLimitError) {
|
|
450
|
+
database.recordProviderKeyUsage({
|
|
451
|
+
id: createId("usage"),
|
|
452
|
+
keyId: lease.keyId,
|
|
453
|
+
jobId: input.jobId,
|
|
454
|
+
provider: input.provider,
|
|
455
|
+
model: input.model,
|
|
456
|
+
eventType: "rate_limit",
|
|
457
|
+
metadata: { retryAfterSeconds: error.retryAfterSeconds, type: "speech_transcription" }
|
|
458
|
+
});
|
|
459
|
+
database.setProviderKeyCooldown(lease.keyId, addSeconds(new Date(), error.retryAfterSeconds));
|
|
460
|
+
}
|
|
461
|
+
else if (error instanceof ProviderAuthError) {
|
|
462
|
+
database.recordProviderKeyUsage({
|
|
463
|
+
id: createId("usage"),
|
|
464
|
+
keyId: lease.keyId,
|
|
465
|
+
jobId: input.jobId,
|
|
466
|
+
provider: input.provider,
|
|
467
|
+
model: input.model,
|
|
468
|
+
eventType: "auth_error",
|
|
469
|
+
metadata: { type: "speech_transcription" }
|
|
470
|
+
});
|
|
471
|
+
database.setProviderKeyCooldown(lease.keyId, null, "invalid", "auth_error");
|
|
472
|
+
}
|
|
473
|
+
throw error;
|
|
474
|
+
}
|
|
475
|
+
finally {
|
|
476
|
+
await this.releaseLease(lease);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
268
479
|
async callOpenAIImageGeneration(input) {
|
|
269
480
|
if (input.promptAttachments?.length) {
|
|
270
481
|
throw new Error("OpenAI image generation attachments are not implemented in this provider path. Use Gemini for reference attachments.");
|
|
@@ -355,6 +566,94 @@ export class ProviderService {
|
|
|
355
566
|
revisedPrompt: null
|
|
356
567
|
};
|
|
357
568
|
}
|
|
569
|
+
async callOpenAICompatibleSpeechGeneration(input) {
|
|
570
|
+
const responseFormat = input.responseFormat ?? "mp3";
|
|
571
|
+
const endpoint = input.provider === "openrouter"
|
|
572
|
+
? "https://openrouter.ai/api/v1/audio/speech"
|
|
573
|
+
: "https://api.openai.com/v1/audio/speech";
|
|
574
|
+
const response = await fetch(endpoint, {
|
|
575
|
+
method: "POST",
|
|
576
|
+
headers: {
|
|
577
|
+
"Content-Type": "application/json",
|
|
578
|
+
Authorization: `Bearer ${input.apiKey}`
|
|
579
|
+
},
|
|
580
|
+
body: JSON.stringify({
|
|
581
|
+
model: input.model,
|
|
582
|
+
input: input.text,
|
|
583
|
+
voice: input.voice ?? "alloy",
|
|
584
|
+
instructions: input.instructions,
|
|
585
|
+
response_format: responseFormat
|
|
586
|
+
})
|
|
587
|
+
});
|
|
588
|
+
if (response.status === 401 || response.status === 403) {
|
|
589
|
+
throw new ProviderAuthError(`${input.provider} authentication failed`);
|
|
590
|
+
}
|
|
591
|
+
if (response.status === 429) {
|
|
592
|
+
throw new ProviderRateLimitError(`${input.provider} rate limited`);
|
|
593
|
+
}
|
|
594
|
+
if (!response.ok) {
|
|
595
|
+
const details = await response.text();
|
|
596
|
+
throw new Error(`${input.provider} speech generation returned ${response.status}${details ? `: ${details}` : ""}`);
|
|
597
|
+
}
|
|
598
|
+
return {
|
|
599
|
+
bytes: Buffer.from(await response.arrayBuffer()),
|
|
600
|
+
contentType: response.headers.get("content-type")?.split(";")[0]?.trim() || inferSpeechContentType(responseFormat),
|
|
601
|
+
usage: {
|
|
602
|
+
inputTokens: 0,
|
|
603
|
+
outputTokens: 0,
|
|
604
|
+
costUsd: 0
|
|
605
|
+
}
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
async callGeminiSpeechGeneration(input) {
|
|
609
|
+
const prompt = input.instructions?.trim()
|
|
610
|
+
? `${input.instructions.trim()}\n\nSpeak exactly the following text:\n${input.text}`
|
|
611
|
+
: input.text;
|
|
612
|
+
const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/${input.model}:generateContent?key=${input.apiKey}`, {
|
|
613
|
+
method: "POST",
|
|
614
|
+
headers: {
|
|
615
|
+
"Content-Type": "application/json"
|
|
616
|
+
},
|
|
617
|
+
body: JSON.stringify({
|
|
618
|
+
contents: [{ parts: [{ text: prompt }] }],
|
|
619
|
+
generationConfig: {
|
|
620
|
+
responseModalities: ["AUDIO"],
|
|
621
|
+
speechConfig: {
|
|
622
|
+
voiceConfig: {
|
|
623
|
+
prebuiltVoiceConfig: {
|
|
624
|
+
voiceName: input.voice ?? "Kore"
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
})
|
|
630
|
+
});
|
|
631
|
+
if (response.status === 401 || response.status === 403) {
|
|
632
|
+
throw new ProviderAuthError("gemini authentication failed");
|
|
633
|
+
}
|
|
634
|
+
if (response.status === 429) {
|
|
635
|
+
throw new ProviderRateLimitError("gemini rate limited");
|
|
636
|
+
}
|
|
637
|
+
if (!response.ok) {
|
|
638
|
+
const details = await response.text();
|
|
639
|
+
throw new Error(`gemini speech generation returned ${response.status}${details ? `: ${details}` : ""}`);
|
|
640
|
+
}
|
|
641
|
+
const data = await response.json();
|
|
642
|
+
const part = data.candidates?.[0]?.content?.parts?.find((item) => item.inlineData?.data || item.inline_data?.data);
|
|
643
|
+
const encoded = part?.inlineData?.data ?? part?.inline_data?.data;
|
|
644
|
+
if (!encoded) {
|
|
645
|
+
throw new Error("gemini speech generation returned no audio payload");
|
|
646
|
+
}
|
|
647
|
+
return {
|
|
648
|
+
bytes: Buffer.from(encoded, "base64"),
|
|
649
|
+
contentType: part?.inlineData?.mimeType ?? part?.inline_data?.mime_type ?? "audio/L16;rate=24000",
|
|
650
|
+
usage: {
|
|
651
|
+
inputTokens: Number(data.usageMetadata?.promptTokenCount ?? 0),
|
|
652
|
+
outputTokens: Number(data.usageMetadata?.candidatesTokenCount ?? 0),
|
|
653
|
+
costUsd: 0
|
|
654
|
+
}
|
|
655
|
+
};
|
|
656
|
+
}
|
|
358
657
|
async callOpenAICompatibleLayoutAnalysis(input) {
|
|
359
658
|
const endpoint = input.provider === "openrouter"
|
|
360
659
|
? "https://openrouter.ai/api/v1/chat/completions"
|
|
@@ -458,6 +757,128 @@ export class ProviderService {
|
|
|
458
757
|
const text = data.candidates?.[0]?.content?.parts?.map((part) => part.text ?? "").join("\n") ?? "";
|
|
459
758
|
return String(text);
|
|
460
759
|
}
|
|
760
|
+
async callOpenAISpeechTranscription(input) {
|
|
761
|
+
const contentType = input.contentType ?? "audio/mpeg";
|
|
762
|
+
const form = new FormData();
|
|
763
|
+
form.set("file", new Blob([Buffer.from(input.audio)], { type: contentType }), `audio.${audioExtensionFromContentType(contentType)}`);
|
|
764
|
+
form.set("model", input.model);
|
|
765
|
+
form.set("response_format", "json");
|
|
766
|
+
if (input.prompt) {
|
|
767
|
+
form.set("prompt", input.prompt);
|
|
768
|
+
}
|
|
769
|
+
if (input.language) {
|
|
770
|
+
form.set("language", input.language);
|
|
771
|
+
}
|
|
772
|
+
const response = await fetch("https://api.openai.com/v1/audio/transcriptions", {
|
|
773
|
+
method: "POST",
|
|
774
|
+
headers: {
|
|
775
|
+
Authorization: `Bearer ${input.apiKey}`
|
|
776
|
+
},
|
|
777
|
+
body: form
|
|
778
|
+
});
|
|
779
|
+
if (response.status === 401 || response.status === 403) {
|
|
780
|
+
throw new ProviderAuthError("openai authentication failed");
|
|
781
|
+
}
|
|
782
|
+
if (response.status === 429) {
|
|
783
|
+
throw new ProviderRateLimitError("openai rate limited");
|
|
784
|
+
}
|
|
785
|
+
if (!response.ok) {
|
|
786
|
+
const details = await response.text();
|
|
787
|
+
throw new Error(`openai transcription returned ${response.status}${details ? `: ${details}` : ""}`);
|
|
788
|
+
}
|
|
789
|
+
const data = await response.json();
|
|
790
|
+
return {
|
|
791
|
+
text: String(data.text ?? ""),
|
|
792
|
+
usage: {
|
|
793
|
+
inputTokens: Number(data.usage?.input_tokens ?? 0),
|
|
794
|
+
outputTokens: Number(data.usage?.output_tokens ?? 0),
|
|
795
|
+
costUsd: Number(data.usage?.cost ?? 0)
|
|
796
|
+
}
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
async callOpenRouterSpeechTranscription(input) {
|
|
800
|
+
const response = await fetch("https://openrouter.ai/api/v1/audio/transcriptions", {
|
|
801
|
+
method: "POST",
|
|
802
|
+
headers: {
|
|
803
|
+
"Content-Type": "application/json",
|
|
804
|
+
Authorization: `Bearer ${input.apiKey}`
|
|
805
|
+
},
|
|
806
|
+
body: JSON.stringify({
|
|
807
|
+
model: input.model,
|
|
808
|
+
input_audio: {
|
|
809
|
+
data: Buffer.from(input.audio).toString("base64"),
|
|
810
|
+
format: audioFormatFromContentType(input.contentType)
|
|
811
|
+
},
|
|
812
|
+
...(input.prompt ? { prompt: input.prompt } : {}),
|
|
813
|
+
...(input.language ? { language: input.language } : {})
|
|
814
|
+
})
|
|
815
|
+
});
|
|
816
|
+
if (response.status === 401 || response.status === 403) {
|
|
817
|
+
throw new ProviderAuthError("openrouter authentication failed");
|
|
818
|
+
}
|
|
819
|
+
if (response.status === 429) {
|
|
820
|
+
throw new ProviderRateLimitError("openrouter rate limited");
|
|
821
|
+
}
|
|
822
|
+
if (!response.ok) {
|
|
823
|
+
const details = await response.text();
|
|
824
|
+
throw new Error(`openrouter transcription returned ${response.status}${details ? `: ${details}` : ""}`);
|
|
825
|
+
}
|
|
826
|
+
const data = await response.json();
|
|
827
|
+
return {
|
|
828
|
+
text: String(data.text ?? ""),
|
|
829
|
+
usage: {
|
|
830
|
+
inputTokens: Number(data.usage?.input_tokens ?? data.usage?.prompt_tokens ?? 0),
|
|
831
|
+
outputTokens: Number(data.usage?.output_tokens ?? data.usage?.completion_tokens ?? 0),
|
|
832
|
+
costUsd: Number(data.usage?.cost ?? 0)
|
|
833
|
+
}
|
|
834
|
+
};
|
|
835
|
+
}
|
|
836
|
+
async callGeminiSpeechTranscription(input) {
|
|
837
|
+
const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/${input.model}:generateContent?key=${input.apiKey}`, {
|
|
838
|
+
method: "POST",
|
|
839
|
+
headers: {
|
|
840
|
+
"Content-Type": "application/json"
|
|
841
|
+
},
|
|
842
|
+
body: JSON.stringify({
|
|
843
|
+
contents: [{
|
|
844
|
+
parts: [
|
|
845
|
+
{
|
|
846
|
+
inline_data: {
|
|
847
|
+
mime_type: input.contentType ?? "audio/mpeg",
|
|
848
|
+
data: Buffer.from(input.audio).toString("base64")
|
|
849
|
+
}
|
|
850
|
+
},
|
|
851
|
+
{
|
|
852
|
+
text: buildGeminiTranscriptionPrompt(input.prompt, input.language)
|
|
853
|
+
}
|
|
854
|
+
]
|
|
855
|
+
}],
|
|
856
|
+
generationConfig: {
|
|
857
|
+
temperature: 0,
|
|
858
|
+
responseMimeType: "text/plain"
|
|
859
|
+
}
|
|
860
|
+
})
|
|
861
|
+
});
|
|
862
|
+
if (response.status === 401 || response.status === 403) {
|
|
863
|
+
throw new ProviderAuthError("gemini authentication failed");
|
|
864
|
+
}
|
|
865
|
+
if (response.status === 429) {
|
|
866
|
+
throw new ProviderRateLimitError("gemini rate limited");
|
|
867
|
+
}
|
|
868
|
+
if (!response.ok) {
|
|
869
|
+
const details = await response.text();
|
|
870
|
+
throw new Error(`gemini transcription returned ${response.status}${details ? `: ${details}` : ""}`);
|
|
871
|
+
}
|
|
872
|
+
const data = await response.json();
|
|
873
|
+
return {
|
|
874
|
+
text: extractGeminiText(data),
|
|
875
|
+
usage: {
|
|
876
|
+
inputTokens: Number(data.usageMetadata?.promptTokenCount ?? 0),
|
|
877
|
+
outputTokens: Number(data.usageMetadata?.candidatesTokenCount ?? 0),
|
|
878
|
+
costUsd: 0
|
|
879
|
+
}
|
|
880
|
+
};
|
|
881
|
+
}
|
|
461
882
|
async callProvider(input) {
|
|
462
883
|
if (input.provider === "gemini") {
|
|
463
884
|
return this.callGemini({
|
|
@@ -543,6 +964,55 @@ export class ProviderService {
|
|
|
543
964
|
function clamp(value, min, max) {
|
|
544
965
|
return Math.max(min, Math.min(max, value));
|
|
545
966
|
}
|
|
967
|
+
function assertSupportedSpeechModel(kind, provider, model) {
|
|
968
|
+
const supported = kind === "tts" ? SUPPORTED_TTS_MODELS[provider] : SUPPORTED_STT_MODELS[provider];
|
|
969
|
+
if (!supported?.has(model)) {
|
|
970
|
+
throw new Error(`Unsupported ${kind.toUpperCase()} model for provider ${provider}: ${model}`);
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
function inferSpeechContentType(format) {
|
|
974
|
+
return format === "wav" ? "audio/wav" : "audio/mpeg";
|
|
975
|
+
}
|
|
976
|
+
function audioFormatFromContentType(contentType) {
|
|
977
|
+
const normalized = contentType?.split(";")[0]?.trim().toLowerCase();
|
|
978
|
+
switch (normalized) {
|
|
979
|
+
case "audio/wav":
|
|
980
|
+
case "audio/x-wav":
|
|
981
|
+
return "wav";
|
|
982
|
+
case "audio/mp4":
|
|
983
|
+
case "audio/m4a":
|
|
984
|
+
return "mp4";
|
|
985
|
+
case "audio/webm":
|
|
986
|
+
return "webm";
|
|
987
|
+
case "audio/ogg":
|
|
988
|
+
return "ogg";
|
|
989
|
+
default:
|
|
990
|
+
return "mp3";
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
function audioExtensionFromContentType(contentType) {
|
|
994
|
+
switch (audioFormatFromContentType(contentType)) {
|
|
995
|
+
case "wav":
|
|
996
|
+
return "wav";
|
|
997
|
+
case "mp4":
|
|
998
|
+
return "m4a";
|
|
999
|
+
case "webm":
|
|
1000
|
+
return "webm";
|
|
1001
|
+
case "ogg":
|
|
1002
|
+
return "ogg";
|
|
1003
|
+
default:
|
|
1004
|
+
return "mp3";
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
function buildGeminiTranscriptionPrompt(prompt, language) {
|
|
1008
|
+
const basePrompt = prompt?.trim() || "Transcribe this audio verbatim. Return plain text only.";
|
|
1009
|
+
return language?.trim()
|
|
1010
|
+
? `${basePrompt}\n\nExpected language: ${language.trim()}.`
|
|
1011
|
+
: basePrompt;
|
|
1012
|
+
}
|
|
1013
|
+
function extractGeminiText(data) {
|
|
1014
|
+
return String(data.candidates?.[0]?.content?.parts?.map((part) => part.text ?? "").join("\n") ?? "");
|
|
1015
|
+
}
|
|
546
1016
|
function supportsGeminiImageSize(model) {
|
|
547
1017
|
return model === "gemini-3.1-flash-image-preview" || model === "gemini-3-pro-image-preview";
|
|
548
1018
|
}
|
|
@@ -115,6 +115,19 @@ export class StorageService {
|
|
|
115
115
|
}
|
|
116
116
|
return readFileSync(path.join(this.localRoot, key), "utf8");
|
|
117
117
|
}
|
|
118
|
+
async readBuffer(key) {
|
|
119
|
+
if (this.s3 && config.AWS_S3_BUCKET) {
|
|
120
|
+
const response = await this.s3.send(new GetObjectCommand({
|
|
121
|
+
Bucket: config.AWS_S3_BUCKET,
|
|
122
|
+
Key: key
|
|
123
|
+
}));
|
|
124
|
+
if (!response.Body) {
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
return Buffer.from(await response.Body.transformToByteArray());
|
|
128
|
+
}
|
|
129
|
+
return readFileSync(path.join(this.localRoot, key));
|
|
130
|
+
}
|
|
118
131
|
async listKeys(prefix) {
|
|
119
132
|
if (this.s3 && config.AWS_S3_BUCKET) {
|
|
120
133
|
const keys = [];
|
|
@@ -13,11 +13,17 @@ export class TemplateCertificationService {
|
|
|
13
13
|
typeof template.about.visual_dna === "string" &&
|
|
14
14
|
Array.isArray(template.about.preview_media) &&
|
|
15
15
|
template.about.preview_media.every((entry) => typeof entry === "string") &&
|
|
16
|
-
typeof template.about.link_to_original === "string"
|
|
16
|
+
typeof template.about.link_to_original === "string" &&
|
|
17
|
+
typeof template.about.proposal_generator === "string" &&
|
|
18
|
+
Array.isArray(template.about.sample_prompts) &&
|
|
19
|
+
template.about.sample_prompts.every((entry) => typeof entry === "string") &&
|
|
20
|
+
template.about.sample_prompts.length > 0), "Template must define id, slugId, version, and complete about metadata, including proposal_generator and sample prompts.", {
|
|
17
21
|
templateId: template.id,
|
|
18
22
|
slugId: template.slugId,
|
|
19
23
|
version: template.version,
|
|
20
|
-
previewMediaCount: template.about?.preview_media?.length ?? 0
|
|
24
|
+
previewMediaCount: template.about?.preview_media?.length ?? 0,
|
|
25
|
+
hasProposalGenerator: typeof template.about?.proposal_generator === "string" && template.about.proposal_generator.length > 0,
|
|
26
|
+
samplePromptCount: template.about?.sample_prompts?.length ?? 0
|
|
21
27
|
}));
|
|
22
28
|
checks.push(this.check("template.operations", Object.keys(template.operations).length > 0, "Template must define at least one operation."));
|
|
23
29
|
checks.push(this.checkSkill(skillPath));
|
|
@@ -124,6 +130,10 @@ function createSmokeContext(templateId) {
|
|
|
124
130
|
providerCalls += 1;
|
|
125
131
|
return { text: "certification text", usage: { inputTokens: 1, outputTokens: 1, costUsd: 0 } };
|
|
126
132
|
},
|
|
133
|
+
async generateSpeech() {
|
|
134
|
+
providerCalls += 1;
|
|
135
|
+
return { bytes: Uint8Array.from([0, 1, 2, 3]), contentType: "audio/mpeg", usage: { inputTokens: 1, outputTokens: 1, costUsd: 0 } };
|
|
136
|
+
},
|
|
127
137
|
async generateImage() {
|
|
128
138
|
providerCalls += 1;
|
|
129
139
|
return { bytes: MOCK_PNG_BYTES, contentType: "image/png", revisedPrompt: null };
|
|
@@ -136,6 +146,10 @@ function createSmokeContext(templateId) {
|
|
|
136
146
|
maxWidthPercent: 80,
|
|
137
147
|
justification: "Certification mock layout."
|
|
138
148
|
};
|
|
149
|
+
},
|
|
150
|
+
async transcribeSpeech() {
|
|
151
|
+
providerCalls += 1;
|
|
152
|
+
return { text: "certification transcript", usage: { inputTokens: 1, outputTokens: 1, costUsd: 0 } };
|
|
139
153
|
}
|
|
140
154
|
},
|
|
141
155
|
remotion: {
|
|
@@ -4,11 +4,22 @@ export async function loadTemplateFromModule(modulePath) {
|
|
|
4
4
|
const candidates = Object.values(loaded);
|
|
5
5
|
for (const candidate of candidates) {
|
|
6
6
|
if (isTemplateDefinition(candidate)) {
|
|
7
|
-
return candidate;
|
|
7
|
+
return normalizeTemplateDefinition(candidate);
|
|
8
8
|
}
|
|
9
9
|
}
|
|
10
10
|
throw new Error(`No template definition export found in ${modulePath}`);
|
|
11
11
|
}
|
|
12
|
+
function normalizeTemplateDefinition(template) {
|
|
13
|
+
return {
|
|
14
|
+
...template,
|
|
15
|
+
about: {
|
|
16
|
+
...template.about,
|
|
17
|
+
// Older runtime template releases may predate proposal metadata fields.
|
|
18
|
+
proposal_generator: template.about.proposal_generator ?? "",
|
|
19
|
+
sample_prompts: template.about.sample_prompts ?? []
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
}
|
|
12
23
|
function isTemplateDefinition(value) {
|
|
13
24
|
if (!value || typeof value !== "object") {
|
|
14
25
|
return false;
|
|
@@ -20,6 +31,9 @@ function isTemplateDefinition(value) {
|
|
|
20
31
|
const previewMedia = about && Array.isArray(about.preview_media)
|
|
21
32
|
? about.preview_media
|
|
22
33
|
: null;
|
|
34
|
+
const samplePrompts = about && Array.isArray(about.sample_prompts)
|
|
35
|
+
? about.sample_prompts
|
|
36
|
+
: null;
|
|
23
37
|
return (typeof candidate.id === "string" &&
|
|
24
38
|
typeof candidate.slugId === "string" &&
|
|
25
39
|
typeof candidate.version === "string" &&
|
|
@@ -31,6 +45,8 @@ function isTemplateDefinition(value) {
|
|
|
31
45
|
Array.isArray(previewMedia) &&
|
|
32
46
|
previewMedia.every((entry) => typeof entry === "string") &&
|
|
33
47
|
typeof about.link_to_original === "string" &&
|
|
48
|
+
(typeof about.proposal_generator === "string" || typeof about.proposal_generator === "undefined") &&
|
|
49
|
+
(samplePrompts === null || samplePrompts.every((entry) => typeof entry === "string")) &&
|
|
34
50
|
typeof candidate.configSchema === "object" &&
|
|
35
51
|
typeof candidate.operations === "object" &&
|
|
36
52
|
typeof candidate.jobs === "object");
|