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