@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-yn3fUDgv.d.ts → index-CXC5gAHs.d.ts} +2 -2
- package/dist/index.d.ts +40 -3
- package/dist/index.js +225 -149
- package/dist/index.js.map +1 -1
- package/dist/primitives/index.js +30 -10
- package/dist/primitives/index.js.map +1 -1
- package/dist/workflows/index.d.ts +1 -1
- package/dist/workflows/index.js +217 -144
- package/dist/workflows/index.js.map +1 -1
- package/package.json +1 -1
package/dist/workflows/index.js
CHANGED
|
@@ -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
|
|
729
|
-
"No public or signed playback ID found for this asset.
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">
|
|
797
|
+
const escapeXmlText = (value) => value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
777
798
|
const escapeXmlAttribute = (value) => escapeXmlText(value).replace(/"/g, """);
|
|
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
|
|
1286
|
-
`No
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
1527
|
-
const
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
const
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
${
|
|
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
|
-
|
|
1544
|
-
|
|
1545
|
-
return
|
|
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
|
|
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
|
|
1609
|
-
`Question at index ${idx} is invalid: must have non-empty
|
|
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
|
|
1626
|
-
|
|
1627
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
1725
|
-
`
|
|
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
|
|
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
|
|
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
|
|
2294
|
-
`No caption track found${languageCode ? ` for language
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
2869
|
-
`Track
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
3926
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
5108
|
+
throw new MuxAiError(`Failed to generate title for asset ${assetId}.`);
|
|
5038
5109
|
}
|
|
5039
5110
|
if (!analysisResponse.result.description) {
|
|
5040
|
-
throw new
|
|
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((
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
5450
|
+
wrapError(error, "Failed to check dubbing status");
|
|
5379
5451
|
}
|
|
5380
5452
|
}
|
|
5381
5453
|
if (dubbingStatus !== "dubbed") {
|
|
5382
|
-
throw new
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
5929
|
-
`Track
|
|
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
|
|
5935
|
-
`Track
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
6060
|
+
wrapError(error, "Failed to upload VTT to S3");
|
|
5988
6061
|
}
|
|
5989
6062
|
if (uploadToMux) {
|
|
5990
6063
|
try {
|