@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.
package/dist/index.js CHANGED
@@ -5,7 +5,27 @@ var __export = (target, all) => {
5
5
  };
6
6
 
7
7
  // package.json
8
- var version = "0.14.0";
8
+ var version = "0.15.1";
9
+
10
+ // src/lib/mux-ai-error.ts
11
+ var MuxAiError = class extends Error {
12
+ constructor(message, opts) {
13
+ super(message);
14
+ /** Structural brand for duck-type identification. */
15
+ this.__robots_error = true;
16
+ this.name = "MuxAiError";
17
+ this.publicType = opts?.type ?? "processing_error";
18
+ this.publicMessage = message;
19
+ this.retryable = opts?.retryable ?? false;
20
+ }
21
+ };
22
+ function wrapError(error, message) {
23
+ if (error instanceof MuxAiError) {
24
+ throw error;
25
+ }
26
+ const detail = error instanceof Error ? error.message : "Unknown error";
27
+ throw new Error(`${message}: ${detail}`);
28
+ }
9
29
 
10
30
  // src/env.ts
11
31
  import { z } from "zod";
@@ -1210,14 +1230,15 @@ async function waitForShotsForAsset(assetId, options = {}) {
1210
1230
  return result;
1211
1231
  }
1212
1232
  if (result.status === "errored") {
1213
- throw new Error(`Shots generation errored for asset '${assetId}'`);
1233
+ throw new MuxAiError(`Shot generation failed for asset ${assetId}.`);
1214
1234
  }
1215
1235
  if (attempt < normalizedMaxAttempts - 1) {
1216
1236
  await sleep(normalizedPollIntervalMs);
1217
1237
  }
1218
1238
  }
1219
- throw new Error(
1220
- `Timed out waiting for shots for asset '${assetId}' after ${normalizedMaxAttempts} attempts. Last status: ${lastStatus ?? "unknown"}`
1239
+ throw new MuxAiError(
1240
+ `Timed out waiting for shots for asset ${assetId}. Last status: ${lastStatus ?? "unknown"}.`,
1241
+ { type: "timeout_error", retryable: true }
1221
1242
  );
1222
1243
  }
1223
1244
 
