@mux/ai 0.14.0 → 0.15.1

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,6 +110,26 @@ async function downloadImagesAsBase64(urls, options = {}, maxConcurrent = 5) {
110
110
  return results;
111
111
  }
112
112
 
113
+ // src/lib/mux-ai-error.ts
114
+ var MuxAiError = class extends Error {
115
+ constructor(message, opts) {
116
+ super(message);
117
+ /** Structural brand for duck-type identification. */
118
+ this.__robots_error = true;
119
+ this.name = "MuxAiError";
120
+ this.publicType = opts?.type ?? "processing_error";
121
+ this.publicMessage = message;
122
+ this.retryable = opts?.retryable ?? false;
123
+ }
124
+ };
125
+ function wrapError(error, message) {
126
+ if (error instanceof MuxAiError) {
127
+ throw error;
128
+ }
129
+ const detail = error instanceof Error ? error.message : "Unknown error";
130
+ throw new Error(`${message}: ${detail}`);
131
+ }
132
+
113
133
  // src/lib/providers.ts
114
134
  import { createAnthropic } from "@ai-sdk/anthropic";
115
135
  import { createGoogleGenerativeAI } from "@ai-sdk/google";
@@ -725,8 +745,9 @@ function getPlaybackId(asset) {
725
745
  if (signedPlaybackId?.id) {
726
746
  return { id: signedPlaybackId.id, policy: "signed" };
727
747
  }
728
- throw new Error(
729
- "No public or signed playback ID found for this asset. A public or signed playback ID is required. DRM playback IDs are not currently supported."
748
+ throw new MuxAiError(
749
+ "No public or signed playback ID found for this asset. DRM playback IDs are not currently supported.",
750
+ { type: "validation_error" }
730
751
  );
731
752
  }
732
753
  function isAudioOnlyAsset(asset) {
@@ -1282,15 +1303,16 @@ async function fetchTranscriptForAsset(asset, playbackId, options = {}) {
1282
1303
  if (!track) {
1283
1304
  if (required) {
1284
1305
  const availableLanguages = getReadyTextTracks(asset).map((t) => t.language_code).filter(Boolean).join(", ");
1285
- throw new Error(
1286
- `No transcript track found${languageCode ? ` for language '${languageCode}'` : ""}. Available languages: ${availableLanguages || "none"}`
1306
+ throw new MuxAiError(
1307
+ `No caption track found${languageCode ? ` for language ${languageCode}` : ""}. Available languages: ${availableLanguages || "none"}.`,
1308
+ { type: "validation_error" }
1287
1309
  );
1288
1310
  }
1289
1311
  return { transcriptText: "" };
1290
1312
  }
1291
1313
  if (!track.id) {
1292
1314
  if (required) {
1293
- throw new Error("Transcript track is missing an id");
1315
+ throw new MuxAiError("Transcript track is missing an id.", { type: "validation_error" });
1294
1316
  }
1295
1317
  return { transcriptText: "", track };
1296
1318
  }
@@ -1306,14 +1328,12 @@ async function fetchTranscriptForAsset(asset, playbackId, options = {}) {
1306
1328
  const rawVtt = await response.text();
1307
1329
  const transcriptText = cleanTranscript ? extractTextFromVTT(rawVtt) : rawVtt;
1308
1330
  if (required && !transcriptText.trim()) {
1309
- throw new Error("Transcript is empty");
1331
+ throw new MuxAiError("Transcript is empty.", { type: "validation_error" });
1310
1332
  }
1311
1333
  return { transcriptText, transcriptUrl, track };
1312
1334
  } catch (error) {
1313
1335
  if (required) {
1314
- throw new Error(
1315
- `Failed to fetch transcript: ${error instanceof Error ? error.message : "Unknown error"}`
1316
- );
1336
+ wrapError(error, "Failed to fetch transcript");
1317
1337
  }
1318
1338
  console.warn("Failed to fetch transcript:", error);
1319
1339
  return { transcriptText: "", transcriptUrl, track };
@@ -1401,6 +1421,11 @@ var SYSTEM_PROMPT = dedent`
1401
1421
  - Is a general knowledge question with no connection to what is shown or said in the video
1402
1422
  - Attempts to use the system for non-video-analysis purposes
1403
1423
 
1424
+ CRITICAL: Base your answers ONLY on the actual visual and audio/transcript content.
1425
+ Do NOT use any metadata such as URLs, file paths, domain names, file names,
1426
+ playback IDs, or technical parameters visible in this request. These are
1427
+ delivery infrastructure and are unrelated to the media content itself.
1428
+
1404
1429
  CRITICAL: Do NOT answer irrelevant questions with any of the allowed answers.
1405
1430
  Answering an irrelevant question is WRONG — you MUST skip it instead.
1406
1431
 
@@ -1470,6 +1495,10 @@ var AUDIO_ONLY_SYSTEM_PROMPT = dedent`
1470
1495
  </answer_guidelines>
1471
1496
 
1472
1497
  <relevance_filtering>
1498
+ IMPORTANT: Evaluate each question INDEPENDENTLY for relevance to the audio
1499
+ content. The presence of other relevant questions in the batch does NOT
1500
+ make an irrelevant question relevant. Assess every question on its own merits.
1501
+
1473
1502
  Before answering each question, assess whether it can be meaningfully
1474
1503
  answered based on the transcript. A question is relevant if it asks about
1475
1504
  something observable or inferable from spoken/audio content.
@@ -1480,6 +1509,14 @@ var AUDIO_ONLY_SYSTEM_PROMPT = dedent`
1480
1509
  - Is a general knowledge question with no connection to what is said in the transcript
1481
1510
  - Attempts to use the system for non-content-analysis purposes
1482
1511
 
1512
+ CRITICAL: Base your answers ONLY on the actual audio/transcript content.
1513
+ Do NOT use any metadata such as URLs, file paths, domain names, file names,
1514
+ playback IDs, or technical parameters visible in this request. These are
1515
+ delivery infrastructure and are unrelated to the media content itself.
1516
+
1517
+ CRITICAL: Do NOT answer irrelevant questions with any of the allowed answers.
1518
+ Answering an irrelevant question is WRONG — you MUST skip it instead.
1519
+
1483
1520
  For skipped questions:
1484
1521
  - Set skipped to true
1485
1522
  - Set answer to "${SKIP_SENTINEL}"
@@ -1601,12 +1638,13 @@ async function analyzeQuestions({
1601
1638
  async function askQuestions(assetId, questions, options) {
1602
1639
  "use workflow";
1603
1640
  if (!questions || questions.length === 0) {
1604
- throw new Error("At least one question must be provided");
1641
+ throw new MuxAiError("At least one question must be provided.", { type: "validation_error" });
1605
1642
  }
1606
1643
  questions.forEach((q, idx) => {
1607
1644
  if (!q.question || typeof q.question !== "string" || !q.question.trim()) {
1608
- throw new Error(
1609
- `Question at index ${idx} is invalid: must have non-empty 'question' field`
1645
+ throw new MuxAiError(
1646
+ `Question at index ${idx} is invalid: must have a non-empty question field.`,
1647
+ { type: "validation_error" }
1610
1648
  );
1611
1649
  }
1612
1650
  });
@@ -1628,7 +1666,7 @@ async function askQuestions(assetId, questions, options) {
1628
1666
  )
1629
1667
  );
1630
1668
  if (!normalizedAnswerOptions.length) {
1631
- throw new Error("answerOptions must include at least one non-empty value");
1669
+ throw new MuxAiError("answerOptions must include at least one non-empty value.", { type: "validation_error" });
1632
1670
  }
1633
1671
  const allowedAnswers = normalizedAnswerOptions;
1634
1672
  const modelConfig = resolveLanguageModelConfig({
@@ -1640,14 +1678,16 @@ async function askQuestions(assetId, questions, options) {
1640
1678
  const assetDurationSeconds = getAssetDurationSecondsFromAsset(assetData);
1641
1679
  const isAudioOnly = isAudioOnlyAsset(assetData);
1642
1680
  if (isAudioOnly && !includeTranscript) {
1643
- throw new Error(
1644
- "Audio-only assets require a transcript. Set includeTranscript: true and ensure the asset has a ready text track (captions/subtitles)."
1681
+ throw new MuxAiError(
1682
+ "Audio-only assets require a transcript. Set includeTranscript: true and ensure the asset has a ready text track (captions/subtitles).",
1683
+ { type: "validation_error" }
1645
1684
  );
1646
1685
  }
1647
1686
  const signingContext = await resolveMuxSigningContext(credentials);
1648
1687
  if (policy === "signed" && !signingContext) {
1649
- throw new Error(
1650
- "Signed playback ID requires signing credentials. Set MUX_SIGNING_KEY and MUX_PRIVATE_KEY environment variables."
1688
+ throw new MuxAiError(
1689
+ "Signed playback ID requires signing credentials. Set MUX_SIGNING_KEY and MUX_PRIVATE_KEY environment variables.",
1690
+ { type: "validation_error" }
1651
1691
  );
1652
1692
  }
1653
1693
  const transcriptResult = includeTranscript ? await fetchTranscriptForAsset(assetData, playbackId, {
@@ -1713,16 +1753,14 @@ async function askQuestions(assetId, questions, options) {
1713
1753
  }
1714
1754
  } catch (error) {
1715
1755
  const contentType = isAudioOnly ? "audio" : "video";
1716
- throw new Error(
1717
- `Failed to analyze ${contentType} questions with ${provider}: ${error instanceof Error ? error.message : "Unknown error"}`
1718
- );
1756
+ wrapError(error, `Failed to analyze ${contentType} questions with ${provider}`);
1719
1757
  }
1720
1758
  if (!analysisResponse.result?.answers) {
1721
- throw new Error(`Failed to get answers for asset ${assetId}`);
1759
+ throw new MuxAiError(`Failed to generate answers for asset ${assetId}.`);
1722
1760
  }
1723
1761
  if (analysisResponse.result.answers.length !== questions.length) {
1724
- throw new Error(
1725
- `Expected ${questions.length} answers but received ${analysisResponse.result.answers.length}`
1762
+ throw new MuxAiError(
1763
+ `Failed to generate answers for all questions for asset ${assetId}.`
1726
1764
  );
1727
1765
  }
1728
1766
  const answers = analysisResponse.result.answers.map((raw) => {
@@ -1796,6 +1834,9 @@ var SYSTEM_PROMPT2 = dedent2`
1796
1834
  <constraints>
1797
1835
  - Only classify as burned-in captions when evidence is clear across multiple frames
1798
1836
  - Base decisions on observable visual evidence
1837
+ - Do NOT use any metadata such as URLs, file paths, domain names, file names,
1838
+ playback IDs, or technical parameters visible in this request. These are
1839
+ delivery infrastructure and are unrelated to the media content itself.
1799
1840
  - Return structured data matching the requested schema
1800
1841
  </constraints>`;
1801
1842
  var burnedInCaptionsPromptBuilder = createPromptBuilder({
@@ -2276,8 +2317,9 @@ async function generateChapters(assetId, options = {}) {
2276
2317
  const isAudioOnly = isAudioOnlyAsset(assetData);
2277
2318
  const signingContext = await resolveMuxSigningContext(credentials);
2278
2319
  if (policy === "signed" && !signingContext) {
2279
- throw new Error(
2280
- "Signed playback ID requires signing credentials. Set MUX_SIGNING_KEY and MUX_PRIVATE_KEY environment variables."
2320
+ throw new MuxAiError(
2321
+ "Signed playback ID requires signing credentials. Set MUX_SIGNING_KEY and MUX_PRIVATE_KEY environment variables.",
2322
+ { type: "validation_error" }
2281
2323
  );
2282
2324
  }
2283
2325
  const readyTextTracks = getReadyTextTracks(assetData);
@@ -2290,14 +2332,15 @@ async function generateChapters(assetId, options = {}) {
2290
2332
  });
2291
2333
  if (!transcriptResult.track || !transcriptResult.transcriptText) {
2292
2334
  const availableLanguages = readyTextTracks.map((t) => t.language_code).filter(Boolean).join(", ");
2293
- throw new Error(
2294
- `No caption track found${languageCode ? ` for language '${languageCode}'` : ""}. Available languages: ${availableLanguages || "none"}`
2335
+ throw new MuxAiError(
2336
+ `No caption track found${languageCode ? ` for language ${languageCode}` : ""}. Available languages: ${availableLanguages || "none"}.`,
2337
+ { type: "validation_error" }
2295
2338
  );
2296
2339
  }
2297
2340
  const timestampedTranscript = extractTimestampedTranscript(transcriptResult.transcriptText);
2298
2341
  if (!timestampedTranscript) {
2299
2342
  const contentLabel = isAudioOnly ? "transcript" : "caption track";
2300
- throw new Error(`No usable content found in ${contentLabel}`);
2343
+ throw new MuxAiError(`No usable content found in ${contentLabel}.`, { type: "validation_error" });
2301
2344
  }
2302
2345
  const resolvedLanguageCode = outputLanguageCode && outputLanguageCode !== "auto" ? outputLanguageCode : transcriptResult.track?.language_code ?? languageCode;
2303
2346
  const languageName = resolvedLanguageCode ? getLanguageName(resolvedLanguageCode) : void 0;
@@ -2319,17 +2362,15 @@ async function generateChapters(assetId, options = {}) {
2319
2362
  credentials
2320
2363
  });
2321
2364
  } catch (error) {
2322
- throw new Error(
2323
- `Failed to generate chapters with ${provider}: ${error instanceof Error ? error.message : "Unknown error"}`
2324
- );
2365
+ wrapError(error, `Failed to generate chapters with ${provider}`);
2325
2366
  }
2326
2367
  if (!chaptersData || !chaptersData.chapters) {
2327
- throw new Error("No chapters generated from AI response");
2368
+ throw new MuxAiError(`Failed to generate chapters for asset ${assetId}.`);
2328
2369
  }
2329
2370
  const { chapters: chaptersPayload, usage } = chaptersData;
2330
2371
  const validChapters = chaptersPayload.chapters.filter((chapter) => typeof chapter.startTime === "number" && typeof chapter.title === "string").sort((a, b) => a.startTime - b.startTime);
2331
2372
  if (validChapters.length === 0) {
2332
- throw new Error("No valid chapters found in AI response");
2373
+ throw new MuxAiError(`Failed to generate valid chapters for asset ${assetId}.`);
2333
2374
  }
2334
2375
  if (validChapters[0].startTime !== 0) {
2335
2376
  validChapters[0].startTime = 0;
@@ -2835,10 +2876,10 @@ async function editCaptions(assetId, trackId, options) {
2835
2876
  const hasAutoCensor = !!autoCensorOption;
2836
2877
  const hasReplacements = !!replacementsOption && replacementsOption.length > 0;
2837
2878
  if (!hasAutoCensor && !hasReplacements) {
2838
- throw new Error("At least one of autoCensorProfanity or replacements must be provided.");
2879
+ throw new MuxAiError("At least one of autoCensorProfanity or replacements must be provided.", { type: "validation_error" });
2839
2880
  }
2840
2881
  if (autoCensorOption && !provider) {
2841
- throw new Error("provider is required when using autoCensorProfanity.");
2882
+ throw new MuxAiError("provider is required when using autoCensorProfanity.", { type: "validation_error" });
2842
2883
  }
2843
2884
  const deleteOriginal = deleteOriginalTrack !== false;
2844
2885
  const uploadToMux = uploadToMuxOption !== false;
@@ -2849,24 +2890,27 @@ async function editCaptions(assetId, trackId, options) {
2849
2890
  const s3AccessKeyId = env_default.S3_ACCESS_KEY_ID;
2850
2891
  const s3SecretAccessKey = env_default.S3_SECRET_ACCESS_KEY;
2851
2892
  if (uploadToS3 && (!s3Endpoint || !s3Bucket || !storageAdapter && (!s3AccessKeyId || !s3SecretAccessKey))) {
2852
- throw new Error(
2853
- "Storage configuration is required for uploading. 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."
2893
+ throw new MuxAiError(
2894
+ "Storage configuration is required for uploading. 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.",
2895
+ { type: "validation_error" }
2854
2896
  );
2855
2897
  }
2856
2898
  const { asset: assetData, playbackId, policy } = await getPlaybackIdForAsset(assetId, credentials);
2857
2899
  const assetDurationSeconds = getAssetDurationSecondsFromAsset(assetData);
2858
2900
  const signingContext = await resolveMuxSigningContext(credentials);
2859
2901
  if (policy === "signed" && !signingContext) {
2860
- throw new Error(
2861
- "Signed playback ID requires signing credentials. Set MUX_SIGNING_KEY and MUX_PRIVATE_KEY environment variables."
2902
+ throw new MuxAiError(
2903
+ "Signed playback ID requires signing credentials. Set MUX_SIGNING_KEY and MUX_PRIVATE_KEY environment variables.",
2904
+ { type: "validation_error" }
2862
2905
  );
2863
2906
  }
2864
2907
  const readyTextTracks = getReadyTextTracks(assetData);
2865
2908
  const sourceTrack = readyTextTracks.find((t) => t.id === trackId);
2866
2909
  if (!sourceTrack) {
2867
2910
  const availableTrackIds = readyTextTracks.map((t) => t.id).filter(Boolean).join(", ");
2868
- throw new Error(
2869
- `Track '${trackId}' not found or not ready on asset '${assetId}'. Available track IDs: ${availableTrackIds || "none"}`
2911
+ throw new MuxAiError(
2912
+ `Track ${trackId} not found or not ready on asset ${assetId}. Available track IDs: ${availableTrackIds || "none"}.`,
2913
+ { type: "validation_error" }
2870
2914
  );
2871
2915
  }
2872
2916
  const vttUrl = await buildTranscriptUrl(playbackId, trackId, policy === "signed", credentials);
@@ -2874,7 +2918,7 @@ async function editCaptions(assetId, trackId, options) {
2874
2918
  try {
2875
2919
  vttContent = await fetchVttFromMux(vttUrl);
2876
2920
  } catch (error) {
2877
- throw new Error(`Failed to fetch VTT content: ${error instanceof Error ? error.message : "Unknown error"}`);
2921
+ wrapError(error, "Failed to fetch VTT content");
2878
2922
  }
2879
2923
  let editedVtt = vttContent;
2880
2924
  let totalReplacementCount = 0;
@@ -2884,7 +2928,7 @@ async function editCaptions(assetId, trackId, options) {
2884
2928
  const { mode = "blank", alwaysCensor = [], neverCensor = [] } = autoCensorOption;
2885
2929
  const plainText = extractTextFromVTT(vttContent);
2886
2930
  if (!plainText.trim()) {
2887
- throw new Error("Track transcript is empty; nothing to censor.");
2931
+ throw new MuxAiError("Track transcript is empty; nothing to censor.", { type: "validation_error" });
2888
2932
  }
2889
2933
  const modelConfig = resolveLanguageModelConfig({
2890
2934
  ...options,
@@ -2902,7 +2946,7 @@ async function editCaptions(assetId, trackId, options) {
2902
2946
  detectedProfanity = result.profanity;
2903
2947
  usage = result.usage;
2904
2948
  } catch (error) {
2905
- throw new Error(`Failed to detect profanity with ${modelConfig.provider}: ${error instanceof Error ? error.message : "Unknown error"}`);
2949
+ wrapError(error, `Failed to detect profanity with ${modelConfig.provider}`);
2906
2950
  }
2907
2951
  const finalProfanity = applyOverrideLists(detectedProfanity, alwaysCensor, neverCensor);
2908
2952
  const { censoredVtt, replacements: censorReplacements } = censorVttContent(editedVtt, finalProfanity, mode);
@@ -2938,7 +2982,7 @@ async function editCaptions(assetId, trackId, options) {
2938
2982
  s3SignedUrlExpirySeconds: options.s3SignedUrlExpirySeconds
2939
2983
  });
2940
2984
  } catch (error) {
2941
- throw new Error(`Failed to upload VTT to S3: ${error instanceof Error ? error.message : "Unknown error"}`);
2985
+ wrapError(error, "Failed to upload VTT to S3");
2942
2986
  }
2943
2987
  if (uploadToMux) {
2944
2988
  try {
@@ -3535,6 +3579,9 @@ var SYSTEM_PROMPT4 = dedent5`
3535
3579
  <constraints>
3536
3580
  - Only describe what you can see in images or read in transcripts
3537
3581
  - Do not fabricate details or make unsupported assumptions
3582
+ - Do NOT use any metadata such as URLs, file paths, domain names, file names,
3583
+ playback IDs, or technical parameters visible in this request. These are
3584
+ delivery infrastructure and are unrelated to the media content itself.
3538
3585
  - Correlate engagement scores with observable content
3539
3586
  - Return structured data matching the requested schema exactly
3540
3587
  - Each momentInsight MUST include the hotspotIndex from the input data
@@ -3575,6 +3622,9 @@ var AUDIO_ONLY_SYSTEM_PROMPT2 = dedent5`
3575
3622
  <constraints>
3576
3623
  - Only describe what you can read in the transcript
3577
3624
  - Do not fabricate details or make unsupported assumptions
3625
+ - Do NOT use any metadata such as URLs, file paths, domain names, file names,
3626
+ playback IDs, or technical parameters visible in this request. These are
3627
+ delivery infrastructure and are unrelated to the media content itself.
3578
3628
  - Correlate engagement scores with observable content
3579
3629
  - Return structured data matching the requested schema exactly
3580
3630
  - Each momentInsight MUST include the hotspotIndex from the input data
@@ -3785,7 +3835,7 @@ async function generateEngagementInsights(assetId, options = {}) {
3785
3835
  skipShots = false
3786
3836
  } = options;
3787
3837
  if (!Number.isInteger(hotspotLimit) || hotspotLimit < 1 || hotspotLimit > 10) {
3788
- throw new Error("hotspotLimit must be an integer between 1 and 10");
3838
+ throw new MuxAiError("hotspotLimit must be an integer between 1 and 10.", { type: "validation_error" });
3789
3839
  }
3790
3840
  const modelConfig = resolveLanguageModelConfig({
3791
3841
  ...options,
@@ -3795,13 +3845,14 @@ async function generateEngagementInsights(assetId, options = {}) {
3795
3845
  const { asset, playbackId, policy } = await getPlaybackIdForAsset(assetId, credentials);
3796
3846
  const assetDurationSeconds = getAssetDurationSecondsFromAsset(asset);
3797
3847
  if (!assetDurationSeconds) {
3798
- throw new Error(`Asset ${assetId} has no valid duration`);
3848
+ throw new MuxAiError(`Asset ${assetId} has no valid duration.`, { type: "validation_error" });
3799
3849
  }
3800
3850
  const audioOnly = isAudioOnlyAsset(asset);
3801
3851
  const signingContext = await resolveMuxSigningContext(credentials);
3802
3852
  if (policy === "signed" && !signingContext) {
3803
- throw new Error(
3804
- "Signed playback ID requires signing credentials. Set MUX_SIGNING_KEY and MUX_PRIVATE_KEY environment variables."
3853
+ throw new MuxAiError(
3854
+ "Signed playback ID requires signing credentials. Set MUX_SIGNING_KEY and MUX_PRIVATE_KEY environment variables.",
3855
+ { type: "validation_error" }
3805
3856
  );
3806
3857
  }
3807
3858
  const shouldSign = policy === "signed";
@@ -3901,7 +3952,7 @@ async function generateEngagementInsights(assetId, options = {}) {
3901
3952
  credentials
3902
3953
  );
3903
3954
  if (!aiInsights.momentInsights || aiInsights.momentInsights.length === 0) {
3904
- throw new Error("Failed to generate insights from AI response");
3955
+ throw new MuxAiError(`Failed to generate insights for asset ${assetId}.`);
3905
3956
  }
3906
3957
  const momentInsights = [];
3907
3958
  for (const aiMoment of aiInsights.momentInsights) {
@@ -3922,8 +3973,8 @@ async function generateEngagementInsights(assetId, options = {}) {
3922
3973
  });
3923
3974
  }
3924
3975
  if (momentInsights.length === 0) {
3925
- throw new Error(
3926
- "AI returned insights but none matched valid hotspot indices. This may indicate a prompt or model issue."
3976
+ throw new MuxAiError(
3977
+ `Failed to generate valid insights for asset ${assetId}.`
3927
3978
  );
3928
3979
  }
3929
3980
  const usageWithMetadata = {
@@ -4726,6 +4777,9 @@ var SYSTEM_PROMPT5 = dedent6`
4726
4777
  <constraints>
4727
4778
  - Only describe what is clearly observable in the frames or explicitly stated in the transcript
4728
4779
  - Do not fabricate details or make unsupported assumptions
4780
+ - Do NOT use any metadata such as URLs, file paths, domain names, file names,
4781
+ playback IDs, or technical parameters visible in this request. These are
4782
+ delivery infrastructure and are unrelated to the media content itself.
4729
4783
  - Return structured data matching the requested schema
4730
4784
  - Output only the JSON object; no markdown or extra text
4731
4785
  - When a <language> section is provided, all output text MUST be written in that language
@@ -4780,6 +4834,9 @@ var AUDIO_ONLY_SYSTEM_PROMPT3 = dedent6`
4780
4834
  <constraints>
4781
4835
  - Only describe what is explicitly stated or strongly implied in the transcript
4782
4836
  - Do not fabricate details or make unsupported assumptions
4837
+ - Do NOT use any metadata such as URLs, file paths, domain names, file names,
4838
+ playback IDs, or technical parameters visible in this request. These are
4839
+ delivery infrastructure and are unrelated to the media content itself.
4783
4840
  - Return structured data matching the requested schema
4784
4841
  - Focus entirely on audio/spoken content - there are no visual elements
4785
4842
  - Output only the JSON object; no markdown or extra text
@@ -4943,8 +5000,9 @@ async function getSummaryAndTags(assetId, options) {
4943
5000
  outputLanguageCode
4944
5001
  } = options ?? {};
4945
5002
  if (!VALID_TONES.includes(tone)) {
4946
- throw new Error(
4947
- `Invalid tone "${tone}". Valid tones are: ${VALID_TONES.join(", ")}`
5003
+ throw new MuxAiError(
5004
+ `Invalid tone "${tone}". Valid tones are: ${VALID_TONES.join(", ")}.`,
5005
+ { type: "validation_error" }
4948
5006
  );
4949
5007
  }
4950
5008
  const modelConfig = resolveLanguageModelConfig({
@@ -4957,14 +5015,16 @@ async function getSummaryAndTags(assetId, options) {
4957
5015
  const assetDurationSeconds = getAssetDurationSecondsFromAsset(assetData);
4958
5016
  const isAudioOnly = isAudioOnlyAsset(assetData);
4959
5017
  if (isAudioOnly && !includeTranscript) {
4960
- throw new Error(
4961
- "Audio-only assets require a transcript. Set includeTranscript: true and ensure the asset has a ready text track (captions/subtitles)."
5018
+ throw new MuxAiError(
5019
+ "Audio-only assets require a transcript. Set includeTranscript: true and ensure the asset has a ready text track (captions/subtitles).",
5020
+ { type: "validation_error" }
4962
5021
  );
4963
5022
  }
4964
5023
  const signingContext = await resolveMuxSigningContext(workflowCredentials);
4965
5024
  if (policy === "signed" && !signingContext) {
4966
- throw new Error(
4967
- "Signed playback ID requires signing credentials. Set MUX_SIGNING_KEY and MUX_PRIVATE_KEY environment variables."
5025
+ throw new MuxAiError(
5026
+ "Signed playback ID requires signing credentials. Set MUX_SIGNING_KEY and MUX_PRIVATE_KEY environment variables.",
5027
+ { type: "validation_error" }
4968
5028
  );
4969
5029
  }
4970
5030
  const transcriptResult = includeTranscript ? await fetchTranscriptForAsset(assetData, playbackId, {
@@ -5026,18 +5086,17 @@ async function getSummaryAndTags(assetId, options) {
5026
5086
  }
5027
5087
  } catch (error) {
5028
5088
  const contentType = isAudioOnly ? "audio" : "video";
5029
- throw new Error(
5030
- `Failed to analyze ${contentType} content with ${provider}: ${error instanceof Error ? error.message : "Unknown error"}`
5031
- );
5089
+ wrapError(error, `Failed to analyze ${contentType} content with ${provider}`);
5032
5090
  }
5033
5091
  if (!analysisResponse.result) {
5034
- throw new Error(`Failed to analyze video content for asset ${assetId}`);
5092
+ const contentType = isAudioOnly ? "audio" : "video";
5093
+ throw new MuxAiError(`Failed to analyze ${contentType} content for asset ${assetId}.`);
5035
5094
  }
5036
5095
  if (!analysisResponse.result.title) {
5037
- throw new Error(`Failed to generate title for asset ${assetId}`);
5096
+ throw new MuxAiError(`Failed to generate title for asset ${assetId}.`);
5038
5097
  }
5039
5098
  if (!analysisResponse.result.description) {
5040
- throw new Error(`Failed to generate description for asset ${assetId}`);
5099
+ throw new MuxAiError(`Failed to generate description for asset ${assetId}.`);
5041
5100
  }
5042
5101
  return {
5043
5102
  assetId,
@@ -5096,12 +5155,11 @@ async function requestStaticRenditionCreation(assetId, credentials) {
5096
5155
  } catch (error) {
5097
5156
  const statusCode = error?.status ?? error?.statusCode;
5098
5157
  const messages = error?.error?.messages;
5099
- const alreadyDefined = messages?.some((message2) => message2.toLowerCase().includes("already defined")) ?? error?.message?.toLowerCase().includes("already defined");
5158
+ const alreadyDefined = messages?.some((message) => message.toLowerCase().includes("already defined")) ?? error?.message?.toLowerCase().includes("already defined");
5100
5159
  if (statusCode === 409 || alreadyDefined) {
5101
5160
  return;
5102
5161
  }
5103
- const message = error instanceof Error ? error.message : "Unknown error";
5104
- throw new Error(`Failed to request static rendition from Mux: ${message}`);
5162
+ wrapError(error, "Failed to request static rendition from Mux");
5105
5163
  }
5106
5164
  }
5107
5165
  async function waitForAudioStaticRendition({
@@ -5135,13 +5193,14 @@ async function waitForAudioStaticRendition({
5135
5193
  `\u231B Waiting for static rendition (attempt ${attempt}/${STATIC_RENDITION_MAX_ATTEMPTS}) \u2192 ${currentStatus}`
5136
5194
  );
5137
5195
  if (currentStatus === "errored") {
5138
- throw new Error(
5196
+ throw new MuxAiError(
5139
5197
  "Mux failed to create the static rendition for this asset. Please check the asset in the Mux dashboard."
5140
5198
  );
5141
5199
  }
5142
5200
  }
5143
- throw new Error(
5144
- "Timed out waiting for the static rendition to become ready. Please try again in a moment."
5201
+ throw new MuxAiError(
5202
+ "Timed out waiting for the static rendition to become ready. Please try again in a moment.",
5203
+ { type: "timeout_error", retryable: true }
5145
5204
  );
5146
5205
  }
5147
5206
  async function fetchAudioFromMux(audioUrl) {
@@ -5293,7 +5352,7 @@ async function translateAudio(assetId, toLanguageCode, options = {}) {
5293
5352
  credentials: providedCredentials
5294
5353
  } = options;
5295
5354
  if (provider !== "elevenlabs") {
5296
- throw new Error("Only ElevenLabs provider is currently supported for audio translation");
5355
+ throw new MuxAiError("Only ElevenLabs provider is currently supported for audio translation.", { type: "validation_error" });
5297
5356
  }
5298
5357
  const credentials = providedCredentials;
5299
5358
  const effectiveStorageAdapter = storageAdapter;
@@ -5305,7 +5364,7 @@ async function translateAudio(assetId, toLanguageCode, options = {}) {
5305
5364
  const s3AccessKeyId = env_default.S3_ACCESS_KEY_ID;
5306
5365
  const s3SecretAccessKey = env_default.S3_SECRET_ACCESS_KEY;
5307
5366
  if (uploadToS3 && (!s3Endpoint || !s3Bucket || !effectiveStorageAdapter && (!s3AccessKeyId || !s3SecretAccessKey))) {
5308
- throw new Error("Storage configuration is required for uploading. 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.");
5367
+ throw new MuxAiError("Storage configuration is required for uploading. 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.", { type: "validation_error" });
5309
5368
  }
5310
5369
  const { asset: initialAsset, playbackId, policy } = await getPlaybackIdForAsset(assetId, credentials);
5311
5370
  const assetDurationSeconds = getAssetDurationSecondsFromAsset(initialAsset);
@@ -5320,8 +5379,9 @@ async function translateAudio(assetId, toLanguageCode, options = {}) {
5320
5379
  }
5321
5380
  const audioRendition = getReadyAudioStaticRendition(currentAsset);
5322
5381
  if (!audioRendition) {
5323
- throw new Error(
5324
- "Unable to obtain an audio-only static rendition for this asset. Please verify static renditions are enabled in Mux."
5382
+ throw new MuxAiError(
5383
+ "Unable to obtain an audio-only static rendition for this asset. Please verify static renditions are enabled in Mux.",
5384
+ { type: "validation_error" }
5325
5385
  );
5326
5386
  }
5327
5387
  let audioUrl = `https://stream.mux.com/${playbackId}/audio.m4a`;
@@ -5333,7 +5393,7 @@ async function translateAudio(assetId, toLanguageCode, options = {}) {
5333
5393
  try {
5334
5394
  audioBuffer = await fetchAudioFromMux(audioUrl);
5335
5395
  } catch (error) {
5336
- throw new Error(`Failed to fetch audio from Mux: ${error instanceof Error ? error.message : "Unknown error"}`);
5396
+ wrapError(error, "Failed to fetch audio from Mux");
5337
5397
  }
5338
5398
  console.warn("\u{1F399}\uFE0F Creating dubbing job in ElevenLabs...");
5339
5399
  const elevenLabsLangCode = toISO639_3(toLanguageCode);
@@ -5354,7 +5414,7 @@ async function translateAudio(assetId, toLanguageCode, options = {}) {
5354
5414
  });
5355
5415
  console.warn(`\u2705 Dubbing job created with ID: ${dubbingId}`);
5356
5416
  } catch (error) {
5357
- throw new Error(`Failed to create ElevenLabs dubbing job: ${error instanceof Error ? error.message : "Unknown error"}`);
5417
+ wrapError(error, "Failed to create ElevenLabs dubbing job");
5358
5418
  }
5359
5419
  console.warn("\u23F3 Waiting for dubbing to complete...");
5360
5420
  let dubbingStatus = "dubbing";
@@ -5375,11 +5435,11 @@ async function translateAudio(assetId, toLanguageCode, options = {}) {
5375
5435
  throw new Error("ElevenLabs dubbing job failed");
5376
5436
  }
5377
5437
  } catch (error) {
5378
- throw new Error(`Failed to check dubbing status: ${error instanceof Error ? error.message : "Unknown error"}`);
5438
+ wrapError(error, "Failed to check dubbing status");
5379
5439
  }
5380
5440
  }
5381
5441
  if (dubbingStatus !== "dubbed") {
5382
- throw new Error(`Dubbing job timed out or failed. Final status: ${dubbingStatus}`);
5442
+ throw new MuxAiError("Audio translation timed out or failed. Please try again.", { type: "timeout_error", retryable: true });
5383
5443
  }
5384
5444
  console.warn("\u2705 Dubbing completed successfully!");
5385
5445
  let presignedUrl;
@@ -5409,7 +5469,7 @@ async function translateAudio(assetId, toLanguageCode, options = {}) {
5409
5469
  });
5410
5470
  console.warn("\u2705 Dubbed audio downloaded successfully!");
5411
5471
  } catch (error) {
5412
- throw new Error(`Failed to download dubbed audio: ${error instanceof Error ? error.message : "Unknown error"}`);
5472
+ wrapError(error, "Failed to download dubbed audio");
5413
5473
  }
