@mevdragon/vidfarm-devcli 0.1.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.
@@ -0,0 +1,529 @@
1
+ import { config } from "../config.js";
2
+ import { database } from "../db.js";
3
+ import { parseJson } from "../lib/json.js";
4
+ import { decryptString } from "../lib/crypto.js";
5
+ import { createId } from "../lib/ids.js";
6
+ import { addSeconds } from "../lib/time.js";
7
+ export class ProviderKeyUnavailableError extends Error {
8
+ }
9
+ export class ProviderAuthError extends Error {
10
+ }
11
+ export class ProviderRateLimitError extends Error {
12
+ retryAfterSeconds;
13
+ constructor(message, retryAfterSeconds = 60) {
14
+ super(message);
15
+ this.retryAfterSeconds = retryAfterSeconds;
16
+ }
17
+ }
18
+ export class ProviderService {
19
+ async leaseCustomerKey(input) {
20
+ const leaseToken = createId("lease");
21
+ const row = database.acquireProviderKeyLease({
22
+ customerId: input.customerId,
23
+ provider: input.provider,
24
+ workerId: input.workerId,
25
+ jobId: input.jobId,
26
+ leaseToken,
27
+ expiresAt: addSeconds(new Date(), 90)
28
+ });
29
+ if (!row) {
30
+ throw new ProviderKeyUnavailableError(`No eligible ${input.provider} key available.`);
31
+ }
32
+ return {
33
+ keyId: row.keyId,
34
+ leaseToken,
35
+ provider: input.provider,
36
+ secret: decryptString(row.encryptedSecret, config.ENCRYPTION_SECRET)
37
+ };
38
+ }
39
+ async releaseLease(lease) {
40
+ database.releaseProviderKeyLease({ keyId: lease.keyId, leaseToken: lease.leaseToken });
41
+ }
42
+ async generateText(input) {
43
+ const lease = await this.leaseCustomerKey({
44
+ customerId: input.customerId,
45
+ provider: input.provider,
46
+ jobId: input.jobId,
47
+ workerId: input.workerId
48
+ });
49
+ try {
50
+ if (config.mockProviders) {
51
+ const text = [
52
+ `Hook: ${input.prompt.slice(0, 80)}`,
53
+ "Beat 1: Identify the pain point.",
54
+ "Beat 2: Show the turning point.",
55
+ "Beat 3: Close with a clear CTA."
56
+ ].join("\n");
57
+ database.recordProviderKeyUsage({
58
+ id: createId("usage"),
59
+ keyId: lease.keyId,
60
+ jobId: input.jobId,
61
+ provider: input.provider,
62
+ model: input.model,
63
+ eventType: "success",
64
+ inputTokens: 180,
65
+ outputTokens: 120,
66
+ costUsd: 0.02,
67
+ metadata: { mock: true }
68
+ });
69
+ database.touchProviderKey(lease.keyId);
70
+ return {
71
+ text,
72
+ usage: {
73
+ inputTokens: 180,
74
+ outputTokens: 120,
75
+ costUsd: 0.02
76
+ }
77
+ };
78
+ }
79
+ const result = await this.callProvider({ ...input, apiKey: lease.secret });
80
+ database.recordProviderKeyUsage({
81
+ id: createId("usage"),
82
+ keyId: lease.keyId,
83
+ jobId: input.jobId,
84
+ provider: input.provider,
85
+ model: input.model,
86
+ eventType: "success",
87
+ inputTokens: result.usage.inputTokens,
88
+ outputTokens: result.usage.outputTokens,
89
+ costUsd: result.usage.costUsd
90
+ });
91
+ database.touchProviderKey(lease.keyId);
92
+ return result;
93
+ }
94
+ catch (error) {
95
+ if (error instanceof ProviderRateLimitError) {
96
+ database.recordProviderKeyUsage({
97
+ id: createId("usage"),
98
+ keyId: lease.keyId,
99
+ jobId: input.jobId,
100
+ provider: input.provider,
101
+ model: input.model,
102
+ eventType: "rate_limit",
103
+ metadata: { retryAfterSeconds: error.retryAfterSeconds }
104
+ });
105
+ database.setProviderKeyCooldown(lease.keyId, addSeconds(new Date(), error.retryAfterSeconds));
106
+ }
107
+ else if (error instanceof ProviderAuthError) {
108
+ database.recordProviderKeyUsage({
109
+ id: createId("usage"),
110
+ keyId: lease.keyId,
111
+ jobId: input.jobId,
112
+ provider: input.provider,
113
+ model: input.model,
114
+ eventType: "auth_error"
115
+ });
116
+ database.setProviderKeyCooldown(lease.keyId, null, "invalid", "auth_error");
117
+ }
118
+ throw error;
119
+ }
120
+ finally {
121
+ await this.releaseLease(lease);
122
+ }
123
+ }
124
+ async generateImage(input) {
125
+ const lease = await this.leaseCustomerKey({
126
+ customerId: input.customerId,
127
+ provider: input.provider,
128
+ jobId: input.jobId,
129
+ workerId: input.workerId
130
+ });
131
+ try {
132
+ if (config.mockProviders) {
133
+ const svg = buildMockSlideSvg(input.prompt);
134
+ database.recordProviderKeyUsage({
135
+ id: createId("usage"),
136
+ keyId: lease.keyId,
137
+ jobId: input.jobId,
138
+ provider: input.provider,
139
+ model: input.model,
140
+ eventType: "success",
141
+ inputTokens: 0,
142
+ outputTokens: 0,
143
+ costUsd: 0.04,
144
+ metadata: { mock: true, type: "image_generation" }
145
+ });
146
+ database.touchProviderKey(lease.keyId);
147
+ return {
148
+ bytes: Buffer.from(svg, "utf8"),
149
+ contentType: "image/svg+xml",
150
+ revisedPrompt: input.prompt
151
+ };
152
+ }
153
+ const result = input.provider === "gemini"
154
+ ? await this.callGeminiImageGeneration({
155
+ model: input.model,
156
+ prompt: input.prompt,
157
+ apiKey: lease.secret
158
+ })
159
+ : await this.callOpenAIImageGeneration({
160
+ model: input.model,
161
+ prompt: input.prompt,
162
+ size: input.size,
163
+ apiKey: lease.secret
164
+ });
165
+ database.recordProviderKeyUsage({
166
+ id: createId("usage"),
167
+ keyId: lease.keyId,
168
+ jobId: input.jobId,
169
+ provider: input.provider,
170
+ model: input.model,
171
+ eventType: "success",
172
+ inputTokens: 0,
173
+ outputTokens: 0,
174
+ costUsd: 0.04,
175
+ metadata: { type: "image_generation" }
176
+ });
177
+ database.touchProviderKey(lease.keyId);
178
+ return result;
179
+ }
180
+ finally {
181
+ await this.releaseLease(lease);
182
+ }
183
+ }
184
+ async analyzeImageLayout(input) {
185
+ const lease = await this.leaseCustomerKey({
186
+ customerId: input.customerId,
187
+ provider: input.provider,
188
+ jobId: input.jobId,
189
+ workerId: input.workerId
190
+ });
191
+ try {
192
+ if (config.mockProviders) {
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: 0,
201
+ outputTokens: 0,
202
+ costUsd: 0.005,
203
+ metadata: { mock: true, type: "layout_analysis" }
204
+ });
205
+ database.touchProviderKey(lease.keyId);
206
+ return {
207
+ zone: "bottom",
208
+ align: "center",
209
+ maxWidthPercent: 82,
210
+ justification: "Mock mode defaults to bottom-center for readable slideshow captions."
211
+ };
212
+ }
213
+ const raw = input.provider === "gemini"
214
+ ? await this.callGeminiLayoutAnalysis({
215
+ model: input.model,
216
+ imageUrl: input.imageUrl,
217
+ overlayText: input.overlayText,
218
+ apiKey: lease.secret
219
+ })
220
+ : await this.callOpenAILayoutAnalysis({
221
+ model: input.model,
222
+ imageUrl: input.imageUrl,
223
+ overlayText: input.overlayText,
224
+ apiKey: lease.secret
225
+ });
226
+ const parsed = parseJson(raw, null);
227
+ if (!parsed?.zone || !parsed?.align) {
228
+ throw new Error(`${input.provider} layout analysis returned invalid JSON`);
229
+ }
230
+ database.recordProviderKeyUsage({
231
+ id: createId("usage"),
232
+ keyId: lease.keyId,
233
+ jobId: input.jobId,
234
+ provider: input.provider,
235
+ model: input.model,
236
+ eventType: "success",
237
+ inputTokens: 0,
238
+ outputTokens: 0,
239
+ costUsd: 0.005,
240
+ metadata: { type: "layout_analysis" }
241
+ });
242
+ database.touchProviderKey(lease.keyId);
243
+ return {
244
+ zone: parsed.zone,
245
+ align: parsed.align,
246
+ maxWidthPercent: clamp(Number(parsed.maxWidthPercent ?? 76), 52, 88),
247
+ justification: parsed.justification ?? "Selected to preserve legibility and subject framing."
248
+ };
249
+ }
250
+ finally {
251
+ await this.releaseLease(lease);
252
+ }
253
+ }
254
+ async callOpenAIImageGeneration(input) {
255
+ const response = await fetch("https://api.openai.com/v1/images/generations", {
256
+ method: "POST",
257
+ headers: {
258
+ "Content-Type": "application/json",
259
+ Authorization: `Bearer ${input.apiKey}`
260
+ },
261
+ body: JSON.stringify({
262
+ model: input.model,
263
+ prompt: input.prompt,
264
+ size: input.size ?? "1024x1536",
265
+ response_format: "b64_json"
266
+ })
267
+ });
268
+ if (response.status === 401) {
269
+ throw new ProviderAuthError("openai authentication failed");
270
+ }
271
+ if (response.status === 429) {
272
+ throw new ProviderRateLimitError("openai rate limited");
273
+ }
274
+ if (!response.ok) {
275
+ throw new Error(`openai image generation returned ${response.status}`);
276
+ }
277
+ const data = await response.json();
278
+ const encoded = data.data?.[0]?.b64_json;
279
+ if (!encoded) {
280
+ throw new Error("openai image generation returned no image payload");
281
+ }
282
+ return {
283
+ bytes: Buffer.from(encoded, "base64"),
284
+ contentType: "image/png",
285
+ revisedPrompt: data.data?.[0]?.revised_prompt ?? null
286
+ };
287
+ }
288
+ async callGeminiImageGeneration(input) {
289
+ const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/${input.model}:generateContent?key=${input.apiKey}`, {
290
+ method: "POST",
291
+ headers: {
292
+ "Content-Type": "application/json"
293
+ },
294
+ body: JSON.stringify({
295
+ contents: [{ parts: [{ text: input.prompt }] }],
296
+ generationConfig: {
297
+ responseModalities: ["TEXT", "IMAGE"]
298
+ }
299
+ })
300
+ });
301
+ if (response.status === 401 || response.status === 403) {
302
+ throw new ProviderAuthError("gemini authentication failed");
303
+ }
304
+ if (response.status === 429) {
305
+ throw new ProviderRateLimitError("gemini rate limited");
306
+ }
307
+ if (!response.ok) {
308
+ throw new Error(`gemini image generation returned ${response.status}`);
309
+ }
310
+ const data = await response.json();
311
+ const part = data.candidates?.[0]?.content?.parts?.find((item) => item.inlineData?.data);
312
+ const encoded = part?.inlineData?.data;
313
+ if (!encoded) {
314
+ throw new Error("gemini image generation returned no image payload");
315
+ }
316
+ return {
317
+ bytes: Buffer.from(encoded, "base64"),
318
+ contentType: part.inlineData?.mimeType ?? "image/png",
319
+ revisedPrompt: null
320
+ };
321
+ }
322
+ async callOpenAILayoutAnalysis(input) {
323
+ const response = await fetch("https://api.openai.com/v1/chat/completions", {
324
+ method: "POST",
325
+ headers: {
326
+ "Content-Type": "application/json",
327
+ Authorization: `Bearer ${input.apiKey}`
328
+ },
329
+ body: JSON.stringify({
330
+ model: input.model,
331
+ temperature: 0.1,
332
+ response_format: { type: "json_object" },
333
+ messages: [
334
+ {
335
+ role: "system",
336
+ content: "Analyze a 9:16 slideshow image and return JSON with zone, align, maxWidthPercent, justification. Prefer negative space and avoid covering a subject."
337
+ },
338
+ {
339
+ role: "user",
340
+ content: [
341
+ {
342
+ type: "text",
343
+ text: `The exact overlay text is: ${input.overlayText}\nChoose zone from top, center, bottom. Choose align from left, center, right. maxWidthPercent must be between 52 and 88.`
344
+ },
345
+ {
346
+ type: "image_url",
347
+ image_url: {
348
+ url: input.imageUrl
349
+ }
350
+ }
351
+ ]
352
+ }
353
+ ]
354
+ })
355
+ });
356
+ if (response.status === 401) {
357
+ throw new ProviderAuthError("openai authentication failed");
358
+ }
359
+ if (response.status === 429) {
360
+ throw new ProviderRateLimitError("openai rate limited");
361
+ }
362
+ if (!response.ok) {
363
+ throw new Error(`openai layout analysis returned ${response.status}`);
364
+ }
365
+ const data = await response.json();
366
+ return String(data.choices?.[0]?.message?.content ?? "");
367
+ }
368
+ async callGeminiLayoutAnalysis(input) {
369
+ const image = await fetch(input.imageUrl);
370
+ if (!image.ok) {
371
+ throw new Error(`could not fetch image for gemini layout analysis: ${image.status}`);
372
+ }
373
+ const bytes = Buffer.from(await image.arrayBuffer());
374
+ const mimeType = image.headers.get("content-type") || "image/png";
375
+ const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/${input.model}:generateContent?key=${input.apiKey}`, {
376
+ method: "POST",
377
+ headers: {
378
+ "Content-Type": "application/json"
379
+ },
380
+ body: JSON.stringify({
381
+ contents: [{
382
+ parts: [
383
+ {
384
+ text: [
385
+ "Analyze this 9:16 slideshow image and return JSON with keys zone, align, maxWidthPercent, justification.",
386
+ "zone must be one of top, center, bottom.",
387
+ "align must be one of left, center, right.",
388
+ "maxWidthPercent must be between 52 and 88.",
389
+ "Prefer negative space and avoid covering the subject.",
390
+ `Exact overlay text: ${input.overlayText}`
391
+ ].join("\n")
392
+ },
393
+ {
394
+ inline_data: {
395
+ mime_type: mimeType,
396
+ data: bytes.toString("base64")
397
+ }
398
+ }
399
+ ]
400
+ }],
401
+ generationConfig: {
402
+ responseMimeType: "application/json"
403
+ }
404
+ })
405
+ });
406
+ if (response.status === 401 || response.status === 403) {
407
+ throw new ProviderAuthError("gemini authentication failed");
408
+ }
409
+ if (response.status === 429) {
410
+ throw new ProviderRateLimitError("gemini rate limited");
411
+ }
412
+ if (!response.ok) {
413
+ throw new Error(`gemini layout analysis returned ${response.status}`);
414
+ }
415
+ const data = await response.json();
416
+ const text = data.candidates?.[0]?.content?.parts?.map((part) => part.text ?? "").join("\n") ?? "";
417
+ return String(text);
418
+ }
419
+ async callProvider(input) {
420
+ if (input.provider === "gemini") {
421
+ return this.callGemini({
422
+ provider: "gemini",
423
+ model: input.model,
424
+ prompt: input.prompt,
425
+ temperature: input.temperature,
426
+ apiKey: input.apiKey
427
+ });
428
+ }
429
+ return this.callOpenAICompatible(input);
430
+ }
431
+ async callOpenAICompatible(input) {
432
+ const endpoint = input.provider === "openrouter"
433
+ ? "https://openrouter.ai/api/v1/chat/completions"
434
+ : input.provider === "perplexity"
435
+ ? "https://api.perplexity.ai/chat/completions"
436
+ : "https://api.openai.com/v1/chat/completions";
437
+ const response = await fetch(endpoint, {
438
+ method: "POST",
439
+ headers: {
440
+ "Content-Type": "application/json",
441
+ Authorization: `Bearer ${input.apiKey}`
442
+ },
443
+ body: JSON.stringify({
444
+ model: input.model,
445
+ temperature: input.temperature ?? 0.7,
446
+ messages: [{ role: "user", content: input.prompt }]
447
+ })
448
+ });
449
+ if (response.status === 401) {
450
+ throw new ProviderAuthError(`${input.provider} authentication failed`);
451
+ }
452
+ if (response.status === 429) {
453
+ throw new ProviderRateLimitError(`${input.provider} rate limited`);
454
+ }
455
+ if (!response.ok) {
456
+ throw new Error(`${input.provider} returned ${response.status}`);
457
+ }
458
+ const data = await response.json();
459
+ return {
460
+ text: data.choices?.[0]?.message?.content ?? "",
461
+ usage: {
462
+ inputTokens: Number(data.usage?.prompt_tokens ?? 0),
463
+ outputTokens: Number(data.usage?.completion_tokens ?? 0),
464
+ costUsd: 0
465
+ }
466
+ };
467
+ }
468
+ async callGemini(input) {
469
+ const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models/${input.model}:generateContent?key=${input.apiKey}`, {
470
+ method: "POST",
471
+ headers: {
472
+ "Content-Type": "application/json"
473
+ },
474
+ body: JSON.stringify({
475
+ contents: [{ parts: [{ text: input.prompt }] }],
476
+ generationConfig: {
477
+ temperature: input.temperature ?? 0.7
478
+ }
479
+ })
480
+ });
481
+ if (response.status === 401 || response.status === 403) {
482
+ throw new ProviderAuthError("Gemini authentication failed");
483
+ }
484
+ if (response.status === 429) {
485
+ throw new ProviderRateLimitError("Gemini rate limited");
486
+ }
487
+ if (!response.ok) {
488
+ throw new Error(`Gemini returned ${response.status}`);
489
+ }
490
+ const data = await response.json();
491
+ return {
492
+ text: data.candidates?.[0]?.content?.parts?.map((part) => part.text ?? "").join("\n") ?? "",
493
+ usage: {
494
+ inputTokens: Number(data.usageMetadata?.promptTokenCount ?? 0),
495
+ outputTokens: Number(data.usageMetadata?.candidatesTokenCount ?? 0),
496
+ costUsd: 0
497
+ }
498
+ };
499
+ }
500
+ }
501
+ function clamp(value, min, max) {
502
+ return Math.max(min, Math.min(max, value));
503
+ }
504
+ function buildMockSlideSvg(prompt) {
505
+ const escaped = escapeXml(prompt.slice(0, 220));
506
+ return `
507
+ <svg xmlns="http://www.w3.org/2000/svg" width="1080" height="1920" viewBox="0 0 1080 1920">
508
+ <defs>
509
+ <linearGradient id="bg" x1="0" x2="1" y1="0" y2="1">
510
+ <stop offset="0%" stop-color="#31241c" />
511
+ <stop offset="52%" stop-color="#866245" />
512
+ <stop offset="100%" stop-color="#d7c0a4" />
513
+ </linearGradient>
514
+ </defs>
515
+ <rect width="1080" height="1920" fill="url(#bg)" />
516
+ <rect x="94" y="1180" width="892" height="470" rx="44" fill="rgba(0,0,0,0.18)" stroke="rgba(255,255,255,0.25)" />
517
+ <text x="108" y="170" fill="#f7f0e7" font-size="54" font-family="Times New Roman, serif">Mock AI image</text>
518
+ <text x="108" y="240" fill="#f7f0e7" font-size="34" font-family="Times New Roman, serif">${escaped}</text>
519
+ <text x="108" y="1280" fill="#f7f0e7" font-size="42" font-family="Times New Roman, serif">Reserved text-safe zone</text>
520
+ </svg>`;
521
+ }
522
+ function escapeXml(value) {
523
+ return value
524
+ .replace(/&/g, "&amp;")
525
+ .replace(/</g, "&lt;")
526
+ .replace(/>/g, "&gt;")
527
+ .replace(/"/g, "&quot;")
528
+ .replace(/'/g, "&apos;");
529
+ }
@@ -0,0 +1,158 @@
1
+ import { mkdirSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { bundle } from "@remotion/bundler";
4
+ import { getRenderProgress, renderMediaOnLambda } from "@remotion/lambda";
5
+ import { getCompositions, renderMedia } from "@remotion/renderer";
6
+ import { config } from "../config.js";
7
+ import { createId } from "../lib/ids.js";
8
+ export class RemotionService {
9
+ async render(input) {
10
+ if ((config.REMOTION_MODE === "local" || (config.REMOTION_MODE === "auto" && input.entryPoint)) && input.entryPoint) {
11
+ return this.renderLocal({
12
+ compositionId: input.compositionId,
13
+ entryPoint: input.entryPoint,
14
+ outputKey: input.outputKey,
15
+ inputProps: input.inputProps
16
+ });
17
+ }
18
+ const serveUrl = input.serveUrl ?? config.REMOTION_SERVE_URL;
19
+ if (config.REMOTION_MODE === "mock" || !config.REMOTION_FUNCTION_NAME || !serveUrl || !config.REMOTION_BUCKET_NAME) {
20
+ return {
21
+ renderId: createId("render"),
22
+ outputUrl: null,
23
+ metadata: {
24
+ mode: "mock",
25
+ compositionId: input.compositionId,
26
+ inputProps: input.inputProps
27
+ }
28
+ };
29
+ }
30
+ return withRemotionCredentials(async () => {
31
+ const render = await renderMediaOnLambda({
32
+ region: config.REMOTION_REGION,
33
+ functionName: config.REMOTION_FUNCTION_NAME,
34
+ serveUrl,
35
+ composition: input.compositionId,
36
+ inputProps: input.inputProps,
37
+ codec: "h264",
38
+ privacy: "public",
39
+ forceBucketName: config.REMOTION_BUCKET_NAME,
40
+ forceWidth: 1080,
41
+ forceHeight: 1920,
42
+ framesPerLambda: 40,
43
+ crf: 18,
44
+ overwrite: true
45
+ });
46
+ const startedAt = Date.now();
47
+ for (;;) {
48
+ const progress = await getRenderProgress({
49
+ region: config.REMOTION_REGION,
50
+ functionName: config.REMOTION_FUNCTION_NAME,
51
+ bucketName: config.REMOTION_BUCKET_NAME,
52
+ renderId: render.renderId
53
+ });
54
+ if (progress.done) {
55
+ if (!progress.outputFile) {
56
+ throw new Error("Remotion render completed without an output file.");
57
+ }
58
+ return {
59
+ renderId: render.renderId,
60
+ outputUrl: progress.outputFile,
61
+ metadata: {
62
+ mode: "lambda",
63
+ bucketName: render.bucketName,
64
+ compositionId: input.compositionId,
65
+ serveUrl,
66
+ overallProgress: progress.overallProgress,
67
+ renderSize: progress.renderSize
68
+ }
69
+ };
70
+ }
71
+ if (Date.now() - startedAt > 10 * 60 * 1000) {
72
+ throw new Error("Timed out waiting for Remotion render output.");
73
+ }
74
+ await new Promise((resolve) => setTimeout(resolve, 5000));
75
+ }
76
+ });
77
+ }
78
+ async renderLocal(input) {
79
+ const renderId = createId("render");
80
+ const bundled = await getOrCreateBundle(input.entryPoint);
81
+ const compositions = await getCompositions(bundled, {
82
+ inputProps: input.inputProps
83
+ });
84
+ const composition = compositions.find((item) => item.id === input.compositionId);
85
+ if (!composition) {
86
+ throw new Error(`Composition ${input.compositionId} not found in ${input.entryPoint}`);
87
+ }
88
+ const outputKey = input.outputKey ?? `renders/${renderId}.mp4`;
89
+ const outputLocation = path.join(config.VIDFARM_DATA_DIR, "storage", outputKey);
90
+ mkdirSync(path.dirname(outputLocation), { recursive: true });
91
+ await renderMedia({
92
+ serveUrl: bundled,
93
+ composition,
94
+ codec: "h264",
95
+ inputProps: input.inputProps,
96
+ outputLocation,
97
+ overwrite: true
98
+ });
99
+ return {
100
+ renderId,
101
+ outputUrl: `${config.PUBLIC_BASE_URL}/storage/${encodeURIComponent(outputKey)}`,
102
+ metadata: {
103
+ mode: "local",
104
+ compositionId: input.compositionId,
105
+ entryPoint: input.entryPoint,
106
+ outputKey
107
+ }
108
+ };
109
+ }
110
+ }
111
+ const bundleCache = new Map();
112
+ function getOrCreateBundle(entryPoint) {
113
+ const existing = bundleCache.get(entryPoint);
114
+ if (existing) {
115
+ return existing;
116
+ }
117
+ const created = bundle({
118
+ entryPoint,
119
+ onProgress: () => undefined
120
+ });
121
+ bundleCache.set(entryPoint, created);
122
+ return created;
123
+ }
124
+ async function withRemotionCredentials(work) {
125
+ const previousAccessKeyId = process.env.AWS_ACCESS_KEY_ID;
126
+ const previousSecretAccessKey = process.env.AWS_SECRET_ACCESS_KEY;
127
+ const previousRegion = process.env.AWS_REGION;
128
+ if (config.REMOTION_AWS_ACCESS_KEY_ID) {
129
+ process.env.AWS_ACCESS_KEY_ID = config.REMOTION_AWS_ACCESS_KEY_ID;
130
+ }
131
+ if (config.REMOTION_AWS_SECRET_ACCESS_KEY) {
132
+ process.env.AWS_SECRET_ACCESS_KEY = config.REMOTION_AWS_SECRET_ACCESS_KEY;
133
+ }
134
+ process.env.AWS_REGION = config.REMOTION_REGION;
135
+ try {
136
+ return await work();
137
+ }
138
+ finally {
139
+ if (previousAccessKeyId === undefined) {
140
+ delete process.env.AWS_ACCESS_KEY_ID;
141
+ }
142
+ else {
143
+ process.env.AWS_ACCESS_KEY_ID = previousAccessKeyId;
144
+ }
145
+ if (previousSecretAccessKey === undefined) {
146
+ delete process.env.AWS_SECRET_ACCESS_KEY;
147
+ }
148
+ else {
149
+ process.env.AWS_SECRET_ACCESS_KEY = previousSecretAccessKey;
150
+ }
151
+ if (previousRegion === undefined) {
152
+ delete process.env.AWS_REGION;
153
+ }
154
+ else {
155
+ process.env.AWS_REGION = previousRegion;
156
+ }
157
+ }
158
+ }