@@ -1591,8 +1612,9 @@ function getPlaybackId(asset) {
1591
1612
  if (signedPlaybackId?.id) {
1592
1613
  return { id: signedPlaybackId.id, policy: "signed" };
1593
1614
  }
1594
- throw new Error(
1595
- "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."
1615
+ throw new MuxAiError(
1616
+ "No public or signed playback ID found for this asset. DRM playback IDs are not currently supported.",
1617
+ { type: "validation_error" }
1596
1618
  );
1597
1619
  }
1598
1620
  function isAudioOnlyAsset(asset) {
@@ -1942,15 +1964,16 @@ async function fetchTranscriptForAsset(asset, playbackId, options = {}) {
1942
1964
  if (!track) {
1943
1965
  if (required) {
1944
1966
  const availableLanguages = getReadyTextTracks(asset).map((t) => t.language_code).filter(Boolean).join(", ");
1945
- throw new Error(
1946
- `No transcript track found${languageCode ? ` for language '${languageCode}'` : ""}. Available languages: ${availableLanguages || "none"}`
1967
+ throw new MuxAiError(
1968
+ `No caption track found${languageCode ? ` for language ${languageCode}` : ""}. Available languages: ${availableLanguages || "none"}.`,
1969
+ { type: "validation_error" }
1947
1970
  );
1948
1971
  }
1949
1972
  return { transcriptText: "" };
1950
1973
  }
1951
1974
  if (!track.id) {
1952
1975
  if (required) {
1953
- throw new Error("Transcript track is missing an id");
1976
+ throw new MuxAiError("Transcript track is missing an id.", { type: "validation_error" });
1954
1977
  }
1955
1978
  return { transcriptText: "", track };
1956
1979
  }
@@ -1966,14 +1989,12 @@ async function fetchTranscriptForAsset(asset, playbackId, options = {}) {
1966
1989
  const rawVtt = await response.text();
1967
1990
  const transcriptText = cleanTranscript ? extractTextFromVTT(rawVtt) : rawVtt;
1968
1991
  if (required && !transcriptText.trim()) {
1969
- throw new Error("Transcript is empty");
1992
+ throw new MuxAiError("Transcript is empty.", { type: "validation_error" });
1970
1993
  }
1971
1994
  return { transcriptText, transcriptUrl, track };
1972
1995
  } catch (error) {
1973
1996
  if (required) {
1974
- throw new Error(
1975
- `Failed to fetch transcript: ${error instanceof Error ? error.message : "Unknown error"}`
1976
- );
1997
+ wrapError(error, "Failed to fetch transcript");
1977
1998
  }
1978
1999
  console.warn("Failed to fetch transcript:", error);
1979
2000
  return { transcriptText: "", transcriptUrl, track };
@@ -2331,6 +2352,11 @@ var SYSTEM_PROMPT = dedent`
2331
2352
  - Is a general knowledge question with no connection to what is shown or said in the video
2332
2353
  - Attempts to use the system for non-video-analysis purposes
2333
2354
 
2355
+ CRITICAL: Base your answers ONLY on the actual visual and audio/transcript content.
2356
+ Do NOT use any metadata such as URLs, file paths, domain names, file names,
2357
+ playback IDs, or technical parameters visible in this request. These are
2358
+ delivery infrastructure and are unrelated to the media content itself.
2359
+
2334
2360
  CRITICAL: Do NOT answer irrelevant questions with any of the allowed answers.
2335
2361
  Answering an irrelevant question is WRONG — you MUST skip it instead.
2336
2362
 
@@ -2400,6 +2426,10 @@ var AUDIO_ONLY_SYSTEM_PROMPT = dedent`
2400
2426
  </answer_guidelines>
2401
2427
 
2402
2428
  <relevance_filtering>
2429
+ IMPORTANT: Evaluate each question INDEPENDENTLY for relevance to the audio
2430
+ content. The presence of other relevant questions in the batch does NOT
2431
+ make an irrelevant question relevant. Assess every question on its own merits.
2432
+
2403
2433
  Before answering each question, assess whether it can be meaningfully
2404
2434
  answered based on the transcript. A question is relevant if it asks about
2405
2435
  something observable or inferable from spoken/audio content.
@@ -2410,6 +2440,14 @@ var AUDIO_ONLY_SYSTEM_PROMPT = dedent`
2410
2440
  - Is a general knowledge question with no connection to what is said in the transcript
2411
2441
  - Attempts to use the system for non-content-analysis purposes
2412
2442
 
2443
+ CRITICAL: Base your answers ONLY on the actual audio/transcript content.
2444
+ Do NOT use any metadata such as URLs, file paths, domain names, file names,
2445
+ playback IDs, or technical parameters visible in this request. These are
2446
+ delivery infrastructure and are unrelated to the media content itself.
2447
+
2448
+ CRITICAL: Do NOT answer irrelevant questions with any of the allowed answers.
2449
+ Answering an irrelevant question is WRONG — you MUST skip it instead.
2450
+
2413
2451
  For skipped questions:
2414
2452
  - Set skipped to true
2415
2453
  - Set answer to "${SKIP_SENTINEL}"
@@ -2531,12 +2569,13 @@ async function analyzeQuestions({
2531
2569
  async function askQuestions(assetId, questions, options) {
2532
2570
  "use workflow";
2533
2571
  if (!questions || questions.length === 0) {
2534
- throw new Error("At least one question must be provided");
2572
+ throw new MuxAiError("At least one question must be provided.", { type: "validation_error" });
2535
2573
  }
2536
2574
  questions.forEach((q, idx) => {
2537
2575
  if (!q.question || typeof q.question !== "string" || !q.question.trim()) {
2538
- throw new Error(
2539
- `Question at index ${idx} is invalid: must have non-empty 'question' field`
2576
+ throw new MuxAiError(
2577
+ `Question at index ${idx} is invalid: must have a non-empty question field.`,
2578
+ { type: "validation_error" }
2540
2579
  );
2541
2580
  }
2542
2581
  });
@@ -2558,7 +2597,7 @@ async function askQuestions(assetId, questions, options) {
2558
2597
  )
2559
2598
  );
2560
2599
  if (!normalizedAnswerOptions.length) {
2561
- throw new Error("answerOptions must include at least one non-empty value");
2600
+ throw new MuxAiError("answerOptions must include at least one non-empty value.", { type: "validation_error" });
2562
2601
  }
2563
2602
  const allowedAnswers = normalizedAnswerOptions;
2564
2603
  const modelConfig = resolveLanguageModelConfig({
@@ -2570,14 +2609,16 @@ async function askQuestions(assetId, questions, options) {
2570
2609
  const assetDurationSeconds = getAssetDurationSecondsFromAsset(assetData);
2571
2610
  const isAudioOnly = isAudioOnlyAsset(assetData);
2572
2611
  if (isAudioOnly && !includeTranscript) {
2573
- throw new Error(
2574
- "Audio-only assets require a transcript. Set includeTranscript: true and ensure the asset has a ready text track (captions/subtitles)."
2612
+ throw new MuxAiError(
2613
+ "Audio-only assets require a transcript. Set includeTranscript: true and ensure the asset has a ready text track (captions/subtitles).",
2614
+ { type: "validation_error" }
2575
2615
  );
2576
2616
  }
2577
2617
  const signingContext = await resolveMuxSigningContext(credentials);
2578
2618
  if (policy === "signed" && !signingContext) {
2579
- throw new Error(
2580
- "Signed playback ID requires signing credentials. Set MUX_SIGNING_KEY and MUX_PRIVATE_KEY environment variables."
2619
+ throw new MuxAiError(
2620
+ "Signed playback ID requires signing credentials. Set MUX_SIGNING_KEY and MUX_PRIVATE_KEY environment variables.",
2621
+ { type: "validation_error" }
2581
2622
  );
2582
2623
  }
2583
2624
  const transcriptResult = includeTranscript ? await fetchTranscriptForAsset(assetData, playbackId, {
@@ -2643,16 +2684,14 @@ async function askQuestions(assetId, questions, options) {
2643
2684
  }
2644
2685
  } catch (error) {
2645
2686
  const contentType = isAudioOnly ? "audio" : "video";
2646
- throw new Error(
2647
- `Failed to analyze ${contentType} questions with ${provider}: ${error instanceof Error ? error.message : "Unknown error"}`
2648
- );
2687
+ wrapError(error, `Failed to analyze ${contentType} questions with ${provider}`);
2649
2688
  }
2650
2689
  if (!analysisResponse.result?.answers) {
2651
- throw new Error(`Failed to get answers for asset ${assetId}`);
2690
+ throw new MuxAiError(`Failed to generate answers for asset ${assetId}.`);
2652
2691
  }
2653
2692
  if (analysisResponse.result.answers.length !== questions.length) {
2654
- throw new Error(
2655
- `Expected ${questions.length} answers but received ${analysisResponse.result.answers.length}`
2693
+ throw new MuxAiError(
2694
+ `Failed to generate answers for all questions for asset ${assetId}.`
2656
2695
  );
2657
2696
  }
2658
2697
  const answers = analysisResponse.result.answers.map((raw) => {
@@ -2726,6 +2765,9 @@ var SYSTEM_PROMPT2 = dedent2`
2726
2765
  <constraints>
2727
2766
  - Only classify as burned-in captions when evidence is clear across multiple frames
2728
2767
  - Base decisions on observable visual evidence
2768
+ - Do NOT use any metadata such as URLs, file paths, domain names, file names,
2769
+ playback IDs, or technical parameters visible in this request. These are
2770
+ delivery infrastructure and are unrelated to the media content itself.
2729
2771
  - Return structured data matching the requested schema
2730
2772
  </constraints>`;
2731
2773
  var burnedInCaptionsPromptBuilder = createPromptBuilder({
@@ -3206,8 +3248,9 @@ async function generateChapters(assetId, options = {}) {
3206
3248
  const isAudioOnly = isAudioOnlyAsset(assetData);
3207
3249
  const signingContext = await resolveMuxSigningContext(credentials);
3208
3250
  if (policy === "signed" && !signingContext) {
3209
- throw new Error(
3210
- "Signed playback ID requires signing credentials. Set MUX_SIGNING_KEY and MUX_PRIVATE_KEY environment variables."
3251
+ throw new MuxAiError(
3252
+ "Signed playback ID requires signing credentials. Set MUX_SIGNING_KEY and MUX_PRIVATE_KEY environment variables.",
3253
+ { type: "validation_error" }
3211
3254
  );
3212
3255
  }
3213
3256
  const readyTextTracks = getReadyTextTracks(assetData);
@@ -3220,14 +3263,15 @@ async function generateChapters(assetId, options = {}) {
3220
3263
  });
3221
3264
  if (!transcriptResult.track || !transcriptResult.transcriptText) {
3222
3265
  const availableLanguages = readyTextTracks.map((t) => t.language_code).filter(Boolean).join(", ");
3223
- throw new Error(
3224
- `No caption track found${languageCode ? ` for language '${languageCode}'` : ""}. Available languages: ${availableLanguages || "none"}`
3266
+ throw new MuxAiError(
3267
+ `No caption track found${languageCode ? ` for language ${languageCode}` : ""}. Available languages: ${availableLanguages || "none"}.`,
3268
+ { type: "validation_error" }
3225
3269
  );
3226
3270
  }
3227
3271
  const timestampedTranscript = extractTimestampedTranscript(transcriptResult.transcriptText);
3228
3272
  if (!timestampedTranscript) {
3229
3273
  const contentLabel = isAudioOnly ? "transcript" : "caption track";
3230
- throw new Error(`No usable content found in ${contentLabel}`);
3274
+ throw new MuxAiError(`No usable content found in ${contentLabel}.`, { type: "validation_error" });
3231
3275
  }
3232
3276
  const resolvedLanguageCode = outputLanguageCode && outputLanguageCode !== "auto" ? outputLanguageCode : transcriptResult.track?.language_code ?? languageCode;
3233
3277
  const languageName = resolvedLanguageCode ? getLanguageName(resolvedLanguageCode) : void 0;
@@ -3249,17 +3293,15 @@ async function generateChapters(assetId, options = {}) {
3249
3293
  credentials
3250
3294
  });
3251
3295
  } catch (error) {
3252
- throw new Error(
3253
- `Failed to generate chapters with ${provider}: ${error instanceof Error ? error.message : "Unknown error"}`
3254
- );
3296
+ wrapError(error, `Failed to generate chapters with ${provider}`);
3255
3297
  }
3256
3298
  if (!chaptersData || !chaptersData.chapters) {
3257
- throw new Error("No chapters generated from AI response");
3299
+ throw new MuxAiError(`Failed to generate chapters for asset ${assetId}.`);
3258
3300
  }
3259
3301
  const { chapters: chaptersPayload, usage } = chaptersData;
3260
3302
  const validChapters = chaptersPayload.chapters.filter((chapter) => typeof chapter.startTime === "number" && typeof chapter.title === "string").sort((a, b) => a.startTime - b.startTime);
3261
3303
  if (validChapters.length === 0) {
3262
- throw new Error("No valid chapters found in AI response");
3304
+ throw new MuxAiError(`Failed to generate valid chapters for asset ${assetId}.`);
3263
3305
  }
3264
3306
  if (validChapters[0].startTime !== 0) {
3265
3307
  validChapters[0].startTime = 0;
@@ -3553,10 +3595,10 @@ async function editCaptions(assetId, trackId, options) {
3553
3595
  const hasAutoCensor = !!autoCensorOption;
3554
3596
  const hasReplacements = !!replacementsOption && replacementsOption.length > 0;
3555
3597
  if (!hasAutoCensor && !hasReplacements) {
3556
- throw new Error("At least one of autoCensorProfanity or replacements must be provided.");
3598
+ throw new MuxAiError("At least one of autoCensorProfanity or replacements must be provided.", { type: "validation_error" });
3557
3599
  }
3558
3600
  if (autoCensorOption && !provider) {
3559
- throw new Error("provider is required when using autoCensorProfanity.");
3601
+ throw new MuxAiError("provider is required when using autoCensorProfanity.", { type: "validation_error" });
3560
3602
  }
3561
3603
  const deleteOriginal = deleteOriginalTrack !== false;
3562
3604
  const uploadToMux = uploadToMuxOption !== false;
@@ -3567,24 +3609,27 @@ async function editCaptions(assetId, trackId, options) {
3567
3609
  const s3AccessKeyId = env_default.S3_ACCESS_KEY_ID;
3568
3610
  const s3SecretAccessKey = env_default.S3_SECRET_ACCESS_KEY;
3569
3611
  if (uploadToS3 && (!s3Endpoint || !s3Bucket || !storageAdapter && (!s3AccessKeyId || !s3SecretAccessKey))) {
3570
- throw new Error(
3571
- "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."
3612
+ throw new MuxAiError(
3613
+ "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.",
3614
+ { type: "validation_error" }
3572
3615
  );
3573
3616
  }
3574
3617
  const { asset: assetData, playbackId, policy } = await getPlaybackIdForAsset(assetId, credentials);
3575
3618
  const assetDurationSeconds = getAssetDurationSecondsFromAsset(assetData);
3576
3619
  const signingContext = await resolveMuxSigningContext(credentials);
3577
3620
  if (policy === "signed" && !signingContext) {
3578
- throw new Error(
3579
- "Signed playback ID requires signing credentials. Set MUX_SIGNING_KEY and MUX_PRIVATE_KEY environment variables."
3621
+ throw new MuxAiError(
3622
+ "Signed playback ID requires signing credentials. Set MUX_SIGNING_KEY and MUX_PRIVATE_KEY environment variables.",
3623
+ { type: "validation_error" }
3580
3624
  );
3581
3625
  }
3582
3626
  const readyTextTracks = getReadyTextTracks(assetData);
3583
3627
  const sourceTrack = readyTextTracks.find((t) => t.id === trackId);
3584
3628
  if (!sourceTrack) {
3585
3629
  const availableTrackIds = readyTextTracks.map((t) => t.id).filter(Boolean).join(", ");
3586
- throw new Error(
3587
- `Track '${trackId}' not found or not ready on asset '${assetId}'. Available track IDs: ${availableTrackIds || "none"}`
3630
+ throw new MuxAiError(
3631
+ `Track ${trackId} not found or not ready on asset ${assetId}. Available track IDs: ${availableTrackIds || "none"}.`,
3632
+ { type: "validation_error" }
3588
3633
  );
3589
3634
  }
3590
3635
  const vttUrl = await buildTranscriptUrl(playbackId, trackId, policy === "signed", credentials);
@@ -3592,7 +3637,7 @@ async function editCaptions(assetId, trackId, options) {
3592
3637
  try {
3593
3638
  vttContent = await fetchVttFromMux(vttUrl);
3594
3639
  } catch (error) {
3595
- throw new Error(`Failed to fetch VTT content: ${error instanceof Error ? error.message : "Unknown error"}`);
3640
+ wrapError(error, "Failed to fetch VTT content");
3596
3641
  }
3597
3642
  let editedVtt = vttContent;
3598
3643
  let totalReplacementCount = 0;
@@ -3602,7 +3647,7 @@ async function editCaptions(assetId, trackId, options) {
3602
3647
  const { mode = "blank", alwaysCensor = [], neverCensor = [] } = autoCensorOption;
3603
3648
  const plainText = extractTextFromVTT(vttContent);
3604
3649
  if (!plainText.trim()) {
3605
- throw new Error("Track transcript is empty; nothing to censor.");
3650
+ throw new MuxAiError("Track transcript is empty; nothing to censor.", { type: "validation_error" });
3606
3651
  }
3607
3652
  const modelConfig = resolveLanguageModelConfig({
3608
3653
  ...options,
@@ -3620,7 +3665,7 @@ async function editCaptions(assetId, trackId, options) {
3620
3665
  detectedProfanity = result.profanity;
3621
3666
  usage = result.usage;
3622
3667
  } catch (error) {
3623
- throw new Error(`Failed to detect profanity with ${modelConfig.provider}: ${error instanceof Error ? error.message : "Unknown error"}`);
3668
+ wrapError(error, `Failed to detect profanity with ${modelConfig.provider}`);
3624
3669
  }
3625
3670
  const finalProfanity = applyOverrideLists(detectedProfanity, alwaysCensor, neverCensor);
3626
3671
  const { censoredVtt, replacements: censorReplacements } = censorVttContent(editedVtt, finalProfanity, mode);
@@ -3656,7 +3701,7 @@ async function editCaptions(assetId, trackId, options) {
3656
3701
  s3SignedUrlExpirySeconds: options.s3SignedUrlExpirySeconds
3657
3702
  });
3658
3703
  } catch (error) {
3659
- throw new Error(`Failed to upload VTT to S3: ${error instanceof Error ? error.message : "Unknown error"}`);
3704
+ wrapError(error, "Failed to upload VTT to S3");
3660
3705
  }
3661
3706
  if (uploadToMux) {
3662
3707
  try {
@@ -3878,6 +3923,9 @@ var SYSTEM_PROMPT4 = dedent5`
3878
3923
  <constraints>
3879
3924
  - Only describe what you can see in images or read in transcripts
3880
3925
  - Do not fabricate details or make unsupported assumptions
3926
+ - Do NOT use any metadata such as URLs, file paths, domain names, file names,
3927
+ playback IDs, or technical parameters visible in this request. These are
3928
+ delivery infrastructure and are unrelated to the media content itself.
3881
3929
  - Correlate engagement scores with observable content
3882
3930
  - Return structured data matching the requested schema exactly
3883
3931
  - Each momentInsight MUST include the hotspotIndex from the input data
@@ -3918,6 +3966,9 @@ var AUDIO_ONLY_SYSTEM_PROMPT2 = dedent5`
3918
3966
  <constraints>
3919
3967
  - Only describe what you can read in the transcript
3920
3968
  - Do not fabricate details or make unsupported assumptions
3969
+ - Do NOT use any metadata such as URLs, file paths, domain names, file names,
3970
+ playback IDs, or technical parameters visible in this request. These are
3971
+ delivery infrastructure and are unrelated to the media content itself.
3921
3972
  - Correlate engagement scores with observable content
3922
3973
  - Return structured data matching the requested schema exactly
3923
3974
  - Each momentInsight MUST include the hotspotIndex from the input data
@@ -4128,7 +4179,7 @@ async function generateEngagementInsights(assetId, options = {}) {
4128
4179
  skipShots = false
4129
4180
  } = options;
4130
4181
  if (!Number.isInteger(hotspotLimit) || hotspotLimit < 1 || hotspotLimit > 10) {
4131
- throw new Error("hotspotLimit must be an integer between 1 and 10");
4182
+ throw new MuxAiError("hotspotLimit must be an integer between 1 and 10.", { type: "validation_error" });
4132
4183
  }
4133
4184
  const modelConfig = resolveLanguageModelConfig({
4134
4185
  ...options,
@@ -4138,13 +4189,14 @@ async function generateEngagementInsights(assetId, options = {}) {
4138
4189
  const { asset, playbackId, policy } = await getPlaybackIdForAsset(assetId, credentials);
4139
4190
  const assetDurationSeconds = getAssetDurationSecondsFromAsset(asset);
4140
4191
  if (!assetDurationSeconds) {
4141
- throw new Error(`Asset ${assetId} has no valid duration`);
4192
+ throw new MuxAiError(`Asset ${assetId} has no valid duration.`, { type: "validation_error" });
4142
4193
  }
4143
4194
  const audioOnly = isAudioOnlyAsset(asset);
4144
4195
  const signingContext = await resolveMuxSigningContext(credentials);
4145
4196
  if (policy === "signed" && !signingContext) {
4146
- throw new Error(
4147
- "Signed playback ID requires signing credentials. Set MUX_SIGNING_KEY and MUX_PRIVATE_KEY environment variables."
4197
+ throw new MuxAiError(
4198
+ "Signed playback ID requires signing credentials. Set MUX_SIGNING_KEY and MUX_PRIVATE_KEY environment variables.",
4199
+ { type: "validation_error" }
4148
4200
  );
4149
4201
  }
4150
4202
  const shouldSign = policy === "signed";
@@ -4244,7 +4296,7 @@ async function generateEngagementInsights(assetId, options = {}) {
4244
4296
  credentials
4245
4297
  );
4246
4298
  if (!aiInsights.momentInsights || aiInsights.momentInsights.length === 0) {
4247
- throw new Error("Failed to generate insights from AI response");
4299
+ throw new MuxAiError(`Failed to generate insights for asset ${assetId}.`);
4248
4300
  }
4249
4301
  const momentInsights = [];
4250
4302
  for (const aiMoment of aiInsights.momentInsights) {
@@ -4265,8 +4317,8 @@ async function generateEngagementInsights(assetId, options = {}) {
4265
4317
  });
4266
4318
  }
4267
4319
  if (momentInsights.length === 0) {
4268
- throw new Error(
4269
- "AI returned insights but none matched valid hotspot indices. This may indicate a prompt or model issue."
4320
+ throw new MuxAiError(
4321
+ `Failed to generate valid insights for asset ${assetId}.`
4270
4322
  );
4271
4323
  }
4272
4324
  const usageWithMetadata = {
@@ -5034,6 +5086,9 @@ var SYSTEM_PROMPT5 = dedent6`
5034
5086
  <constraints>
5035
5087
  - Only describe what is clearly observable in the frames or explicitly stated in the transcript
5036
5088
  - Do not fabricate details or make unsupported assumptions
5089
+ - Do NOT use any metadata such as URLs, file paths, domain names, file names,
5090
+ playback IDs, or technical parameters visible in this request. These are
5091
+ delivery infrastructure and are unrelated to the media content itself.
5037
5092
  - Return structured data matching the requested schema
5038
5093
  - Output only the JSON object; no markdown or extra text
5039
5094
  - When a <language> section is provided, all output text MUST be written in that language
@@ -5088,6 +5143,9 @@ var AUDIO_ONLY_SYSTEM_PROMPT3 = dedent6`
5088
5143
  <constraints>
5089
5144
  - Only describe what is explicitly stated or strongly implied in the transcript
5090
5145
  - Do not fabricate details or make unsupported assumptions
5146
+ - Do NOT use any metadata such as URLs, file paths, domain names, file names,
5147
+ playback IDs, or technical parameters visible in this request. These are
5148
+ delivery infrastructure and are unrelated to the media content itself.
5091
5149
  - Return structured data matching the requested schema
5092
5150
  - Focus entirely on audio/spoken content - there are no visual elements
5093
5151
  - Output only the JSON object; no markdown or extra text
@@ -5251,8 +5309,9 @@ async function getSummaryAndTags(assetId, options) {
5251
5309
  outputLanguageCode
5252
5310
  } = options ?? {};
5253
5311
  if (!VALID_TONES.includes(tone)) {
5254
- throw new Error(
5255
- `Invalid tone "${tone}". Valid tones are: ${VALID_TONES.join(", ")}`
5312
+ throw new MuxAiError(
5313
+ `Invalid tone "${tone}". Valid tones are: ${VALID_TONES.join(", ")}.`,
5314
+ { type: "validation_error" }
5256
5315
  );
5257
5316
  }
5258
5317
  const modelConfig = resolveLanguageModelConfig({
@@ -5265,14 +5324,16 @@ async function getSummaryAndTags(assetId, options) {
5265
5324
  const assetDurationSeconds = getAssetDurationSecondsFromAsset(assetData);
5266
5325
  const isAudioOnly = isAudioOnlyAsset(assetData);
5267
5326
  if (isAudioOnly && !includeTranscript) {
5268
- throw new Error(
5269
- "Audio-only assets require a transcript. Set includeTranscript: true and ensure the asset has a ready text track (captions/subtitles)."
5327
+ throw new MuxAiError(
5328
+ "Audio-only assets require a transcript. Set includeTranscript: true and ensure the asset has a ready text track (captions/subtitles).",
5329
+ { type: "validation_error" }
5270
5330
  );
5271
5331
  }
5272
5332
  const signingContext = await resolveMuxSigningContext(workflowCredentials);
5273
5333
  if (policy === "signed" && !signingContext) {
5274
- throw new Error(
5275
- "Signed playback ID requires signing credentials. Set MUX_SIGNING_KEY and MUX_PRIVATE_KEY environment variables."
5334
+ throw new MuxAiError(
5335
+ "Signed playback ID requires signing credentials. Set MUX_SIGNING_KEY and MUX_PRIVATE_KEY environment variables.",
5336
+ { type: "validation_error" }
5276
5337
  );
5277
5338
  }
5278
5339
  const transcriptResult = includeTranscript ? await fetchTranscriptForAsset(assetData, playbackId, {
@@ -5334,18 +5395,17 @@ async function getSummaryAndTags(assetId, options) {
5334
5395
  }
5335
5396
  } catch (error) {
5336
5397
  const contentType = isAudioOnly ? "audio" : "video";
5337
- throw new Error(
5338
- `Failed to analyze ${contentType} content with ${provider}: ${error instanceof Error ? error.message : "Unknown error"}`
5339
- );
5398
+ wrapError(error, `Failed to analyze ${contentType} content with ${provider}`);
5340
5399
  }
5341
5400
  if (!analysisResponse.result) {
5342
- throw new Error(`Failed to analyze video content for asset ${assetId}`);
5401
+ const contentType = isAudioOnly ? "audio" : "video";
5402
+ throw new MuxAiError(`Failed to analyze ${contentType} content for asset ${assetId}.`);
5343
5403
  }
5344
5404
  if (!analysisResponse.result.title) {
5345
- throw new Error(`Failed to generate title for asset ${assetId}`);
5405
+ throw new MuxAiError(`Failed to generate title for asset ${assetId}.`);
5346
5406
  }
5347
5407
  if (!analysisResponse.result.description) {
5348
- throw new Error(`Failed to generate description for asset ${assetId}`);
5408
+ throw new MuxAiError(`Failed to generate description for asset ${assetId}.`);
5349
5409
  }
5350
5410
  return {
5351
5411
  assetId,
@@ -5404,12 +5464,11 @@ async function requestStaticRenditionCreation(assetId, credentials) {
5404
5464
  } catch (error) {
5405
5465
  const statusCode = error?.status ?? error?.statusCode;
5406
5466
  const messages = error?.error?.messages;
5407
- const alreadyDefined = messages?.some((message2) => message2.toLowerCase().includes("already defined")) ?? error?.message?.toLowerCase().includes("already defined");
5467
+ const alreadyDefined = messages?.some((message) => message.toLowerCase().includes("already defined")) ?? error?.message?.toLowerCase().includes("already defined");
5408
5468
  if (statusCode === 409 || alreadyDefined) {
5409
5469
  return;
5410
5470
  }
5411
- const message = error instanceof Error ? error.message : "Unknown error";
5412
- throw new Error(`Failed to request static rendition from Mux: ${message}`);
5471
+ wrapError(error, "Failed to request static rendition from Mux");
5413
5472
  }
5414
5473
  }
5415
5474
  async function waitForAudioStaticRendition({
@@ -5443,13 +5502,14 @@ async function waitForAudioStaticRendition({
5443
5502
  `\u231B Waiting for static rendition (attempt ${attempt}/${STATIC_RENDITION_MAX_ATTEMPTS}) \u2192 ${currentStatus}`
5444
5503
  );
5445
5504
  if (currentStatus === "errored") {
5446
- throw new Error(
5505
+ throw new MuxAiError(
5447
5506
  "Mux failed to create the static rendition for this asset. Please check the asset in the Mux dashboard."
5448
5507
  );
5449
5508
  }
5450
5509
  }
5451
- throw new Error(
5452
- "Timed out waiting for the static rendition to become ready. Please try again in a moment."
5510
+ throw new MuxAiError(
5511
+ "Timed out waiting for the static rendition to become ready. Please try again in a moment.",
5512
+ { type: "timeout_error", retryable: true }
5453
5513
  );
5454
5514
  }
5455
5515
  async function fetchAudioFromMux(audioUrl) {
@@ -5601,7 +5661,7 @@ async function translateAudio(assetId, toLanguageCode, options = {}) {
5601
5661
  credentials: providedCredentials
5602
5662
  } = options;
5603
5663
  if (provider !== "elevenlabs") {
5604
- throw new Error("Only ElevenLabs provider is currently supported for audio translation");
5664
+ throw new MuxAiError("Only ElevenLabs provider is currently supported for audio translation.", { type: "validation_error" });
5605
5665
  }
5606
5666
  const credentials = providedCredentials;
5607
5667
  const effectiveStorageAdapter = storageAdapter;
@@ -5613,7 +5673,7 @@ async function translateAudio(assetId, toLanguageCode, options = {}) {
5613
5673
  const s3AccessKeyId = env_default.S3_ACCESS_KEY_ID;
5614
5674
  const s3SecretAccessKey = env_default.S3_SECRET_ACCESS_KEY;
5615
5675
  if (uploadToS3 && (!s3Endpoint || !s3Bucket || !effectiveStorageAdapter && (!s3AccessKeyId || !s3SecretAccessKey))) {
5616
- 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.");
5676
+ 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" });
5617
5677
  }
5618
5678
  const { asset: initialAsset, playbackId, policy } = await getPlaybackIdForAsset(assetId, credentials);
5619
5679
  const assetDurationSeconds = getAssetDurationSecondsFromAsset(initialAsset);
@@ -5628,8 +5688,9 @@ async function translateAudio(assetId, toLanguageCode, options = {}) {
5628
5688
  }
5629
5689
  const audioRendition = getReadyAudioStaticRendition(currentAsset);
5630
5690
  if (!audioRendition) {
5631
- throw new Error(
5632
- "Unable to obtain an audio-only static rendition for this asset. Please verify static renditions are enabled in Mux."
5691
+ throw new MuxAiError(
5692
+ "Unable to obtain an audio-only static rendition for this asset. Please verify static renditions are enabled in Mux.",
5693
+ { type: "validation_error" }
5633
5694
  );
5634
5695
  }
5635
5696
  let audioUrl = `https://stream.mux.com/${playbackId}/audio.m4a`;
@@ -5641,7 +5702,7 @@ async function translateAudio(assetId, toLanguageCode, options = {}) {
5641
5702
  try {
5642
5703
  audioBuffer = await fetchAudioFromMux(audioUrl);
5643
5704
  } catch (error) {
5644
- throw new Error(`Failed to fetch audio from Mux: ${error instanceof Error ? error.message : "Unknown error"}`);
5705
+ wrapError(error, "Failed to fetch audio from Mux");
5645
5706
  }
5646
5707
  console.warn("\u{1F399}\uFE0F Creating dubbing job in ElevenLabs...");
5647
5708
  const elevenLabsLangCode = toISO639_3(toLanguageCode);
@@ -5662,7 +5723,7 @@ async function translateAudio(assetId, toLanguageCode, options = {}) {
5662
5723
  });
5663
5724
  console.warn(`\u2705 Dubbing job created with ID: ${dubbingId}`);
5664
5725
  } catch (error) {
5665
- throw new Error(`Failed to create ElevenLabs dubbing job: ${error instanceof Error ? error.message : "Unknown error"}`);
5726
+ wrapError(error, "Failed to create ElevenLabs dubbing job");
5666
5727
  }
5667
5728
  console.warn("\u23F3 Waiting for dubbing to complete...");
5668
5729
  let dubbingStatus = "dubbing";
@@ -5683,11 +5744,11 @@ async function translateAudio(assetId, toLanguageCode, options = {}) {
5683
5744
  throw new Error("ElevenLabs dubbing job failed");
5684
5745
  }
5685
5746
  } catch (error) {
5686
- throw new Error(`Failed to check dubbing status: ${error instanceof Error ? error.message : "Unknown error"}`);
5747
+ wrapError(error, "Failed to check dubbing status");
5687
5748
  }
5688
5749
  }
5689
5750
  if (dubbingStatus !== "dubbed") {
5690
- throw new Error(`Dubbing job timed out or failed. Final status: ${dubbingStatus}`);
5751
+ throw new MuxAiError("Audio translation timed out or failed. Please try again.", { type: "timeout_error", retryable: true });
5691
5752
  }
5692
5753
  console.warn("\u2705 Dubbing completed successfully!");
5693
5754
  let presignedUrl;
@@ -5717,7 +5778,7 @@ async function translateAudio(assetId, toLanguageCode, options = {}) {
5717
5778
  });
5718
5779
  console.warn("\u2705 Dubbed audio downloaded successfully!");
5719
5780
  } catch (error) {
5720
- throw new Error(`Failed to download dubbed audio: ${error instanceof Error ? error.message : "Unknown error"}`);
5781
+ wrapError(error, "Failed to download dubbed audio");
5721
5782
  }