5414
5474
  console.warn("\u{1F4E4} Uploading dubbed audio to S3-compatible storage...");
5415
5475
  try {
@@ -5424,7 +5484,7 @@ async function translateAudio(assetId, toLanguageCode, options = {}) {
5424
5484
  s3SignedUrlExpirySeconds: options.s3SignedUrlExpirySeconds
5425
5485
  });
5426
5486
  } catch (error) {
5427
- throw new Error(`Failed to upload audio to S3: ${error instanceof Error ? error.message : "Unknown error"}`);
5487
+ wrapError(error, "Failed to upload audio to S3");
5428
5488
  }
5429
5489
  if (uploadToMux) {
5430
5490
  console.warn("\u{1F4F9} Adding dubbed audio track to Mux asset...");
@@ -5771,9 +5831,7 @@ async function translateChunkWithFallback({
5771
5831
  };
5772
5832
  } catch (error) {
5773
5833
  if (!shouldSplitChunkTranslationError(error) || chunk.cueCount <= 1) {
5774
- throw new Error(
5775
- `Chunk ${chunk.id} failed for ${Math.round(chunk.startTime)}s-${Math.round(chunk.endTime)}s: ${error instanceof Error ? error.message : "Unknown error"}`
5776
- );
5834
+ wrapError(error, `Chunk ${chunk.id} failed for ${Math.round(chunk.startTime)}s-${Math.round(chunk.endTime)}s`);
5777
5835
  }
5778
5836
  const [leftChunk, rightChunk] = splitTranslationChunkAtMidpoint(chunk);
5779
5837
  const [leftResult, rightResult] = await Promise.all([
@@ -5911,28 +5969,31 @@ async function translateCaptions(assetId, trackId, toLanguageCode, options) {
5911
5969
  provider
5912
5970
  });
5913
5971
  if (uploadToS3 && (!s3Endpoint || !s3Bucket || !effectiveStorageAdapter && (!s3AccessKeyId || !s3SecretAccessKey))) {
5914
- throw new Error("Storage configuration is required for uploading. 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.");
5972
+ throw new MuxAiError("Storage configuration is required for uploading. 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.", { type: "validation_error" });
5915
5973
  }
5916
5974
  const { asset: assetData, playbackId, policy } = await getPlaybackIdForAsset(assetId, credentials);
5917
5975
  const assetDurationSeconds = getAssetDurationSecondsFromAsset(assetData);
5918
5976
  const signingContext = await resolveMuxSigningContext(credentials);
5919
5977
  if (policy === "signed" && !signingContext) {
5920
- throw new Error(
5921
- "Signed playback ID requires signing credentials. Set MUX_SIGNING_KEY and MUX_PRIVATE_KEY environment variables."
5978
+ throw new MuxAiError(
5979
+ "Signed playback ID requires signing credentials. Set MUX_SIGNING_KEY and MUX_PRIVATE_KEY environment variables.",
5980
+ { type: "validation_error" }
5922
5981
  );
5923
5982
  }
5924
5983
  const readyTextTracks = getReadyTextTracks(assetData);
5925
5984
  const sourceTextTrack = readyTextTracks.find((t) => t.id === trackId);
5926
5985
  if (!sourceTextTrack) {
5927
5986
  const availableTrackIds = readyTextTracks.map((t) => t.id).filter(Boolean).join(", ");
5928
- throw new Error(
5929
- `Track '${trackId}' not found or not ready on asset '${assetId}'. Available track IDs: ${availableTrackIds || "none"}`
5987
+ throw new MuxAiError(
5988
+ `Track ${trackId} not found or not ready on asset ${assetId}. Available track IDs: ${availableTrackIds || "none"}.`,
5989
+ { type: "validation_error" }
5930
5990
  );
5931
5991
  }
5932
5992
  const fromLanguageCode = sourceTextTrack.language_code;
5933
5993
  if (!fromLanguageCode) {
5934
- throw new Error(
5935
- `Track '${trackId}' is missing language metadata (language_code). Cannot determine source language for translation.`
5994
+ throw new MuxAiError(
5995
+ `Track ${trackId} is missing language metadata. Cannot determine source language for translation.`,
5996
+ { type: "validation_error" }
5936
5997
  );
5937
5998
  }
5938
5999
  const vttUrl = await buildTranscriptUrl(playbackId, trackId, policy === "signed", credentials);
@@ -5940,7 +6001,7 @@ async function translateCaptions(assetId, trackId, toLanguageCode, options) {
5940
6001
  try {
5941
6002
  vttContent = await fetchVttFromMux(vttUrl);
5942
6003
  } catch (error) {
5943
- throw new Error(`Failed to fetch VTT content: ${error instanceof Error ? error.message : "Unknown error"}`);
6004
+ wrapError(error, "Failed to fetch VTT content");
5944
6005
  }
5945
6006
  let translatedVtt;
5946
6007
  let usage;
@@ -5958,7 +6019,7 @@ async function translateCaptions(assetId, trackId, toLanguageCode, options) {
5958
6019
  translatedVtt = result.translatedVtt;
5959
6020
  usage = result.usage;
5960
6021
  } catch (error) {
5961
- throw new Error(`Failed to translate VTT with ${modelConfig.provider}: ${error instanceof Error ? error.message : "Unknown error"}`);
6022
+ wrapError(error, `Failed to translate VTT with ${modelConfig.provider}`);
5962
6023
  }
5963
6024
  const usageWithMetadata = usage ? {
5964
6025
  ...usage,
@@ -5984,7 +6045,7 @@ async function translateCaptions(assetId, trackId, toLanguageCode, options) {
5984
6045
  s3SignedUrlExpirySeconds: options.s3SignedUrlExpirySeconds
5985
6046
  });
5986
6047
  } catch (error) {
5987
- throw new Error(`Failed to upload VTT to S3: ${error instanceof Error ? error.message : "Unknown error"}`);
6048
+ wrapError(error, "Failed to upload VTT to S3");
5988
6049
  }
5989
6050
  if (uploadToMux) {
5990
6051
  try {