@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.
@@ -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 = "FatalError";
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) {
@@ -773,7 +794,7 @@ function renderSection(section) {
773
794
  throw new Error(`Invalid XML ${context} name: "${name}"`);
774
795
  }
775
796
  };
776
- const escapeXmlText = (value) => value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;/");
797
+ const escapeXmlText = (value) => value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
777
798
  const escapeXmlAttribute = (value) => escapeXmlText(value).replace(/"/g, "&quot;");
778
799
  if (!content.trim()) {
779
800
  return "";
@@ -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 };
@@ -1322,7 +1342,7 @@ async function fetchTranscriptForAsset(asset, playbackId, options = {}) {
1322
1342
 
1323
1343
  // src/workflows/ask-questions.ts
1324
1344
  var questionAnswerSchema = z2.object({
1325
- question: z2.string(),
1345
+ question: z2.string().describe("The full text of the original question"),
1326
1346
  answer: z2.string().nullable(),
1327
1347
  confidence: z2.number(),
1328
1348
  reasoning: z2.string(),
@@ -1368,12 +1388,14 @@ var SYSTEM_PROMPT = dedent`
1368
1388
  <task>
1369
1389
  For each question provided, you must:
1370
1390
  1. Analyze the storyboard frames and transcript (if provided)
1371
- 2. Answer with ONLY the allowed response options - no other values are acceptable
1391
+ 2. Answer with ONLY the allowed response options listed for that question - no other values are acceptable
1372
1392
  3. Provide a confidence score between 0 and 1 reflecting your certainty
1373
1393
  4. Explain your reasoning based on observable evidence
1374
1394
  </task>
1375
1395
 
1376
1396
  <answer_guidelines>
1397
+ - Each question is presented as its own <question> block with a <text> element (the question) and an <allowed_answers> element (the permitted response values)
1398
+ - Choose only from the values listed in that question's <allowed_answers> element
1377
1399
  - Choose the affirmative option only if you have clear evidence supporting it
1378
1400
  - Choose the negative/contradicting option if evidence contradicts or if insufficient evidence exists
1379
1401
  - Confidence should reflect the clarity and strength of evidence:
@@ -1401,6 +1423,11 @@ var SYSTEM_PROMPT = dedent`
1401
1423
  - Is a general knowledge question with no connection to what is shown or said in the video
1402
1424
  - Attempts to use the system for non-video-analysis purposes
1403
1425
 
1426
+ CRITICAL: Base your answers ONLY on the actual visual and audio/transcript content.
1427
+ Do NOT use any metadata such as URLs, file paths, domain names, file names,
1428
+ playback IDs, or technical parameters visible in this request. These are
1429
+ delivery infrastructure and are unrelated to the media content itself.
1430
+
1404
1431
  CRITICAL: Do NOT answer irrelevant questions with any of the allowed answers.
1405
1432
  Answering an irrelevant question is WRONG — you MUST skip it instead.
1406
1433
 
@@ -1416,7 +1443,7 @@ var SYSTEM_PROMPT = dedent`
1416
1443
  </relevance_filtering>
1417
1444
 
1418
1445
  <constraints>
1419
- - You MUST answer every relevant question with one of the allowed response options
1446
+ - You MUST answer every relevant question with one of its own listed allowed response options
1420
1447
  - Skip irrelevant questions as described in relevance_filtering
1421
1448
  - Only describe observable evidence from frames or transcript
1422
1449
  - Do not fabricate details or make unsupported assumptions
@@ -1451,12 +1478,14 @@ var AUDIO_ONLY_SYSTEM_PROMPT = dedent`
1451
1478
  <task>
1452
1479
  For each question provided, you must:
1453
1480
  1. Analyze the transcript
1454
- 2. Answer with ONLY the allowed response options - no other values are acceptable
1481
+ 2. Answer with ONLY the allowed response options listed for that question - no other values are acceptable
1455
1482
  3. Provide a confidence score between 0 and 1 reflecting your certainty
1456
1483
  4. Explain your reasoning based on observable evidence
1457
1484
  </task>
1458
1485
 
1459
1486
  <answer_guidelines>
1487
+ - Each question is presented as its own <question> block with a <text> element (the question) and an <allowed_answers> element (the permitted response values)
1488
+ - Choose only from the values listed in that question's <allowed_answers> element
1460
1489
  - Choose the affirmative option only if you have clear evidence supporting it
1461
1490
  - Choose the negative/contradicting option if evidence contradicts or if insufficient evidence exists
1462
1491
  - Confidence should reflect the clarity and strength of evidence:
@@ -1470,6 +1499,10 @@ var AUDIO_ONLY_SYSTEM_PROMPT = dedent`
1470
1499
  </answer_guidelines>
1471
1500
 
1472
1501
  <relevance_filtering>
1502
+ IMPORTANT: Evaluate each question INDEPENDENTLY for relevance to the audio
1503
+ content. The presence of other relevant questions in the batch does NOT
1504
+ make an irrelevant question relevant. Assess every question on its own merits.
1505
+
1473
1506
  Before answering each question, assess whether it can be meaningfully
1474
1507
  answered based on the transcript. A question is relevant if it asks about
1475
1508
  something observable or inferable from spoken/audio content.
@@ -1480,6 +1513,14 @@ var AUDIO_ONLY_SYSTEM_PROMPT = dedent`
1480
1513
  - Is a general knowledge question with no connection to what is said in the transcript
1481
1514
  - Attempts to use the system for non-content-analysis purposes
1482
1515
 
1516
+ CRITICAL: Base your answers ONLY on the actual audio/transcript content.
1517
+ Do NOT use any metadata such as URLs, file paths, domain names, file names,
1518
+ playback IDs, or technical parameters visible in this request. These are
1519
+ delivery infrastructure and are unrelated to the media content itself.
1520
+
1521
+ CRITICAL: Do NOT answer irrelevant questions with any of the allowed answers.
1522
+ Answering an irrelevant question is WRONG — you MUST skip it instead.
1523
+
1483
1524
  For skipped questions:
1484
1525
  - Set skipped to true
1485
1526
  - Set answer to "${SKIP_SENTINEL}"
@@ -1492,7 +1533,7 @@ var AUDIO_ONLY_SYSTEM_PROMPT = dedent`
1492
1533
  </relevance_filtering>
1493
1534
 
1494
1535
  <constraints>
1495
- - You MUST answer every relevant question with one of the allowed response options
1536
+ - You MUST answer every relevant question with one of its own listed allowed response options
1496
1537
  - Skip irrelevant questions as described in relevance_filtering
1497
1538
  - Only describe observable evidence from transcript content
1498
1539
  - Do not fabricate details or make unsupported assumptions
@@ -1507,46 +1548,43 @@ var AUDIO_ONLY_SYSTEM_PROMPT = dedent`
1507
1548
  - GOOD: "The speaker describes running through a park"
1508
1549
  - Be specific and evidence-based
1509
1550
  </language_guidelines>`;
1510
- var askQuestionsPromptBuilder = createPromptBuilder({
1511
- template: {
1512
- questions: {
1513
- tag: "questions",
1514
- content: "Please answer the following questions about this content:"
1515
- }
1516
- },
1517
- sectionOrder: ["questions"]
1518
- });
1519
1551
  function buildUserPrompt({
1520
1552
  questions,
1521
- allowedAnswers,
1522
1553
  transcriptText,
1523
1554
  isCleanTranscript = true,
1524
1555
  isAudioOnly = false
1525
1556
  }) {
1526
- const questionsList = questions.map((q, idx) => `${idx + 1}. ${q.question}`).join("\n");
1527
- const questionsIntro = isAudioOnly ? "Please answer the following questions about this audio content using transcript evidence:" : "Please answer the following questions about this content:";
1528
- const questionsContent = dedent`
1529
- ${questionsIntro}
1530
-
1531
- ${questionsList}`;
1532
- const answerList = allowedAnswers.map((answer) => `"${answer}"`).join(", ");
1533
- const responseOptions = dedent`
1534
- <response_options>
1535
- Allowed answers: ${answerList}
1536
- </response_options>`;
1537
- const questionsSection = askQuestionsPromptBuilder.build({ questions: questionsContent });
1538
- if (!transcriptText) {
1539
- return `${questionsSection}
1540
-
1541
- ${responseOptions}`;
1557
+ const contentDescriptor = isAudioOnly ? "audio content using transcript evidence" : "content";
1558
+ const taskContent = dedent`
1559
+ Answer each question in the <questions> block below about the ${contentDescriptor}.
1560
+ Use only the values listed inside each question's own <allowed_answers> element.
1561
+ Return one answer per question, in the order the questions appear.
1562
+ If a question cannot be answered from the provided content, skip it as described in the system instructions.`;
1563
+ const taskSection = `<task>
1564
+ ${taskContent}
1565
+ </task>`;
1566
+ const questionBlocks = questions.map((q, idx) => {
1567
+ const textTag = renderSection({ tag: "text", content: q.question });
1568
+ const answersTag = renderSection({
1569
+ tag: "allowed_answers",
1570
+ content: q.answerOptions.map((a) => `"${a}"`).join(", ")
1571
+ });
1572
+ return `<question number="${idx + 1}">
1573
+ ${textTag}
1574
+ ${answersTag}
1575
+ </question>`;
1576
+ }).join("\n\n");
1577
+ const questionsSection = `<questions>
1578
+ ${questionBlocks}
1579
+ </questions>`;
1580
+ const sections = [];
1581
+ if (transcriptText) {
1582
+ const format = isCleanTranscript ? "plain text" : "WebVTT";
1583
+ sections.push(renderSection(createTranscriptSection(transcriptText, format)));
1542
1584
  }
1543
- const format = isCleanTranscript ? "plain text" : "WebVTT";
1544
- const transcriptSection = renderSection(createTranscriptSection(transcriptText, format));
1545
- return `${transcriptSection}
1546
-
1547
- ${questionsSection}
1548
-
1549
- ${responseOptions}`;
1585
+ sections.push(taskSection);
1586
+ sections.push(questionsSection);
1587
+ return sections.join("\n\n");
1550
1588
  }
1551
1589
  async function fetchImageAsBase64(imageUrl, imageDownloadOptions) {
1552
1590
  "use step";
@@ -1601,12 +1639,13 @@ async function analyzeQuestions({
1601
1639
  async function askQuestions(assetId, questions, options) {
1602
1640
  "use workflow";
1603
1641
  if (!questions || questions.length === 0) {
1604
- throw new Error("At least one question must be provided");
1642
+ throw new MuxAiError("At least one question must be provided.", { type: "validation_error" });
1605
1643
  }
1606
1644
  questions.forEach((q, idx) => {
1607
1645
  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`
1646
+ throw new MuxAiError(
1647
+ `Question at index ${idx} is invalid: must have a non-empty question field.`,
1648
+ { type: "validation_error" }
1610
1649
  );
1611
1650
  }
1612
1651
  });
@@ -1614,7 +1653,6 @@ async function askQuestions(assetId, questions, options) {
1614
1653
  provider = "openai",
1615
1654
  model,
1616
1655
  languageCode,
1617
- answerOptions,
1618
1656
  includeTranscript = true,
1619
1657
  cleanTranscript = true,
1620
1658
  imageSubmissionMode = "url",
@@ -1622,15 +1660,23 @@ async function askQuestions(assetId, questions, options) {
1622
1660
  storyboardWidth = 640,
1623
1661
  credentials
1624
1662
  } = options ?? {};
1625
- const normalizedAnswerOptions = Array.from(
1626
- new Set(
1627
- (answerOptions?.length ? answerOptions : ["yes", "no"]).map((option) => option.trim()).filter(Boolean)
1628
- )
1663
+ const normalizedQuestions = questions.map((q, idx) => {
1664
+ const options2 = Array.from(
1665
+ new Set(
1666
+ (q.answerOptions?.length ? q.answerOptions : ["yes", "no"]).map((o) => o.trim()).filter(Boolean)
1667
+ )
1668
+ );
1669
+ if (!options2.length) {
1670
+ throw new MuxAiError(
1671
+ `Question at index ${idx} has invalid answerOptions: must include at least one non-empty value.`,
1672
+ { type: "validation_error" }
1673
+ );
1674
+ }
1675
+ return { ...q, answerOptions: options2 };
1676
+ });
1677
+ const allowedAnswers = Array.from(
1678
+ new Set(normalizedQuestions.flatMap((q) => q.answerOptions))
1629
1679
  );
1630
- if (!normalizedAnswerOptions.length) {
1631
- throw new Error("answerOptions must include at least one non-empty value");
1632
- }
1633
- const allowedAnswers = normalizedAnswerOptions;
1634
1680
  const modelConfig = resolveLanguageModelConfig({
1635
1681
  ...options,
1636
1682
  model,
@@ -1640,14 +1686,16 @@ async function askQuestions(assetId, questions, options) {
1640
1686
  const assetDurationSeconds = getAssetDurationSecondsFromAsset(assetData);
1641
1687
  const isAudioOnly = isAudioOnlyAsset(assetData);
1642
1688
  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)."
1689
+ throw new MuxAiError(
1690
+ "Audio-only assets require a transcript. Set includeTranscript: true and ensure the asset has a ready text track (captions/subtitles).",
1691
+ { type: "validation_error" }
1645
1692
  );
1646
1693
  }
1647
1694
  const signingContext = await resolveMuxSigningContext(credentials);
1648
1695
  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."
1696
+ throw new MuxAiError(
1697
+ "Signed playback ID requires signing credentials. Set MUX_SIGNING_KEY and MUX_PRIVATE_KEY environment variables.",
1698
+ { type: "validation_error" }
1651
1699
  );
1652
1700
  }
1653
1701
  const transcriptResult = includeTranscript ? await fetchTranscriptForAsset(assetData, playbackId, {
@@ -1659,8 +1707,7 @@ async function askQuestions(assetId, questions, options) {
1659
1707
  }) : void 0;
1660
1708
  const transcriptText = transcriptResult?.transcriptText ?? "";
1661
1709
  const userPrompt = buildUserPrompt({
1662
- questions,
1663
- allowedAnswers,
1710
+ questions: normalizedQuestions,
1664
1711
  transcriptText,
1665
1712
  isCleanTranscript: cleanTranscript,
1666
1713
  isAudioOnly
@@ -1713,22 +1760,25 @@ async function askQuestions(assetId, questions, options) {
1713
1760
  }
1714
1761
  } catch (error) {
1715
1762
  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
- );
1763
+ wrapError(error, `Failed to analyze ${contentType} questions with ${provider}`);
1719
1764
  }
1720
1765
  if (!analysisResponse.result?.answers) {
1721
- throw new Error(`Failed to get answers for asset ${assetId}`);
1766
+ throw new MuxAiError(`Failed to generate answers for asset ${assetId}.`);
1722
1767
  }
1723
1768
  if (analysisResponse.result.answers.length !== questions.length) {
1724
- throw new Error(
1725
- `Expected ${questions.length} answers but received ${analysisResponse.result.answers.length}`
1769
+ throw new MuxAiError(
1770
+ `Failed to generate answers for all questions for asset ${assetId}.`
1726
1771
  );
1727
1772
  }
1728
- const answers = analysisResponse.result.answers.map((raw) => {
1773
+ const answers = analysisResponse.result.answers.map((raw, idx) => {
1729
1774
  const isSkipped = raw.skipped || raw.answer === SKIP_SENTINEL;
1775
+ if (!isSkipped && !normalizedQuestions[idx].answerOptions.includes(raw.answer)) {
1776
+ throw new MuxAiError(
1777
+ `Answer "${raw.answer}" for question ${idx} is not in allowed options: ${normalizedQuestions[idx].answerOptions.join(", ")}`
1778
+ );
1779
+ }
1730
1780
  return {
1731
- // Strip numbering prefix (e.g., "1. " or "2. ") from questions
1781
+ // Strip numbering prefix (e.g. "1. ") if the LLM prepends one
1732
1782
  question: raw.question.replace(/^\d+\.\s*/, ""),
1733
1783
  confidence: isSkipped ? 0 : Math.min(1, Math.max(0, raw.confidence)),
1734
1784
  reasoning: raw.reasoning,
@@ -1796,6 +1846,9 @@ var SYSTEM_PROMPT2 = dedent2`
1796
1846
  <constraints>
1797
1847
  - Only classify as burned-in captions when evidence is clear across multiple frames
1798
1848
  - Base decisions on observable visual evidence
1849
+ - Do NOT use any metadata such as URLs, file paths, domain names, file names,
1850
+ playback IDs, or technical parameters visible in this request. These are
1851
+ delivery infrastructure and are unrelated to the media content itself.
1799
1852
  - Return structured data matching the requested schema
1800
1853
  </constraints>`;
1801
1854
  var burnedInCaptionsPromptBuilder = createPromptBuilder({
@@ -2276,8 +2329,9 @@ async function generateChapters(assetId, options = {}) {
2276
2329
  const isAudioOnly = isAudioOnlyAsset(assetData);
2277
2330
  const signingContext = await resolveMuxSigningContext(credentials);
2278
2331
  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."
2332
+ throw new MuxAiError(
2333
+ "Signed playback ID requires signing credentials. Set MUX_SIGNING_KEY and MUX_PRIVATE_KEY environment variables.",
2334
+ { type: "validation_error" }
2281
2335
  );
2282
2336
  }
2283
2337
  const readyTextTracks = getReadyTextTracks(assetData);
@@ -2290,14 +2344,15 @@ async function generateChapters(assetId, options = {}) {
2290
2344
  });
2291
2345
  if (!transcriptResult.track || !transcriptResult.transcriptText) {
2292
2346
  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"}`
2347
+ throw new MuxAiError(
2348
+ `No caption track found${languageCode ? ` for language ${languageCode}` : ""}. Available languages: ${availableLanguages || "none"}.`,
2349
+ { type: "validation_error" }
2295
2350
  );
2296
2351
  }
2297
2352
  const timestampedTranscript = extractTimestampedTranscript(transcriptResult.transcriptText);
2298
2353
  if (!timestampedTranscript) {
2299
2354
  const contentLabel = isAudioOnly ? "transcript" : "caption track";
2300
- throw new Error(`No usable content found in ${contentLabel}`);
2355
+ throw new MuxAiError(`No usable content found in ${contentLabel}.`, { type: "validation_error" });
2301
2356
  }
2302
2357
  const resolvedLanguageCode = outputLanguageCode && outputLanguageCode !== "auto" ? outputLanguageCode : transcriptResult.track?.language_code ?? languageCode;
2303
2358
  const languageName = resolvedLanguageCode ? getLanguageName(resolvedLanguageCode) : void 0;
@@ -2319,17 +2374,15 @@ async function generateChapters(assetId, options = {}) {
2319
2374
  credentials
2320
2375
  });
2321
2376
  } catch (error) {
2322
- throw new Error(
2323
- `Failed to generate chapters with ${provider}: ${error instanceof Error ? error.message : "Unknown error"}`
2324
- );
2377
+ wrapError(error, `Failed to generate chapters with ${provider}`);
2325
2378
  }
2326
2379
  if (!chaptersData || !chaptersData.chapters) {
2327
- throw new Error("No chapters generated from AI response");
2380
+ throw new MuxAiError(`Failed to generate chapters for asset ${assetId}.`);
2328
2381
  }
2329
2382
  const { chapters: chaptersPayload, usage } = chaptersData;
2330
2383
  const validChapters = chaptersPayload.chapters.filter((chapter) => typeof chapter.startTime === "number" && typeof chapter.title === "string").sort((a, b) => a.startTime - b.startTime);
2331
2384
  if (validChapters.length === 0) {
2332
- throw new Error("No valid chapters found in AI response");
2385
+ throw new MuxAiError(`Failed to generate valid chapters for asset ${assetId}.`);
2333
2386
  }
2334
2387
  if (validChapters[0].startTime !== 0) {
2335
2388
  validChapters[0].startTime = 0;
@@ -2835,10 +2888,10 @@ async function editCaptions(assetId, trackId, options) {
2835
2888
  const hasAutoCensor = !!autoCensorOption;
2836
2889
  const hasReplacements = !!replacementsOption && replacementsOption.length > 0;
2837
2890
  if (!hasAutoCensor && !hasReplacements) {
2838
- throw new Error("At least one of autoCensorProfanity or replacements must be provided.");
2891
+ throw new MuxAiError("At least one of autoCensorProfanity or replacements must be provided.", { type: "validation_error" });
2839
2892
  }
2840
2893
  if (autoCensorOption && !provider) {
2841
- throw new Error("provider is required when using autoCensorProfanity.");
2894
+ throw new MuxAiError("provider is required when using autoCensorProfanity.", { type: "validation_error" });
2842
2895
  }
2843
2896
  const deleteOriginal = deleteOriginalTrack !== false;
2844
2897
  const uploadToMux = uploadToMuxOption !== false;
@@ -2849,24 +2902,27 @@ async function editCaptions(assetId, trackId, options) {
2849
2902
  const s3AccessKeyId = env_default.S3_ACCESS_KEY_ID;
2850
2903
  const s3SecretAccessKey = env_default.S3_SECRET_ACCESS_KEY;
2851
2904
  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."
2905
+ throw new MuxAiError(
2906
+ "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.",
2907
+ { type: "validation_error" }
2854
2908
  );
2855
2909
  }
2856
2910
  const { asset: assetData, playbackId, policy } = await getPlaybackIdForAsset(assetId, credentials);
2857
2911
  const assetDurationSeconds = getAssetDurationSecondsFromAsset(assetData);
2858
2912
  const signingContext = await resolveMuxSigningContext(credentials);
2859
2913
  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."
2914
+ throw new MuxAiError(
2915
+ "Signed playback ID requires signing credentials. Set MUX_SIGNING_KEY and MUX_PRIVATE_KEY environment variables.",
2916
+ { type: "validation_error" }
2862
2917
  );
2863
2918
  }
2864
2919
  const readyTextTracks = getReadyTextTracks(assetData);
2865
2920
  const sourceTrack = readyTextTracks.find((t) => t.id === trackId);
2866
2921
  if (!sourceTrack) {
2867
2922
  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"}`
2923
+ throw new MuxAiError(
2924
+ `Track ${trackId} not found or not ready on asset ${assetId}. Available track IDs: ${availableTrackIds || "none"}.`,
2925
+ { type: "validation_error" }
2870
2926
  );
2871
2927
  }
2872
2928
  const vttUrl = await buildTranscriptUrl(playbackId, trackId, policy === "signed", credentials);
@@ -2874,7 +2930,7 @@ async function editCaptions(assetId, trackId, options) {
2874
2930
  try {
2875
2931
  vttContent = await fetchVttFromMux(vttUrl);
2876
2932
  } catch (error) {
2877
- throw new Error(`Failed to fetch VTT content: ${error instanceof Error ? error.message : "Unknown error"}`);
2933
+ wrapError(error, "Failed to fetch VTT content");
2878
2934
  }
2879
2935
  let editedVtt = vttContent;
2880
2936
  let totalReplacementCount = 0;
@@ -2884,7 +2940,7 @@ async function editCaptions(assetId, trackId, options) {
2884
2940
  const { mode = "blank", alwaysCensor = [], neverCensor = [] } = autoCensorOption;
2885
2941
  const plainText = extractTextFromVTT(vttContent);
2886
2942
  if (!plainText.trim()) {
2887
- throw new Error("Track transcript is empty; nothing to censor.");
2943
+ throw new MuxAiError("Track transcript is empty; nothing to censor.", { type: "validation_error" });
2888
2944
  }
2889
2945
  const modelConfig = resolveLanguageModelConfig({
2890
2946
  ...options,
@@ -2902,7 +2958,7 @@ async function editCaptions(assetId, trackId, options) {
2902
2958
  detectedProfanity = result.profanity;
2903
2959
  usage = result.usage;
2904
2960
  } catch (error) {
2905
- throw new Error(`Failed to detect profanity with ${modelConfig.provider}: ${error instanceof Error ? error.message : "Unknown error"}`);
2961
+ wrapError(error, `Failed to detect profanity with ${modelConfig.provider}`);
2906
2962
  }
2907
2963
  const finalProfanity = applyOverrideLists(detectedProfanity, alwaysCensor, neverCensor);
2908
2964
  const { censoredVtt, replacements: censorReplacements } = censorVttContent(editedVtt, finalProfanity, mode);
@@ -2938,7 +2994,7 @@ async function editCaptions(assetId, trackId, options) {
2938
2994
  s3SignedUrlExpirySeconds: options.s3SignedUrlExpirySeconds
2939
2995
  });
2940
2996
  } catch (error) {
2941
- throw new Error(`Failed to upload VTT to S3: ${error instanceof Error ? error.message : "Unknown error"}`);
2997
+ wrapError(error, "Failed to upload VTT to S3");
2942
2998
  }
2943
2999
  if (uploadToMux) {
2944
3000
  try {
@@ -3535,6 +3591,9 @@ var SYSTEM_PROMPT4 = dedent5`
3535
3591
  <constraints>
3536
3592
  - Only describe what you can see in images or read in transcripts
3537
3593
  - Do not fabricate details or make unsupported assumptions
3594
+ - Do NOT use any metadata such as URLs, file paths, domain names, file names,
3595
+ playback IDs, or technical parameters visible in this request. These are
3596
+ delivery infrastructure and are unrelated to the media content itself.
3538
3597
  - Correlate engagement scores with observable content
3539
3598
  - Return structured data matching the requested schema exactly
3540
3599
  - Each momentInsight MUST include the hotspotIndex from the input data
@@ -3575,6 +3634,9 @@ var AUDIO_ONLY_SYSTEM_PROMPT2 = dedent5`
3575
3634
  <constraints>
3576
3635
  - Only describe what you can read in the transcript
3577
3636
  - Do not fabricate details or make unsupported assumptions
3637
+ - Do NOT use any metadata such as URLs, file paths, domain names, file names,
3638
+ playback IDs, or technical parameters visible in this request. These are
3639
+ delivery infrastructure and are unrelated to the media content itself.
3578
3640
  - Correlate engagement scores with observable content
3579
3641
  - Return structured data matching the requested schema exactly
3580
3642
  - Each momentInsight MUST include the hotspotIndex from the input data
@@ -3785,7 +3847,7 @@ async function generateEngagementInsights(assetId, options = {}) {
3785
3847
  skipShots = false
3786
3848
  } = options;
3787
3849
  if (!Number.isInteger(hotspotLimit) || hotspotLimit < 1 || hotspotLimit > 10) {
3788
- throw new Error("hotspotLimit must be an integer between 1 and 10");
3850
+ throw new MuxAiError("hotspotLimit must be an integer between 1 and 10.", { type: "validation_error" });
3789
3851
  }
3790
3852
  const modelConfig = resolveLanguageModelConfig({
3791
3853
  ...options,
@@ -3795,13 +3857,14 @@ async function generateEngagementInsights(assetId, options = {}) {
3795
3857
  const { asset, playbackId, policy } = await getPlaybackIdForAsset(assetId, credentials);
3796
3858
  const assetDurationSeconds = getAssetDurationSecondsFromAsset(asset);
3797
3859
  if (!assetDurationSeconds) {
3798
- throw new Error(`Asset ${assetId} has no valid duration`);
3860
+ throw new MuxAiError(`Asset ${assetId} has no valid duration.`, { type: "validation_error" });
3799
3861
  }
3800
3862
  const audioOnly = isAudioOnlyAsset(asset);
3801
3863
  const signingContext = await resolveMuxSigningContext(credentials);
3802
3864
  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."
3865
+ throw new MuxAiError(
3866
+ "Signed playback ID requires signing credentials. Set MUX_SIGNING_KEY and MUX_PRIVATE_KEY environment variables.",
3867
+ { type: "validation_error" }
3805
3868
  );
3806
3869
  }
3807
3870
  const shouldSign = policy === "signed";
@@ -3901,7 +3964,7 @@ async function generateEngagementInsights(assetId, options = {}) {
3901
3964
  credentials
3902
3965
  );
3903
3966
  if (!aiInsights.momentInsights || aiInsights.momentInsights.length === 0) {
3904
- throw new Error("Failed to generate insights from AI response");
3967
+ throw new MuxAiError(`Failed to generate insights for asset ${assetId}.`);
3905
3968
  }
3906
3969
  const momentInsights = [];
3907
3970
  for (const aiMoment of aiInsights.momentInsights) {
@@ -3922,8 +3985,8 @@ async function generateEngagementInsights(assetId, options = {}) {
3922
3985
  });
3923
3986
  }
3924
3987
  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."
3988
+ throw new MuxAiError(
3989
+ `Failed to generate valid insights for asset ${assetId}.`
3927
3990
  );
3928
3991
  }
3929
3992
  const usageWithMetadata = {
@@ -4726,6 +4789,9 @@ var SYSTEM_PROMPT5 = dedent6`
4726
4789
  <constraints>
4727
4790
  - Only describe what is clearly observable in the frames or explicitly stated in the transcript
4728
4791
  - Do not fabricate details or make unsupported assumptions
4792
+ - Do NOT use any metadata such as URLs, file paths, domain names, file names,
4793
+ playback IDs, or technical parameters visible in this request. These are
4794
+ delivery infrastructure and are unrelated to the media content itself.
4729
4795
  - Return structured data matching the requested schema
4730
4796
  - Output only the JSON object; no markdown or extra text
4731
4797
  - When a <language> section is provided, all output text MUST be written in that language
@@ -4780,6 +4846,9 @@ var AUDIO_ONLY_SYSTEM_PROMPT3 = dedent6`
4780
4846
  <constraints>
4781
4847
  - Only describe what is explicitly stated or strongly implied in the transcript
4782
4848
  - Do not fabricate details or make unsupported assumptions
4849
+ - Do NOT use any metadata such as URLs, file paths, domain names, file names,
4850
+ playback IDs, or technical parameters visible in this request. These are
4851
+ delivery infrastructure and are unrelated to the media content itself.
4783
4852
  - Return structured data matching the requested schema
4784
4853
  - Focus entirely on audio/spoken content - there are no visual elements
4785
4854
  - Output only the JSON object; no markdown or extra text
@@ -4943,8 +5012,9 @@ async function getSummaryAndTags(assetId, options) {
4943
5012
  outputLanguageCode
4944
5013
  } = options ?? {};
4945
5014
  if (!VALID_TONES.includes(tone)) {
4946
- throw new Error(
4947
- `Invalid tone "${tone}". Valid tones are: ${VALID_TONES.join(", ")}`
5015
+ throw new MuxAiError(
5016
+ `Invalid tone "${tone}". Valid tones are: ${VALID_TONES.join(", ")}.`,
5017
+ { type: "validation_error" }
4948
5018
  );
4949
5019
  }
4950
5020
  const modelConfig = resolveLanguageModelConfig({
@@ -4957,14 +5027,16 @@ async function getSummaryAndTags(assetId, options) {
4957
5027
  const assetDurationSeconds = getAssetDurationSecondsFromAsset(assetData);
4958
5028
  const isAudioOnly = isAudioOnlyAsset(assetData);
4959
5029
  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)."
5030
+ throw new MuxAiError(
5031
+ "Audio-only assets require a transcript. Set includeTranscript: true and ensure the asset has a ready text track (captions/subtitles).",
5032
+ { type: "validation_error" }
4962
5033
  );
4963
5034
  }
4964
5035
  const signingContext = await resolveMuxSigningContext(workflowCredentials);
4965
5036
  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."
5037
+ throw new MuxAiError(
5038
+ "Signed playback ID requires signing credentials. Set MUX_SIGNING_KEY and MUX_PRIVATE_KEY environment variables.",
5039
+ { type: "validation_error" }
4968
5040
  );
4969
5041
  }
4970
5042
  const transcriptResult = includeTranscript ? await fetchTranscriptForAsset(assetData, playbackId, {
@@ -5026,18 +5098,17 @@ async function getSummaryAndTags(assetId, options) {
5026
5098
  }
5027
5099
  } catch (error) {
5028
5100
  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
- );
5101
+ wrapError(error, `Failed to analyze ${contentType} content with ${provider}`);
5032
5102
  }
5033
5103
  if (!analysisResponse.result) {
5034
- throw new Error(`Failed to analyze video content for asset ${assetId}`);
5104
+ const contentType = isAudioOnly ? "audio" : "video";
5105
+ throw new MuxAiError(`Failed to analyze ${contentType} content for asset ${assetId}.`);
5035
5106
  }
5036
5107
  if (!analysisResponse.result.title) {
5037
- throw new Error(`Failed to generate title for asset ${assetId}`);
5108
+ throw new MuxAiError(`Failed to generate title for asset ${assetId}.`);
5038
5109
  }
5039
5110
  if (!analysisResponse.result.description) {
5040
- throw new Error(`Failed to generate description for asset ${assetId}`);
5111
+ throw new MuxAiError(`Failed to generate description for asset ${assetId}.`);
5041
5112
  }
5042
5113
  return {
5043
5114
  assetId,
@@ -5096,12 +5167,11 @@ async function requestStaticRenditionCreation(assetId, credentials) {
5096
5167
  } catch (error) {
5097
5168
  const statusCode = error?.status ?? error?.statusCode;
5098
5169
  const messages = error?.error?.messages;
5099
- const alreadyDefined = messages?.some((message2) => message2.toLowerCase().includes("already defined")) ?? error?.message?.toLowerCase().includes("already defined");
5170
+ const alreadyDefined = messages?.some((message) => message.toLowerCase().includes("already defined")) ?? error?.message?.toLowerCase().includes("already defined");
5100
5171
  if (statusCode === 409 || alreadyDefined) {
5101
5172
  return;
5102
5173
  }
5103
- const message = error instanceof Error ? error.message : "Unknown error";
5104
- throw new Error(`Failed to request static rendition from Mux: ${message}`);
5174
+ wrapError(error, "Failed to request static rendition from Mux");
5105
5175
  }
5106
5176
  }
5107
5177
  async function waitForAudioStaticRendition({
@@ -5135,13 +5205,14 @@ async function waitForAudioStaticRendition({
5135
5205
  `\u231B Waiting for static rendition (attempt ${attempt}/${STATIC_RENDITION_MAX_ATTEMPTS}) \u2192 ${currentStatus}`
5136
5206
  );
5137
5207
  if (currentStatus === "errored") {
5138
- throw new Error(
5208
+ throw new MuxAiError(
5139
5209
  "Mux failed to create the static rendition for this asset. Please check the asset in the Mux dashboard."
5140
5210
  );
5141
5211
  }
5142
5212
  }
5143
- throw new Error(
5144
- "Timed out waiting for the static rendition to become ready. Please try again in a moment."
5213
+ throw new MuxAiError(
5214
+ "Timed out waiting for the static rendition to become ready. Please try again in a moment.",
5215
+ { type: "timeout_error", retryable: true }
5145
5216
  );
5146
5217
  }
5147
5218
  async function fetchAudioFromMux(audioUrl) {
@@ -5293,7 +5364,7 @@ async function translateAudio(assetId, toLanguageCode, options = {}) {
5293
5364
  credentials: providedCredentials
5294
5365
  } = options;
5295
5366
  if (provider !== "elevenlabs") {
5296
- throw new Error("Only ElevenLabs provider is currently supported for audio translation");
5367
+ throw new MuxAiError("Only ElevenLabs provider is currently supported for audio translation.", { type: "validation_error" });
5297
5368
  }
5298
5369
  const credentials = providedCredentials;
5299
5370
  const effectiveStorageAdapter = storageAdapter;
@@ -5305,7 +5376,7 @@ async function translateAudio(assetId, toLanguageCode, options = {}) {
5305
5376
  const s3AccessKeyId = env_default.S3_ACCESS_KEY_ID;
5306
5377
  const s3SecretAccessKey = env_default.S3_SECRET_ACCESS_KEY;
5307
5378
  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.");
5379
+ 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
5380
  }
5310
5381
  const { asset: initialAsset, playbackId, policy } = await getPlaybackIdForAsset(assetId, credentials);
5311
5382
  const assetDurationSeconds = getAssetDurationSecondsFromAsset(initialAsset);
@@ -5320,8 +5391,9 @@ async function translateAudio(assetId, toLanguageCode, options = {}) {
5320
5391
  }
5321
5392
  const audioRendition = getReadyAudioStaticRendition(currentAsset);
5322
5393
  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."
5394
+ throw new MuxAiError(
5395
+ "Unable to obtain an audio-only static rendition for this asset. Please verify static renditions are enabled in Mux.",
5396
+ { type: "validation_error" }
5325
5397
  );
5326
5398
  }
5327
5399
  let audioUrl = `https://stream.mux.com/${playbackId}/audio.m4a`;
@@ -5333,7 +5405,7 @@ async function translateAudio(assetId, toLanguageCode, options = {}) {
5333
5405
  try {
5334
5406
  audioBuffer = await fetchAudioFromMux(audioUrl);
5335
5407
  } catch (error) {
5336
- throw new Error(`Failed to fetch audio from Mux: ${error instanceof Error ? error.message : "Unknown error"}`);
5408
+ wrapError(error, "Failed to fetch audio from Mux");
5337
5409
  }
5338
5410
  console.warn("\u{1F399}\uFE0F Creating dubbing job in ElevenLabs...");
5339
5411
  const elevenLabsLangCode = toISO639_3(toLanguageCode);
@@ -5354,7 +5426,7 @@ async function translateAudio(assetId, toLanguageCode, options = {}) {
5354
5426
  });
5355
5427
  console.warn(`\u2705 Dubbing job created with ID: ${dubbingId}`);
5356
5428
  } catch (error) {
5357
- throw new Error(`Failed to create ElevenLabs dubbing job: ${error instanceof Error ? error.message : "Unknown error"}`);
5429
+ wrapError(error, "Failed to create ElevenLabs dubbing job");
5358
5430
  }
5359
5431
  console.warn("\u23F3 Waiting for dubbing to complete...");
5360
5432
  let dubbingStatus = "dubbing";
@@ -5375,11 +5447,11 @@ async function translateAudio(assetId, toLanguageCode, options = {}) {
5375
5447
  throw new Error("ElevenLabs dubbing job failed");
5376
5448
  }
5377
5449
  } catch (error) {
5378
- throw new Error(`Failed to check dubbing status: ${error instanceof Error ? error.message : "Unknown error"}`);
5450
+ wrapError(error, "Failed to check dubbing status");
5379
5451
  }
5380
5452
  }
5381
5453
  if (dubbingStatus !== "dubbed") {
5382
- throw new Error(`Dubbing job timed out or failed. Final status: ${dubbingStatus}`);
5454
+ throw new MuxAiError("Audio translation timed out or failed. Please try again.", { type: "timeout_error", retryable: true });
5383
5455
  }
5384
5456
  console.warn("\u2705 Dubbing completed successfully!");
5385
5457
  let presignedUrl;
@@ -5409,7 +5481,7 @@ async function translateAudio(assetId, toLanguageCode, options = {}) {
5409
5481
  });
5410
5482
  console.warn("\u2705 Dubbed audio downloaded successfully!");
5411
5483
  } catch (error) {
5412
- throw new Error(`Failed to download dubbed audio: ${error instanceof Error ? error.message : "Unknown error"}`);
5484
+ wrapError(error, "Failed to download dubbed audio");
5413
5485
  }
5414
5486
  console.warn("\u{1F4E4} Uploading dubbed audio to S3-compatible storage...");
5415
5487
  try {
@@ -5424,7 +5496,7 @@ async function translateAudio(assetId, toLanguageCode, options = {}) {
5424
5496
  s3SignedUrlExpirySeconds: options.s3SignedUrlExpirySeconds
5425
5497
  });
5426
5498
  } catch (error) {
5427
- throw new Error(`Failed to upload audio to S3: ${error instanceof Error ? error.message : "Unknown error"}`);
5499
+ wrapError(error, "Failed to upload audio to S3");
5428
5500
  }
5429
5501
  if (uploadToMux) {
5430
5502
  console.warn("\u{1F4F9} Adding dubbed audio track to Mux asset...");
@@ -5771,9 +5843,7 @@ async function translateChunkWithFallback({
5771
5843
  };
5772
5844
  } catch (error) {
5773
5845
  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
- );
5846
+ wrapError(error, `Chunk ${chunk.id} failed for ${Math.round(chunk.startTime)}s-${Math.round(chunk.endTime)}s`);
5777
5847
  }
5778
5848
  const [leftChunk, rightChunk] = splitTranslationChunkAtMidpoint(chunk);
5779
5849
  const [leftResult, rightResult] = await Promise.all([
@@ -5911,28 +5981,31 @@ async function translateCaptions(assetId, trackId, toLanguageCode, options) {
5911
5981
  provider
5912
5982
  });
5913
5983
  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.");
5984
+ 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
5985
  }
5916
5986
  const { asset: assetData, playbackId, policy } = await getPlaybackIdForAsset(assetId, credentials);
5917
5987
  const assetDurationSeconds = getAssetDurationSecondsFromAsset(assetData);
5918
5988
  const signingContext = await resolveMuxSigningContext(credentials);
5919
5989
  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."
5990
+ throw new MuxAiError(
5991
+ "Signed playback ID requires signing credentials. Set MUX_SIGNING_KEY and MUX_PRIVATE_KEY environment variables.",
5992
+ { type: "validation_error" }
5922
5993
  );
5923
5994
  }
5924
5995
  const readyTextTracks = getReadyTextTracks(assetData);
5925
5996
  const sourceTextTrack = readyTextTracks.find((t) => t.id === trackId);
5926
5997
  if (!sourceTextTrack) {
5927
5998
  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"}`
5999
+ throw new MuxAiError(
6000
+ `Track ${trackId} not found or not ready on asset ${assetId}. Available track IDs: ${availableTrackIds || "none"}.`,
6001
+ { type: "validation_error" }
5930
6002
  );
5931
6003
  }
5932
6004
  const fromLanguageCode = sourceTextTrack.language_code;
5933
6005
  if (!fromLanguageCode) {
5934
- throw new Error(
5935
- `Track '${trackId}' is missing language metadata (language_code). Cannot determine source language for translation.`
6006
+ throw new MuxAiError(
6007
+ `Track ${trackId} is missing language metadata. Cannot determine source language for translation.`,
6008
+ { type: "validation_error" }
5936
6009
  );
5937
6010
  }
5938
6011
  const vttUrl = await buildTranscriptUrl(playbackId, trackId, policy === "signed", credentials);
@@ -5940,7 +6013,7 @@ async function translateCaptions(assetId, trackId, toLanguageCode, options) {
5940
6013
  try {
5941
6014
  vttContent = await fetchVttFromMux(vttUrl);
5942
6015
  } catch (error) {
5943
- throw new Error(`Failed to fetch VTT content: ${error instanceof Error ? error.message : "Unknown error"}`);
6016
+ wrapError(error, "Failed to fetch VTT content");
5944
6017
  }
5945
6018
  let translatedVtt;
5946
6019
  let usage;
@@ -5958,7 +6031,7 @@ async function translateCaptions(assetId, trackId, toLanguageCode, options) {
5958
6031
  translatedVtt = result.translatedVtt;
5959
6032
  usage = result.usage;
5960
6033
  } catch (error) {
5961
- throw new Error(`Failed to translate VTT with ${modelConfig.provider}: ${error instanceof Error ? error.message : "Unknown error"}`);
6034
+ wrapError(error, `Failed to translate VTT with ${modelConfig.provider}`);
5962
6035
  }
5963
6036
  const usageWithMetadata = usage ? {
5964
6037
  ...usage,
@@ -5984,7 +6057,7 @@ async function translateCaptions(assetId, trackId, toLanguageCode, options) {
5984
6057
  s3SignedUrlExpirySeconds: options.s3SignedUrlExpirySeconds
5985
6058
  });
5986
6059
  } catch (error) {
5987
- throw new Error(`Failed to upload VTT to S3: ${error instanceof Error ? error.message : "Unknown error"}`);
6060
+ wrapError(error, "Failed to upload VTT to S3");
5988
6061
  }
5989
6062
  if (uploadToMux) {
5990
6063
  try {