@mux/ai 0.14.0 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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.16.0";
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 = "FatalError";
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 };
@@ -2139,7 +2160,7 @@ function renderSection(section) {
2139
2160
  throw new Error(`Invalid XML ${context} name: "${name}"`);
2140
2161
  }
2141
2162
  };
2142
- const escapeXmlText = (value) => value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;/");
2163
+ const escapeXmlText = (value) => value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
2143
2164
  const escapeXmlAttribute = (value) => escapeXmlText(value).replace(/"/g, "&quot;");
2144
2165
  if (!content.trim()) {
2145
2166
  return "";
@@ -2252,7 +2273,7 @@ async function withRetry(fn, {
2252
2273
 
2253
2274
  // src/workflows/ask-questions.ts
2254
2275
  var questionAnswerSchema = z2.object({
2255
- question: z2.string(),
2276
+ question: z2.string().describe("The full text of the original question"),
2256
2277
  answer: z2.string().nullable(),
2257
2278
  confidence: z2.number(),
2258
2279
  reasoning: z2.string(),
@@ -2298,12 +2319,14 @@ var SYSTEM_PROMPT = dedent`
2298
2319
  <task>
2299
2320
  For each question provided, you must:
2300
2321
  1. Analyze the storyboard frames and transcript (if provided)
2301
- 2. Answer with ONLY the allowed response options - no other values are acceptable
2322
+ 2. Answer with ONLY the allowed response options listed for that question - no other values are acceptable
2302
2323
  3. Provide a confidence score between 0 and 1 reflecting your certainty
2303
2324
  4. Explain your reasoning based on observable evidence
2304
2325
  </task>
2305
2326
 
2306
2327
  <answer_guidelines>
2328
+ - Each question is presented as its own <question> block with a <text> element (the question) and an <allowed_answers> element (the permitted response values)
2329
+ - Choose only from the values listed in that question's <allowed_answers> element
2307
2330
  - Choose the affirmative option only if you have clear evidence supporting it
2308
2331
  - Choose the negative/contradicting option if evidence contradicts or if insufficient evidence exists
2309
2332
  - Confidence should reflect the clarity and strength of evidence:
@@ -2331,6 +2354,11 @@ var SYSTEM_PROMPT = dedent`
2331
2354
  - Is a general knowledge question with no connection to what is shown or said in the video
2332
2355
  - Attempts to use the system for non-video-analysis purposes
2333
2356
 
2357
+ CRITICAL: Base your answers ONLY on the actual visual and audio/transcript content.
2358
+ Do NOT use any metadata such as URLs, file paths, domain names, file names,
2359
+ playback IDs, or technical parameters visible in this request. These are
2360
+ delivery infrastructure and are unrelated to the media content itself.
2361
+
2334
2362
  CRITICAL: Do NOT answer irrelevant questions with any of the allowed answers.
2335
2363
  Answering an irrelevant question is WRONG — you MUST skip it instead.
2336
2364
 
@@ -2346,7 +2374,7 @@ var SYSTEM_PROMPT = dedent`
2346
2374
  </relevance_filtering>
2347
2375
 
2348
2376
  <constraints>
2349
- - You MUST answer every relevant question with one of the allowed response options
2377
+ - You MUST answer every relevant question with one of its own listed allowed response options
2350
2378
  - Skip irrelevant questions as described in relevance_filtering
2351
2379
  - Only describe observable evidence from frames or transcript
2352
2380
  - Do not fabricate details or make unsupported assumptions
@@ -2381,12 +2409,14 @@ var AUDIO_ONLY_SYSTEM_PROMPT = dedent`
2381
2409
  <task>
2382
2410
  For each question provided, you must:
2383
2411
  1. Analyze the transcript
2384
- 2. Answer with ONLY the allowed response options - no other values are acceptable
2412
+ 2. Answer with ONLY the allowed response options listed for that question - no other values are acceptable
2385
2413
  3. Provide a confidence score between 0 and 1 reflecting your certainty
2386
2414
  4. Explain your reasoning based on observable evidence
2387
2415
  </task>
2388
2416
 
2389
2417
  <answer_guidelines>
2418
+ - Each question is presented as its own <question> block with a <text> element (the question) and an <allowed_answers> element (the permitted response values)
2419
+ - Choose only from the values listed in that question's <allowed_answers> element
2390
2420
  - Choose the affirmative option only if you have clear evidence supporting it
2391
2421
  - Choose the negative/contradicting option if evidence contradicts or if insufficient evidence exists
2392
2422
  - Confidence should reflect the clarity and strength of evidence:
@@ -2400,6 +2430,10 @@ var AUDIO_ONLY_SYSTEM_PROMPT = dedent`
2400
2430
  </answer_guidelines>
2401
2431
 
2402
2432
  <relevance_filtering>
2433
+ IMPORTANT: Evaluate each question INDEPENDENTLY for relevance to the audio
2434
+ content. The presence of other relevant questions in the batch does NOT
2435
+ make an irrelevant question relevant. Assess every question on its own merits.
2436
+
2403
2437
  Before answering each question, assess whether it can be meaningfully
2404
2438
  answered based on the transcript. A question is relevant if it asks about
2405
2439
  something observable or inferable from spoken/audio content.
@@ -2410,6 +2444,14 @@ var AUDIO_ONLY_SYSTEM_PROMPT = dedent`
2410
2444
  - Is a general knowledge question with no connection to what is said in the transcript
2411
2445
  - Attempts to use the system for non-content-analysis purposes
2412
2446
 
2447
+ CRITICAL: Base your answers ONLY on the actual audio/transcript content.
2448
+ Do NOT use any metadata such as URLs, file paths, domain names, file names,
2449
+ playback IDs, or technical parameters visible in this request. These are
2450
+ delivery infrastructure and are unrelated to the media content itself.
2451
+
2452
+ CRITICAL: Do NOT answer irrelevant questions with any of the allowed answers.
2453
+ Answering an irrelevant question is WRONG — you MUST skip it instead.
2454
+
2413
2455
  For skipped questions:
2414
2456
  - Set skipped to true
2415
2457
  - Set answer to "${SKIP_SENTINEL}"
@@ -2422,7 +2464,7 @@ var AUDIO_ONLY_SYSTEM_PROMPT = dedent`
2422
2464
  </relevance_filtering>
2423
2465
 
2424
2466
  <constraints>
2425
- - You MUST answer every relevant question with one of the allowed response options
2467
+ - You MUST answer every relevant question with one of its own listed allowed response options
2426
2468
  - Skip irrelevant questions as described in relevance_filtering
2427
2469
  - Only describe observable evidence from transcript content
2428
2470
  - Do not fabricate details or make unsupported assumptions
@@ -2437,46 +2479,43 @@ var AUDIO_ONLY_SYSTEM_PROMPT = dedent`
2437
2479
  - GOOD: "The speaker describes running through a park"
2438
2480
  - Be specific and evidence-based
2439
2481
  </language_guidelines>`;
2440
- var askQuestionsPromptBuilder = createPromptBuilder({
2441
- template: {
2442
- questions: {
2443
- tag: "questions",
2444
- content: "Please answer the following questions about this content:"
2445
- }
2446
- },
2447
- sectionOrder: ["questions"]
2448
- });
2449
2482
  function buildUserPrompt({
2450
2483
  questions,
2451
- allowedAnswers,
2452
2484
  transcriptText,
2453
2485
  isCleanTranscript = true,
2454
2486
  isAudioOnly = false
2455
2487
  }) {
2456
- const questionsList = questions.map((q, idx) => `${idx + 1}. ${q.question}`).join("\n");
2457
- const questionsIntro = isAudioOnly ? "Please answer the following questions about this audio content using transcript evidence:" : "Please answer the following questions about this content:";
2458
- const questionsContent = dedent`
2459
- ${questionsIntro}
2460
-
2461
- ${questionsList}`;
2462
- const answerList = allowedAnswers.map((answer) => `"${answer}"`).join(", ");
2463
- const responseOptions = dedent`
2464
- <response_options>
2465
- Allowed answers: ${answerList}
2466
- </response_options>`;
2467
- const questionsSection = askQuestionsPromptBuilder.build({ questions: questionsContent });
2468
- if (!transcriptText) {
2469
- return `${questionsSection}
2470
-
2471
- ${responseOptions}`;
2488
+ const contentDescriptor = isAudioOnly ? "audio content using transcript evidence" : "content";
2489
+ const taskContent = dedent`
2490
+ Answer each question in the <questions> block below about the ${contentDescriptor}.
2491
+ Use only the values listed inside each question's own <allowed_answers> element.
2492
+ Return one answer per question, in the order the questions appear.
2493
+ If a question cannot be answered from the provided content, skip it as described in the system instructions.`;
2494
+ const taskSection = `<task>
2495
+ ${taskContent}
2496
+ </task>`;
2497
+ const questionBlocks = questions.map((q, idx) => {
2498
+ const textTag = renderSection({ tag: "text", content: q.question });
2499
+ const answersTag = renderSection({
2500
+ tag: "allowed_answers",
2501
+ content: q.answerOptions.map((a) => `"${a}"`).join(", ")
2502
+ });
2503
+ return `<question number="${idx + 1}">
2504
+ ${textTag}
2505
+ ${answersTag}
2506
+ </question>`;
2507
+ }).join("\n\n");
2508
+ const questionsSection = `<questions>
2509
+ ${questionBlocks}
2510
+ </questions>`;
2511
+ const sections = [];
2512
+ if (transcriptText) {
2513
+ const format = isCleanTranscript ? "plain text" : "WebVTT";
2514
+ sections.push(renderSection(createTranscriptSection(transcriptText, format)));
2472
2515
  }
2473
- const format = isCleanTranscript ? "plain text" : "WebVTT";
2474
- const transcriptSection = renderSection(createTranscriptSection(transcriptText, format));
2475
- return `${transcriptSection}
2476
-
2477
- ${questionsSection}
2478
-
2479
- ${responseOptions}`;
2516
+ sections.push(taskSection);
2517
+ sections.push(questionsSection);
2518
+ return sections.join("\n\n");
2480
2519
  }
2481
2520
  async function fetchImageAsBase64(imageUrl, imageDownloadOptions) {
2482
2521
  "use step";
@@ -2531,12 +2570,13 @@ async function analyzeQuestions({
2531
2570
  async function askQuestions(assetId, questions, options) {
2532
2571
  "use workflow";
2533
2572
  if (!questions || questions.length === 0) {
2534
- throw new Error("At least one question must be provided");
2573
+ throw new MuxAiError("At least one question must be provided.", { type: "validation_error" });
2535
2574
  }
2536
2575
  questions.forEach((q, idx) => {
2537
2576
  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`
2577
+ throw new MuxAiError(
2578
+ `Question at index ${idx} is invalid: must have a non-empty question field.`,
2579
+ { type: "validation_error" }
2540
2580
  );
2541
2581
  }
2542
2582
  });
@@ -2544,7 +2584,6 @@ async function askQuestions(assetId, questions, options) {
2544
2584
  provider = "openai",
2545
2585
  model,
2546
2586
  languageCode,
2547
- answerOptions,
2548
2587
  includeTranscript = true,
2549
2588
  cleanTranscript = true,
2550
2589
  imageSubmissionMode = "url",
@@ -2552,15 +2591,23 @@ async function askQuestions(assetId, questions, options) {
2552
2591
  storyboardWidth = 640,
2553
2592
  credentials
2554
2593
  } = options ?? {};
2555
- const normalizedAnswerOptions = Array.from(
2556
- new Set(
2557
- (answerOptions?.length ? answerOptions : ["yes", "no"]).map((option) => option.trim()).filter(Boolean)
2558
- )
2594
+ const normalizedQuestions = questions.map((q, idx) => {
2595
+ const options2 = Array.from(
2596
+ new Set(
2597
+ (q.answerOptions?.length ? q.answerOptions : ["yes", "no"]).map((o) => o.trim()).filter(Boolean)
2598
+ )
2599
+ );
2600
+ if (!options2.length) {
2601
+ throw new MuxAiError(
2602
+ `Question at index ${idx} has invalid answerOptions: must include at least one non-empty value.`,
2603
+ { type: "validation_error" }
2604
+ );
2605
+ }
2606
+ return { ...q, answerOptions: options2 };
2607
+ });
2608
+ const allowedAnswers = Array.from(
2609
+ new Set(normalizedQuestions.flatMap((q) => q.answerOptions))
2559
2610
  );
2560
- if (!normalizedAnswerOptions.length) {
2561
- throw new Error("answerOptions must include at least one non-empty value");
2562
- }
2563
- const allowedAnswers = normalizedAnswerOptions;
2564
2611
  const modelConfig = resolveLanguageModelConfig({
2565
2612
  ...options,
2566
2613
  model,
@@ -2570,14 +2617,16 @@ async function askQuestions(assetId, questions, options) {
2570
2617
  const assetDurationSeconds = getAssetDurationSecondsFromAsset(assetData);
2571
2618
  const isAudioOnly = isAudioOnlyAsset(assetData);
2572
2619
  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)."
2620
+ throw new MuxAiError(
2621
+ "Audio-only assets require a transcript. Set includeTranscript: true and ensure the asset has a ready text track (captions/subtitles).",
2622
+ { type: "validation_error" }
2575
2623
  );
2576
2624
  }
2577
2625
  const signingContext = await resolveMuxSigningContext(credentials);
2578
2626
  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."
2627
+ throw new MuxAiError(
2628
+ "Signed playback ID requires signing credentials. Set MUX_SIGNING_KEY and MUX_PRIVATE_KEY environment variables.",
2629
+ { type: "validation_error" }
2581
2630
  );
2582
2631
  }
2583
2632
  const transcriptResult = includeTranscript ? await fetchTranscriptForAsset(assetData, playbackId, {
@@ -2589,8 +2638,7 @@ async function askQuestions(assetId, questions, options) {
2589
2638
  }) : void 0;
2590
2639
  const transcriptText = transcriptResult?.transcriptText ?? "";
2591
2640
  const userPrompt = buildUserPrompt({
2592
- questions,
2593
- allowedAnswers,
2641
+ questions: normalizedQuestions,
2594
2642
  transcriptText,
2595
2643
  isCleanTranscript: cleanTranscript,
2596
2644
  isAudioOnly
@@ -2643,22 +2691,25 @@ async function askQuestions(assetId, questions, options) {
2643
2691
  }
2644
2692
  } catch (error) {
2645
2693
  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
- );
2694
+ wrapError(error, `Failed to analyze ${contentType} questions with ${provider}`);
2649
2695
  }
2650
2696
  if (!analysisResponse.result?.answers) {
2651
- throw new Error(`Failed to get answers for asset ${assetId}`);
2697
+ throw new MuxAiError(`Failed to generate answers for asset ${assetId}.`);
2652
2698
  }
2653
2699
  if (analysisResponse.result.answers.length !== questions.length) {
2654
- throw new Error(
2655
- `Expected ${questions.length} answers but received ${analysisResponse.result.answers.length}`
2700
+ throw new MuxAiError(
2701
+ `Failed to generate answers for all questions for asset ${assetId}.`
2656
2702
  );
2657
2703
  }
2658
- const answers = analysisResponse.result.answers.map((raw) => {
2704
+ const answers = analysisResponse.result.answers.map((raw, idx) => {
2659
2705
  const isSkipped = raw.skipped || raw.answer === SKIP_SENTINEL;
2706
+ if (!isSkipped && !normalizedQuestions[idx].answerOptions.includes(raw.answer)) {
2707
+ throw new MuxAiError(
2708
+ `Answer "${raw.answer}" for question ${idx} is not in allowed options: ${normalizedQuestions[idx].answerOptions.join(", ")}`
2709
+ );
2710
+ }
2660
2711
  return {
2661
- // Strip numbering prefix (e.g., "1. " or "2. ") from questions
2712
+ // Strip numbering prefix (e.g. "1. ") if the LLM prepends one
2662
2713
  question: raw.question.replace(/^\d+\.\s*/, ""),
2663
2714
  confidence: isSkipped ? 0 : Math.min(1, Math.max(0, raw.confidence)),
2664
2715
  reasoning: raw.reasoning,
@@ -2726,6 +2777,9 @@ var SYSTEM_PROMPT2 = dedent2`
2726
2777
  <constraints>
2727
2778
  - Only classify as burned-in captions when evidence is clear across multiple frames
2728
2779
  - Base decisions on observable visual evidence
2780
+ - Do NOT use any metadata such as URLs, file paths, domain names, file names,
2781
+ playback IDs, or technical parameters visible in this request. These are
2782
+ delivery infrastructure and are unrelated to the media content itself.
2729
2783
  - Return structured data matching the requested schema
2730
2784
  </constraints>`;
2731
2785
  var burnedInCaptionsPromptBuilder = createPromptBuilder({
@@ -3206,8 +3260,9 @@ async function generateChapters(assetId, options = {}) {
3206
3260
  const isAudioOnly = isAudioOnlyAsset(assetData);
3207
3261
  const signingContext = await resolveMuxSigningContext(credentials);
3208
3262
  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."
3263
+ throw new MuxAiError(
3264
+ "Signed playback ID requires signing credentials. Set MUX_SIGNING_KEY and MUX_PRIVATE_KEY environment variables.",
3265
+ { type: "validation_error" }
3211
3266
  );
3212
3267
  }
3213
3268
  const readyTextTracks = getReadyTextTracks(assetData);
@@ -3220,14 +3275,15 @@ async function generateChapters(assetId, options = {}) {
3220
3275
  });
3221
3276
  if (!transcriptResult.track || !transcriptResult.transcriptText) {
3222
3277
  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"}`
3278
+ throw new MuxAiError(
3279
+ `No caption track found${languageCode ? ` for language ${languageCode}` : ""}. Available languages: ${availableLanguages || "none"}.`,
3280
+ { type: "validation_error" }
3225
3281
  );
3226
3282
  }
3227
3283
  const timestampedTranscript = extractTimestampedTranscript(transcriptResult.transcriptText);
3228
3284
  if (!timestampedTranscript) {
3229
3285
  const contentLabel = isAudioOnly ? "transcript" : "caption track";
3230
- throw new Error(`No usable content found in ${contentLabel}`);
3286
+ throw new MuxAiError(`No usable content found in ${contentLabel}.`, { type: "validation_error" });
3231
3287
  }
3232
3288
  const resolvedLanguageCode = outputLanguageCode && outputLanguageCode !== "auto" ? outputLanguageCode : transcriptResult.track?.language_code ?? languageCode;
3233
3289
  const languageName = resolvedLanguageCode ? getLanguageName(resolvedLanguageCode) : void 0;
@@ -3249,17 +3305,15 @@ async function generateChapters(assetId, options = {}) {
3249
3305
  credentials
3250
3306
  });
3251
3307
  } catch (error) {
3252
- throw new Error(
3253
- `Failed to generate chapters with ${provider}: ${error instanceof Error ? error.message : "Unknown error"}`
3254
- );
3308
+ wrapError(error, `Failed to generate chapters with ${provider}`);
3255
3309
  }
3256
3310
  if (!chaptersData || !chaptersData.chapters) {
3257
- throw new Error("No chapters generated from AI response");
3311
+ throw new MuxAiError(`Failed to generate chapters for asset ${assetId}.`);
3258
3312
  }
3259
3313
  const { chapters: chaptersPayload, usage } = chaptersData;
3260
3314
  const validChapters = chaptersPayload.chapters.filter((chapter) => typeof chapter.startTime === "number" && typeof chapter.title === "string").sort((a, b) => a.startTime - b.startTime);
3261
3315
  if (validChapters.length === 0) {
3262
- throw new Error("No valid chapters found in AI response");
3316
+ throw new MuxAiError(`Failed to generate valid chapters for asset ${assetId}.`);
3263
3317
  }
3264
3318
  if (validChapters[0].startTime !== 0) {
3265
3319
  validChapters[0].startTime = 0;
@@ -3553,10 +3607,10 @@ async function editCaptions(assetId, trackId, options) {
3553
3607
  const hasAutoCensor = !!autoCensorOption;
3554
3608
  const hasReplacements = !!replacementsOption && replacementsOption.length > 0;
3555
3609
  if (!hasAutoCensor && !hasReplacements) {
3556
- throw new Error("At least one of autoCensorProfanity or replacements must be provided.");
3610
+ throw new MuxAiError("At least one of autoCensorProfanity or replacements must be provided.", { type: "validation_error" });
3557
3611
  }
3558
3612
  if (autoCensorOption && !provider) {
3559
- throw new Error("provider is required when using autoCensorProfanity.");
3613
+ throw new MuxAiError("provider is required when using autoCensorProfanity.", { type: "validation_error" });
3560
3614
  }
3561
3615
  const deleteOriginal = deleteOriginalTrack !== false;
3562
3616
  const uploadToMux = uploadToMuxOption !== false;
@@ -3567,24 +3621,27 @@ async function editCaptions(assetId, trackId, options) {
3567
3621
  const s3AccessKeyId = env_default.S3_ACCESS_KEY_ID;
3568
3622
  const s3SecretAccessKey = env_default.S3_SECRET_ACCESS_KEY;
3569
3623
  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."
3624
+ throw new MuxAiError(
3625
+ "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.",
3626
+ { type: "validation_error" }
3572
3627
  );
3573
3628
  }
3574
3629
  const { asset: assetData, playbackId, policy } = await getPlaybackIdForAsset(assetId, credentials);
3575
3630
  const assetDurationSeconds = getAssetDurationSecondsFromAsset(assetData);
3576
3631
  const signingContext = await resolveMuxSigningContext(credentials);
3577
3632
  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."
3633
+ throw new MuxAiError(
3634
+ "Signed playback ID requires signing credentials. Set MUX_SIGNING_KEY and MUX_PRIVATE_KEY environment variables.",
3635
+ { type: "validation_error" }
3580
3636
  );
3581
3637
  }
3582
3638
  const readyTextTracks = getReadyTextTracks(assetData);
3583
3639
  const sourceTrack = readyTextTracks.find((t) => t.id === trackId);
3584
3640
  if (!sourceTrack) {
3585
3641
  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"}`
3642
+ throw new MuxAiError(
3643
+ `Track ${trackId} not found or not ready on asset ${assetId}. Available track IDs: ${availableTrackIds || "none"}.`,
3644
+ { type: "validation_error" }
3588
3645
  );
3589
3646
  }
3590
3647
  const vttUrl = await buildTranscriptUrl(playbackId, trackId, policy === "signed", credentials);
@@ -3592,7 +3649,7 @@ async function editCaptions(assetId, trackId, options) {
3592
3649
  try {
3593
3650
  vttContent = await fetchVttFromMux(vttUrl);
3594
3651
  } catch (error) {
3595
- throw new Error(`Failed to fetch VTT content: ${error instanceof Error ? error.message : "Unknown error"}`);
3652
+ wrapError(error, "Failed to fetch VTT content");
3596
3653
  }
3597
3654
  let editedVtt = vttContent;
3598
3655
  let totalReplacementCount = 0;
@@ -3602,7 +3659,7 @@ async function editCaptions(assetId, trackId, options) {
3602
3659
  const { mode = "blank", alwaysCensor = [], neverCensor = [] } = autoCensorOption;
3603
3660
  const plainText = extractTextFromVTT(vttContent);
3604
3661
  if (!plainText.trim()) {
3605
- throw new Error("Track transcript is empty; nothing to censor.");
3662
+ throw new MuxAiError("Track transcript is empty; nothing to censor.", { type: "validation_error" });
3606
3663
  }
3607
3664
  const modelConfig = resolveLanguageModelConfig({
3608
3665
  ...options,
@@ -3620,7 +3677,7 @@ async function editCaptions(assetId, trackId, options) {
3620
3677
  detectedProfanity = result.profanity;
3621
3678
  usage = result.usage;
3622
3679
  } catch (error) {
3623
- throw new Error(`Failed to detect profanity with ${modelConfig.provider}: ${error instanceof Error ? error.message : "Unknown error"}`);
3680
+ wrapError(error, `Failed to detect profanity with ${modelConfig.provider}`);
3624
3681
  }
3625
3682
  const finalProfanity = applyOverrideLists(detectedProfanity, alwaysCensor, neverCensor);
3626
3683
  const { censoredVtt, replacements: censorReplacements } = censorVttContent(editedVtt, finalProfanity, mode);
@@ -3656,7 +3713,7 @@ async function editCaptions(assetId, trackId, options) {
3656
3713
  s3SignedUrlExpirySeconds: options.s3SignedUrlExpirySeconds
3657
3714
  });
3658
3715
  } catch (error) {
3659
- throw new Error(`Failed to upload VTT to S3: ${error instanceof Error ? error.message : "Unknown error"}`);
3716
+ wrapError(error, "Failed to upload VTT to S3");
3660
3717
  }
3661
3718
  if (uploadToMux) {
3662
3719
  try {
@@ -3878,6 +3935,9 @@ var SYSTEM_PROMPT4 = dedent5`
3878
3935
  <constraints>
3879
3936
  - Only describe what you can see in images or read in transcripts
3880
3937
  - Do not fabricate details or make unsupported assumptions
3938
+ - Do NOT use any metadata such as URLs, file paths, domain names, file names,
3939
+ playback IDs, or technical parameters visible in this request. These are
3940
+ delivery infrastructure and are unrelated to the media content itself.
3881
3941
  - Correlate engagement scores with observable content
3882
3942
  - Return structured data matching the requested schema exactly
3883
3943
  - Each momentInsight MUST include the hotspotIndex from the input data
@@ -3918,6 +3978,9 @@ var AUDIO_ONLY_SYSTEM_PROMPT2 = dedent5`
3918
3978
  <constraints>
3919
3979
  - Only describe what you can read in the transcript
3920
3980
  - Do not fabricate details or make unsupported assumptions
3981
+ - Do NOT use any metadata such as URLs, file paths, domain names, file names,
3982
+ playback IDs, or technical parameters visible in this request. These are
3983
+ delivery infrastructure and are unrelated to the media content itself.
3921
3984
  - Correlate engagement scores with observable content
3922
3985
  - Return structured data matching the requested schema exactly
3923
3986
  - Each momentInsight MUST include the hotspotIndex from the input data
@@ -4128,7 +4191,7 @@ async function generateEngagementInsights(assetId, options = {}) {
4128
4191
  skipShots = false
4129
4192
  } = options;
4130
4193
  if (!Number.isInteger(hotspotLimit) || hotspotLimit < 1 || hotspotLimit > 10) {
4131
- throw new Error("hotspotLimit must be an integer between 1 and 10");
4194
+ throw new MuxAiError("hotspotLimit must be an integer between 1 and 10.", { type: "validation_error" });
4132
4195
  }
4133
4196
  const modelConfig = resolveLanguageModelConfig({
4134
4197
  ...options,
@@ -4138,13 +4201,14 @@ async function generateEngagementInsights(assetId, options = {}) {
4138
4201
  const { asset, playbackId, policy } = await getPlaybackIdForAsset(assetId, credentials);
4139
4202
  const assetDurationSeconds = getAssetDurationSecondsFromAsset(asset);
4140
4203
  if (!assetDurationSeconds) {
4141
- throw new Error(`Asset ${assetId} has no valid duration`);
4204
+ throw new MuxAiError(`Asset ${assetId} has no valid duration.`, { type: "validation_error" });
4142
4205
  }
4143
4206
  const audioOnly = isAudioOnlyAsset(asset);
4144
4207
  const signingContext = await resolveMuxSigningContext(credentials);
4145
4208
  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."
4209
+ throw new MuxAiError(
4210
+ "Signed playback ID requires signing credentials. Set MUX_SIGNING_KEY and MUX_PRIVATE_KEY environment variables.",
4211
+ { type: "validation_error" }
4148
4212
  );
4149
4213
  }
4150
4214
  const shouldSign = policy === "signed";
@@ -4244,7 +4308,7 @@ async function generateEngagementInsights(assetId, options = {}) {
4244
4308
  credentials
4245
4309
  );
4246
4310
  if (!aiInsights.momentInsights || aiInsights.momentInsights.length === 0) {
4247
- throw new Error("Failed to generate insights from AI response");
4311
+ throw new MuxAiError(`Failed to generate insights for asset ${assetId}.`);
4248
4312
  }
4249
4313
  const momentInsights = [];
4250
4314
  for (const aiMoment of aiInsights.momentInsights) {
@@ -4265,8 +4329,8 @@ async function generateEngagementInsights(assetId, options = {}) {
4265
4329
  });
4266
4330
  }
4267
4331
  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."
4332
+ throw new MuxAiError(
4333
+ `Failed to generate valid insights for asset ${assetId}.`
4270
4334
  );
4271
4335
  }
4272
4336
  const usageWithMetadata = {
@@ -5034,6 +5098,9 @@ var SYSTEM_PROMPT5 = dedent6`
5034
5098
  <constraints>
5035
5099
  - Only describe what is clearly observable in the frames or explicitly stated in the transcript
5036
5100
  - Do not fabricate details or make unsupported assumptions
5101
+ - Do NOT use any metadata such as URLs, file paths, domain names, file names,
5102
+ playback IDs, or technical parameters visible in this request. These are
5103
+ delivery infrastructure and are unrelated to the media content itself.
5037
5104
  - Return structured data matching the requested schema
5038
5105
  - Output only the JSON object; no markdown or extra text
5039
5106
  - When a <language> section is provided, all output text MUST be written in that language
@@ -5088,6 +5155,9 @@ var AUDIO_ONLY_SYSTEM_PROMPT3 = dedent6`
5088
5155
  <constraints>
5089
5156
  - Only describe what is explicitly stated or strongly implied in the transcript
5090
5157
  - Do not fabricate details or make unsupported assumptions
5158
+ - Do NOT use any metadata such as URLs, file paths, domain names, file names,
5159
+ playback IDs, or technical parameters visible in this request. These are
5160
+ delivery infrastructure and are unrelated to the media content itself.
5091
5161
  - Return structured data matching the requested schema
5092
5162
  - Focus entirely on audio/spoken content - there are no visual elements
5093
5163
  - Output only the JSON object; no markdown or extra text
@@ -5251,8 +5321,9 @@ async function getSummaryAndTags(assetId, options) {
5251
5321
  outputLanguageCode
5252
5322
  } = options ?? {};
5253
5323
  if (!VALID_TONES.includes(tone)) {
5254
- throw new Error(
5255
- `Invalid tone "${tone}". Valid tones are: ${VALID_TONES.join(", ")}`
5324
+ throw new MuxAiError(
5325
+ `Invalid tone "${tone}". Valid tones are: ${VALID_TONES.join(", ")}.`,
5326
+ { type: "validation_error" }
5256
5327
  );
5257
5328
  }
5258
5329
  const modelConfig = resolveLanguageModelConfig({
@@ -5265,14 +5336,16 @@ async function getSummaryAndTags(assetId, options) {
5265
5336
  const assetDurationSeconds = getAssetDurationSecondsFromAsset(assetData);
5266
5337
  const isAudioOnly = isAudioOnlyAsset(assetData);
5267
5338
  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)."
5339
+ throw new MuxAiError(
5340
+ "Audio-only assets require a transcript. Set includeTranscript: true and ensure the asset has a ready text track (captions/subtitles).",
5341
+ { type: "validation_error" }
5270
5342
  );
5271
5343
  }
5272
5344
  const signingContext = await resolveMuxSigningContext(workflowCredentials);
5273
5345
  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."
5346
+ throw new MuxAiError(
5347
+ "Signed playback ID requires signing credentials. Set MUX_SIGNING_KEY and MUX_PRIVATE_KEY environment variables.",
5348
+ { type: "validation_error" }
5276
5349
  );
5277
5350
  }
5278
5351
  const transcriptResult = includeTranscript ? await fetchTranscriptForAsset(assetData, playbackId, {
@@ -5334,18 +5407,17 @@ async function getSummaryAndTags(assetId, options) {
5334
5407
  }
5335
5408
  } catch (error) {
5336
5409
  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
- );
5410
+ wrapError(error, `Failed to analyze ${contentType} content with ${provider}`);
5340
5411
  }
5341
5412
  if (!analysisResponse.result) {
5342
- throw new Error(`Failed to analyze video content for asset ${assetId}`);
5413
+ const contentType = isAudioOnly ? "audio" : "video";
5414
+ throw new MuxAiError(`Failed to analyze ${contentType} content for asset ${assetId}.`);
5343
5415
  }
5344
5416
  if (!analysisResponse.result.title) {
5345
- throw new Error(`Failed to generate title for asset ${assetId}`);
5417
+ throw new MuxAiError(`Failed to generate title for asset ${assetId}.`);
5346
5418
  }
5347
5419
  if (!analysisResponse.result.description) {
5348
- throw new Error(`Failed to generate description for asset ${assetId}`);
5420
+ throw new MuxAiError(`Failed to generate description for asset ${assetId}.`);
5349
5421
  }
5350
5422
  return {
5351
5423
  assetId,
@@ -5404,12 +5476,11 @@ async function requestStaticRenditionCreation(assetId, credentials) {
5404
5476
  } catch (error) {
5405
5477
  const statusCode = error?.status ?? error?.statusCode;
5406
5478
  const messages = error?.error?.messages;
5407
- const alreadyDefined = messages?.some((message2) => message2.toLowerCase().includes("already defined")) ?? error?.message?.toLowerCase().includes("already defined");
5479
+ const alreadyDefined = messages?.some((message) => message.toLowerCase().includes("already defined")) ?? error?.message?.toLowerCase().includes("already defined");
5408
5480
  if (statusCode === 409 || alreadyDefined) {
5409
5481
  return;
5410
5482
  }
5411
- const message = error instanceof Error ? error.message : "Unknown error";
5412
- throw new Error(`Failed to request static rendition from Mux: ${message}`);
5483
+ wrapError(error, "Failed to request static rendition from Mux");
5413
5484
  }
5414
5485
  }
5415
5486
  async function waitForAudioStaticRendition({
@@ -5443,13 +5514,14 @@ async function waitForAudioStaticRendition({
5443
5514
  `\u231B Waiting for static rendition (attempt ${attempt}/${STATIC_RENDITION_MAX_ATTEMPTS}) \u2192 ${currentStatus}`
5444
5515
  );
5445
5516
  if (currentStatus === "errored") {
5446
- throw new Error(
5517
+ throw new MuxAiError(
5447
5518
  "Mux failed to create the static rendition for this asset. Please check the asset in the Mux dashboard."
5448
5519
  );
5449
5520
  }
5450
5521
  }
5451
- throw new Error(
5452
- "Timed out waiting for the static rendition to become ready. Please try again in a moment."
5522
+ throw new MuxAiError(
5523
+ "Timed out waiting for the static rendition to become ready. Please try again in a moment.",
5524
+ { type: "timeout_error", retryable: true }
5453
5525
  );
5454
5526
  }
5455
5527
  async function fetchAudioFromMux(audioUrl) {
@@ -5601,7 +5673,7 @@ async function translateAudio(assetId, toLanguageCode, options = {}) {
5601
5673
  credentials: providedCredentials
5602
5674
  } = options;
5603
5675
  if (provider !== "elevenlabs") {
5604
- throw new Error("Only ElevenLabs provider is currently supported for audio translation");
5676
+ throw new MuxAiError("Only ElevenLabs provider is currently supported for audio translation.", { type: "validation_error" });
5605
5677
  }
5606
5678
  const credentials = providedCredentials;
5607
5679
  const effectiveStorageAdapter = storageAdapter;
@@ -5613,7 +5685,7 @@ async function translateAudio(assetId, toLanguageCode, options = {}) {
5613
5685
  const s3AccessKeyId = env_default.S3_ACCESS_KEY_ID;
5614
5686
  const s3SecretAccessKey = env_default.S3_SECRET_ACCESS_KEY;
5615
5687
  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.");
5688
+ 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
5689
  }
5618
5690
  const { asset: initialAsset, playbackId, policy } = await getPlaybackIdForAsset(assetId, credentials);
5619
5691
  const assetDurationSeconds = getAssetDurationSecondsFromAsset(initialAsset);
@@ -5628,8 +5700,9 @@ async function translateAudio(assetId, toLanguageCode, options = {}) {
5628
5700
  }
5629
5701
  const audioRendition = getReadyAudioStaticRendition(currentAsset);
5630
5702
  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."
5703
+ throw new MuxAiError(
5704
+ "Unable to obtain an audio-only static rendition for this asset. Please verify static renditions are enabled in Mux.",
5705
+ { type: "validation_error" }
5633
5706
  );
5634
5707
  }
5635
5708
  let audioUrl = `https://stream.mux.com/${playbackId}/audio.m4a`;
@@ -5641,7 +5714,7 @@ async function translateAudio(assetId, toLanguageCode, options = {}) {
5641
5714
  try {
5642
5715
  audioBuffer = await fetchAudioFromMux(audioUrl);
5643
5716
  } catch (error) {
5644
- throw new Error(`Failed to fetch audio from Mux: ${error instanceof Error ? error.message : "Unknown error"}`);
5717
+ wrapError(error, "Failed to fetch audio from Mux");
5645
5718
  }
5646
5719
  console.warn("\u{1F399}\uFE0F Creating dubbing job in ElevenLabs...");
5647
5720
  const elevenLabsLangCode = toISO639_3(toLanguageCode);
@@ -5662,7 +5735,7 @@ async function translateAudio(assetId, toLanguageCode, options = {}) {
5662
5735
  });
5663
5736
  console.warn(`\u2705 Dubbing job created with ID: ${dubbingId}`);
5664
5737
  } catch (error) {
5665
- throw new Error(`Failed to create ElevenLabs dubbing job: ${error instanceof Error ? error.message : "Unknown error"}`);
5738
+ wrapError(error, "Failed to create ElevenLabs dubbing job");
5666
5739
  }
5667
5740
  console.warn("\u23F3 Waiting for dubbing to complete...");
5668
5741
  let dubbingStatus = "dubbing";
@@ -5683,11 +5756,11 @@ async function translateAudio(assetId, toLanguageCode, options = {}) {
5683
5756
  throw new Error("ElevenLabs dubbing job failed");
5684
5757
  }
5685
5758
  } catch (error) {
5686
- throw new Error(`Failed to check dubbing status: ${error instanceof Error ? error.message : "Unknown error"}`);
5759
+ wrapError(error, "Failed to check dubbing status");
5687
5760
  }
5688
5761
  }
5689
5762
  if (dubbingStatus !== "dubbed") {
5690
- throw new Error(`Dubbing job timed out or failed. Final status: ${dubbingStatus}`);
5763
+ throw new MuxAiError("Audio translation timed out or failed. Please try again.", { type: "timeout_error", retryable: true });
5691
5764
  }
5692
5765
  console.warn("\u2705 Dubbing completed successfully!");
5693
5766
  let presignedUrl;
@@ -5717,7 +5790,7 @@ async function translateAudio(assetId, toLanguageCode, options = {}) {
5717
5790
  });
5718
5791
  console.warn("\u2705 Dubbed audio downloaded successfully!");
5719
5792
  } catch (error) {
5720
- throw new Error(`Failed to download dubbed audio: ${error instanceof Error ? error.message : "Unknown error"}`);
5793
+ wrapError(error, "Failed to download dubbed audio");
5721
5794
  }
5722
5795
  console.warn("\u{1F4E4} Uploading dubbed audio to S3-compatible storage...");
5723
5796
  try {
@@ -5732,7 +5805,7 @@ async function translateAudio(assetId, toLanguageCode, options = {}) {
5732
5805
  s3SignedUrlExpirySeconds: options.s3SignedUrlExpirySeconds
5733
5806
  });
5734
5807
  } catch (error) {
5735
- throw new Error(`Failed to upload audio to S3: ${error instanceof Error ? error.message : "Unknown error"}`);
5808
+ wrapError(error, "Failed to upload audio to S3");
5736
5809
  }
5737
5810
  if (uploadToMux) {
5738
5811
  console.warn("\u{1F4F9} Adding dubbed audio track to Mux asset...");
@@ -6079,9 +6152,7 @@ async function translateChunkWithFallback({
6079
6152
  };
6080
6153
  } catch (error) {
6081
6154
  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
- );
6155
+ wrapError(error, `Chunk ${chunk.id} failed for ${Math.round(chunk.startTime)}s-${Math.round(chunk.endTime)}s`);
6085
6156
  }
6086
6157
  const [leftChunk, rightChunk] = splitTranslationChunkAtMidpoint(chunk);
6087
6158
  const [leftResult, rightResult] = await Promise.all([
@@ -6219,28 +6290,31 @@ async function translateCaptions(assetId, trackId, toLanguageCode, options) {
6219
6290
  provider
6220
6291
  });
6221
6292
  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.");
6293
+ 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
6294
  }
6224
6295
  const { asset: assetData, playbackId, policy } = await getPlaybackIdForAsset(assetId, credentials);
6225
6296
  const assetDurationSeconds = getAssetDurationSecondsFromAsset(assetData);
6226
6297
  const signingContext = await resolveMuxSigningContext(credentials);
6227
6298
  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."
6299
+ throw new MuxAiError(
6300
+ "Signed playback ID requires signing credentials. Set MUX_SIGNING_KEY and MUX_PRIVATE_KEY environment variables.",
6301
+ { type: "validation_error" }
6230
6302
  );
6231
6303
  }
6232
6304
  const readyTextTracks = getReadyTextTracks(assetData);
6233
6305
  const sourceTextTrack = readyTextTracks.find((t) => t.id === trackId);
6234
6306
  if (!sourceTextTrack) {
6235
6307
  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"}`
6308
+ throw new MuxAiError(
6309
+ `Track ${trackId} not found or not ready on asset ${assetId}. Available track IDs: ${availableTrackIds || "none"}.`,
6310
+ { type: "validation_error" }
6238
6311
  );
6239
6312
  }
6240
6313
  const fromLanguageCode = sourceTextTrack.language_code;
6241
6314
  if (!fromLanguageCode) {
6242
- throw new Error(
6243
- `Track '${trackId}' is missing language metadata (language_code). Cannot determine source language for translation.`
6315
+ throw new MuxAiError(
6316
+ `Track ${trackId} is missing language metadata. Cannot determine source language for translation.`,
6317
+ { type: "validation_error" }
6244
6318
  );
6245
6319
  }
6246
6320
  const vttUrl = await buildTranscriptUrl(playbackId, trackId, policy === "signed", credentials);
@@ -6248,7 +6322,7 @@ async function translateCaptions(assetId, trackId, toLanguageCode, options) {
6248
6322
  try {
6249
6323
  vttContent = await fetchVttFromMux(vttUrl);
6250
6324
  } catch (error) {
6251
- throw new Error(`Failed to fetch VTT content: ${error instanceof Error ? error.message : "Unknown error"}`);
6325
+ wrapError(error, "Failed to fetch VTT content");
6252
6326
  }
6253
6327
  let translatedVtt;
6254
6328
  let usage;
@@ -6266,7 +6340,7 @@ async function translateCaptions(assetId, trackId, toLanguageCode, options) {
6266
6340
  translatedVtt = result.translatedVtt;
6267
6341
  usage = result.usage;
6268
6342
  } catch (error) {
6269
- throw new Error(`Failed to translate VTT with ${modelConfig.provider}: ${error instanceof Error ? error.message : "Unknown error"}`);
6343
+ wrapError(error, `Failed to translate VTT with ${modelConfig.provider}`);
6270
6344
  }
6271
6345
  const usageWithMetadata = usage ? {
6272
6346
  ...usage,
@@ -6292,7 +6366,7 @@ async function translateCaptions(assetId, trackId, toLanguageCode, options) {
6292
6366
  s3SignedUrlExpirySeconds: options.s3SignedUrlExpirySeconds
6293
6367
  });
6294
6368
  } catch (error) {
6295
- throw new Error(`Failed to upload VTT to S3: ${error instanceof Error ? error.message : "Unknown error"}`);
6369
+ wrapError(error, "Failed to upload VTT to S3");
6296
6370
  }
6297
6371
  if (uploadToMux) {
6298
6372
  try {
@@ -6325,6 +6399,7 @@ async function translateCaptions(assetId, trackId, toLanguageCode, options) {
6325
6399
  };
6326
6400
  }
6327
6401
  export {
6402
+ MuxAiError,
6328
6403
  WorkflowStorageClient,
6329
6404
  createWorkflowStorageClient,
6330
6405
  decryptFromWorkflow,
@@ -6332,6 +6407,7 @@ export {
6332
6407
  primitives_exports as primitives,
6333
6408
  setWorkflowCredentialsProvider,
6334
6409
  version,
6335
- workflows_exports as workflows
6410
+ workflows_exports as workflows,
6411
+ wrapError
6336
6412
  };
6337
6413
  //# sourceMappingURL=index.js.map