@mux/ai 0.40.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.40.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");
@@ -4747,32 +4975,6 @@ async function generateChaptersInternal(assetId, options, collectedUsage) {
4747
4975
  import { generateText as generateText4, Output as Output4 } from "ai";
4748
4976
  import { z as z6 } from "zod";
4749
4977
 
4750
- // src/lib/mux-tracks.ts
4751
- async function fetchVttFromMux(vttUrl) {
4752
- "use step";
4753
- const vttResponse = await fetch(vttUrl);
4754
- if (!vttResponse.ok) {
4755
- throw new Error(`Failed to fetch VTT file: ${vttResponse.statusText}`);
4756
- }
4757
- return vttResponse.text();
4758
- }
4759
- async function createTextTrackOnMux(assetId, languageCode, trackName, presignedUrl, credentials) {
4760
- "use step";
4761
- const muxClient = await resolveMuxClient(credentials);
4762
- const mux = await muxClient.createClient();
4763
- const trackResponse = await mux.video.assets.createTrack(assetId, {
4764
- type: "text",
4765
- text_type: "subtitles",
4766
- language_code: languageCode,
4767
- name: trackName,
4768
- url: presignedUrl
4769
- });
4770
- if (!trackResponse.id) {
4771
- throw new Error("Failed to create text track: no track ID returned from Mux");
4772
- }
4773
- return trackResponse.id;
4774
- }
4775
-
4776
4978
  // src/lib/storage-adapter.ts
4777
4979
  function requireCredentials(accessKeyId, secretAccessKey) {
4778
4980
  if (!accessKeyId || !secretAccessKey) {
@@ -5051,6 +5253,7 @@ async function uploadEditedVttToS3({
5051
5253
  editedVtt,
5052
5254
  assetId,
5053
5255
  trackId,
5256
+ variant = "edited",
5054
5257
  s3Endpoint,
5055
5258
  s3Region,
5056
5259
  s3Bucket,
@@ -5060,7 +5263,7 @@ async function uploadEditedVttToS3({
5060
5263
  "use step";
5061
5264
  const s3AccessKeyId = env_default.S3_ACCESS_KEY_ID;
5062
5265
  const s3SecretAccessKey = env_default.S3_SECRET_ACCESS_KEY;
5063
- const vttKey = `edited/${assetId}/${trackId}-edited-${Date.now()}.vtt`;
5266
+ const vttKey = `edited/${assetId}/${trackId}-${variant}-${Date.now()}.vtt`;
5064
5267
  await putObjectWithStorageAdapter({
5065
5268
  accessKeyId: s3AccessKeyId,
5066
5269
  secretAccessKey: s3SecretAccessKey,
@@ -5110,6 +5313,9 @@ async function editCaptionsInternal(assetId, trackId, options, collectedUsage) {
5110
5313
  s3Region: providedS3Region,
5111
5314
  s3Bucket: providedS3Bucket,
5112
5315
  trackNameSuffix,
5316
+ replaceExistingTracks: replaceExistingTracksOption,
5317
+ trackName: providedTrackName,
5318
+ trackPassthrough: providedTrackPassthrough,
5113
5319
  storageAdapter,
5114
5320
  credentials
5115
5321
  } = options;
@@ -5122,6 +5328,21 @@ async function editCaptionsInternal(assetId, trackId, options, collectedUsage) {
5122
5328
  if (autoCensorOption && !provider) {
5123
5329
  throw new MuxAiError("provider is required when using autoCensorProfanity.", { type: "validation_error" });
5124
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");
5125
5346
  const deleteOriginal = deleteOriginalTrack !== false;
5126
5347
  const uploadToMux = uploadToMuxOption !== false;
5127
5348
  const uploadToS3 = uploadToS3Option || uploadToMux;
@@ -5164,6 +5385,25 @@ async function editCaptionsInternal(assetId, trackId, options, collectedUsage) {
5164
5385
  { type: "validation_error" }
5165
5386
  );
5166
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
+ }
5167
5407
  const vttUrl = await buildTranscriptUrl(playbackId, trackId, policy === "signed", credentials);
5168
5408
  let vttContent;
5169
5409
  try {
@@ -5241,32 +5481,32 @@ async function editCaptionsInternal(assetId, trackId, options, collectedUsage) {
5241
5481
  } : void 0;
5242
5482
  let presignedUrl;
5243
5483
  let uploadedTrackId;
5484
+ let replacedTracks;
5244
5485
  if (uploadToS3) {
5486
+ const s3Config = {
5487
+ assetId,
5488
+ trackId,
5489
+ s3Endpoint,
5490
+ s3Region,
5491
+ s3Bucket,
5492
+ storageAdapter,
5493
+ s3SignedUrlExpirySeconds: options.s3SignedUrlExpirySeconds
5494
+ };
5245
5495
  try {
5246
- presignedUrl = await uploadEditedVttToS3({
5247
- editedVtt,
5248
- assetId,
5249
- trackId,
5250
- s3Endpoint,
5251
- s3Region,
5252
- s3Bucket,
5253
- storageAdapter,
5254
- s3SignedUrlExpirySeconds: options.s3SignedUrlExpirySeconds
5255
- });
5496
+ presignedUrl = await uploadEditedVttToS3({ editedVtt, ...s3Config });
5256
5497
  } catch (error) {
5257
5498
  wrapError(error, "Failed to upload VTT to S3");
5258
5499
  }
5259
- if (uploadToMux) {
5500
+ if (uploadToMux && legacyTrackNaming) {
5260
5501
  try {
5261
- const languageCode = sourceTrack.language_code || "en";
5262
- const suffix = trackNameSuffix ?? "edited";
5263
- const trackName = `${sourceTrack.name || "Subtitles"} (${suffix})`;
5502
+ const trackName = `${sourceName} (${trackNameSuffix ?? "edited"})`;
5264
5503
  uploadedTrackId = await createTextTrackOnMux(
5265
5504
  assetId,
5266
- languageCode,
5505
+ sourceLanguageCode,
5267
5506
  trackName,
5268
5507
  presignedUrl,
5269
- credentials
5508
+ credentials,
5509
+ { closedCaptions: sourceTrack.closed_captions, passthrough: trackPassthrough }
5270
5510
  );
5271
5511
  } catch (error) {
5272
5512
  wrapError(error, "Failed to add edited track to Mux asset");
@@ -5278,6 +5518,48 @@ async function editCaptionsInternal(assetId, trackId, options, collectedUsage) {
5278
5518
  wrapError(error, "Failed to delete original track");
5279
5519
  }
5280
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;
5281
5563
  }
5282
5564
  }
5283
5565
  return {
@@ -5290,6 +5572,7 @@ async function editCaptionsInternal(assetId, trackId, options, collectedUsage) {
5290
5572
  replacements: replacementsResult,
5291
5573
  speakerReplacements: speakerReplacementsResult,
5292
5574
  uploadedTrackId,
5575
+ replacedTracks,
5293
5576
  presignedUrl,
5294
5577
  usage: usageWithMetadata,
5295
5578
  safety: safety.report()
@@ -7550,23 +7833,6 @@ async function downloadAndUploadDubTranscript({
7550
7833
  console.warn(`\u2705 Dub transcript uploaded successfully to: ${vttKey}`);
7551
7834
  return presignedUrl;
7552
7835
  }
7553
- async function createAudioTrackOnMux(assetId, languageCode, presignedUrl, credentials) {
7554
- "use step";
7555
- const muxClient = await resolveMuxClient(credentials);
7556
- const mux = await muxClient.createClient();
7557
- const languageName = new Intl.DisplayNames(["en"], { type: "language" }).of(languageCode) || languageCode.toUpperCase();
7558
- const trackName = `${languageName} (auto-dubbed)`;
7559
- const trackResponse = await mux.video.assets.createTrack(assetId, {
7560
- type: "audio",
7561
- language_code: languageCode,
7562
- name: trackName,
7563
- url: presignedUrl
7564
- });
7565
- if (!trackResponse.id) {
7566
- throw new Error("Failed to create audio track: no track ID returned from Mux");
7567
- }
7568
- return trackResponse.id;
7569
- }
7570
7836
  async function translateAudio(assetId, toLanguageCode, options = {}) {
7571
7837
  "use workflow";
7572
7838
  const {
@@ -7579,15 +7845,23 @@ async function translateAudio(assetId, toLanguageCode, options = {}) {
7579
7845
  uploadCaptionsToMux = false,
7580
7846
  staticRenditionCleanup: staticRenditionCleanupOption = "delete",
7581
7847
  storageAdapter,
7582
- credentials: providedCredentials
7848
+ credentials: providedCredentials,
7849
+ replaceExistingTracks = "fail",
7850
+ trackName: providedTrackName,
7851
+ trackPassthrough: providedTrackPassthrough
7583
7852
  } = options;
7584
7853
  if (provider !== "elevenlabs") {
7585
7854
  throw new MuxAiError("Only ElevenLabs provider is currently supported for audio translation.", { type: "validation_error" });
7586
7855
  }
7587
7856
  const credentials = providedCredentials;
7588
7857
  const effectiveStorageAdapter = storageAdapter;
7858
+ const trackPassthrough = validateTrackPassthrough(providedTrackPassthrough) ?? buildMuxAiTrackPassthrough("translate-audio");
7589
7859
  const uploadToMux = uploadToMuxOption !== false;
7590
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 };
7591
7865
  const s3Endpoint = options.s3Endpoint ?? env_default.S3_ENDPOINT;
7592
7866
  const s3Region = options.s3Region ?? env_default.S3_REGION ?? "auto";
7593
7867
  const s3Bucket = options.s3Bucket ?? env_default.S3_BUCKET;
@@ -7598,6 +7872,15 @@ async function translateAudio(assetId, toLanguageCode, options = {}) {
7598
7872
  }
7599
7873
  const { asset: initialAsset, playbackId, policy } = await getPlaybackIdForAsset(assetId, credentials, options.assetSnapshot);
7600
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
+ }
7601
7884
  let currentAsset = initialAsset;
7602
7885
  let createdStaticRenditionId;
7603
7886
  if (!hasReadyAudioStaticRendition(currentAsset)) {
@@ -7613,6 +7896,7 @@ async function translateAudio(assetId, toLanguageCode, options = {}) {
7613
7896
  let dubbingId;
7614
7897
  let presignedUrl;
7615
7898
  let uploadedTrackId;
7899
+ let replacedTracks;
7616
7900
  let captionsPresignedUrl;
7617
7901
  let captionsTrackId;
7618
7902
  try {
@@ -7703,18 +7987,29 @@ async function translateAudio(assetId, toLanguageCode, options = {}) {
7703
7987
  }
7704
7988
  if (uploadToMux) {
7705
7989
  console.warn("\u{1F4F9} Adding dubbed audio track to Mux asset...");
7706
- const muxLangCode = toISO639_1(toLanguageCode);
7990
+ let outcome;
7707
7991
  try {
7708
- uploadedTrackId = await createAudioTrackOnMux(assetId, muxLangCode, presignedUrl, credentials);
7709
- const languageName = new Intl.DisplayNames(["en"], { type: "language" }).of(muxLangCode) || muxLangCode.toUpperCase();
7710
- const trackName = `${languageName} (auto-dubbed)`;
7711
- console.warn(`\u2705 Track added to Mux asset with ID: ${uploadedTrackId}`);
7712
- 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
+ });
7713
8000
  } catch (error) {
7714
- console.warn(`\u26A0\uFE0F Failed to add audio track to Mux asset: ${error instanceof Error ? error.message : "Unknown error"}`);
7715
- console.warn("\u{1F517} You can manually add the track using this presigned URL:");
7716
- 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}`);
7717
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}"`);
7718
8013
  }
7719
8014
  try {
7720
8015
  console.warn("\u{1F4E5} Downloading dub transcript from ElevenLabs and staging to S3...");
@@ -7732,15 +8027,21 @@ async function translateAudio(assetId, toLanguageCode, options = {}) {
7732
8027
  });
7733
8028
  if (uploadCaptionsToMux) {
7734
8029
  console.warn("\u{1F4F9} Adding dubbed captions track to Mux asset...");
7735
- const muxLangCode = toISO639_1(toLanguageCode);
7736
- const languageName = new Intl.DisplayNames(["en"], { type: "language" }).of(muxLangCode) || muxLangCode.toUpperCase();
7737
- captionsTrackId = await createTextTrackOnMux(
8030
+ const captionsOutcome = await replaceAndCreateTrack({
7738
8031
  assetId,
7739
- muxLangCode,
7740
- `${languageName} (auto-dubbed)`,
7741
- captionsPresignedUrl,
8032
+ target: captionsTrackTarget,
8033
+ policy: replaceExistingTracks,
8034
+ presignedUrl: captionsPresignedUrl,
8035
+ passthrough: trackPassthrough,
7742
8036
  credentials
7743
- );
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;
7744
8045
  console.warn(`\u2705 Captions track added to Mux asset with ID: ${captionsTrackId}`);
7745
8046
  }
7746
8047
  } catch (error) {
@@ -7771,6 +8072,7 @@ async function translateAudio(assetId, toLanguageCode, options = {}) {
7771
8072
  dubbingId,
7772
8073
  uploadedTrackId,
7773
8074
  presignedUrl,
8075
+ replacedTracks,
7774
8076
  captionsTrackId,
7775
8077
  captionsPresignedUrl,
7776
8078
  createdStaticRenditionId,
@@ -8440,11 +8742,15 @@ async function translateCaptionsInternal(assetId, trackId, toLanguageCode, optio
8440
8742
  storageAdapter,
8441
8743
  credentials: providedCredentials,
8442
8744
  chunking,
8443
- neverTranslate: neverTranslateOption
8745
+ neverTranslate: neverTranslateOption,
8746
+ replaceExistingTracks = "fail",
8747
+ trackName: providedTrackName,
8748
+ trackPassthrough: providedTrackPassthrough
8444
8749
  } = options;
8445
8750
  const credentials = providedCredentials;
8446
8751
  const effectiveStorageAdapter = storageAdapter;
8447
8752
  const neverTranslateTerms = neverTranslateOption ? validateNeverTranslateTerms(neverTranslateOption) : [];
8753
+ const trackPassthrough = validateTrackPassthrough(providedTrackPassthrough) ?? buildMuxAiTrackPassthrough("translate-captions");
8448
8754
  const s3Endpoint = providedS3Endpoint ?? env_default.S3_ENDPOINT;
8449
8755
  const s3Region = providedS3Region ?? env_default.S3_REGION ?? "auto";
8450
8756
  const s3Bucket = providedS3Bucket ?? env_default.S3_BUCKET;
@@ -8485,6 +8791,16 @@ async function translateCaptionsInternal(assetId, trackId, toLanguageCode, optio
8485
8791
  { type: "validation_error" }
8486
8792
  );
8487
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
+ }
8488
8804
  const vttUrl = await buildTranscriptUrl(playbackId, trackId, policy === "signed", credentials);
8489
8805
  let vttContent;
8490
8806
  try {
@@ -8552,6 +8868,7 @@ async function translateCaptionsInternal(assetId, trackId, toLanguageCode, optio
8552
8868
  const targetLanguage = getLanguageCodePair(toLanguageCode);
8553
8869
  let presignedUrl;
8554
8870
  let uploadedTrackId;
8871
+ let replacedTracks;
8555
8872
  if (uploadToS3) {
8556
8873
  try {
8557
8874
  presignedUrl = await uploadVttToS3({
@@ -8569,19 +8886,28 @@ async function translateCaptionsInternal(assetId, trackId, toLanguageCode, optio
8569
8886
  wrapError(error, "Failed to upload VTT to S3");
8570
8887
  }
8571
8888
  if (uploadToMux) {
8889
+ let outcome;
8572
8890
  try {
8573
- const languageName = getLanguageName(toLanguageCode) ?? toLanguageCode.toUpperCase();
8574
- const trackName = `${languageName} (auto-translated)`;
8575
- uploadedTrackId = await createTextTrackOnMux(
8891
+ outcome = await replaceAndCreateTextTrack({
8576
8892
  assetId,
8577
- toLanguageCode,
8578
- trackName,
8893
+ target: outputTrack,
8894
+ policy: replaceExistingTracks,
8579
8895
  presignedUrl,
8896
+ passthrough: trackPassthrough,
8580
8897
  credentials
8581
- );
8898
+ });
8582
8899
  } catch (error) {
8583
- 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}`);
8584
8908
  }
8909
+ uploadedTrackId = outcome.trackId;
8910
+ replacedTracks = outcome.deleted;
8585
8911
  }
8586
8912
  }
8587
8913
  return {
@@ -8594,6 +8920,7 @@ async function translateCaptionsInternal(assetId, trackId, toLanguageCode, optio
8594
8920
  originalVtt: vttContent,
8595
8921
  translatedVtt,
8596
8922
  uploadedTrackId,
8923
+ replacedTracks,
8597
8924
  presignedUrl,
8598
8925
  usage: usageWithMetadata,
8599
8926
  safety,
@@ -8601,20 +8928,33 @@ async function translateCaptionsInternal(assetId, trackId, toLanguageCode, optio
8601
8928
  };
8602
8929
  }
8603
8930
  export {
8931
+ MUX_TRACK_PASSTHROUGH_MAX_CHARS,
8604
8932
  MuxAiError,
8605
8933
  WorkflowStorageClient,
8934
+ buildMuxAiTrackPassthrough,
8606
8935
  createWorkflowStorageClient,
8607
8936
  decryptFromWorkflow,
8608
8937
  encryptForWorkflow,
8938
+ findNameCollisionTextTracks,
8939
+ findSameLanguageTextTracks,
8609
8940
  getLanguageCodePair,
8610
8941
  getLanguageName,
8942
+ isDuplicateTrackNameError,
8943
+ isMuxGeneratedTextTrack,
8611
8944
  isUndeterminedLanguageCode,
8612
8945
  isValidISO639_1,
8613
8946
  isValidISO639_3,
8947
+ normalizeTrackLanguageCode,
8948
+ normalizeTrackName,
8949
+ planTextTrackReplacement,
8614
8950
  primitives_exports as primitives,
8951
+ replaceAndCreateTextTrack,
8952
+ replaceAndCreateTrack,
8615
8953
  setWorkflowCredentialsProvider,
8954
+ summarizeTextTrack,
8616
8955
  toISO639_1,
8617
8956
  toISO639_3,
8957
+ validateTrackPassthrough,
8618
8958
  version,
8619
8959
  workflows_exports as workflows,
8620
8960
  wrapError