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