@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.
@@ -110,8 +110,10 @@ async function downloadImagesAsBase64(urls, options = {}, maxConcurrent = 5) {
110
110
  return results;
111
111
  }
112
112
 
113
- // src/lib/mux-assets.ts
114
- import Mux from "@mux/mux-node";
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 = value.length % 4 === 0 ? value : value + "=".repeat(4 - value.length % 4);
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
- const cleaned = normalized.replace(/\s+/g, "");
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 = cleaned.endsWith("==") ? 2 : cleaned.endsWith("=") ? 1 : 0;
242
- const outputLength = cleaned.length / 4 * 3 - padding;
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 < cleaned.length; i += 4) {
246
- const c0 = cleaned.charCodeAt(i);
247
- const c1 = cleaned.charCodeAt(i + 1);
248
- const c2 = cleaned.charCodeAt(i + 2);
249
- const c3 = cleaned.charCodeAt(i + 3);
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("Workflow secret key is required. Set MUX_AI_WORKFLOW_SECRET_KEY environment variable.");
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 : String(error);
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
- async function resolveMuxCredentials(credentials) {
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 muxTokenId = resolved.muxTokenId ?? env_default.MUX_TOKEN_ID;
415
- const muxTokenSecret = resolved.muxTokenSecret ?? env_default.MUX_TOKEN_SECRET;
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 encrypted workflow credentials or set MUX_TOKEN_ID and MUX_TOKEN_SECRET environment variables."
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 { muxTokenId, muxTokenSecret };
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
- async function resolveProviderApiKey(provider, credentials) {
424
- const resolved = await resolveWorkflowCredentials(credentials);
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: resolved.openaiApiKey ?? env_default.OPENAI_API_KEY,
427
- anthropic: resolved.anthropicApiKey ?? env_default.ANTHROPIC_API_KEY,
428
- google: resolved.googleApiKey ?? env_default.GOOGLE_GENERATIVE_AI_API_KEY,
429
- hive: resolved.hiveApiKey ?? env_default.HIVE_API_KEY,
430
- elevenlabs: resolved.elevenLabsApiKey ?? env_default.ELEVENLABS_API_KEY
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 encrypted workflow credentials or set ${envVarNames[provider]} environment variable.`
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 keyId = resolved.muxSigningKey ?? env_default.MUX_SIGNING_KEY;
450
- const keySecret = resolved.muxPrivateKey ?? env_default.MUX_PRIVATE_KEY;
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 getMuxCredentialsFromEnv(credentials) {
521
- return resolveMuxCredentials(credentials);
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
- const { id: playbackId, policy } = getPlaybackId(asset);
551
- return { asset, playbackId, policy };
620
+ return toPlaybackAsset(asset);
552
621
  }
553
622
  async function getMuxAsset(assetId, credentials) {
554
623
  "use step";
555
- const { muxTokenId, muxTokenSecret } = await getMuxCredentialsFromEnv(credentials);
556
- const mux = new 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
- import Mux2 from "@mux/mux-node";
683
- function createSigningClient(context) {
684
- return new Mux2({
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 in credentials or set MUX_SIGNING_KEY and MUX_PRIVATE_KEY environment variables."
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 res = await fetch(HIVE_ENDPOINT, {
2086
- method: "POST",
2087
- headers: {
2088
- Accept: "application/json",
2089
- Authorization: `Token ${apiKey}`
2090
- },
2091
- body: formData
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
- const classes = json?.status?.[0]?.response?.output?.[0]?.classes || [];
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: getHiveCategoryScores(classes, HIVE_SEXUAL_CATEGORIES),
2103
- violence: getHiveCategoryScores(classes, HIVE_VIOLENCE_CATEGORIES),
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 duration = assetDurationSeconds ?? 0;
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 = await getThumbnailUrls(playbackId, duration, {
2193
- interval: thumbnailInterval,
2194
- width: thumbnailWidth,
2195
- shouldSign: policy === "signed",
2196
- maxSamples,
2197
- credentials
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 { asset: assetData, playbackId, policy } = await getPlaybackIdForAsset(assetId, credentials);
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(credentials);
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
- credentials
2823
+ workflowCredentials
2630
2824
  );
2631
2825
  } else {
2632
- const storyboardUrl = await getStoryboardUrl(playbackId, 640, policy === "signed", credentials);
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
- credentials
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
- credentials
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 { muxTokenId, muxTokenSecret } = await getMuxCredentialsFromEnv(credentials);
2868
- const mux = new Mux3({
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 { muxTokenId, muxTokenSecret } = await getMuxCredentialsFromEnv(credentials);
2894
- const mux = new Mux3({
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 s3Client = new S3Client({
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
- credentials: {
3022
- accessKeyId: s3AccessKeyId,
3023
- secretAccessKey: s3SecretAccessKey
3024
- },
3025
- forcePathStyle: true
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 { muxTokenId, muxTokenSecret } = await getMuxCredentialsFromEnv(credentials);
3053
- const mux = new Mux3({
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
- let credentials;
3084
- if (isEncryptedPayload(providedCredentials)) {
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("S3 configuration is required for uploading to Mux. Provide s3Endpoint, s3Bucket, s3AccessKeyId, and s3SecretAccessKey in options or set S3_ENDPOINT, S3_BUCKET, S3_ACCESS_KEY_ID, and S3_SECRET_ACCESS_KEY environment variables.");
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 s3Client = new S3Client({
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
- credentials: {
3319
- accessKeyId: s3AccessKeyId,
3320
- secretAccessKey: s3SecretAccessKey
3321
- },
3322
- forcePathStyle: true
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 { muxTokenId, muxTokenSecret } = await getMuxCredentialsFromEnv(credentials);
3348
- const mux = new Mux4({
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
- credentials
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("S3 configuration is required for uploading to Mux. Provide s3Endpoint, s3Bucket, s3AccessKeyId, and s3SecretAccessKey in options or set S3_ENDPOINT, S3_BUCKET, S3_ACCESS_KEY_ID, and S3_SECRET_ACCESS_KEY environment variables.");
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"}`);