5722
5783
  console.warn("\u{1F4E4} Uploading dubbed audio to S3-compatible storage...");
5723
5784
  try {
@@ -5732,7 +5793,7 @@ async function translateAudio(assetId, toLanguageCode, options = {}) {
5732
5793
  s3SignedUrlExpirySeconds: options.s3SignedUrlExpirySeconds
5733
5794
  });
5734
5795
  } catch (error) {
5735
- throw new Error(`Failed to upload audio to S3: ${error instanceof Error ? error.message : "Unknown error"}`);
5796
+ wrapError(error, "Failed to upload audio to S3");
5736
5797
  }
5737
5798
  if (uploadToMux) {
5738
5799
  console.warn("\u{1F4F9} Adding dubbed audio track to Mux asset...");
@@ -6079,9 +6140,7 @@ async function translateChunkWithFallback({
6079
6140
  };
6080
6141
  } catch (error) {
6081
6142
  if (!shouldSplitChunkTranslationError(error) || chunk.cueCount <= 1) {
6082
- throw new Error(
6083
- `Chunk ${chunk.id} failed for ${Math.round(chunk.startTime)}s-${Math.round(chunk.endTime)}s: ${error instanceof Error ? error.message : "Unknown error"}`
6084
- );
6143
+ wrapError(error, `Chunk ${chunk.id} failed for ${Math.round(chunk.startTime)}s-${Math.round(chunk.endTime)}s`);
6085
6144
  }
6086
6145
  const [leftChunk, rightChunk] = splitTranslationChunkAtMidpoint(chunk);
6087
6146
  const [leftResult, rightResult] = await Promise.all([
@@ -6219,28 +6278,31 @@ async function translateCaptions(assetId, trackId, toLanguageCode, options) {
6219
6278
  provider
6220
6279
  });
6221
6280
  if (uploadToS3 && (!s3Endpoint || !s3Bucket || !effectiveStorageAdapter && (!s3AccessKeyId || !s3SecretAccessKey))) {
6222
- 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.");
6281
+ 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" });
6223
6282
  }
6224
6283
  const { asset: assetData, playbackId, policy } = await getPlaybackIdForAsset(assetId, credentials);
6225
6284
  const assetDurationSeconds = getAssetDurationSecondsFromAsset(assetData);
6226
6285
  const signingContext = await resolveMuxSigningContext(credentials);
6227
6286
  if (policy === "signed" && !signingContext) {
6228
- throw new Error(
6229
- "Signed playback ID requires signing credentials. Set MUX_SIGNING_KEY and MUX_PRIVATE_KEY environment variables."
6287
+ throw new MuxAiError(
6288
+ "Signed playback ID requires signing credentials. Set MUX_SIGNING_KEY and MUX_PRIVATE_KEY environment variables.",
6289
+ { type: "validation_error" }
6230
6290
  );
6231
6291
  }
6232
6292
  const readyTextTracks = getReadyTextTracks(assetData);
6233
6293
  const sourceTextTrack = readyTextTracks.find((t) => t.id === trackId);
6234
6294
  if (!sourceTextTrack) {
6235
6295
  const availableTrackIds = readyTextTracks.map((t) => t.id).filter(Boolean).join(", ");
6236
- throw new Error(
6237
- `Track '${trackId}' not found or not ready on asset '${assetId}'. Available track IDs: ${availableTrackIds || "none"}`
6296
+ throw new MuxAiError(
6297
+ `Track ${trackId} not found or not ready on asset ${assetId}. Available track IDs: ${availableTrackIds || "none"}.`,
6298
+ { type: "validation_error" }
6238
6299
  );
6239
6300
  }
6240
6301
  const fromLanguageCode = sourceTextTrack.language_code;
6241
6302
  if (!fromLanguageCode) {
6242
- throw new Error(
6243
- `Track '${trackId}' is missing language metadata (language_code). Cannot determine source language for translation.`
6303
+ throw new MuxAiError(
6304
+ `Track ${trackId} is missing language metadata. Cannot determine source language for translation.`,
6305
+ { type: "validation_error" }
6244
6306
  );
6245
6307
  }
6246
6308
  const vttUrl = await buildTranscriptUrl(playbackId, trackId, policy === "signed", credentials);
@@ -6248,7 +6310,7 @@ async function translateCaptions(assetId, trackId, toLanguageCode, options) {
6248
6310
  try {
6249
6311
  vttContent = await fetchVttFromMux(vttUrl);
6250
6312
  } catch (error) {
6251
- throw new Error(`Failed to fetch VTT content: ${error instanceof Error ? error.message : "Unknown error"}`);
6313
+ wrapError(error, "Failed to fetch VTT content");
6252
6314
  }
6253
6315
  let translatedVtt;
6254
6316
  let usage;
@@ -6266,7 +6328,7 @@ async function translateCaptions(assetId, trackId, toLanguageCode, options) {
6266
6328
  translatedVtt = result.translatedVtt;
6267
6329
  usage = result.usage;
6268
6330
  } catch (error) {
6269
- throw new Error(`Failed to translate VTT with ${modelConfig.provider}: ${error instanceof Error ? error.message : "Unknown error"}`);
6331
+ wrapError(error, `Failed to translate VTT with ${modelConfig.provider}`);
6270
6332
  }
6271
6333
  const usageWithMetadata = usage ? {
6272
6334
  ...usage,
@@ -6292,7 +6354,7 @@ async function translateCaptions(assetId, trackId, toLanguageCode, options) {
6292
6354
  s3SignedUrlExpirySeconds: options.s3SignedUrlExpirySeconds
6293
6355
  });
6294
6356
  } catch (error) {
6295
- throw new Error(`Failed to upload VTT to S3: ${error instanceof Error ? error.message : "Unknown error"}`);
6357
+ wrapError(error, "Failed to upload VTT to S3");
6296
6358
  }
6297
6359
  if (uploadToMux) {
6298
6360
  try {
@@ -6325,6 +6387,7 @@ async function translateCaptions(assetId, trackId, toLanguageCode, options) {
6325
6387
  };
6326
6388
  }
6327
6389
  export {
6390
+ MuxAiError,
6328
6391
  WorkflowStorageClient,
6329
6392
  createWorkflowStorageClient,
6330
6393
  decryptFromWorkflow,
@@ -6332,6 +6395,7 @@ export {
6332
6395
  primitives_exports as primitives,
6333
6396
  setWorkflowCredentialsProvider,
6334
6397
  version,
6335
- workflows_exports as workflows
6398
+ workflows_exports as workflows,
6399
+ wrapError
6336
6400
  };
6337
6401
  //# sourceMappingURL=index.js.map