@mux/ai 0.6.0 → 0.7.2
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/README.md +27 -23
- package/dist/{index-B6ro59tn.d.ts → index-BMqnP1RV.d.ts} +7 -4
- package/dist/{index-DAlX4SNJ.d.ts → index-DZlygsvb.d.ts} +113 -3
- package/dist/index.d.ts +30 -6
- package/dist/index.js +858 -282
- package/dist/index.js.map +1 -1
- package/dist/primitives/index.d.ts +2 -2
- package/dist/primitives/index.js +189 -21
- package/dist/primitives/index.js.map +1 -1
- package/dist/{types-CqjLMB84.d.ts → types-BQVi_wnh.d.ts} +49 -18
- package/dist/workflows/index.d.ts +2 -2
- package/dist/workflows/index.js +617 -214
- package/dist/workflows/index.js.map +1 -1
- package/package.json +3 -5
package/dist/workflows/index.js
CHANGED
|
@@ -110,8 +110,10 @@ async function downloadImagesAsBase64(urls, options = {}, maxConcurrent = 5) {
|
|
|
110
110
|
return results;
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
-
// src/lib/
|
|
114
|
-
import
|
|
113
|
+
// src/lib/providers.ts
|
|
114
|
+
import { createAnthropic } from "@ai-sdk/anthropic";
|
|
115
|
+
import { createGoogleGenerativeAI } from "@ai-sdk/google";
|
|
116
|
+
import { createOpenAI } from "@ai-sdk/openai";
|
|
115
117
|
|
|
116
118
|
// src/env.ts
|
|
117
119
|
import { z } from "zod";
|
|
@@ -166,6 +168,9 @@ var EnvSchema = z.object({
|
|
|
166
168
|
S3_BUCKET: optionalString("Bucket used for caption and audio uploads.", "S3 bucket"),
|
|
167
169
|
S3_ACCESS_KEY_ID: optionalString("Access key ID for S3-compatible uploads.", "S3 access key id"),
|
|
168
170
|
S3_SECRET_ACCESS_KEY: optionalString("Secret access key for S3-compatible uploads.", "S3 secret access key"),
|
|
171
|
+
S3_ALLOWED_ENDPOINT_HOSTS: optionalString(
|
|
172
|
+
"Comma-separated S3 endpoint allowlist (supports exact hosts and *.suffix patterns)."
|
|
173
|
+
),
|
|
169
174
|
EVALITE_RESULTS_ENDPOINT: optionalString(
|
|
170
175
|
"Full URL for posting Evalite results (e.g., https://example.com/api/evalite-results).",
|
|
171
176
|
"Evalite results endpoint"
|
|
@@ -192,11 +197,6 @@ function parseEnv() {
|
|
|
192
197
|
var env = parseEnv();
|
|
193
198
|
var env_default = env;
|
|
194
199
|
|
|
195
|
-
// src/lib/providers.ts
|
|
196
|
-
import { createAnthropic } from "@ai-sdk/anthropic";
|
|
197
|
-
import { createGoogleGenerativeAI } from "@ai-sdk/google";
|
|
198
|
-
import { createOpenAI } from "@ai-sdk/openai";
|
|
199
|
-
|
|
200
200
|
// src/lib/workflow-crypto.ts
|
|
201
201
|
import { gcm } from "@noble/ciphers/aes.js";
|
|
202
202
|
var BASE64_ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
@@ -212,11 +212,15 @@ var WORKFLOW_ENCRYPTION_VERSION = 1;
|
|
|
212
212
|
var WORKFLOW_ENCRYPTION_ALGORITHM = "aes-256-gcm";
|
|
213
213
|
var IV_LENGTH_BYTES = 12;
|
|
214
214
|
var AUTH_TAG_LENGTH_BYTES = 16;
|
|
215
|
+
function normalizeBase64Input(value) {
|
|
216
|
+
const cleaned = value.replace(/\s+/g, "").replace(/-/g, "+").replace(/_/g, "/");
|
|
217
|
+
return cleaned?.length % 4 === 0 ? cleaned : cleaned + "=".repeat(4 - cleaned.length % 4);
|
|
218
|
+
}
|
|
215
219
|
function base64ToBytes(value, label) {
|
|
216
220
|
if (!value) {
|
|
217
221
|
throw new Error(`${label} is missing`);
|
|
218
222
|
}
|
|
219
|
-
const normalized =
|
|
223
|
+
const normalized = normalizeBase64Input(value);
|
|
220
224
|
const atobImpl = typeof globalThis.atob === "function" ? globalThis.atob.bind(globalThis) : void 0;
|
|
221
225
|
if (atobImpl) {
|
|
222
226
|
let binary;
|
|
@@ -234,19 +238,18 @@ function base64ToBytes(value, label) {
|
|
|
234
238
|
}
|
|
235
239
|
return bytes2;
|
|
236
240
|
}
|
|
237
|
-
|
|
238
|
-
if (cleaned.length % 4 !== 0) {
|
|
241
|
+
if (normalized.length % 4 !== 0) {
|
|
239
242
|
throw new Error(`${label} is not valid base64`);
|
|
240
243
|
}
|
|
241
|
-
const padding =
|
|
242
|
-
const outputLength =
|
|
244
|
+
const padding = normalized.endsWith("==") ? 2 : normalized.endsWith("=") ? 1 : 0;
|
|
245
|
+
const outputLength = normalized.length / 4 * 3 - padding;
|
|
243
246
|
const bytes = new Uint8Array(outputLength);
|
|
244
247
|
let offset = 0;
|
|
245
|
-
for (let i = 0; i <
|
|
246
|
-
const c0 =
|
|
247
|
-
const c1 =
|
|
248
|
-
const c2 =
|
|
249
|
-
const c3 =
|
|
248
|
+
for (let i = 0; i < normalized.length; i += 4) {
|
|
249
|
+
const c0 = normalized.charCodeAt(i);
|
|
250
|
+
const c1 = normalized.charCodeAt(i + 1);
|
|
251
|
+
const c2 = normalized.charCodeAt(i + 2);
|
|
252
|
+
const c3 = normalized.charCodeAt(i + 3);
|
|
250
253
|
if (c0 === 61 || c1 === 61) {
|
|
251
254
|
throw new Error(`${label} is not valid base64`);
|
|
252
255
|
}
|
|
@@ -367,7 +370,9 @@ async function shouldEnforceEncryptedCredentials() {
|
|
|
367
370
|
function getWorkflowSecretKeyFromEnv() {
|
|
368
371
|
const key = env_default.MUX_AI_WORKFLOW_SECRET_KEY;
|
|
369
372
|
if (!key) {
|
|
370
|
-
throw new Error(
|
|
373
|
+
throw new Error(
|
|
374
|
+
"Workflow secret key is required. Set MUX_AI_WORKFLOW_SECRET_KEY environment variable."
|
|
375
|
+
);
|
|
371
376
|
}
|
|
372
377
|
return key;
|
|
373
378
|
}
|
|
@@ -398,36 +403,92 @@ async function resolveWorkflowCredentials(credentials) {
|
|
|
398
403
|
);
|
|
399
404
|
return { ...resolved, ...decrypted };
|
|
400
405
|
} catch (error) {
|
|
401
|
-
const detail = error instanceof Error ? error.message :
|
|
406
|
+
const detail = error instanceof Error ? error.message : "Unknown error.";
|
|
402
407
|
throw new Error(`Failed to decrypt workflow credentials. ${detail}`);
|
|
403
408
|
}
|
|
404
409
|
}
|
|
405
410
|
if (await shouldEnforceEncryptedCredentials()) {
|
|
406
411
|
throw new Error(
|
|
407
|
-
"Plaintext workflow credentials are not allowed when using Workflow Dev Kit.Pass encrypted credentials (encryptForWorkflow) or resolve secrets via environment variables."
|
|
412
|
+
"Plaintext workflow credentials are not allowed when using Workflow Dev Kit. Pass encrypted credentials (encryptForWorkflow) or resolve secrets via environment variables."
|
|
408
413
|
);
|
|
409
414
|
}
|
|
410
415
|
return { ...resolved, ...credentials };
|
|
411
416
|
}
|
|
412
|
-
|
|
417
|
+
function readString(record, key) {
|
|
418
|
+
const value = record?.[key];
|
|
419
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
420
|
+
}
|
|
421
|
+
function resolveDirectMuxCredentials(record) {
|
|
422
|
+
const tokenId = readString(record, "muxTokenId");
|
|
423
|
+
const tokenSecret = readString(record, "muxTokenSecret");
|
|
424
|
+
const signingKey = readString(record, "muxSigningKey");
|
|
425
|
+
const privateKey = readString(record, "muxPrivateKey");
|
|
426
|
+
if (!tokenId && !tokenSecret && !signingKey && !privateKey) {
|
|
427
|
+
return void 0;
|
|
428
|
+
}
|
|
429
|
+
if (!tokenId || !tokenSecret) {
|
|
430
|
+
throw new Error(
|
|
431
|
+
"Both muxTokenId and muxTokenSecret are required when passing direct Mux workflow credentials."
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
return {
|
|
435
|
+
tokenId,
|
|
436
|
+
tokenSecret,
|
|
437
|
+
signingKey,
|
|
438
|
+
privateKey
|
|
439
|
+
};
|
|
440
|
+
}
|
|
441
|
+
function createWorkflowMuxClient(options) {
|
|
442
|
+
return {
|
|
443
|
+
async createClient() {
|
|
444
|
+
const { default: MuxClient } = await import("@mux/mux-node");
|
|
445
|
+
return new MuxClient({
|
|
446
|
+
tokenId: options.tokenId,
|
|
447
|
+
tokenSecret: options.tokenSecret
|
|
448
|
+
});
|
|
449
|
+
},
|
|
450
|
+
getSigningKey() {
|
|
451
|
+
return options.signingKey;
|
|
452
|
+
},
|
|
453
|
+
getPrivateKey() {
|
|
454
|
+
return options.privateKey;
|
|
455
|
+
}
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
async function resolveMuxClient(credentials) {
|
|
413
459
|
const resolved = await resolveWorkflowCredentials(credentials);
|
|
414
|
-
const
|
|
415
|
-
const
|
|
460
|
+
const resolvedRecord = resolved;
|
|
461
|
+
const resolvedMuxCredentials = resolveDirectMuxCredentials(resolvedRecord);
|
|
462
|
+
if (resolvedMuxCredentials) {
|
|
463
|
+
return createWorkflowMuxClient(resolvedMuxCredentials);
|
|
464
|
+
}
|
|
465
|
+
const muxTokenId = env_default.MUX_TOKEN_ID;
|
|
466
|
+
const muxTokenSecret = env_default.MUX_TOKEN_SECRET;
|
|
416
467
|
if (!muxTokenId || !muxTokenSecret) {
|
|
417
468
|
throw new Error(
|
|
418
|
-
"Mux credentials are required. Provide
|
|
469
|
+
"Mux credentials are required. Provide muxTokenId/muxTokenSecret via workflow credentials, or set MUX_TOKEN_ID and MUX_TOKEN_SECRET environment variables."
|
|
419
470
|
);
|
|
420
471
|
}
|
|
421
|
-
return {
|
|
472
|
+
return createWorkflowMuxClient({
|
|
473
|
+
tokenId: muxTokenId,
|
|
474
|
+
tokenSecret: muxTokenSecret,
|
|
475
|
+
signingKey: env_default.MUX_SIGNING_KEY,
|
|
476
|
+
privateKey: env_default.MUX_PRIVATE_KEY
|
|
477
|
+
});
|
|
422
478
|
}
|
|
423
|
-
|
|
424
|
-
const
|
|
479
|
+
function resolveProviderApiKeyFromCredentials(provider, resolved) {
|
|
480
|
+
const record = resolved;
|
|
481
|
+
const openaiApiKey = readString(record, "openaiApiKey");
|
|
482
|
+
const anthropicApiKey = readString(record, "anthropicApiKey");
|
|
483
|
+
const googleApiKey = readString(record, "googleApiKey");
|
|
484
|
+
const hiveApiKey = readString(record, "hiveApiKey");
|
|
485
|
+
const elevenLabsApiKey = readString(record, "elevenLabsApiKey");
|
|
425
486
|
const apiKeyMap = {
|
|
426
|
-
openai:
|
|
427
|
-
anthropic:
|
|
428
|
-
google:
|
|
429
|
-
hive:
|
|
430
|
-
elevenlabs:
|
|
487
|
+
openai: openaiApiKey ?? env_default.OPENAI_API_KEY,
|
|
488
|
+
anthropic: anthropicApiKey ?? env_default.ANTHROPIC_API_KEY,
|
|
489
|
+
google: googleApiKey ?? env_default.GOOGLE_GENERATIVE_AI_API_KEY,
|
|
490
|
+
hive: hiveApiKey ?? env_default.HIVE_API_KEY,
|
|
491
|
+
elevenlabs: elevenLabsApiKey ?? env_default.ELEVENLABS_API_KEY
|
|
431
492
|
};
|
|
432
493
|
const apiKey = apiKeyMap[provider];
|
|
433
494
|
if (!apiKey) {
|
|
@@ -439,15 +500,20 @@ async function resolveProviderApiKey(provider, credentials) {
|
|
|
439
500
|
elevenlabs: "ELEVENLABS_API_KEY"
|
|
440
501
|
};
|
|
441
502
|
throw new Error(
|
|
442
|
-
`${provider} API key is required. Provide
|
|
503
|
+
`${provider} API key is required. Provide ${provider} credentials via workflow credentials or set ${envVarNames[provider]} environment variable.`
|
|
443
504
|
);
|
|
444
505
|
}
|
|
445
506
|
return apiKey;
|
|
446
507
|
}
|
|
508
|
+
async function resolveProviderApiKey(provider, credentials) {
|
|
509
|
+
const resolved = await resolveWorkflowCredentials(credentials);
|
|
510
|
+
return resolveProviderApiKeyFromCredentials(provider, resolved);
|
|
511
|
+
}
|
|
447
512
|
async function resolveMuxSigningContext(credentials) {
|
|
448
513
|
const resolved = await resolveWorkflowCredentials(credentials);
|
|
449
|
-
const
|
|
450
|
-
const
|
|
514
|
+
const resolvedRecord = resolved;
|
|
515
|
+
const keyId = readString(resolvedRecord, "muxSigningKey") ?? env_default.MUX_SIGNING_KEY;
|
|
516
|
+
const keySecret = readString(resolvedRecord, "muxPrivateKey") ?? env_default.MUX_PRIVATE_KEY;
|
|
451
517
|
if (!keyId || !keySecret) {
|
|
452
518
|
return void 0;
|
|
453
519
|
}
|
|
@@ -517,8 +583,8 @@ async function createEmbeddingModelFromConfig(provider, modelId, credentials) {
|
|
|
517
583
|
}
|
|
518
584
|
|
|
519
585
|
// src/lib/client-factory.ts
|
|
520
|
-
async function
|
|
521
|
-
return
|
|
586
|
+
async function getMuxClientFromEnv(credentials) {
|
|
587
|
+
return resolveMuxClient(credentials);
|
|
522
588
|
}
|
|
523
589
|
async function getApiKeyFromEnv(provider, credentials) {
|
|
524
590
|
return resolveProviderApiKey(provider, credentials);
|
|
@@ -544,25 +610,35 @@ function isAudioOnlyAsset(asset) {
|
|
|
544
610
|
const hasVideoTrack = asset.tracks?.some((track) => track.type === "video") ?? false;
|
|
545
611
|
return hasAudioTrack && !hasVideoTrack;
|
|
546
612
|
}
|
|
613
|
+
function toPlaybackAsset(asset) {
|
|
614
|
+
const { id: playbackId, policy } = getPlaybackId(asset);
|
|
615
|
+
return { asset, playbackId, policy };
|
|
616
|
+
}
|
|
547
617
|
async function getPlaybackIdForAsset(assetId, credentials) {
|
|
548
618
|
"use step";
|
|
549
619
|
const asset = await getMuxAsset(assetId, credentials);
|
|
550
|
-
|
|
551
|
-
return { asset, playbackId, policy };
|
|
620
|
+
return toPlaybackAsset(asset);
|
|
552
621
|
}
|
|
553
622
|
async function getMuxAsset(assetId, credentials) {
|
|
554
623
|
"use step";
|
|
555
|
-
const
|
|
556
|
-
const mux =
|
|
557
|
-
tokenId: muxTokenId,
|
|
558
|
-
tokenSecret: muxTokenSecret
|
|
559
|
-
});
|
|
624
|
+
const muxClient = await getMuxClientFromEnv(credentials);
|
|
625
|
+
const mux = await muxClient.createClient();
|
|
560
626
|
return mux.video.assets.retrieve(assetId);
|
|
561
627
|
}
|
|
562
628
|
function getAssetDurationSecondsFromAsset(asset) {
|
|
563
629
|
const duration = asset.duration;
|
|
564
630
|
return typeof duration === "number" && Number.isFinite(duration) ? duration : void 0;
|
|
565
631
|
}
|
|
632
|
+
function getVideoTrackDurationSecondsFromAsset(asset) {
|
|
633
|
+
const videoTrack = asset.tracks?.find((track) => track.type === "video");
|
|
634
|
+
const duration = videoTrack?.duration;
|
|
635
|
+
return typeof duration === "number" && Number.isFinite(duration) ? duration : void 0;
|
|
636
|
+
}
|
|
637
|
+
function getVideoTrackMaxFrameRateFromAsset(asset) {
|
|
638
|
+
const videoTrack = asset.tracks?.find((track) => track.type === "video");
|
|
639
|
+
const maxFrameRate = videoTrack?.max_frame_rate;
|
|
640
|
+
return typeof maxFrameRate === "number" && Number.isFinite(maxFrameRate) && maxFrameRate > 0 ? maxFrameRate : void 0;
|
|
641
|
+
}
|
|
566
642
|
|
|
567
643
|
// src/lib/prompt-builder.ts
|
|
568
644
|
function renderSection(section) {
|
|
@@ -679,9 +755,9 @@ async function withRetry(fn, {
|
|
|
679
755
|
}
|
|
680
756
|
|
|
681
757
|
// src/lib/url-signing.ts
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
return new
|
|
758
|
+
async function createSigningClient(context) {
|
|
759
|
+
const { default: MuxClient } = await import("@mux/mux-node");
|
|
760
|
+
return new MuxClient({
|
|
685
761
|
// These are not needed for signing, but the SDK requires them
|
|
686
762
|
// Using empty strings as we only need the jwt functionality
|
|
687
763
|
tokenId: env_default.MUX_TOKEN_ID || "",
|
|
@@ -692,7 +768,7 @@ function createSigningClient(context) {
|
|
|
692
768
|
}
|
|
693
769
|
async function signPlaybackId(playbackId, context, type = "video", params) {
|
|
694
770
|
"use step";
|
|
695
|
-
const client = createSigningClient(context);
|
|
771
|
+
const client = await createSigningClient(context);
|
|
696
772
|
const stringParams = params ? Object.fromEntries(
|
|
697
773
|
Object.entries(params).map(([key, value]) => [key, String(value)])
|
|
698
774
|
) : void 0;
|
|
@@ -707,7 +783,7 @@ async function signUrl(url, playbackId, context, type = "video", params, credent
|
|
|
707
783
|
const resolvedContext = context ?? await resolveMuxSigningContext(credentials);
|
|
708
784
|
if (!resolvedContext) {
|
|
709
785
|
throw new Error(
|
|
710
|
-
"Signed playback ID requires signing credentials. Provide muxSigningKey and muxPrivateKey
|
|
786
|
+
"Signed playback ID requires signing credentials. Provide muxSigningKey and muxPrivateKey via workflow credentials or set MUX_SIGNING_KEY and MUX_PRIVATE_KEY environment variables."
|
|
711
787
|
);
|
|
712
788
|
}
|
|
713
789
|
const token = await signPlaybackId(playbackId, resolvedContext, type, params);
|
|
@@ -1317,10 +1393,7 @@ async function hasBurnedInCaptions(assetId, options = {}) {
|
|
|
1317
1393
|
model,
|
|
1318
1394
|
provider
|
|
1319
1395
|
});
|
|
1320
|
-
const { asset: assetData, playbackId, policy } = await getPlaybackIdForAsset(
|
|
1321
|
-
assetId,
|
|
1322
|
-
credentials
|
|
1323
|
-
);
|
|
1396
|
+
const { asset: assetData, playbackId, policy } = await getPlaybackIdForAsset(assetId, credentials);
|
|
1324
1397
|
const assetDurationSeconds = getAssetDurationSecondsFromAsset(assetData);
|
|
1325
1398
|
const imageUrl = await getStoryboardUrl(playbackId, 640, policy === "signed", credentials);
|
|
1326
1399
|
let analysisResponse;
|
|
@@ -1518,10 +1591,7 @@ async function generateChapters(assetId, languageCode, options = {}) {
|
|
|
1518
1591
|
model,
|
|
1519
1592
|
provider
|
|
1520
1593
|
});
|
|
1521
|
-
const { asset: assetData, playbackId, policy } = await getPlaybackIdForAsset(
|
|
1522
|
-
assetId,
|
|
1523
|
-
credentials
|
|
1524
|
-
);
|
|
1594
|
+
const { asset: assetData, playbackId, policy } = await getPlaybackIdForAsset(assetId, credentials);
|
|
1525
1595
|
const assetDurationSeconds = getAssetDurationSecondsFromAsset(assetData);
|
|
1526
1596
|
const isAudioOnly = isAudioOnlyAsset(assetData);
|
|
1527
1597
|
const signingContext = await resolveMuxSigningContext(credentials);
|
|
@@ -1847,6 +1917,66 @@ async function generateVideoEmbeddings(assetId, options = {}) {
|
|
|
1847
1917
|
return generateEmbeddingsInternal(assetId, options);
|
|
1848
1918
|
}
|
|
1849
1919
|
|
|
1920
|
+
// src/lib/sampling-plan.ts
|
|
1921
|
+
var DEFAULT_FPS = 30;
|
|
1922
|
+
function roundToNearestFrameMs(tsMs, fps = DEFAULT_FPS) {
|
|
1923
|
+
const frameMs = 1e3 / fps;
|
|
1924
|
+
return Math.round(Math.round(tsMs / frameMs) * frameMs * 100) / 100;
|
|
1925
|
+
}
|
|
1926
|
+
function planSamplingTimestamps(options) {
|
|
1927
|
+
const DEFAULT_MIN_CANDIDATES = 10;
|
|
1928
|
+
const DEFAULT_MAX_CANDIDATES = 30;
|
|
1929
|
+
const {
|
|
1930
|
+
duration_sec,
|
|
1931
|
+
min_candidates = DEFAULT_MIN_CANDIDATES,
|
|
1932
|
+
max_candidates = DEFAULT_MAX_CANDIDATES,
|
|
1933
|
+
trim_start_sec = 1,
|
|
1934
|
+
trim_end_sec = 1,
|
|
1935
|
+
fps = DEFAULT_FPS,
|
|
1936
|
+
base_cadence_hz,
|
|
1937
|
+
anchor_percents = [0.2, 0.5, 0.8],
|
|
1938
|
+
anchor_window_sec = 1.5
|
|
1939
|
+
} = options;
|
|
1940
|
+
const usableSec = Math.max(0, duration_sec - (trim_start_sec + trim_end_sec));
|
|
1941
|
+
if (usableSec <= 0)
|
|
1942
|
+
return [];
|
|
1943
|
+
const cadenceHz = base_cadence_hz ?? (duration_sec < 15 ? 3 : duration_sec < 60 ? 2 : duration_sec < 180 ? 1.5 : 1);
|
|
1944
|
+
let target = Math.round(usableSec * cadenceHz);
|
|
1945
|
+
target = Math.max(min_candidates, Math.min(max_candidates, target));
|
|
1946
|
+
const stepSec = usableSec / target;
|
|
1947
|
+
const t0 = trim_start_sec;
|
|
1948
|
+
const base = [];
|
|
1949
|
+
for (let i = 0; i < target; i++) {
|
|
1950
|
+
const tsSec = t0 + (i + 0.5) * stepSec;
|
|
1951
|
+
base.push(tsSec * 1e3);
|
|
1952
|
+
}
|
|
1953
|
+
const slack = Math.max(0, max_candidates - base.length);
|
|
1954
|
+
const extra = [];
|
|
1955
|
+
if (slack > 0 && anchor_percents.length > 0) {
|
|
1956
|
+
const perAnchor = Math.max(1, Math.min(5, Math.floor(slack / anchor_percents.length)));
|
|
1957
|
+
for (const p of anchor_percents) {
|
|
1958
|
+
const centerSec = Math.min(
|
|
1959
|
+
t0 + usableSec - 1e-3,
|
|
1960
|
+
// nudge just inside the end bound
|
|
1961
|
+
Math.max(t0 + 1e-3, duration_sec * p)
|
|
1962
|
+
// nudge just inside the start bound
|
|
1963
|
+
);
|
|
1964
|
+
const startSec = Math.max(t0, centerSec - anchor_window_sec / 2);
|
|
1965
|
+
const endSec = Math.min(t0 + usableSec, centerSec + anchor_window_sec / 2);
|
|
1966
|
+
if (endSec <= startSec)
|
|
1967
|
+
continue;
|
|
1968
|
+
const wStep = (endSec - startSec) / perAnchor;
|
|
1969
|
+
for (let i = 0; i < perAnchor; i++) {
|
|
1970
|
+
const tsSec = startSec + (i + 0.5) * wStep;
|
|
1971
|
+
extra.push(tsSec * 1e3);
|
|
1972
|
+
}
|
|
1973
|
+
}
|
|
1974
|
+
}
|
|
1975
|
+
const all = base.concat(extra).map((ms) => roundToNearestFrameMs(ms, fps)).filter((ms) => ms >= trim_start_sec * 1e3 && ms <= (duration_sec - trim_end_sec) * 1e3);
|
|
1976
|
+
const uniqSorted = Array.from(new Set(all)).sort((a, b) => a - b);
|
|
1977
|
+
return uniqSorted.slice(0, max_candidates);
|
|
1978
|
+
}
|
|
1979
|
+
|
|
1850
1980
|
// src/primitives/thumbnails.ts
|
|
1851
1981
|
async function getThumbnailUrls(playbackId, duration, options = {}) {
|
|
1852
1982
|
"use step";
|
|
@@ -1895,31 +2025,19 @@ var HIVE_SEXUAL_CATEGORIES = [
|
|
|
1895
2025
|
"general_nsfw",
|
|
1896
2026
|
"general_suggestive",
|
|
1897
2027
|
"yes_sexual_activity",
|
|
1898
|
-
"female_underwear",
|
|
1899
|
-
"male_underwear",
|
|
1900
|
-
"bra",
|
|
1901
|
-
"panties",
|
|
1902
2028
|
"sex_toys",
|
|
1903
2029
|
"nudity_female",
|
|
1904
|
-
"nudity_male"
|
|
1905
|
-
"cleavage",
|
|
1906
|
-
"swimwear"
|
|
2030
|
+
"nudity_male"
|
|
1907
2031
|
];
|
|
1908
2032
|
var HIVE_VIOLENCE_CATEGORIES = [
|
|
1909
2033
|
"gun_in_hand",
|
|
1910
2034
|
"gun_not_in_hand",
|
|
1911
|
-
"animated_gun",
|
|
1912
2035
|
"knife_in_hand",
|
|
1913
|
-
"knife_not_in_hand",
|
|
1914
|
-
"culinary_knife_not_in_hand",
|
|
1915
|
-
"culinary_knife_in_hand",
|
|
1916
2036
|
"very_bloody",
|
|
1917
|
-
"a_little_bloody",
|
|
1918
2037
|
"other_blood",
|
|
1919
2038
|
"hanging",
|
|
1920
2039
|
"noose",
|
|
1921
2040
|
"human_corpse",
|
|
1922
|
-
"animated_corpse",
|
|
1923
2041
|
"emaciated_body",
|
|
1924
2042
|
"self_harm",
|
|
1925
2043
|
"animal_abuse",
|
|
@@ -1978,7 +2096,8 @@ async function moderateImageWithOpenAI(entry) {
|
|
|
1978
2096
|
url: entry.url,
|
|
1979
2097
|
sexual: 0,
|
|
1980
2098
|
violence: 0,
|
|
1981
|
-
error: true
|
|
2099
|
+
error: true,
|
|
2100
|
+
errorMessage: error instanceof Error ? error.message : String(error)
|
|
1982
2101
|
};
|
|
1983
2102
|
}
|
|
1984
2103
|
}
|
|
@@ -2023,7 +2142,8 @@ async function requestOpenAITextModeration(text, model, url, credentials) {
|
|
|
2023
2142
|
url,
|
|
2024
2143
|
sexual: 0,
|
|
2025
2144
|
violence: 0,
|
|
2026
|
-
error: true
|
|
2145
|
+
error: true,
|
|
2146
|
+
errorMessage: error instanceof Error ? error.message : String(error)
|
|
2027
2147
|
};
|
|
2028
2148
|
}
|
|
2029
2149
|
}
|
|
@@ -2048,7 +2168,7 @@ async function requestOpenAITranscriptModeration(transcriptText, model, maxConcu
|
|
|
2048
2168
|
const chunks = chunkTextByUtf16CodeUnits(transcriptText, 1e4);
|
|
2049
2169
|
if (!chunks.length) {
|
|
2050
2170
|
return [
|
|
2051
|
-
{ url: "transcript:0", sexual: 0, violence: 0, error: true }
|
|
2171
|
+
{ url: "transcript:0", sexual: 0, violence: 0, error: true, errorMessage: "No transcript chunks to moderate" }
|
|
2052
2172
|
];
|
|
2053
2173
|
}
|
|
2054
2174
|
const targets = chunks.map((chunk, idx) => ({
|
|
@@ -2082,34 +2202,59 @@ async function moderateImageWithHive(entry) {
|
|
|
2082
2202
|
});
|
|
2083
2203
|
formData.append("media", blob, `thumbnail.${extension}`);
|
|
2084
2204
|
}
|
|
2085
|
-
const
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2205
|
+
const controller = new AbortController();
|
|
2206
|
+
const timeout = setTimeout(() => controller.abort(), 15e3);
|
|
2207
|
+
let res;
|
|
2208
|
+
try {
|
|
2209
|
+
res = await fetch(HIVE_ENDPOINT, {
|
|
2210
|
+
method: "POST",
|
|
2211
|
+
headers: {
|
|
2212
|
+
Accept: "application/json",
|
|
2213
|
+
Authorization: `Token ${apiKey}`
|
|
2214
|
+
},
|
|
2215
|
+
body: formData,
|
|
2216
|
+
signal: controller.signal
|
|
2217
|
+
});
|
|
2218
|
+
} catch (err) {
|
|
2219
|
+
if (err?.name === "AbortError") {
|
|
2220
|
+
throw new Error("Hive request timed out after 15s");
|
|
2221
|
+
}
|
|
2222
|
+
throw err;
|
|
2223
|
+
} finally {
|
|
2224
|
+
clearTimeout(timeout);
|
|
2225
|
+
}
|
|
2093
2226
|
const json = await res.json().catch(() => void 0);
|
|
2094
2227
|
if (!res.ok) {
|
|
2095
2228
|
throw new Error(
|
|
2096
2229
|
`Hive moderation error: ${res.status} ${res.statusText} - ${JSON.stringify(json)}`
|
|
2097
2230
|
);
|
|
2098
2231
|
}
|
|
2099
|
-
|
|
2232
|
+
if (json?.return_code != null && json.return_code !== 0) {
|
|
2233
|
+
throw new Error(
|
|
2234
|
+
`Hive API error (return_code ${json.return_code}): ${json.message || "Unknown error"}`
|
|
2235
|
+
);
|
|
2236
|
+
}
|
|
2237
|
+
const classes = json?.status?.[0]?.response?.output?.[0]?.classes;
|
|
2238
|
+
if (!Array.isArray(classes)) {
|
|
2239
|
+
throw new TypeError(
|
|
2240
|
+
`Unexpected Hive response structure: ${JSON.stringify(json)}`
|
|
2241
|
+
);
|
|
2242
|
+
}
|
|
2243
|
+
const sexual = getHiveCategoryScores(classes, HIVE_SEXUAL_CATEGORIES);
|
|
2244
|
+
const violence = getHiveCategoryScores(classes, HIVE_VIOLENCE_CATEGORIES);
|
|
2100
2245
|
return {
|
|
2101
2246
|
url: entry.url,
|
|
2102
|
-
sexual
|
|
2103
|
-
violence
|
|
2247
|
+
sexual,
|
|
2248
|
+
violence,
|
|
2104
2249
|
error: false
|
|
2105
2250
|
};
|
|
2106
2251
|
} catch (error) {
|
|
2107
|
-
console.error("Hive moderation failed:", error);
|
|
2108
2252
|
return {
|
|
2109
2253
|
url: entry.url,
|
|
2110
2254
|
sexual: 0,
|
|
2111
2255
|
violence: 0,
|
|
2112
|
-
error: true
|
|
2256
|
+
error: true,
|
|
2257
|
+
errorMessage: error instanceof Error ? error.message : String(error)
|
|
2113
2258
|
};
|
|
2114
2259
|
}
|
|
2115
2260
|
}
|
|
@@ -2128,7 +2273,20 @@ async function requestHiveModeration(imageUrls, maxConcurrent = 5, submissionMod
|
|
|
2128
2273
|
source: { kind: "url", value: url },
|
|
2129
2274
|
credentials
|
|
2130
2275
|
}));
|
|
2131
|
-
return processConcurrently(targets, moderateImageWithHive, maxConcurrent);
|
|
2276
|
+
return await processConcurrently(targets, moderateImageWithHive, maxConcurrent);
|
|
2277
|
+
}
|
|
2278
|
+
async function getThumbnailUrlsFromTimestamps(playbackId, timestampsMs, options) {
|
|
2279
|
+
"use step";
|
|
2280
|
+
const { width, shouldSign, credentials } = options;
|
|
2281
|
+
const baseUrl = `https://image.mux.com/${playbackId}/thumbnail.png`;
|
|
2282
|
+
const urlPromises = timestampsMs.map(async (tsMs) => {
|
|
2283
|
+
const time = Number((tsMs / 1e3).toFixed(2));
|
|
2284
|
+
if (shouldSign) {
|
|
2285
|
+
return signUrl(baseUrl, playbackId, void 0, "thumbnail", { time, width }, credentials);
|
|
2286
|
+
}
|
|
2287
|
+
return `${baseUrl}?time=${time}&width=${width}`;
|
|
2288
|
+
});
|
|
2289
|
+
return Promise.all(urlPromises);
|
|
2132
2290
|
}
|
|
2133
2291
|
async function getModerationScores(assetId, options = {}) {
|
|
2134
2292
|
"use workflow";
|
|
@@ -2143,11 +2301,17 @@ async function getModerationScores(assetId, options = {}) {
|
|
|
2143
2301
|
maxConcurrent = 5,
|
|
2144
2302
|
imageSubmissionMode = "url",
|
|
2145
2303
|
imageDownloadOptions,
|
|
2146
|
-
credentials
|
|
2304
|
+
credentials: providedCredentials
|
|
2147
2305
|
} = options;
|
|
2306
|
+
const credentials = providedCredentials;
|
|
2148
2307
|
const { asset, playbackId, policy } = await getPlaybackIdForAsset(assetId, credentials);
|
|
2308
|
+
const videoTrackDurationSeconds = getVideoTrackDurationSecondsFromAsset(asset);
|
|
2309
|
+
const videoTrackFps = getVideoTrackMaxFrameRateFromAsset(asset);
|
|
2149
2310
|
const assetDurationSeconds = getAssetDurationSecondsFromAsset(asset);
|
|
2150
|
-
const
|
|
2311
|
+
const candidateDurations = [videoTrackDurationSeconds, assetDurationSeconds].filter(
|
|
2312
|
+
(d) => d != null
|
|
2313
|
+
);
|
|
2314
|
+
const duration = candidateDurations.length > 0 ? Math.min(...candidateDurations) : 0;
|
|
2151
2315
|
const isAudioOnly = isAudioOnlyAsset(asset);
|
|
2152
2316
|
const signingContext = await resolveMuxSigningContext(credentials);
|
|
2153
2317
|
if (policy === "signed" && !signingContext) {
|
|
@@ -2189,13 +2353,35 @@ async function getModerationScores(assetId, options = {}) {
|
|
|
2189
2353
|
throw new Error(`Unsupported moderation provider: ${provider}`);
|
|
2190
2354
|
}
|
|
2191
2355
|
} else {
|
|
2192
|
-
const thumbnailUrls =
|
|
2193
|
-
interval
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2356
|
+
const thumbnailUrls = maxSamples === void 0 ? (
|
|
2357
|
+
// Generate thumbnail URLs (signed if needed) using existing interval-based logic.
|
|
2358
|
+
await getThumbnailUrls(playbackId, duration, {
|
|
2359
|
+
interval: thumbnailInterval,
|
|
2360
|
+
width: thumbnailWidth,
|
|
2361
|
+
shouldSign: policy === "signed",
|
|
2362
|
+
credentials
|
|
2363
|
+
})
|
|
2364
|
+
) : (
|
|
2365
|
+
// In maxSamples mode, sample valid timestamps over the trimmed usable span.
|
|
2366
|
+
// Use proportional trims (≈ duration/6, capped at 5s) to stay well inside the
|
|
2367
|
+
// renderable range — Mux can't always serve thumbnails at the very edges.
|
|
2368
|
+
await getThumbnailUrlsFromTimestamps(
|
|
2369
|
+
playbackId,
|
|
2370
|
+
planSamplingTimestamps({
|
|
2371
|
+
duration_sec: duration,
|
|
2372
|
+
max_candidates: maxSamples,
|
|
2373
|
+
trim_start_sec: duration > 2 ? Math.min(5, Math.max(1, duration / 6)) : 0,
|
|
2374
|
+
trim_end_sec: duration > 2 ? Math.min(5, Math.max(1, duration / 6)) : 0,
|
|
2375
|
+
fps: videoTrackFps,
|
|
2376
|
+
base_cadence_hz: thumbnailInterval > 0 ? 1 / thumbnailInterval : void 0
|
|
2377
|
+
}),
|
|
2378
|
+
{
|
|
2379
|
+
width: thumbnailWidth,
|
|
2380
|
+
shouldSign: policy === "signed",
|
|
2381
|
+
credentials
|
|
2382
|
+
}
|
|
2383
|
+
)
|
|
2384
|
+
);
|
|
2199
2385
|
thumbnailCount = thumbnailUrls.length;
|
|
2200
2386
|
if (provider === "openai") {
|
|
2201
2387
|
thumbnailScores = await requestOpenAIModeration(
|
|
@@ -2218,6 +2404,13 @@ async function getModerationScores(assetId, options = {}) {
|
|
|
2218
2404
|
throw new Error(`Unsupported moderation provider: ${provider}`);
|
|
2219
2405
|
}
|
|
2220
2406
|
}
|
|
2407
|
+
const failed = thumbnailScores.filter((s) => s.error);
|
|
2408
|
+
if (failed.length > 0) {
|
|
2409
|
+
const details = failed.map((s) => `${s.url}: ${s.errorMessage || "Unknown error"}`).join("; ");
|
|
2410
|
+
throw new Error(
|
|
2411
|
+
`Moderation failed for ${failed.length}/${thumbnailScores.length} thumbnail(s): ${details}`
|
|
2412
|
+
);
|
|
2413
|
+
}
|
|
2221
2414
|
const maxSexual = Math.max(...thumbnailScores.map((s) => s.sexual));
|
|
2222
2415
|
const maxViolence = Math.max(...thumbnailScores.map((s) => s.violence));
|
|
2223
2416
|
const finalThresholds = { ...DEFAULT_THRESHOLDS, ...thresholds };
|
|
@@ -2228,7 +2421,7 @@ async function getModerationScores(assetId, options = {}) {
|
|
|
2228
2421
|
thumbnailScores,
|
|
2229
2422
|
usage: {
|
|
2230
2423
|
metadata: {
|
|
2231
|
-
assetDurationSeconds,
|
|
2424
|
+
assetDurationSeconds: duration,
|
|
2232
2425
|
...thumbnailCount === void 0 ? {} : { thumbnailCount }
|
|
2233
2426
|
}
|
|
2234
2427
|
},
|
|
@@ -2589,7 +2782,8 @@ async function getSummaryAndTags(assetId, options) {
|
|
|
2589
2782
|
model,
|
|
2590
2783
|
provider
|
|
2591
2784
|
});
|
|
2592
|
-
const
|
|
2785
|
+
const workflowCredentials = credentials;
|
|
2786
|
+
const { asset: assetData, playbackId, policy } = await getPlaybackIdForAsset(assetId, workflowCredentials);
|
|
2593
2787
|
const assetDurationSeconds = getAssetDurationSecondsFromAsset(assetData);
|
|
2594
2788
|
const isAudioOnly = isAudioOnlyAsset(assetData);
|
|
2595
2789
|
if (isAudioOnly && !includeTranscript) {
|
|
@@ -2597,7 +2791,7 @@ async function getSummaryAndTags(assetId, options) {
|
|
|
2597
2791
|
"Audio-only assets require a transcript. Set includeTranscript: true and ensure the asset has a ready text track (captions/subtitles)."
|
|
2598
2792
|
);
|
|
2599
2793
|
}
|
|
2600
|
-
const signingContext = await resolveMuxSigningContext(
|
|
2794
|
+
const signingContext = await resolveMuxSigningContext(workflowCredentials);
|
|
2601
2795
|
if (policy === "signed" && !signingContext) {
|
|
2602
2796
|
throw new Error(
|
|
2603
2797
|
"Signed playback ID requires signing credentials. Set MUX_SIGNING_KEY and MUX_PRIVATE_KEY environment variables."
|
|
@@ -2606,7 +2800,7 @@ async function getSummaryAndTags(assetId, options) {
|
|
|
2606
2800
|
const transcriptText = includeTranscript ? (await fetchTranscriptForAsset(assetData, playbackId, {
|
|
2607
2801
|
cleanTranscript,
|
|
2608
2802
|
shouldSign: policy === "signed",
|
|
2609
|
-
credentials,
|
|
2803
|
+
credentials: workflowCredentials,
|
|
2610
2804
|
required: isAudioOnly
|
|
2611
2805
|
})).transcriptText : "";
|
|
2612
2806
|
const userPrompt = buildUserPrompt4({
|
|
@@ -2626,10 +2820,10 @@ async function getSummaryAndTags(assetId, options) {
|
|
|
2626
2820
|
modelConfig.modelId,
|
|
2627
2821
|
userPrompt,
|
|
2628
2822
|
systemPrompt,
|
|
2629
|
-
|
|
2823
|
+
workflowCredentials
|
|
2630
2824
|
);
|
|
2631
2825
|
} else {
|
|
2632
|
-
const storyboardUrl = await getStoryboardUrl(playbackId, 640, policy === "signed",
|
|
2826
|
+
const storyboardUrl = await getStoryboardUrl(playbackId, 640, policy === "signed", workflowCredentials);
|
|
2633
2827
|
imageUrl = storyboardUrl;
|
|
2634
2828
|
if (imageSubmissionMode === "base64") {
|
|
2635
2829
|
const downloadResult = await downloadImageAsBase64(storyboardUrl, imageDownloadOptions);
|
|
@@ -2639,7 +2833,7 @@ async function getSummaryAndTags(assetId, options) {
|
|
|
2639
2833
|
modelConfig.modelId,
|
|
2640
2834
|
userPrompt,
|
|
2641
2835
|
systemPrompt,
|
|
2642
|
-
|
|
2836
|
+
workflowCredentials
|
|
2643
2837
|
);
|
|
2644
2838
|
} else {
|
|
2645
2839
|
analysisResponse = await withRetry(() => analyzeStoryboard2(
|
|
@@ -2648,7 +2842,7 @@ async function getSummaryAndTags(assetId, options) {
|
|
|
2648
2842
|
modelConfig.modelId,
|
|
2649
2843
|
userPrompt,
|
|
2650
2844
|
systemPrompt,
|
|
2651
|
-
|
|
2845
|
+
workflowCredentials
|
|
2652
2846
|
));
|
|
2653
2847
|
}
|
|
2654
2848
|
}
|
|
@@ -2684,9 +2878,6 @@ async function getSummaryAndTags(assetId, options) {
|
|
|
2684
2878
|
};
|
|
2685
2879
|
}
|
|
2686
2880
|
|
|
2687
|
-
// src/workflows/translate-audio.ts
|
|
2688
|
-
import Mux3 from "@mux/mux-node";
|
|
2689
|
-
|
|
2690
2881
|
// src/lib/language-codes.ts
|
|
2691
2882
|
var ISO639_1_TO_3 = {
|
|
2692
2883
|
// Major world languages
|
|
@@ -2845,6 +3036,260 @@ function getLanguageName(code) {
|
|
|
2845
3036
|
}
|
|
2846
3037
|
}
|
|
2847
3038
|
|
|
3039
|
+
// src/lib/s3-sigv4.ts
|
|
3040
|
+
var AWS4_ALGORITHM = "AWS4-HMAC-SHA256";
|
|
3041
|
+
var AWS4_REQUEST_TERMINATOR = "aws4_request";
|
|
3042
|
+
var AWS4_SERVICE = "s3";
|
|
3043
|
+
var S3_ALLOWED_ENDPOINT_PATTERNS = parseEndpointAllowlist(
|
|
3044
|
+
env_default.S3_ALLOWED_ENDPOINT_HOSTS
|
|
3045
|
+
);
|
|
3046
|
+
function getCrypto() {
|
|
3047
|
+
const webCrypto = globalThis.crypto;
|
|
3048
|
+
if (!webCrypto?.subtle) {
|
|
3049
|
+
throw new Error("Web Crypto API is required for S3 signing.");
|
|
3050
|
+
}
|
|
3051
|
+
return webCrypto;
|
|
3052
|
+
}
|
|
3053
|
+
var textEncoder = new TextEncoder();
|
|
3054
|
+
function toBytes(value) {
|
|
3055
|
+
return typeof value === "string" ? textEncoder.encode(value) : value;
|
|
3056
|
+
}
|
|
3057
|
+
function bytesToHex(bytes) {
|
|
3058
|
+
return Array.from(bytes).map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
3059
|
+
}
|
|
3060
|
+
async function sha256Hex(value) {
|
|
3061
|
+
const digest = await getCrypto().subtle.digest("SHA-256", toBytes(value));
|
|
3062
|
+
return bytesToHex(new Uint8Array(digest));
|
|
3063
|
+
}
|
|
3064
|
+
async function hmacSha256Raw(key, value) {
|
|
3065
|
+
const cryptoKey = await getCrypto().subtle.importKey(
|
|
3066
|
+
"raw",
|
|
3067
|
+
key,
|
|
3068
|
+
{ name: "HMAC", hash: "SHA-256" },
|
|
3069
|
+
false,
|
|
3070
|
+
["sign"]
|
|
3071
|
+
);
|
|
3072
|
+
const signature = await getCrypto().subtle.sign("HMAC", cryptoKey, textEncoder.encode(value));
|
|
3073
|
+
return new Uint8Array(signature);
|
|
3074
|
+
}
|
|
3075
|
+
async function deriveSigningKey(secretAccessKey, shortDate, region) {
|
|
3076
|
+
const kDate = await hmacSha256Raw(textEncoder.encode(`AWS4${secretAccessKey}`), shortDate);
|
|
3077
|
+
const kRegion = await hmacSha256Raw(kDate, region);
|
|
3078
|
+
const kService = await hmacSha256Raw(kRegion, AWS4_SERVICE);
|
|
3079
|
+
return hmacSha256Raw(kService, AWS4_REQUEST_TERMINATOR);
|
|
3080
|
+
}
|
|
3081
|
+
function formatAmzDate(date = /* @__PURE__ */ new Date()) {
|
|
3082
|
+
const iso = date.toISOString();
|
|
3083
|
+
const shortDate = iso.slice(0, 10).replace(/-/g, "");
|
|
3084
|
+
const amzDate = `${iso.slice(0, 19).replace(/[-:]/g, "")}Z`;
|
|
3085
|
+
return { amzDate, shortDate };
|
|
3086
|
+
}
|
|
3087
|
+
function encodeRFC3986(value) {
|
|
3088
|
+
return encodeURIComponent(value).replace(/[!'()*]/g, (char) => `%${char.charCodeAt(0).toString(16).toUpperCase()}`);
|
|
3089
|
+
}
|
|
3090
|
+
function encodePath(path) {
|
|
3091
|
+
return path.split("/").map((segment) => encodeRFC3986(segment)).join("/");
|
|
3092
|
+
}
|
|
3093
|
+
function normalizeEndpoint(endpoint) {
|
|
3094
|
+
let url;
|
|
3095
|
+
try {
|
|
3096
|
+
url = new URL(endpoint);
|
|
3097
|
+
} catch {
|
|
3098
|
+
throw new Error(`Invalid S3 endpoint: ${endpoint}`);
|
|
3099
|
+
}
|
|
3100
|
+
if (url.search || url.hash) {
|
|
3101
|
+
throw new Error("S3 endpoint must not include query params or hash fragments.");
|
|
3102
|
+
}
|
|
3103
|
+
enforceEndpointPolicy(url);
|
|
3104
|
+
return url;
|
|
3105
|
+
}
|
|
3106
|
+
function parseEndpointAllowlist(allowlist) {
|
|
3107
|
+
if (!allowlist) {
|
|
3108
|
+
return [];
|
|
3109
|
+
}
|
|
3110
|
+
return allowlist.split(",").map((value) => value.trim().toLowerCase()).filter(Boolean);
|
|
3111
|
+
}
|
|
3112
|
+
function hostnameMatchesPattern(hostname, pattern) {
|
|
3113
|
+
if (pattern.startsWith("*.")) {
|
|
3114
|
+
const suffix = pattern.slice(1);
|
|
3115
|
+
return hostname.endsWith(suffix) && hostname.length > suffix.length;
|
|
3116
|
+
}
|
|
3117
|
+
return hostname === pattern;
|
|
3118
|
+
}
|
|
3119
|
+
function enforceEndpointPolicy(url) {
|
|
3120
|
+
const hostname = url.hostname.toLowerCase();
|
|
3121
|
+
if (url.protocol !== "https:") {
|
|
3122
|
+
throw new Error(
|
|
3123
|
+
`Insecure S3 endpoint protocol "${url.protocol}" is not allowed. Use HTTPS.`
|
|
3124
|
+
);
|
|
3125
|
+
}
|
|
3126
|
+
if (S3_ALLOWED_ENDPOINT_PATTERNS.length > 0 && !S3_ALLOWED_ENDPOINT_PATTERNS.some((pattern) => hostnameMatchesPattern(hostname, pattern))) {
|
|
3127
|
+
throw new Error(
|
|
3128
|
+
`S3 endpoint host "${hostname}" is not in S3_ALLOWED_ENDPOINT_HOSTS.`
|
|
3129
|
+
);
|
|
3130
|
+
}
|
|
3131
|
+
}
|
|
3132
|
+
function buildCanonicalUri(endpoint, bucket, key) {
|
|
3133
|
+
const endpointPath = endpoint.pathname === "/" ? "" : encodePath(endpoint.pathname.replace(/\/+$/, ""));
|
|
3134
|
+
const encodedBucket = encodeRFC3986(bucket);
|
|
3135
|
+
const encodedKey = encodePath(key);
|
|
3136
|
+
return `${endpointPath}/${encodedBucket}/${encodedKey}`;
|
|
3137
|
+
}
|
|
3138
|
+
function buildCanonicalQuery(params) {
|
|
3139
|
+
return Object.entries(params).sort(([a], [b]) => a.localeCompare(b)).map(([key, value]) => `${encodeRFC3986(key)}=${encodeRFC3986(value)}`).join("&");
|
|
3140
|
+
}
|
|
3141
|
+
async function signString(secretAccessKey, shortDate, region, value) {
|
|
3142
|
+
const signingKey = await deriveSigningKey(secretAccessKey, shortDate, region);
|
|
3143
|
+
const signatureBytes = await hmacSha256Raw(signingKey, value);
|
|
3144
|
+
return bytesToHex(signatureBytes);
|
|
3145
|
+
}
|
|
3146
|
+
function buildCredentialScope(shortDate, region) {
|
|
3147
|
+
return `${shortDate}/${region}/${AWS4_SERVICE}/${AWS4_REQUEST_TERMINATOR}`;
|
|
3148
|
+
}
|
|
3149
|
+
async function putObjectToS3({
|
|
3150
|
+
accessKeyId,
|
|
3151
|
+
secretAccessKey,
|
|
3152
|
+
endpoint,
|
|
3153
|
+
region,
|
|
3154
|
+
bucket,
|
|
3155
|
+
key,
|
|
3156
|
+
body,
|
|
3157
|
+
contentType
|
|
3158
|
+
}) {
|
|
3159
|
+
const resolvedEndpoint = normalizeEndpoint(endpoint);
|
|
3160
|
+
const canonicalUri = buildCanonicalUri(resolvedEndpoint, bucket, key);
|
|
3161
|
+
const host = resolvedEndpoint.host;
|
|
3162
|
+
const normalizedContentType = contentType?.trim();
|
|
3163
|
+
const { amzDate, shortDate } = formatAmzDate();
|
|
3164
|
+
const payloadHash = await sha256Hex(body);
|
|
3165
|
+
const signingHeaders = [
|
|
3166
|
+
["host", host],
|
|
3167
|
+
["x-amz-content-sha256", payloadHash],
|
|
3168
|
+
["x-amz-date", amzDate],
|
|
3169
|
+
...normalizedContentType ? [["content-type", normalizedContentType]] : []
|
|
3170
|
+
].sort(([a], [b]) => a.localeCompare(b));
|
|
3171
|
+
const canonicalHeaders = signingHeaders.map(([name, value]) => `${name}:${value}`).join("\n");
|
|
3172
|
+
const signedHeaders = signingHeaders.map(([name]) => name).join(";");
|
|
3173
|
+
const canonicalRequest = [
|
|
3174
|
+
"PUT",
|
|
3175
|
+
canonicalUri,
|
|
3176
|
+
"",
|
|
3177
|
+
`${canonicalHeaders}
|
|
3178
|
+
`,
|
|
3179
|
+
signedHeaders,
|
|
3180
|
+
payloadHash
|
|
3181
|
+
].join("\n");
|
|
3182
|
+
const credentialScope = buildCredentialScope(shortDate, region);
|
|
3183
|
+
const stringToSign = [
|
|
3184
|
+
AWS4_ALGORITHM,
|
|
3185
|
+
amzDate,
|
|
3186
|
+
credentialScope,
|
|
3187
|
+
await sha256Hex(canonicalRequest)
|
|
3188
|
+
].join("\n");
|
|
3189
|
+
const signature = await signString(secretAccessKey, shortDate, region, stringToSign);
|
|
3190
|
+
const authorization = `${AWS4_ALGORITHM} Credential=${accessKeyId}/${credentialScope}, SignedHeaders=${signedHeaders}, Signature=${signature}`;
|
|
3191
|
+
const requestUrl = `${resolvedEndpoint.origin}${canonicalUri}`;
|
|
3192
|
+
const response = await fetch(requestUrl, {
|
|
3193
|
+
method: "PUT",
|
|
3194
|
+
headers: {
|
|
3195
|
+
"Authorization": authorization,
|
|
3196
|
+
"x-amz-content-sha256": payloadHash,
|
|
3197
|
+
"x-amz-date": amzDate,
|
|
3198
|
+
...normalizedContentType ? { "content-type": normalizedContentType } : {}
|
|
3199
|
+
},
|
|
3200
|
+
body
|
|
3201
|
+
});
|
|
3202
|
+
if (!response.ok) {
|
|
3203
|
+
const errorBody = await response.text().catch(() => "");
|
|
3204
|
+
const detail = errorBody ? ` ${errorBody}` : "";
|
|
3205
|
+
throw new Error(`S3 PUT failed (${response.status} ${response.statusText}).${detail}`);
|
|
3206
|
+
}
|
|
3207
|
+
}
|
|
3208
|
+
async function createPresignedGetUrl({
|
|
3209
|
+
accessKeyId,
|
|
3210
|
+
secretAccessKey,
|
|
3211
|
+
endpoint,
|
|
3212
|
+
region,
|
|
3213
|
+
bucket,
|
|
3214
|
+
key,
|
|
3215
|
+
expiresInSeconds = 3600
|
|
3216
|
+
}) {
|
|
3217
|
+
const resolvedEndpoint = normalizeEndpoint(endpoint);
|
|
3218
|
+
const canonicalUri = buildCanonicalUri(resolvedEndpoint, bucket, key);
|
|
3219
|
+
const host = resolvedEndpoint.host;
|
|
3220
|
+
const { amzDate, shortDate } = formatAmzDate();
|
|
3221
|
+
const credentialScope = buildCredentialScope(shortDate, region);
|
|
3222
|
+
const signedHeaders = "host";
|
|
3223
|
+
const queryParams = {
|
|
3224
|
+
"X-Amz-Algorithm": AWS4_ALGORITHM,
|
|
3225
|
+
"X-Amz-Credential": `${accessKeyId}/${credentialScope}`,
|
|
3226
|
+
"X-Amz-Date": amzDate,
|
|
3227
|
+
"X-Amz-Expires": `${expiresInSeconds}`,
|
|
3228
|
+
"X-Amz-SignedHeaders": signedHeaders
|
|
3229
|
+
};
|
|
3230
|
+
const canonicalQuery = buildCanonicalQuery(queryParams);
|
|
3231
|
+
const canonicalRequest = [
|
|
3232
|
+
"GET",
|
|
3233
|
+
canonicalUri,
|
|
3234
|
+
canonicalQuery,
|
|
3235
|
+
`host:${host}
|
|
3236
|
+
`,
|
|
3237
|
+
signedHeaders,
|
|
3238
|
+
"UNSIGNED-PAYLOAD"
|
|
3239
|
+
].join("\n");
|
|
3240
|
+
const stringToSign = [
|
|
3241
|
+
AWS4_ALGORITHM,
|
|
3242
|
+
amzDate,
|
|
3243
|
+
credentialScope,
|
|
3244
|
+
await sha256Hex(canonicalRequest)
|
|
3245
|
+
].join("\n");
|
|
3246
|
+
const signature = await signString(secretAccessKey, shortDate, region, stringToSign);
|
|
3247
|
+
const queryWithSignature = `${canonicalQuery}&X-Amz-Signature=${signature}`;
|
|
3248
|
+
return `${resolvedEndpoint.origin}${canonicalUri}?${queryWithSignature}`;
|
|
3249
|
+
}
|
|
3250
|
+
|
|
3251
|
+
// src/lib/storage-adapter.ts
|
|
3252
|
+
function requireCredentials(accessKeyId, secretAccessKey) {
|
|
3253
|
+
if (!accessKeyId || !secretAccessKey) {
|
|
3254
|
+
throw new Error(
|
|
3255
|
+
"S3 credentials are required for default storage operations. Provide S3_ACCESS_KEY_ID and S3_SECRET_ACCESS_KEY or pass options.storageAdapter."
|
|
3256
|
+
);
|
|
3257
|
+
}
|
|
3258
|
+
return { accessKeyId, secretAccessKey };
|
|
3259
|
+
}
|
|
3260
|
+
async function putObjectWithStorageAdapter(input, adapter) {
|
|
3261
|
+
if (adapter) {
|
|
3262
|
+
await adapter.putObject(input);
|
|
3263
|
+
return;
|
|
3264
|
+
}
|
|
3265
|
+
const credentials = requireCredentials(input.accessKeyId, input.secretAccessKey);
|
|
3266
|
+
await putObjectToS3({
|
|
3267
|
+
accessKeyId: credentials.accessKeyId,
|
|
3268
|
+
secretAccessKey: credentials.secretAccessKey,
|
|
3269
|
+
endpoint: input.endpoint,
|
|
3270
|
+
region: input.region,
|
|
3271
|
+
bucket: input.bucket,
|
|
3272
|
+
key: input.key,
|
|
3273
|
+
body: input.body,
|
|
3274
|
+
contentType: input.contentType
|
|
3275
|
+
});
|
|
3276
|
+
}
|
|
3277
|
+
async function createPresignedGetUrlWithStorageAdapter(input, adapter) {
|
|
3278
|
+
if (adapter) {
|
|
3279
|
+
return adapter.createPresignedGetUrl(input);
|
|
3280
|
+
}
|
|
3281
|
+
const credentials = requireCredentials(input.accessKeyId, input.secretAccessKey);
|
|
3282
|
+
return createPresignedGetUrl({
|
|
3283
|
+
accessKeyId: credentials.accessKeyId,
|
|
3284
|
+
secretAccessKey: credentials.secretAccessKey,
|
|
3285
|
+
endpoint: input.endpoint,
|
|
3286
|
+
region: input.region,
|
|
3287
|
+
bucket: input.bucket,
|
|
3288
|
+
key: input.key,
|
|
3289
|
+
expiresInSeconds: input.expiresInSeconds
|
|
3290
|
+
});
|
|
3291
|
+
}
|
|
3292
|
+
|
|
2848
3293
|
// src/workflows/translate-audio.ts
|
|
2849
3294
|
var STATIC_RENDITION_POLL_INTERVAL_MS = 5e3;
|
|
2850
3295
|
var STATIC_RENDITION_MAX_ATTEMPTS = 36;
|
|
@@ -2864,11 +3309,8 @@ function getReadyAudioStaticRendition(asset) {
|
|
|
2864
3309
|
var hasReadyAudioStaticRendition = (asset) => Boolean(getReadyAudioStaticRendition(asset));
|
|
2865
3310
|
async function requestStaticRenditionCreation(assetId, credentials) {
|
|
2866
3311
|
"use step";
|
|
2867
|
-
const
|
|
2868
|
-
const mux =
|
|
2869
|
-
tokenId: muxTokenId,
|
|
2870
|
-
tokenSecret: muxTokenSecret
|
|
2871
|
-
});
|
|
3312
|
+
const muxClient = await resolveMuxClient(credentials);
|
|
3313
|
+
const mux = await muxClient.createClient();
|
|
2872
3314
|
try {
|
|
2873
3315
|
await mux.video.assets.createStaticRendition(assetId, {
|
|
2874
3316
|
resolution: "audio-only"
|
|
@@ -2890,11 +3332,8 @@ async function waitForAudioStaticRendition({
|
|
|
2890
3332
|
credentials
|
|
2891
3333
|
}) {
|
|
2892
3334
|
"use step";
|
|
2893
|
-
const
|
|
2894
|
-
const mux =
|
|
2895
|
-
tokenId: muxTokenId,
|
|
2896
|
-
tokenSecret: muxTokenSecret
|
|
2897
|
-
});
|
|
3335
|
+
const muxClient = await resolveMuxClient(credentials);
|
|
3336
|
+
const mux = await muxClient.createClient();
|
|
2898
3337
|
let currentAsset = initialAsset;
|
|
2899
3338
|
if (hasReadyAudioStaticRendition(currentAsset)) {
|
|
2900
3339
|
return currentAsset;
|
|
@@ -3007,53 +3446,40 @@ async function uploadDubbedAudioToS3({
|
|
|
3007
3446
|
toLanguageCode,
|
|
3008
3447
|
s3Endpoint,
|
|
3009
3448
|
s3Region,
|
|
3010
|
-
s3Bucket
|
|
3449
|
+
s3Bucket,
|
|
3450
|
+
storageAdapter
|
|
3011
3451
|
}) {
|
|
3012
3452
|
"use step";
|
|
3013
|
-
const { S3Client, GetObjectCommand } = await import("@aws-sdk/client-s3");
|
|
3014
|
-
const { Upload } = await import("@aws-sdk/lib-storage");
|
|
3015
|
-
const { getSignedUrl } = await import("@aws-sdk/s3-request-presigner");
|
|
3016
3453
|
const s3AccessKeyId = env_default.S3_ACCESS_KEY_ID;
|
|
3017
3454
|
const s3SecretAccessKey = env_default.S3_SECRET_ACCESS_KEY;
|
|
3018
|
-
const
|
|
3455
|
+
const audioKey = `audio-translations/${assetId}/auto-to-${toLanguageCode}-${Date.now()}.m4a`;
|
|
3456
|
+
await putObjectWithStorageAdapter({
|
|
3457
|
+
accessKeyId: s3AccessKeyId,
|
|
3458
|
+
secretAccessKey: s3SecretAccessKey,
|
|
3459
|
+
endpoint: s3Endpoint,
|
|
3019
3460
|
region: s3Region,
|
|
3461
|
+
bucket: s3Bucket,
|
|
3462
|
+
key: audioKey,
|
|
3463
|
+
body: new Uint8Array(dubbedAudioBuffer),
|
|
3464
|
+
contentType: "audio/mp4"
|
|
3465
|
+
}, storageAdapter);
|
|
3466
|
+
const presignedUrl = await createPresignedGetUrlWithStorageAdapter({
|
|
3467
|
+
accessKeyId: s3AccessKeyId,
|
|
3468
|
+
secretAccessKey: s3SecretAccessKey,
|
|
3020
3469
|
endpoint: s3Endpoint,
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
});
|
|
3027
|
-
const audioKey = `audio-translations/${assetId}/auto-to-${toLanguageCode}-${Date.now()}.m4a`;
|
|
3028
|
-
const upload = new Upload({
|
|
3029
|
-
client: s3Client,
|
|
3030
|
-
params: {
|
|
3031
|
-
Bucket: s3Bucket,
|
|
3032
|
-
Key: audioKey,
|
|
3033
|
-
Body: new Uint8Array(dubbedAudioBuffer),
|
|
3034
|
-
ContentType: "audio/mp4"
|
|
3035
|
-
}
|
|
3036
|
-
});
|
|
3037
|
-
await upload.done();
|
|
3038
|
-
const getObjectCommand = new GetObjectCommand({
|
|
3039
|
-
Bucket: s3Bucket,
|
|
3040
|
-
Key: audioKey
|
|
3041
|
-
});
|
|
3042
|
-
const presignedUrl = await getSignedUrl(s3Client, getObjectCommand, {
|
|
3043
|
-
expiresIn: 3600
|
|
3044
|
-
// 1 hour
|
|
3045
|
-
});
|
|
3470
|
+
region: s3Region,
|
|
3471
|
+
bucket: s3Bucket,
|
|
3472
|
+
key: audioKey,
|
|
3473
|
+
expiresInSeconds: 3600
|
|
3474
|
+
}, storageAdapter);
|
|
3046
3475
|
console.warn(`\u2705 Audio uploaded successfully to: ${audioKey}`);
|
|
3047
3476
|
console.warn(`\u{1F517} Generated presigned URL (expires in 1 hour)`);
|
|
3048
3477
|
return presignedUrl;
|
|
3049
3478
|
}
|
|
3050
3479
|
async function createAudioTrackOnMux(assetId, languageCode, presignedUrl, credentials) {
|
|
3051
3480
|
"use step";
|
|
3052
|
-
const
|
|
3053
|
-
const mux =
|
|
3054
|
-
tokenId: muxTokenId,
|
|
3055
|
-
tokenSecret: muxTokenSecret
|
|
3056
|
-
});
|
|
3481
|
+
const muxClient = await resolveMuxClient(credentials);
|
|
3482
|
+
const mux = await muxClient.createClient();
|
|
3057
3483
|
const languageName = new Intl.DisplayNames(["en"], { type: "language" }).of(languageCode) || languageCode.toUpperCase();
|
|
3058
3484
|
const trackName = `${languageName} (auto-dubbed)`;
|
|
3059
3485
|
const trackResponse = await mux.video.assets.createTrack(assetId, {
|
|
@@ -3073,34 +3499,24 @@ async function translateAudio(assetId, toLanguageCode, options = {}) {
|
|
|
3073
3499
|
provider = "elevenlabs",
|
|
3074
3500
|
numSpeakers = 0,
|
|
3075
3501
|
// 0 = auto-detect
|
|
3076
|
-
elevenLabsApiKey,
|
|
3077
3502
|
uploadToMux = true,
|
|
3503
|
+
storageAdapter,
|
|
3078
3504
|
credentials: providedCredentials
|
|
3079
3505
|
} = options;
|
|
3080
3506
|
if (provider !== "elevenlabs") {
|
|
3081
3507
|
throw new Error("Only ElevenLabs provider is currently supported for audio translation");
|
|
3082
3508
|
}
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
credentials = providedCredentials;
|
|
3086
|
-
} else if (providedCredentials || elevenLabsApiKey) {
|
|
3087
|
-
credentials = {
|
|
3088
|
-
...providedCredentials ?? {},
|
|
3089
|
-
...elevenLabsApiKey ? { elevenLabsApiKey } : {}
|
|
3090
|
-
};
|
|
3091
|
-
}
|
|
3509
|
+
const credentials = providedCredentials;
|
|
3510
|
+
const effectiveStorageAdapter = storageAdapter;
|
|
3092
3511
|
const s3Endpoint = options.s3Endpoint ?? env_default.S3_ENDPOINT;
|
|
3093
3512
|
const s3Region = options.s3Region ?? env_default.S3_REGION ?? "auto";
|
|
3094
3513
|
const s3Bucket = options.s3Bucket ?? env_default.S3_BUCKET;
|
|
3095
3514
|
const s3AccessKeyId = env_default.S3_ACCESS_KEY_ID;
|
|
3096
3515
|
const s3SecretAccessKey = env_default.S3_SECRET_ACCESS_KEY;
|
|
3097
|
-
if (uploadToMux && (!s3Endpoint || !s3Bucket || !s3AccessKeyId || !s3SecretAccessKey)) {
|
|
3098
|
-
throw new Error("
|
|
3516
|
+
if (uploadToMux && (!s3Endpoint || !s3Bucket || !effectiveStorageAdapter && (!s3AccessKeyId || !s3SecretAccessKey))) {
|
|
3517
|
+
throw new Error("Storage configuration is required for uploading to Mux. Provide s3Endpoint and s3Bucket. If no storageAdapter is supplied, also provide s3AccessKeyId and s3SecretAccessKey in options or set S3_ENDPOINT, S3_BUCKET, S3_ACCESS_KEY_ID, and S3_SECRET_ACCESS_KEY environment variables.");
|
|
3099
3518
|
}
|
|
3100
|
-
const { asset: initialAsset, playbackId, policy } = await getPlaybackIdForAsset(
|
|
3101
|
-
assetId,
|
|
3102
|
-
credentials
|
|
3103
|
-
);
|
|
3519
|
+
const { asset: initialAsset, playbackId, policy } = await getPlaybackIdForAsset(assetId, credentials);
|
|
3104
3520
|
const assetDurationSeconds = getAssetDurationSecondsFromAsset(initialAsset);
|
|
3105
3521
|
let currentAsset = initialAsset;
|
|
3106
3522
|
if (!hasReadyAudioStaticRendition(currentAsset)) {
|
|
@@ -3214,7 +3630,8 @@ async function translateAudio(assetId, toLanguageCode, options = {}) {
|
|
|
3214
3630
|
toLanguageCode,
|
|
3215
3631
|
s3Endpoint,
|
|
3216
3632
|
s3Region,
|
|
3217
|
-
s3Bucket
|
|
3633
|
+
s3Bucket,
|
|
3634
|
+
storageAdapter: effectiveStorageAdapter
|
|
3218
3635
|
});
|
|
3219
3636
|
} catch (error) {
|
|
3220
3637
|
throw new Error(`Failed to upload audio to S3: ${error instanceof Error ? error.message : "Unknown error"}`);
|
|
@@ -3250,7 +3667,6 @@ async function translateAudio(assetId, toLanguageCode, options = {}) {
|
|
|
3250
3667
|
}
|
|
3251
3668
|
|
|
3252
3669
|
// src/workflows/translate-captions.ts
|
|
3253
|
-
import Mux4 from "@mux/mux-node";
|
|
3254
3670
|
import { generateText as generateText5, Output as Output5 } from "ai";
|
|
3255
3671
|
import { z as z6 } from "zod";
|
|
3256
3672
|
var translationSchema = z6.object({
|
|
@@ -3304,51 +3720,37 @@ async function uploadVttToS3({
|
|
|
3304
3720
|
toLanguageCode,
|
|
3305
3721
|
s3Endpoint,
|
|
3306
3722
|
s3Region,
|
|
3307
|
-
s3Bucket
|
|
3723
|
+
s3Bucket,
|
|
3724
|
+
storageAdapter
|
|
3308
3725
|
}) {
|
|
3309
3726
|
"use step";
|
|
3310
|
-
const { S3Client, GetObjectCommand } = await import("@aws-sdk/client-s3");
|
|
3311
|
-
const { Upload } = await import("@aws-sdk/lib-storage");
|
|
3312
|
-
const { getSignedUrl } = await import("@aws-sdk/s3-request-presigner");
|
|
3313
3727
|
const s3AccessKeyId = env_default.S3_ACCESS_KEY_ID;
|
|
3314
3728
|
const s3SecretAccessKey = env_default.S3_SECRET_ACCESS_KEY;
|
|
3315
|
-
const
|
|
3729
|
+
const vttKey = `translations/${assetId}/${fromLanguageCode}-to-${toLanguageCode}-${Date.now()}.vtt`;
|
|
3730
|
+
await putObjectWithStorageAdapter({
|
|
3731
|
+
accessKeyId: s3AccessKeyId,
|
|
3732
|
+
secretAccessKey: s3SecretAccessKey,
|
|
3733
|
+
endpoint: s3Endpoint,
|
|
3316
3734
|
region: s3Region,
|
|
3735
|
+
bucket: s3Bucket,
|
|
3736
|
+
key: vttKey,
|
|
3737
|
+
body: translatedVtt,
|
|
3738
|
+
contentType: "text/vtt"
|
|
3739
|
+
}, storageAdapter);
|
|
3740
|
+
return createPresignedGetUrlWithStorageAdapter({
|
|
3741
|
+
accessKeyId: s3AccessKeyId,
|
|
3742
|
+
secretAccessKey: s3SecretAccessKey,
|
|
3317
3743
|
endpoint: s3Endpoint,
|
|
3318
|
-
|
|
3319
|
-
|
|
3320
|
-
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
});
|
|
3324
|
-
const vttKey = `translations/${assetId}/${fromLanguageCode}-to-${toLanguageCode}-${Date.now()}.vtt`;
|
|
3325
|
-
const upload = new Upload({
|
|
3326
|
-
client: s3Client,
|
|
3327
|
-
params: {
|
|
3328
|
-
Bucket: s3Bucket,
|
|
3329
|
-
Key: vttKey,
|
|
3330
|
-
Body: translatedVtt,
|
|
3331
|
-
ContentType: "text/vtt"
|
|
3332
|
-
}
|
|
3333
|
-
});
|
|
3334
|
-
await upload.done();
|
|
3335
|
-
const getObjectCommand = new GetObjectCommand({
|
|
3336
|
-
Bucket: s3Bucket,
|
|
3337
|
-
Key: vttKey
|
|
3338
|
-
});
|
|
3339
|
-
const presignedUrl = await getSignedUrl(s3Client, getObjectCommand, {
|
|
3340
|
-
expiresIn: 3600
|
|
3341
|
-
// 1 hour
|
|
3342
|
-
});
|
|
3343
|
-
return presignedUrl;
|
|
3744
|
+
region: s3Region,
|
|
3745
|
+
bucket: s3Bucket,
|
|
3746
|
+
key: vttKey,
|
|
3747
|
+
expiresInSeconds: 3600
|
|
3748
|
+
}, storageAdapter);
|
|
3344
3749
|
}
|
|
3345
3750
|
async function createTextTrackOnMux(assetId, languageCode, trackName, presignedUrl, credentials) {
|
|
3346
3751
|
"use step";
|
|
3347
|
-
const
|
|
3348
|
-
const mux =
|
|
3349
|
-
tokenId: muxTokenId,
|
|
3350
|
-
tokenSecret: muxTokenSecret
|
|
3351
|
-
});
|
|
3752
|
+
const muxClient = await resolveMuxClient(credentials);
|
|
3753
|
+
const mux = await muxClient.createClient();
|
|
3352
3754
|
const trackResponse = await mux.video.assets.createTrack(assetId, {
|
|
3353
3755
|
type: "text",
|
|
3354
3756
|
text_type: "subtitles",
|
|
@@ -3370,8 +3772,11 @@ async function translateCaptions(assetId, fromLanguageCode, toLanguageCode, opti
|
|
|
3370
3772
|
s3Region: providedS3Region,
|
|
3371
3773
|
s3Bucket: providedS3Bucket,
|
|
3372
3774
|
uploadToMux: uploadToMuxOption,
|
|
3373
|
-
|
|
3775
|
+
storageAdapter,
|
|
3776
|
+
credentials: providedCredentials
|
|
3374
3777
|
} = options;
|
|
3778
|
+
const credentials = providedCredentials;
|
|
3779
|
+
const effectiveStorageAdapter = storageAdapter;
|
|
3375
3780
|
const s3Endpoint = providedS3Endpoint ?? env_default.S3_ENDPOINT;
|
|
3376
3781
|
const s3Region = providedS3Region ?? env_default.S3_REGION ?? "auto";
|
|
3377
3782
|
const s3Bucket = providedS3Bucket ?? env_default.S3_BUCKET;
|
|
@@ -3383,13 +3788,10 @@ async function translateCaptions(assetId, fromLanguageCode, toLanguageCode, opti
|
|
|
3383
3788
|
model,
|
|
3384
3789
|
provider
|
|
3385
3790
|
});
|
|
3386
|
-
if (uploadToMux && (!s3Endpoint || !s3Bucket || !s3AccessKeyId || !s3SecretAccessKey)) {
|
|
3387
|
-
throw new Error("
|
|
3791
|
+
if (uploadToMux && (!s3Endpoint || !s3Bucket || !effectiveStorageAdapter && (!s3AccessKeyId || !s3SecretAccessKey))) {
|
|
3792
|
+
throw new Error("Storage configuration is required for uploading to Mux. Provide s3Endpoint and s3Bucket. If no storageAdapter is supplied, also provide s3AccessKeyId and s3SecretAccessKey in options or set S3_ENDPOINT, S3_BUCKET, S3_ACCESS_KEY_ID, and S3_SECRET_ACCESS_KEY environment variables.");
|
|
3388
3793
|
}
|
|
3389
|
-
const { asset: assetData, playbackId, policy } = await getPlaybackIdForAsset(
|
|
3390
|
-
assetId,
|
|
3391
|
-
credentials
|
|
3392
|
-
);
|
|
3794
|
+
const { asset: assetData, playbackId, policy } = await getPlaybackIdForAsset(assetId, credentials);
|
|
3393
3795
|
const assetDurationSeconds = getAssetDurationSecondsFromAsset(assetData);
|
|
3394
3796
|
const isAudioOnly = isAudioOnlyAsset(assetData);
|
|
3395
3797
|
const signingContext = await resolveMuxSigningContext(credentials);
|
|
@@ -3474,7 +3876,8 @@ async function translateCaptions(assetId, fromLanguageCode, toLanguageCode, opti
|
|
|
3474
3876
|
toLanguageCode,
|
|
3475
3877
|
s3Endpoint,
|
|
3476
3878
|
s3Region,
|
|
3477
|
-
s3Bucket
|
|
3879
|
+
s3Bucket,
|
|
3880
|
+
storageAdapter: effectiveStorageAdapter
|
|
3478
3881
|
});
|
|
3479
3882
|
} catch (error) {
|
|
3480
3883
|
throw new Error(`Failed to upload VTT to S3: ${error instanceof Error ? error.message : "Unknown error"}`);
|