@mux/ai 0.39.0 → 0.41.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -5,7 +5,7 @@ var __export = (target, all) => {
5
5
  };
6
6
 
7
7
  // package.json
8
- var version = "0.39.0";
8
+ var version = "0.41.0";
9
9
 
10
10
  // src/lib/language-codes.ts
11
11
  var ISO639_1_TO_3 = {
@@ -1113,6 +1113,234 @@ async function resolveMuxSigningContext(credentials) {
1113
1113
  return { keyId, keySecret };
1114
1114
  }
1115
1115
 
1116
+ // src/lib/mux-tracks.ts
1117
+ var MUX_TRACK_PASSTHROUGH_MAX_CHARS = 255;
1118
+ function normalizeTrackLanguageCode(code) {
1119
+ if (!code) {
1120
+ return void 0;
1121
+ }
1122
+ const primarySubtag = code.trim().toLowerCase().split(/[-_]/)[0];
1123
+ if (!primarySubtag || primarySubtag === "auto" || isUndeterminedLanguageCode(primarySubtag)) {
1124
+ return void 0;
1125
+ }
1126
+ return toISO639_1(primarySubtag);
1127
+ }
1128
+ function normalizeTrackName(name) {
1129
+ const normalized = name?.trim().toLowerCase();
1130
+ return normalized || void 0;
1131
+ }
1132
+ function isMuxGeneratedTextTrack(track) {
1133
+ return track.text_source === "generated_vod" || track.text_source === "generated_live" || track.text_source === "generated_live_final";
1134
+ }
1135
+ function targetType(target) {
1136
+ return target.type ?? "text";
1137
+ }
1138
+ function listTracks(asset, type) {
1139
+ return (asset.tracks ?? []).filter((track) => track.type === type && track.status !== "deleted" && !!track.id);
1140
+ }
1141
+ function summarizeTextTrack(track) {
1142
+ return {
1143
+ id: track.id,
1144
+ type: track.type === "audio" ? "audio" : "text",
1145
+ name: track.name,
1146
+ languageCode: track.language_code,
1147
+ status: track.status,
1148
+ textSource: track.text_source,
1149
+ passthrough: track.passthrough,
1150
+ ...track.type === "audio" ? { primary: track.primary === true } : {}
1151
+ };
1152
+ }
1153
+ function findSameLanguageTextTracks(asset, target) {
1154
+ const targetLanguage = normalizeTrackLanguageCode(target.languageCode);
1155
+ if (!targetLanguage) {
1156
+ return [];
1157
+ }
1158
+ const type = targetType(target);
1159
+ return listTracks(asset, type).filter(
1160
+ (track) => (type === "audio" || track.text_type === "subtitles") && normalizeTrackLanguageCode(track.language_code) === targetLanguage
1161
+ );
1162
+ }
1163
+ function findNameCollisionTextTracks(asset, target) {
1164
+ const targetName = normalizeTrackName(target.name);
1165
+ if (!targetName) {
1166
+ return [];
1167
+ }
1168
+ return listTracks(asset, targetType(target)).filter((track) => normalizeTrackName(track.name) === targetName);
1169
+ }
1170
+ function describeTracks(tracks) {
1171
+ return tracks.map((track) => `${track.name ?? "(unnamed)"} [${track.language_code ?? "?"}, ${track.type === "audio" ? track.primary ? "primary audio" : "audio" : track.text_source ?? "unknown source"}, ${track.status ?? "ready"}]`).join("; ");
1172
+ }
1173
+ function planTextTrackReplacement(asset, target, policy, options = {}) {
1174
+ const label = targetType(target) === "audio" ? "Audio" : "Text";
1175
+ const keep = new Set(options.keepTrackIds ?? []);
1176
+ const conflicts = /* @__PURE__ */ new Map();
1177
+ for (const track of [...findSameLanguageTextTracks(asset, target), ...findNameCollisionTextTracks(asset, target)]) {
1178
+ if (!keep.has(track.id)) {
1179
+ conflicts.set(track.id, track);
1180
+ }
1181
+ }
1182
+ const conflicting = [...conflicts.values()];
1183
+ if (conflicting.length === 0) {
1184
+ return { kind: "clear" };
1185
+ }
1186
+ if (policy === "fail") {
1187
+ return {
1188
+ kind: "blocked",
1189
+ reason: `${label} track(s) already exist for language '${target.languageCode}' or name '${target.name}': ${describeTracks(conflicting)}. Set replaceExistingTracks to replace them.`,
1190
+ tracks: conflicting.map(summarizeTextTrack)
1191
+ };
1192
+ }
1193
+ const primary = conflicting.filter((track) => track.type === "audio" && track.primary === true);
1194
+ if (primary.length > 0) {
1195
+ return {
1196
+ kind: "blocked",
1197
+ reason: `The asset's primary audio track is in the way for language '${target.languageCode}' or name '${target.name}': ${describeTracks(primary)}. Primary audio cannot be replaced; choose a different trackName.`,
1198
+ tracks: primary.map(summarizeTextTrack)
1199
+ };
1200
+ }
1201
+ if (policy === "replace_generated") {
1202
+ const notGenerated = conflicting.filter((track) => track.type !== "text" || !isMuxGeneratedTextTrack(track));
1203
+ if (notGenerated.length > 0) {
1204
+ return {
1205
+ kind: "blocked",
1206
+ reason: `${label} track(s) that are not Mux-generated exist for language '${target.languageCode}' or name '${target.name}': ${describeTracks(notGenerated)}. Use replaceExistingTracks: "replace_all" to replace them.`,
1207
+ tracks: notGenerated.map(summarizeTextTrack)
1208
+ };
1209
+ }
1210
+ }
1211
+ return { kind: "replace", toDelete: conflicting.map(summarizeTextTrack) };
1212
+ }
1213
+ function isDuplicateTrackNameError(error) {
1214
+ if (typeof error !== "object" || error === null) {
1215
+ return false;
1216
+ }
1217
+ const record = error;
1218
+ if (record.status !== 400) {
1219
+ return false;
1220
+ }
1221
+ const messages = Array.isArray(record.error?.messages) ? record.error.messages : [];
1222
+ const haystack = [...messages, record.message].filter((value) => typeof value === "string").map((value) => value.toLowerCase());
1223
+ return haystack.some((value) => value.includes("is not unique"));
1224
+ }
1225
+ function isNotFoundError(error) {
1226
+ return typeof error === "object" && error !== null && error.status === 404;
1227
+ }
1228
+ function buildMuxAiTrackPassthrough(workflow) {
1229
+ return JSON.stringify({ mux_ai: { workflow } });
1230
+ }
1231
+ function validateTrackPassthrough(passthrough) {
1232
+ if (passthrough !== void 0 && passthrough.length > MUX_TRACK_PASSTHROUGH_MAX_CHARS) {
1233
+ throw new MuxAiError(
1234
+ `Track passthrough must be at most ${MUX_TRACK_PASSTHROUGH_MAX_CHARS} characters (received ${passthrough.length}).`,
1235
+ { type: "validation_error" }
1236
+ );
1237
+ }
1238
+ return passthrough;
1239
+ }
1240
+ async function fetchVttFromMux(vttUrl) {
1241
+ "use step";
1242
+ const vttResponse = await fetch(vttUrl);
1243
+ if (!vttResponse.ok) {
1244
+ throw new Error(`Failed to fetch VTT file: ${vttResponse.statusText}`);
1245
+ }
1246
+ return vttResponse.text();
1247
+ }
1248
+ async function createTextTrackOnMux(assetId, languageCode, trackName, presignedUrl, credentials, options = {}) {
1249
+ "use step";
1250
+ const muxClient = await resolveMuxClient(credentials);
1251
+ const mux = await muxClient.createClient();
1252
+ const trackResponse = await mux.video.assets.createTrack(assetId, {
1253
+ type: "text",
1254
+ text_type: "subtitles",
1255
+ language_code: languageCode,
1256
+ name: trackName,
1257
+ url: presignedUrl,
1258
+ closed_captions: options.closedCaptions,
1259
+ passthrough: options.passthrough
1260
+ });
1261
+ if (!trackResponse.id) {
1262
+ throw new Error("Failed to create text track: no track ID returned from Mux");
1263
+ }
1264
+ return trackResponse.id;
1265
+ }
1266
+ async function replaceAndCreateTrack(input) {
1267
+ "use step";
1268
+ const muxClient = await resolveMuxClient(input.credentials);
1269
+ const mux = await muxClient.createClient();
1270
+ const type = targetType(input.target);
1271
+ const deleted = [];
1272
+ const failed = (error) => ({
1273
+ kind: "create_failed",
1274
+ reason: error instanceof Error ? error.message : String(error),
1275
+ deleted
1276
+ });
1277
+ const clearConflicts = async () => {
1278
+ let asset;
1279
+ try {
1280
+ asset = await mux.video.assets.retrieve(input.assetId);
1281
+ } catch (error) {
1282
+ if (deleted.length === 0) {
1283
+ throw error;
1284
+ }
1285
+ return failed(error);
1286
+ }
1287
+ const plan = planTextTrackReplacement(asset, input.target, input.policy, { keepTrackIds: input.keepTrackIds });
1288
+ if (plan.kind === "blocked") {
1289
+ return { ...plan, deleted };
1290
+ }
1291
+ if (plan.kind === "replace") {
1292
+ for (const track of plan.toDelete) {
1293
+ try {
1294
+ await mux.video.assets.deleteTrack(input.assetId, track.id);
1295
+ deleted.push(track);
1296
+ } catch (error) {
1297
+ if (!isNotFoundError(error)) {
1298
+ return failed(error);
1299
+ }
1300
+ }
1301
+ }
1302
+ }
1303
+ return void 0;
1304
+ };
1305
+ const create = async () => {
1306
+ const track = await mux.video.assets.createTrack(input.assetId, {
1307
+ type,
1308
+ ...type === "text" ? { text_type: "subtitles", closed_captions: input.closedCaptions } : {},
1309
+ language_code: input.target.languageCode,
1310
+ name: input.target.name.trim(),
1311
+ url: input.presignedUrl,
1312
+ passthrough: input.passthrough
1313
+ });
1314
+ if (!track.id) {
1315
+ throw new Error(`Failed to create ${type} track: no track ID returned from Mux`);
1316
+ }
1317
+ return track.id;
1318
+ };
1319
+ const stopped = await clearConflicts();
1320
+ if (stopped) {
1321
+ return stopped;
1322
+ }
1323
+ for (let attempt = 0; attempt < 2; attempt++) {
1324
+ try {
1325
+ return { kind: "created", trackId: await create(), deleted };
1326
+ } catch (error) {
1327
+ if (isDuplicateTrackNameError(error) && attempt === 0) {
1328
+ const stoppedOnRetry = await clearConflicts();
1329
+ if (stoppedOnRetry) {
1330
+ return stoppedOnRetry;
1331
+ }
1332
+ continue;
1333
+ }
1334
+ return failed(error);
1335
+ }
1336
+ }
1337
+ return { kind: "create_failed", reason: "Mux rejected the track name as not unique after retrying.", deleted };
1338
+ }
1339
+ async function replaceAndCreateTextTrack(input) {
1340
+ "use step";
1341
+ return replaceAndCreateTrack({ ...input, target: { ...input.target, type: "text" } });
1342
+ }
1343
+
1116
1344
  // node_modules/@workflow/serde/dist/index.js
1117
1345
  var WORKFLOW_SERIALIZE = /* @__PURE__ */ Symbol.for("workflow-serialize");
1118
1346
  var WORKFLOW_DESERIALIZE = /* @__PURE__ */ Symbol.for("workflow-deserialize");
@@ -3897,11 +4125,8 @@ async function analyzeQuestions({
3897
4125
  maxRetries: 0,
3898
4126
  output: Output.object({ schema: responseSchema }),
3899
4127
  experimental_telemetry: { isEnabled: true },
4128
+ system: systemPrompt,
3900
4129
  messages: [
3901
- {
3902
- role: "system",
3903
- content: systemPrompt
3904
- },
3905
4130
  {
3906
4131
  role: "user",
3907
4132
  content: imageDataUrl ? [
@@ -4315,11 +4540,8 @@ async function analyzeStoryboard({
4315
4540
  maxRetries: 0,
4316
4541
  output: Output2.object({ schema: burnedInCaptionsSchema }),
4317
4542
  experimental_telemetry: { isEnabled: true },
4543
+ system: systemPrompt,
4318
4544
  messages: [
4319
- {
4320
- role: "system",
4321
- content: systemPrompt
4322
- },
4323
4545
  {
4324
4546
  role: "user",
4325
4547
  content: [
@@ -4464,11 +4686,8 @@ async function generateChaptersWithAI({
4464
4686
  model,
4465
4687
  maxRetries: 0,
4466
4688
  output: Output3.object({ schema: chaptersSchema }),
4689
+ system: systemPrompt,
4467
4690
  messages: [
4468
- {
4469
- role: "system",
4470
- content: systemPrompt
4471
- },
4472
4691
  {
4473
4692
  role: "user",
4474
4693
  content: userPrompt
@@ -4756,32 +4975,6 @@ async function generateChaptersInternal(assetId, options, collectedUsage) {
4756
4975
  import { generateText as generateText4, Output as Output4 } from "ai";
4757
4976
  import { z as z6 } from "zod";
4758
4977
 
4759
- // src/lib/mux-tracks.ts
4760
- async function fetchVttFromMux(vttUrl) {
4761
- "use step";
4762
- const vttResponse = await fetch(vttUrl);
4763
- if (!vttResponse.ok) {
4764
- throw new Error(`Failed to fetch VTT file: ${vttResponse.statusText}`);
4765
- }
4766
- return vttResponse.text();
4767
- }
4768
- async function createTextTrackOnMux(assetId, languageCode, trackName, presignedUrl, credentials) {
4769
- "use step";
4770
- const muxClient = await resolveMuxClient(credentials);
4771
- const mux = await muxClient.createClient();
4772
- const trackResponse = await mux.video.assets.createTrack(assetId, {
4773
- type: "text",
4774
- text_type: "subtitles",
4775
- language_code: languageCode,
4776
- name: trackName,
4777
- url: presignedUrl
4778
- });
4779
- if (!trackResponse.id) {
4780
- throw new Error("Failed to create text track: no track ID returned from Mux");
4781
- }
4782
- return trackResponse.id;
4783
- }
4784
-
4785
4978
  // src/lib/storage-adapter.ts
4786
4979
  function requireCredentials(accessKeyId, secretAccessKey) {
4787
4980
  if (!accessKeyId || !secretAccessKey) {
@@ -5028,11 +5221,8 @@ async function identifyProfanityWithAI({
5028
5221
  model,
5029
5222
  maxRetries: 0,
5030
5223
  output: Output4.object({ schema: profanityDetectionSchema }),
5224
+ system: SYSTEM_PROMPT3,
5031
5225
  messages: [
5032
- {
5033
- role: "system",
5034
- content: SYSTEM_PROMPT3
5035
- },
5036
5226
  {
5037
5227
  role: "user",
5038
5228
  content: `Identify all profane words and phrases in the following subtitle transcript. Return each unique profane word or phrase exactly as it appears in the text.
@@ -5063,6 +5253,7 @@ async function uploadEditedVttToS3({
5063
5253
  editedVtt,
5064
5254
  assetId,
5065
5255
  trackId,
5256
+ variant = "edited",
5066
5257
  s3Endpoint,
5067
5258
  s3Region,
5068
5259
  s3Bucket,
@@ -5072,7 +5263,7 @@ async function uploadEditedVttToS3({
5072
5263
  "use step";
5073
5264
  const s3AccessKeyId = env_default.S3_ACCESS_KEY_ID;
5074
5265
  const s3SecretAccessKey = env_default.S3_SECRET_ACCESS_KEY;
5075
- const vttKey = `edited/${assetId}/${trackId}-edited-${Date.now()}.vtt`;
5266
+ const vttKey = `edited/${assetId}/${trackId}-${variant}-${Date.now()}.vtt`;
5076
5267
  await putObjectWithStorageAdapter({
5077
5268
  accessKeyId: s3AccessKeyId,
5078
5269
  secretAccessKey: s3SecretAccessKey,
@@ -5122,6 +5313,9 @@ async function editCaptionsInternal(assetId, trackId, options, collectedUsage) {
5122
5313
  s3Region: providedS3Region,
5123
5314
  s3Bucket: providedS3Bucket,
5124
5315
  trackNameSuffix,
5316
+ replaceExistingTracks: replaceExistingTracksOption,
5317
+ trackName: providedTrackName,
5318
+ trackPassthrough: providedTrackPassthrough,
5125
5319
  storageAdapter,
5126
5320
  credentials
5127
5321
  } = options;
@@ -5134,6 +5328,21 @@ async function editCaptionsInternal(assetId, trackId, options, collectedUsage) {
5134
5328
  if (autoCensorOption && !provider) {
5135
5329
  throw new MuxAiError("provider is required when using autoCensorProfanity.", { type: "validation_error" });
5136
5330
  }
5331
+ const legacyTrackNaming = deleteOriginalTrack !== void 0 || trackNameSuffix !== void 0;
5332
+ if (legacyTrackNaming && (replaceExistingTracksOption !== void 0 || providedTrackName !== void 0)) {
5333
+ throw new MuxAiError(
5334
+ "deleteOriginalTrack and trackNameSuffix are deprecated and cannot be combined with replaceExistingTracks or trackName.",
5335
+ { type: "validation_error" }
5336
+ );
5337
+ }
5338
+ const replaceExistingTracks = replaceExistingTracksOption ?? "replace_all";
5339
+ if (!legacyTrackNaming && replaceExistingTracks === "fail" && !providedTrackName) {
5340
+ throw new MuxAiError(
5341
+ `trackName is required when replaceExistingTracks is "fail": the edited track cannot reuse the source track's name while the source is kept.`,
5342
+ { type: "validation_error" }
5343
+ );
5344
+ }
5345
+ const trackPassthrough = validateTrackPassthrough(providedTrackPassthrough) ?? buildMuxAiTrackPassthrough("edit-captions");
5137
5346
  const deleteOriginal = deleteOriginalTrack !== false;
5138
5347
  const uploadToMux = uploadToMuxOption !== false;
5139
5348
  const uploadToS3 = uploadToS3Option || uploadToMux;
@@ -5176,6 +5385,25 @@ async function editCaptionsInternal(assetId, trackId, options, collectedUsage) {
5176
5385
  { type: "validation_error" }
5177
5386
  );
5178
5387
  }
5388
+ const sourceLanguageCode = sourceTrack.language_code || "en";
5389
+ const sourceName = sourceTrack.name || "Subtitles";
5390
+ const outputTrack = {
5391
+ languageCode: sourceLanguageCode,
5392
+ name: providedTrackName ?? sourceName
5393
+ };
5394
+ const keepTrackIds = replaceExistingTracks === "fail" ? [trackId] : [];
5395
+ if (replaceExistingTracks === "fail" && normalizeTrackName(outputTrack.name) === normalizeTrackName(sourceName)) {
5396
+ throw new MuxAiError(
5397
+ `trackName "${outputTrack.name}" matches the source track's name. Mux track names must be unique; choose a different trackName or use replaceExistingTracks: "replace_all" to replace the source.`,
5398
+ { type: "validation_error" }
5399
+ );
5400
+ }
5401
+ if (uploadToMux && !legacyTrackNaming) {
5402
+ const plan = planTextTrackReplacement(assetData, outputTrack, replaceExistingTracks, { keepTrackIds });
5403
+ if (plan.kind === "blocked") {
5404
+ throw new MuxAiError(plan.reason, { type: "validation_error" });
5405
+ }
5406
+ }
5179
5407
  const vttUrl = await buildTranscriptUrl(playbackId, trackId, policy === "signed", credentials);
5180
5408
  let vttContent;
5181
5409
  try {
@@ -5253,32 +5481,32 @@ async function editCaptionsInternal(assetId, trackId, options, collectedUsage) {
5253
5481
  } : void 0;
5254
5482
  let presignedUrl;
5255
5483
  let uploadedTrackId;
5484
+ let replacedTracks;
5256
5485
  if (uploadToS3) {
5486
+ const s3Config = {
5487
+ assetId,
5488
+ trackId,
5489
+ s3Endpoint,
5490
+ s3Region,
5491
+ s3Bucket,
5492
+ storageAdapter,
5493
+ s3SignedUrlExpirySeconds: options.s3SignedUrlExpirySeconds
5494
+ };
5257
5495
  try {
5258
- presignedUrl = await uploadEditedVttToS3({
5259
- editedVtt,
5260
- assetId,
5261
- trackId,
5262
- s3Endpoint,
5263
- s3Region,
5264
- s3Bucket,
5265
- storageAdapter,
5266
- s3SignedUrlExpirySeconds: options.s3SignedUrlExpirySeconds
5267
- });
5496
+ presignedUrl = await uploadEditedVttToS3({ editedVtt, ...s3Config });
5268
5497
  } catch (error) {
5269
5498
  wrapError(error, "Failed to upload VTT to S3");
5270
5499
  }
5271
- if (uploadToMux) {
5500
+ if (uploadToMux && legacyTrackNaming) {
5272
5501
  try {
5273
- const languageCode = sourceTrack.language_code || "en";
5274
- const suffix = trackNameSuffix ?? "edited";
5275
- const trackName = `${sourceTrack.name || "Subtitles"} (${suffix})`;
5502
+ const trackName = `${sourceName} (${trackNameSuffix ?? "edited"})`;
5276
5503
  uploadedTrackId = await createTextTrackOnMux(
5277
5504
  assetId,
5278
- languageCode,
5505
+ sourceLanguageCode,
5279
5506
  trackName,
5280
5507
  presignedUrl,
5281
- credentials
5508
+ credentials,
5509
+ { closedCaptions: sourceTrack.closed_captions, passthrough: trackPassthrough }
5282
5510
  );
5283
5511
  } catch (error) {
5284
5512
  wrapError(error, "Failed to add edited track to Mux asset");
@@ -5290,6 +5518,48 @@ async function editCaptionsInternal(assetId, trackId, options, collectedUsage) {
5290
5518
  wrapError(error, "Failed to delete original track");
5291
5519
  }
5292
5520
  }
5521
+ } else if (uploadToMux) {
5522
+ let outcome;
5523
+ try {
5524
+ outcome = await replaceAndCreateTextTrack({
5525
+ assetId,
5526
+ target: outputTrack,
5527
+ policy: replaceExistingTracks,
5528
+ presignedUrl,
5529
+ closedCaptions: sourceTrack.closed_captions,
5530
+ passthrough: trackPassthrough,
5531
+ keepTrackIds,
5532
+ credentials
5533
+ });
5534
+ } catch (error) {
5535
+ wrapError(error, "Failed to add edited track to Mux asset");
5536
+ }
5537
+ if (outcome.kind !== "created") {
5538
+ const sourceWasDeleted = outcome.deleted.some((track) => track.id === trackId);
5539
+ let restoreNote = "";
5540
+ if (sourceWasDeleted) {
5541
+ try {
5542
+ const originalUrl = await uploadEditedVttToS3({ editedVtt: vttContent, variant: "original", ...s3Config });
5543
+ const restoredTrackId = await createTextTrackOnMux(
5544
+ assetId,
5545
+ sourceLanguageCode,
5546
+ sourceName,
5547
+ originalUrl,
5548
+ credentials,
5549
+ { closedCaptions: sourceTrack.closed_captions, passthrough: sourceTrack.passthrough }
5550
+ );
5551
+ restoreNote = ` The source track was restored as ${restoredTrackId}.`;
5552
+ } catch (restoreError) {
5553
+ restoreNote = ` Restoring the source track also failed: ${restoreError instanceof Error ? restoreError.message : String(restoreError)}.`;
5554
+ }
5555
+ }
5556
+ if (outcome.kind === "blocked") {
5557
+ throw new MuxAiError(`${outcome.reason}${restoreNote}`, { type: "validation_error" });
5558
+ }
5559
+ throw new Error(`Failed to add edited track to Mux asset: ${outcome.reason}.${restoreNote}`);
5560
+ }
5561
+ uploadedTrackId = outcome.trackId;
5562
+ replacedTracks = outcome.deleted;
5293
5563
  }
5294
5564
  }
5295
5565
  return {
@@ -5302,6 +5572,7 @@ async function editCaptionsInternal(assetId, trackId, options, collectedUsage) {
5302
5572
  replacements: replacementsResult,
5303
5573
  speakerReplacements: speakerReplacementsResult,
5304
5574
  uploadedTrackId,
5575
+ replacedTracks,
5305
5576
  presignedUrl,
5306
5577
  usage: usageWithMetadata,
5307
5578
  safety: safety.report()
@@ -5761,11 +6032,8 @@ async function generateInsightsWithAI(provider, modelId, systemPrompt, userPromp
5761
6032
  maxRetries: 0,
5762
6033
  output: Output5.object({ schema: engagementInsightsSchema }),
5763
6034
  experimental_telemetry: { isEnabled: true },
6035
+ system: systemPrompt,
5764
6036
  messages: [
5765
- {
5766
- role: "system",
5767
- content: systemPrompt
5768
- },
5769
6037
  {
5770
6038
  role: "user",
5771
6039
  content: [
@@ -7027,11 +7295,8 @@ async function analyzeStoryboard2(imageDataUrl, provider, modelId, userPrompt, s
7027
7295
  description: "Structured summary with title, description, and keywords.",
7028
7296
  schema
7029
7297
  }),
7298
+ system: systemPrompt,
7030
7299
  messages: [
7031
- {
7032
- role: "system",
7033
- content: systemPrompt
7034
- },
7035
7300
  {
7036
7301
  role: "user",
7037
7302
  content: [
@@ -7075,11 +7340,8 @@ async function analyzeAudioOnly(provider, modelId, userPrompt, systemPrompt, des
7075
7340
  description: "Structured summary with title, description, and keywords.",
7076
7341
  schema
7077
7342
  }),
7343
+ system: systemPrompt,
7078
7344
  messages: [
7079
- {
7080
- role: "system",
7081
- content: systemPrompt
7082
- },
7083
7345
  {
7084
7346
  role: "user",
7085
7347
  content: userPrompt
@@ -7571,23 +7833,6 @@ async function downloadAndUploadDubTranscript({
7571
7833
  console.warn(`\u2705 Dub transcript uploaded successfully to: ${vttKey}`);
7572
7834
  return presignedUrl;
7573
7835
  }
7574
- async function createAudioTrackOnMux(assetId, languageCode, presignedUrl, credentials) {
7575
- "use step";
7576
- const muxClient = await resolveMuxClient(credentials);
7577
- const mux = await muxClient.createClient();
7578
- const languageName = new Intl.DisplayNames(["en"], { type: "language" }).of(languageCode) || languageCode.toUpperCase();
7579
- const trackName = `${languageName} (auto-dubbed)`;
7580
- const trackResponse = await mux.video.assets.createTrack(assetId, {
7581
- type: "audio",
7582
- language_code: languageCode,
7583
- name: trackName,
7584
- url: presignedUrl
7585
- });
7586
- if (!trackResponse.id) {
7587
- throw new Error("Failed to create audio track: no track ID returned from Mux");
7588
- }
7589
- return trackResponse.id;
7590
- }
7591
7836
  async function translateAudio(assetId, toLanguageCode, options = {}) {
7592
7837
  "use workflow";
7593
7838
  const {
@@ -7600,15 +7845,23 @@ async function translateAudio(assetId, toLanguageCode, options = {}) {
7600
7845
  uploadCaptionsToMux = false,
7601
7846
  staticRenditionCleanup: staticRenditionCleanupOption = "delete",
7602
7847
  storageAdapter,
7603
- credentials: providedCredentials
7848
+ credentials: providedCredentials,
7849
+ replaceExistingTracks = "fail",
7850
+ trackName: providedTrackName,
7851
+ trackPassthrough: providedTrackPassthrough
7604
7852
  } = options;
7605
7853
  if (provider !== "elevenlabs") {
7606
7854
  throw new MuxAiError("Only ElevenLabs provider is currently supported for audio translation.", { type: "validation_error" });
7607
7855
  }
7608
7856
  const credentials = providedCredentials;
7609
7857
  const effectiveStorageAdapter = storageAdapter;
7858
+ const trackPassthrough = validateTrackPassthrough(providedTrackPassthrough) ?? buildMuxAiTrackPassthrough("translate-audio");
7610
7859
  const uploadToMux = uploadToMuxOption !== false;
7611
7860
  const uploadToS3 = uploadToS3Option || uploadToMux || uploadCaptionsToMux;
7861
+ const muxLangCode = toISO639_1(toLanguageCode);
7862
+ const trackName = providedTrackName ?? `${getLanguageName(muxLangCode) ?? muxLangCode.toUpperCase()} (Auto-dubbed)`;
7863
+ const audioTrackTarget = { type: "audio", languageCode: muxLangCode, name: trackName };
7864
+ const captionsTrackTarget = { type: "text", languageCode: muxLangCode, name: trackName };
7612
7865
  const s3Endpoint = options.s3Endpoint ?? env_default.S3_ENDPOINT;
7613
7866
  const s3Region = options.s3Region ?? env_default.S3_REGION ?? "auto";
7614
7867
  const s3Bucket = options.s3Bucket ?? env_default.S3_BUCKET;
@@ -7619,6 +7872,15 @@ async function translateAudio(assetId, toLanguageCode, options = {}) {
7619
7872
  }
7620
7873
  const { asset: initialAsset, playbackId, policy } = await getPlaybackIdForAsset(assetId, credentials, options.assetSnapshot);
7621
7874
  const assetDurationSeconds = getAssetDurationSecondsFromAsset(initialAsset);
7875
+ for (const target of [
7876
+ ...uploadToMux ? [audioTrackTarget] : [],
7877
+ ...uploadCaptionsToMux ? [captionsTrackTarget] : []
7878
+ ]) {
7879
+ const plan = planTextTrackReplacement(initialAsset, target, replaceExistingTracks);
7880
+ if (plan.kind === "blocked") {
7881
+ throw new MuxAiError(plan.reason, { type: "validation_error" });
7882
+ }
7883
+ }
7622
7884
  let currentAsset = initialAsset;
7623
7885
  let createdStaticRenditionId;
7624
7886
  if (!hasReadyAudioStaticRendition(currentAsset)) {
@@ -7634,6 +7896,7 @@ async function translateAudio(assetId, toLanguageCode, options = {}) {
7634
7896
  let dubbingId;
7635
7897
  let presignedUrl;
7636
7898
  let uploadedTrackId;
7899
+ let replacedTracks;
7637
7900
  let captionsPresignedUrl;
7638
7901
  let captionsTrackId;
7639
7902
  try {
@@ -7724,18 +7987,29 @@ async function translateAudio(assetId, toLanguageCode, options = {}) {
7724
7987
  }
7725
7988
  if (uploadToMux) {
7726
7989
  console.warn("\u{1F4F9} Adding dubbed audio track to Mux asset...");
7727
- const muxLangCode = toISO639_1(toLanguageCode);
7990
+ let outcome;
7728
7991
  try {
7729
- uploadedTrackId = await createAudioTrackOnMux(assetId, muxLangCode, presignedUrl, credentials);
7730
- const languageName = new Intl.DisplayNames(["en"], { type: "language" }).of(muxLangCode) || muxLangCode.toUpperCase();
7731
- const trackName = `${languageName} (auto-dubbed)`;
7732
- console.warn(`\u2705 Track added to Mux asset with ID: ${uploadedTrackId}`);
7733
- console.warn(`\u{1F4CB} Track name: "${trackName}"`);
7992
+ outcome = await replaceAndCreateTrack({
7993
+ assetId,
7994
+ target: audioTrackTarget,
7995
+ policy: replaceExistingTracks,
7996
+ presignedUrl,
7997
+ passthrough: trackPassthrough,
7998
+ credentials
7999
+ });
7734
8000
  } catch (error) {
7735
- console.warn(`\u26A0\uFE0F Failed to add audio track to Mux asset: ${error instanceof Error ? error.message : "Unknown error"}`);
7736
- console.warn("\u{1F517} You can manually add the track using this presigned URL:");
7737
- console.warn(presignedUrl);
8001
+ wrapError(error, `Failed to add dubbed audio track to Mux asset (dubbed audio is staged at: ${presignedUrl})`);
8002
+ }
8003
+ if (outcome.kind === "blocked") {
8004
+ throw new MuxAiError(`${outcome.reason} Dubbed audio is staged at: ${presignedUrl}`, { type: "validation_error" });
8005
+ }
8006
+ if (outcome.kind === "create_failed") {
8007
+ throw new Error(`Failed to add dubbed audio track to Mux asset: ${outcome.reason}. Dubbed audio is staged at: ${presignedUrl}`);
7738
8008
  }
8009
+ uploadedTrackId = outcome.trackId;
8010
+ replacedTracks = outcome.deleted;
8011
+ console.warn(`\u2705 Track added to Mux asset with ID: ${uploadedTrackId}`);
8012
+ console.warn(`\u{1F4CB} Track name: "${trackName}"`);
7739
8013
  }
7740
8014
  try {
7741
8015
  console.warn("\u{1F4E5} Downloading dub transcript from ElevenLabs and staging to S3...");
@@ -7753,15 +8027,21 @@ async function translateAudio(assetId, toLanguageCode, options = {}) {
7753
8027
  });
7754
8028
  if (uploadCaptionsToMux) {
7755
8029
  console.warn("\u{1F4F9} Adding dubbed captions track to Mux asset...");
7756
- const muxLangCode = toISO639_1(toLanguageCode);
7757
- const languageName = new Intl.DisplayNames(["en"], { type: "language" }).of(muxLangCode) || muxLangCode.toUpperCase();
7758
- captionsTrackId = await createTextTrackOnMux(
8030
+ const captionsOutcome = await replaceAndCreateTrack({
7759
8031
  assetId,
7760
- muxLangCode,
7761
- `${languageName} (auto-dubbed)`,
7762
- captionsPresignedUrl,
8032
+ target: captionsTrackTarget,
8033
+ policy: replaceExistingTracks,
8034
+ presignedUrl: captionsPresignedUrl,
8035
+ passthrough: trackPassthrough,
7763
8036
  credentials
7764
- );
8037
+ });
8038
+ if (captionsOutcome.deleted.length > 0) {
8039
+ replacedTracks = [...replacedTracks ?? [], ...captionsOutcome.deleted];
8040
+ }
8041
+ if (captionsOutcome.kind !== "created") {
8042
+ throw new Error(captionsOutcome.reason);
8043
+ }
8044
+ captionsTrackId = captionsOutcome.trackId;
7765
8045
  console.warn(`\u2705 Captions track added to Mux asset with ID: ${captionsTrackId}`);
7766
8046
  }
7767
8047
  } catch (error) {
@@ -7792,6 +8072,7 @@ async function translateAudio(assetId, toLanguageCode, options = {}) {
7792
8072
  dubbingId,
7793
8073
  uploadedTrackId,
7794
8074
  presignedUrl,
8075
+ replacedTracks,
7795
8076
  captionsTrackId,
7796
8077
  captionsPresignedUrl,
7797
8078
  createdStaticRenditionId,
@@ -8143,11 +8424,8 @@ async function translateVttWithAI({
8143
8424
  model,
8144
8425
  maxRetries: 0,
8145
8426
  output: Output7.object({ schema: translationSchema }),
8427
+ system: SYSTEM_PROMPT6,
8146
8428
  messages: [
8147
- {
8148
- role: "system",
8149
- content: SYSTEM_PROMPT6
8150
- },
8151
8429
  {
8152
8430
  role: "user",
8153
8431
  content: `Translate from ${fromLanguageCode} to ${toLanguageCode}:${buildNeverTranslateSection(neverTranslate)}
@@ -8211,11 +8489,8 @@ async function translateCueChunkWithAI({
8211
8489
  model,
8212
8490
  maxRetries: 0,
8213
8491
  output: Output7.object({ schema }),
8492
+ system: CUE_TRANSLATION_SYSTEM_PROMPT,
8214
8493
  messages: [
8215
- {
8216
- role: "system",
8217
- content: CUE_TRANSLATION_SYSTEM_PROMPT
8218
- },
8219
8494
  {
8220
8495
  role: "user",
8221
8496
  content: `Translate from ${fromLanguageCode} to ${toLanguageCode}.
@@ -8467,11 +8742,15 @@ async function translateCaptionsInternal(assetId, trackId, toLanguageCode, optio
8467
8742
  storageAdapter,
8468
8743
  credentials: providedCredentials,
8469
8744
  chunking,
8470
- neverTranslate: neverTranslateOption
8745
+ neverTranslate: neverTranslateOption,
8746
+ replaceExistingTracks = "fail",
8747
+ trackName: providedTrackName,
8748
+ trackPassthrough: providedTrackPassthrough
8471
8749
  } = options;
8472
8750
  const credentials = providedCredentials;
8473
8751
  const effectiveStorageAdapter = storageAdapter;
8474
8752
  const neverTranslateTerms = neverTranslateOption ? validateNeverTranslateTerms(neverTranslateOption) : [];
8753
+ const trackPassthrough = validateTrackPassthrough(providedTrackPassthrough) ?? buildMuxAiTrackPassthrough("translate-captions");
8475
8754
  const s3Endpoint = providedS3Endpoint ?? env_default.S3_ENDPOINT;
8476
8755
  const s3Region = providedS3Region ?? env_default.S3_REGION ?? "auto";
8477
8756
  const s3Bucket = providedS3Bucket ?? env_default.S3_BUCKET;
@@ -8512,6 +8791,16 @@ async function translateCaptionsInternal(assetId, trackId, toLanguageCode, optio
8512
8791
  { type: "validation_error" }
8513
8792
  );
8514
8793
  }
8794
+ const outputTrack = {
8795
+ languageCode: toLanguageCode,
8796
+ name: providedTrackName ?? `${getLanguageName(toLanguageCode) ?? toLanguageCode.toUpperCase()} (Auto-translated)`
8797
+ };
8798
+ if (uploadToMux) {
8799
+ const plan = planTextTrackReplacement(assetData, outputTrack, replaceExistingTracks);
8800
+ if (plan.kind === "blocked") {
8801
+ throw new MuxAiError(plan.reason, { type: "validation_error" });
8802
+ }
8803
+ }
8515
8804
  const vttUrl = await buildTranscriptUrl(playbackId, trackId, policy === "signed", credentials);
8516
8805
  let vttContent;
8517
8806
  try {
@@ -8579,6 +8868,7 @@ async function translateCaptionsInternal(assetId, trackId, toLanguageCode, optio
8579
8868
  const targetLanguage = getLanguageCodePair(toLanguageCode);
8580
8869
  let presignedUrl;
8581
8870
  let uploadedTrackId;
8871
+ let replacedTracks;
8582
8872
  if (uploadToS3) {
8583
8873
  try {
8584
8874
  presignedUrl = await uploadVttToS3({
@@ -8596,19 +8886,28 @@ async function translateCaptionsInternal(assetId, trackId, toLanguageCode, optio
8596
8886
  wrapError(error, "Failed to upload VTT to S3");
8597
8887
  }
8598
8888
  if (uploadToMux) {
8889
+ let outcome;
8599
8890
  try {
8600
- const languageName = getLanguageName(toLanguageCode) ?? toLanguageCode.toUpperCase();
8601
- const trackName = `${languageName} (auto-translated)`;
8602
- uploadedTrackId = await createTextTrackOnMux(
8891
+ outcome = await replaceAndCreateTextTrack({
8603
8892
  assetId,
8604
- toLanguageCode,
8605
- trackName,
8893
+ target: outputTrack,
8894
+ policy: replaceExistingTracks,
8606
8895
  presignedUrl,
8896
+ passthrough: trackPassthrough,
8607
8897
  credentials
8608
- );
8898
+ });
8609
8899
  } catch (error) {
8610
- console.warn(`Failed to add track to Mux asset: ${error instanceof Error ? error.message : "Unknown error"}`);
8900
+ wrapError(error, "Failed to add translated track to Mux asset");
8901
+ }
8902
+ if (outcome.kind !== "created") {
8903
+ const deletedNote = outcome.deleted.length > 0 ? ` Tracks already deleted before the failure: ${outcome.deleted.map((track) => `${track.name ?? track.id} (${track.id})`).join(", ")}.` : "";
8904
+ if (outcome.kind === "blocked") {
8905
+ throw new MuxAiError(`${outcome.reason}${deletedNote}`, { type: "validation_error" });
8906
+ }
8907
+ throw new Error(`Failed to add translated track to Mux asset: ${outcome.reason}.${deletedNote}`);
8611
8908
  }
8909
+ uploadedTrackId = outcome.trackId;
8910
+ replacedTracks = outcome.deleted;
8612
8911
  }
8613
8912
  }
8614
8913
  return {
@@ -8621,6 +8920,7 @@ async function translateCaptionsInternal(assetId, trackId, toLanguageCode, optio
8621
8920
  originalVtt: vttContent,
8622
8921
  translatedVtt,
8623
8922
  uploadedTrackId,
8923
+ replacedTracks,
8624
8924
  presignedUrl,
8625
8925
  usage: usageWithMetadata,
8626
8926
  safety,
@@ -8628,20 +8928,33 @@ async function translateCaptionsInternal(assetId, trackId, toLanguageCode, optio
8628
8928
  };
8629
8929
  }
8630
8930
  export {
8931
+ MUX_TRACK_PASSTHROUGH_MAX_CHARS,
8631
8932
  MuxAiError,
8632
8933
  WorkflowStorageClient,
8934
+ buildMuxAiTrackPassthrough,
8633
8935
  createWorkflowStorageClient,
8634
8936
  decryptFromWorkflow,
8635
8937
  encryptForWorkflow,
8938
+ findNameCollisionTextTracks,
8939
+ findSameLanguageTextTracks,
8636
8940
  getLanguageCodePair,
8637
8941
  getLanguageName,
8942
+ isDuplicateTrackNameError,
8943
+ isMuxGeneratedTextTrack,
8638
8944
  isUndeterminedLanguageCode,
8639
8945
  isValidISO639_1,
8640
8946
  isValidISO639_3,
8947
+ normalizeTrackLanguageCode,
8948
+ normalizeTrackName,
8949
+ planTextTrackReplacement,
8641
8950
  primitives_exports as primitives,
8951
+ replaceAndCreateTextTrack,
8952
+ replaceAndCreateTrack,
8642
8953
  setWorkflowCredentialsProvider,
8954
+ summarizeTextTrack,
8643
8955
  toISO639_1,
8644
8956
  toISO639_3,
8957
+ validateTrackPassthrough,
8645
8958
  version,
8646
8959
  workflows_exports as workflows,
8647
8960
  wrapError