@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.
@@ -2928,11 +2928,8 @@ async function analyzeQuestions({
2928
2928
  maxRetries: 0,
2929
2929
  output: Output.object({ schema: responseSchema }),
2930
2930
  experimental_telemetry: { isEnabled: true },
2931
+ system: systemPrompt,
2931
2932
  messages: [
2932
- {
2933
- role: "system",
2934
- content: systemPrompt
2935
- },
2936
2933
  {
2937
2934
  role: "user",
2938
2935
  content: imageDataUrl ? [
@@ -3346,11 +3343,8 @@ async function analyzeStoryboard({
3346
3343
  maxRetries: 0,
3347
3344
  output: Output2.object({ schema: burnedInCaptionsSchema }),
3348
3345
  experimental_telemetry: { isEnabled: true },
3346
+ system: systemPrompt,
3349
3347
  messages: [
3350
- {
3351
- role: "system",
3352
- content: systemPrompt
3353
- },
3354
3348
  {
3355
3349
  role: "user",
3356
3350
  content: [
@@ -3495,11 +3489,8 @@ async function generateChaptersWithAI({
3495
3489
  model,
3496
3490
  maxRetries: 0,
3497
3491
  output: Output3.object({ schema: chaptersSchema }),
3492
+ system: systemPrompt,
3498
3493
  messages: [
3499
- {
3500
- role: "system",
3501
- content: systemPrompt
3502
- },
3503
3494
  {
3504
3495
  role: "user",
3505
3496
  content: userPrompt
@@ -3788,6 +3779,129 @@ import { generateText as generateText4, Output as Output4 } from "ai";
3788
3779
  import { z as z6 } from "zod";
3789
3780
 
3790
3781
  // src/lib/mux-tracks.ts
3782
+ var MUX_TRACK_PASSTHROUGH_MAX_CHARS = 255;
3783
+ function normalizeTrackLanguageCode(code) {
3784
+ if (!code) {
3785
+ return void 0;
3786
+ }
3787
+ const primarySubtag = code.trim().toLowerCase().split(/[-_]/)[0];
3788
+ if (!primarySubtag || primarySubtag === "auto" || isUndeterminedLanguageCode(primarySubtag)) {
3789
+ return void 0;
3790
+ }
3791
+ return toISO639_1(primarySubtag);
3792
+ }
3793
+ function normalizeTrackName(name) {
3794
+ const normalized = name?.trim().toLowerCase();
3795
+ return normalized || void 0;
3796
+ }
3797
+ function isMuxGeneratedTextTrack(track) {
3798
+ return track.text_source === "generated_vod" || track.text_source === "generated_live" || track.text_source === "generated_live_final";
3799
+ }
3800
+ function targetType(target) {
3801
+ return target.type ?? "text";
3802
+ }
3803
+ function listTracks(asset, type) {
3804
+ return (asset.tracks ?? []).filter((track) => track.type === type && track.status !== "deleted" && !!track.id);
3805
+ }
3806
+ function summarizeTextTrack(track) {
3807
+ return {
3808
+ id: track.id,
3809
+ type: track.type === "audio" ? "audio" : "text",
3810
+ name: track.name,
3811
+ languageCode: track.language_code,
3812
+ status: track.status,
3813
+ textSource: track.text_source,
3814
+ passthrough: track.passthrough,
3815
+ ...track.type === "audio" ? { primary: track.primary === true } : {}
3816
+ };
3817
+ }
3818
+ function findSameLanguageTextTracks(asset, target) {
3819
+ const targetLanguage = normalizeTrackLanguageCode(target.languageCode);
3820
+ if (!targetLanguage) {
3821
+ return [];
3822
+ }
3823
+ const type = targetType(target);
3824
+ return listTracks(asset, type).filter(
3825
+ (track) => (type === "audio" || track.text_type === "subtitles") && normalizeTrackLanguageCode(track.language_code) === targetLanguage
3826
+ );
3827
+ }
3828
+ function findNameCollisionTextTracks(asset, target) {
3829
+ const targetName = normalizeTrackName(target.name);
3830
+ if (!targetName) {
3831
+ return [];
3832
+ }
3833
+ return listTracks(asset, targetType(target)).filter((track) => normalizeTrackName(track.name) === targetName);
3834
+ }
3835
+ function describeTracks(tracks) {
3836
+ 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("; ");
3837
+ }
3838
+ function planTextTrackReplacement(asset, target, policy, options = {}) {
3839
+ const label = targetType(target) === "audio" ? "Audio" : "Text";
3840
+ const keep = new Set(options.keepTrackIds ?? []);
3841
+ const conflicts = /* @__PURE__ */ new Map();
3842
+ for (const track of [...findSameLanguageTextTracks(asset, target), ...findNameCollisionTextTracks(asset, target)]) {
3843
+ if (!keep.has(track.id)) {
3844
+ conflicts.set(track.id, track);
3845
+ }
3846
+ }
3847
+ const conflicting = [...conflicts.values()];
3848
+ if (conflicting.length === 0) {
3849
+ return { kind: "clear" };
3850
+ }
3851
+ if (policy === "fail") {
3852
+ return {
3853
+ kind: "blocked",
3854
+ reason: `${label} track(s) already exist for language '${target.languageCode}' or name '${target.name}': ${describeTracks(conflicting)}. Set replaceExistingTracks to replace them.`,
3855
+ tracks: conflicting.map(summarizeTextTrack)
3856
+ };
3857
+ }
3858
+ const primary = conflicting.filter((track) => track.type === "audio" && track.primary === true);
3859
+ if (primary.length > 0) {
3860
+ return {
3861
+ kind: "blocked",
3862
+ 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.`,
3863
+ tracks: primary.map(summarizeTextTrack)
3864
+ };
3865
+ }
3866
+ if (policy === "replace_generated") {
3867
+ const notGenerated = conflicting.filter((track) => track.type !== "text" || !isMuxGeneratedTextTrack(track));
3868
+ if (notGenerated.length > 0) {
3869
+ return {
3870
+ kind: "blocked",
3871
+ 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.`,
3872
+ tracks: notGenerated.map(summarizeTextTrack)
3873
+ };
3874
+ }
3875
+ }
3876
+ return { kind: "replace", toDelete: conflicting.map(summarizeTextTrack) };
3877
+ }
3878
+ function isDuplicateTrackNameError(error) {
3879
+ if (typeof error !== "object" || error === null) {
3880
+ return false;
3881
+ }
3882
+ const record = error;
3883
+ if (record.status !== 400) {
3884
+ return false;
3885
+ }
3886
+ const messages = Array.isArray(record.error?.messages) ? record.error.messages : [];
3887
+ const haystack = [...messages, record.message].filter((value) => typeof value === "string").map((value) => value.toLowerCase());
3888
+ return haystack.some((value) => value.includes("is not unique"));
3889
+ }
3890
+ function isNotFoundError(error) {
3891
+ return typeof error === "object" && error !== null && error.status === 404;
3892
+ }
3893
+ function buildMuxAiTrackPassthrough(workflow) {
3894
+ return JSON.stringify({ mux_ai: { workflow } });
3895
+ }
3896
+ function validateTrackPassthrough(passthrough) {
3897
+ if (passthrough !== void 0 && passthrough.length > MUX_TRACK_PASSTHROUGH_MAX_CHARS) {
3898
+ throw new MuxAiError(
3899
+ `Track passthrough must be at most ${MUX_TRACK_PASSTHROUGH_MAX_CHARS} characters (received ${passthrough.length}).`,
3900
+ { type: "validation_error" }
3901
+ );
3902
+ }
3903
+ return passthrough;
3904
+ }
3791
3905
  async function fetchVttFromMux(vttUrl) {
3792
3906
  "use step";
3793
3907
  const vttResponse = await fetch(vttUrl);
@@ -3796,7 +3910,7 @@ async function fetchVttFromMux(vttUrl) {
3796
3910
  }
3797
3911
  return vttResponse.text();
3798
3912
  }
3799
- async function createTextTrackOnMux(assetId, languageCode, trackName, presignedUrl, credentials) {
3913
+ async function createTextTrackOnMux(assetId, languageCode, trackName, presignedUrl, credentials, options = {}) {
3800
3914
  "use step";
3801
3915
  const muxClient = await resolveMuxClient(credentials);
3802
3916
  const mux = await muxClient.createClient();
@@ -3805,13 +3919,92 @@ async function createTextTrackOnMux(assetId, languageCode, trackName, presignedU
3805
3919
  text_type: "subtitles",
3806
3920
  language_code: languageCode,
3807
3921
  name: trackName,
3808
- url: presignedUrl
3922
+ url: presignedUrl,
3923
+ closed_captions: options.closedCaptions,
3924
+ passthrough: options.passthrough
3809
3925
  });
3810
3926
  if (!trackResponse.id) {
3811
3927
  throw new Error("Failed to create text track: no track ID returned from Mux");
3812
3928
  }
3813
3929
  return trackResponse.id;
3814
3930
  }
3931
+ async function replaceAndCreateTrack(input) {
3932
+ "use step";
3933
+ const muxClient = await resolveMuxClient(input.credentials);
3934
+ const mux = await muxClient.createClient();
3935
+ const type = targetType(input.target);
3936
+ const deleted = [];
3937
+ const failed = (error) => ({
3938
+ kind: "create_failed",
3939
+ reason: error instanceof Error ? error.message : String(error),
3940
+ deleted
3941
+ });
3942
+ const clearConflicts = async () => {
3943
+ let asset;
3944
+ try {
3945
+ asset = await mux.video.assets.retrieve(input.assetId);
3946
+ } catch (error) {
3947
+ if (deleted.length === 0) {
3948
+ throw error;
3949
+ }
3950
+ return failed(error);
3951
+ }
3952
+ const plan = planTextTrackReplacement(asset, input.target, input.policy, { keepTrackIds: input.keepTrackIds });
3953
+ if (plan.kind === "blocked") {
3954
+ return { ...plan, deleted };
3955
+ }
3956
+ if (plan.kind === "replace") {
3957
+ for (const track of plan.toDelete) {
3958
+ try {
3959
+ await mux.video.assets.deleteTrack(input.assetId, track.id);
3960
+ deleted.push(track);
3961
+ } catch (error) {
3962
+ if (!isNotFoundError(error)) {
3963
+ return failed(error);
3964
+ }
3965
+ }
3966
+ }
3967
+ }
3968
+ return void 0;
3969
+ };
3970
+ const create = async () => {
3971
+ const track = await mux.video.assets.createTrack(input.assetId, {
3972
+ type,
3973
+ ...type === "text" ? { text_type: "subtitles", closed_captions: input.closedCaptions } : {},
3974
+ language_code: input.target.languageCode,
3975
+ name: input.target.name.trim(),
3976
+ url: input.presignedUrl,
3977
+ passthrough: input.passthrough
3978
+ });
3979
+ if (!track.id) {
3980
+ throw new Error(`Failed to create ${type} track: no track ID returned from Mux`);
3981
+ }
3982
+ return track.id;
3983
+ };
3984
+ const stopped = await clearConflicts();
3985
+ if (stopped) {
3986
+ return stopped;
3987
+ }
3988
+ for (let attempt = 0; attempt < 2; attempt++) {
3989
+ try {
3990
+ return { kind: "created", trackId: await create(), deleted };
3991
+ } catch (error) {
3992
+ if (isDuplicateTrackNameError(error) && attempt === 0) {
3993
+ const stoppedOnRetry = await clearConflicts();
3994
+ if (stoppedOnRetry) {
3995
+ return stoppedOnRetry;
3996
+ }
3997
+ continue;
3998
+ }
3999
+ return failed(error);
4000
+ }
4001
+ }
4002
+ return { kind: "create_failed", reason: "Mux rejected the track name as not unique after retrying.", deleted };
4003
+ }
4004
+ async function replaceAndCreateTextTrack(input) {
4005
+ "use step";
4006
+ return replaceAndCreateTrack({ ...input, target: { ...input.target, type: "text" } });
4007
+ }
3815
4008
 
3816
4009
  // src/lib/s3-sigv4.ts
3817
4010
  var AWS4_ALGORITHM = "AWS4-HMAC-SHA256";
@@ -4271,11 +4464,8 @@ async function identifyProfanityWithAI({
4271
4464
  model,
4272
4465
  maxRetries: 0,
4273
4466
  output: Output4.object({ schema: profanityDetectionSchema }),
4467
+ system: SYSTEM_PROMPT3,
4274
4468
  messages: [
4275
- {
4276
- role: "system",
4277
- content: SYSTEM_PROMPT3
4278
- },
4279
4469
  {
4280
4470
  role: "user",
4281
4471
  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.
@@ -4306,6 +4496,7 @@ async function uploadEditedVttToS3({
4306
4496
  editedVtt,
4307
4497
  assetId,
4308
4498
  trackId,
4499
+ variant = "edited",
4309
4500
  s3Endpoint,
4310
4501
  s3Region,
4311
4502
  s3Bucket,
@@ -4315,7 +4506,7 @@ async function uploadEditedVttToS3({
4315
4506
  "use step";
4316
4507
  const s3AccessKeyId = env_default.S3_ACCESS_KEY_ID;
4317
4508
  const s3SecretAccessKey = env_default.S3_SECRET_ACCESS_KEY;
4318
- const vttKey = `edited/${assetId}/${trackId}-edited-${Date.now()}.vtt`;
4509
+ const vttKey = `edited/${assetId}/${trackId}-${variant}-${Date.now()}.vtt`;
4319
4510
  await putObjectWithStorageAdapter({
4320
4511
  accessKeyId: s3AccessKeyId,
4321
4512
  secretAccessKey: s3SecretAccessKey,
@@ -4365,6 +4556,9 @@ async function editCaptionsInternal(assetId, trackId, options, collectedUsage) {
4365
4556
  s3Region: providedS3Region,
4366
4557
  s3Bucket: providedS3Bucket,
4367
4558
  trackNameSuffix,
4559
+ replaceExistingTracks: replaceExistingTracksOption,
4560
+ trackName: providedTrackName,
4561
+ trackPassthrough: providedTrackPassthrough,
4368
4562
  storageAdapter,
4369
4563
  credentials
4370
4564
  } = options;
@@ -4377,6 +4571,21 @@ async function editCaptionsInternal(assetId, trackId, options, collectedUsage) {
4377
4571
  if (autoCensorOption && !provider) {
4378
4572
  throw new MuxAiError("provider is required when using autoCensorProfanity.", { type: "validation_error" });
4379
4573
  }
4574
+ const legacyTrackNaming = deleteOriginalTrack !== void 0 || trackNameSuffix !== void 0;
4575
+ if (legacyTrackNaming && (replaceExistingTracksOption !== void 0 || providedTrackName !== void 0)) {
4576
+ throw new MuxAiError(
4577
+ "deleteOriginalTrack and trackNameSuffix are deprecated and cannot be combined with replaceExistingTracks or trackName.",
4578
+ { type: "validation_error" }
4579
+ );
4580
+ }
4581
+ const replaceExistingTracks = replaceExistingTracksOption ?? "replace_all";
4582
+ if (!legacyTrackNaming && replaceExistingTracks === "fail" && !providedTrackName) {
4583
+ throw new MuxAiError(
4584
+ `trackName is required when replaceExistingTracks is "fail": the edited track cannot reuse the source track's name while the source is kept.`,
4585
+ { type: "validation_error" }
4586
+ );
4587
+ }
4588
+ const trackPassthrough = validateTrackPassthrough(providedTrackPassthrough) ?? buildMuxAiTrackPassthrough("edit-captions");
4380
4589
  const deleteOriginal = deleteOriginalTrack !== false;
4381
4590
  const uploadToMux = uploadToMuxOption !== false;
4382
4591
  const uploadToS3 = uploadToS3Option || uploadToMux;
@@ -4419,6 +4628,25 @@ async function editCaptionsInternal(assetId, trackId, options, collectedUsage) {
4419
4628
  { type: "validation_error" }
4420
4629
  );
4421
4630
  }
4631
+ const sourceLanguageCode = sourceTrack.language_code || "en";
4632
+ const sourceName = sourceTrack.name || "Subtitles";
4633
+ const outputTrack = {
4634
+ languageCode: sourceLanguageCode,
4635
+ name: providedTrackName ?? sourceName
4636
+ };
4637
+ const keepTrackIds = replaceExistingTracks === "fail" ? [trackId] : [];
4638
+ if (replaceExistingTracks === "fail" && normalizeTrackName(outputTrack.name) === normalizeTrackName(sourceName)) {
4639
+ throw new MuxAiError(
4640
+ `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.`,
4641
+ { type: "validation_error" }
4642
+ );
4643
+ }
4644
+ if (uploadToMux && !legacyTrackNaming) {
4645
+ const plan = planTextTrackReplacement(assetData, outputTrack, replaceExistingTracks, { keepTrackIds });
4646
+ if (plan.kind === "blocked") {
4647
+ throw new MuxAiError(plan.reason, { type: "validation_error" });
4648
+ }
4649
+ }
4422
4650
  const vttUrl = await buildTranscriptUrl(playbackId, trackId, policy === "signed", credentials);
4423
4651
  let vttContent;
4424
4652
  try {
@@ -4496,32 +4724,32 @@ async function editCaptionsInternal(assetId, trackId, options, collectedUsage) {
4496
4724
  } : void 0;
4497
4725
  let presignedUrl;
4498
4726
  let uploadedTrackId;
4727
+ let replacedTracks;
4499
4728
  if (uploadToS3) {
4729
+ const s3Config = {
4730
+ assetId,
4731
+ trackId,
4732
+ s3Endpoint,
4733
+ s3Region,
4734
+ s3Bucket,
4735
+ storageAdapter,
4736
+ s3SignedUrlExpirySeconds: options.s3SignedUrlExpirySeconds
4737
+ };
4500
4738
  try {
4501
- presignedUrl = await uploadEditedVttToS3({
4502
- editedVtt,
4503
- assetId,
4504
- trackId,
4505
- s3Endpoint,
4506
- s3Region,
4507
- s3Bucket,
4508
- storageAdapter,
4509
- s3SignedUrlExpirySeconds: options.s3SignedUrlExpirySeconds
4510
- });
4739
+ presignedUrl = await uploadEditedVttToS3({ editedVtt, ...s3Config });
4511
4740
  } catch (error) {
4512
4741
  wrapError(error, "Failed to upload VTT to S3");
4513
4742
  }
4514
- if (uploadToMux) {
4743
+ if (uploadToMux && legacyTrackNaming) {
4515
4744
  try {
4516
- const languageCode = sourceTrack.language_code || "en";
4517
- const suffix = trackNameSuffix ?? "edited";
4518
- const trackName = `${sourceTrack.name || "Subtitles"} (${suffix})`;
4745
+ const trackName = `${sourceName} (${trackNameSuffix ?? "edited"})`;
4519
4746
  uploadedTrackId = await createTextTrackOnMux(
4520
4747
  assetId,
4521
- languageCode,
4748
+ sourceLanguageCode,
4522
4749
  trackName,
4523
4750
  presignedUrl,
4524
- credentials
4751
+ credentials,
4752
+ { closedCaptions: sourceTrack.closed_captions, passthrough: trackPassthrough }
4525
4753
  );
4526
4754
  } catch (error) {
4527
4755
  wrapError(error, "Failed to add edited track to Mux asset");
@@ -4533,6 +4761,48 @@ async function editCaptionsInternal(assetId, trackId, options, collectedUsage) {
4533
4761
  wrapError(error, "Failed to delete original track");
4534
4762
  }
4535
4763
  }
4764
+ } else if (uploadToMux) {
4765
+ let outcome;
4766
+ try {
4767
+ outcome = await replaceAndCreateTextTrack({
4768
+ assetId,
4769
+ target: outputTrack,
4770
+ policy: replaceExistingTracks,
4771
+ presignedUrl,
4772
+ closedCaptions: sourceTrack.closed_captions,
4773
+ passthrough: trackPassthrough,
4774
+ keepTrackIds,
4775
+ credentials
4776
+ });
4777
+ } catch (error) {
4778
+ wrapError(error, "Failed to add edited track to Mux asset");
4779
+ }
4780
+ if (outcome.kind !== "created") {
4781
+ const sourceWasDeleted = outcome.deleted.some((track) => track.id === trackId);
4782
+ let restoreNote = "";
4783
+ if (sourceWasDeleted) {
4784
+ try {
4785
+ const originalUrl = await uploadEditedVttToS3({ editedVtt: vttContent, variant: "original", ...s3Config });
4786
+ const restoredTrackId = await createTextTrackOnMux(
4787
+ assetId,
4788
+ sourceLanguageCode,
4789
+ sourceName,
4790
+ originalUrl,
4791
+ credentials,
4792
+ { closedCaptions: sourceTrack.closed_captions, passthrough: sourceTrack.passthrough }
4793
+ );
4794
+ restoreNote = ` The source track was restored as ${restoredTrackId}.`;
4795
+ } catch (restoreError) {
4796
+ restoreNote = ` Restoring the source track also failed: ${restoreError instanceof Error ? restoreError.message : String(restoreError)}.`;
4797
+ }
4798
+ }
4799
+ if (outcome.kind === "blocked") {
4800
+ throw new MuxAiError(`${outcome.reason}${restoreNote}`, { type: "validation_error" });
4801
+ }
4802
+ throw new Error(`Failed to add edited track to Mux asset: ${outcome.reason}.${restoreNote}`);
4803
+ }
4804
+ uploadedTrackId = outcome.trackId;
4805
+ replacedTracks = outcome.deleted;
4536
4806
  }
4537
4807
  }
4538
4808
  return {
@@ -4545,6 +4815,7 @@ async function editCaptionsInternal(assetId, trackId, options, collectedUsage) {
4545
4815
  replacements: replacementsResult,
4546
4816
  speakerReplacements: speakerReplacementsResult,
4547
4817
  uploadedTrackId,
4818
+ replacedTracks,
4548
4819
  presignedUrl,
4549
4820
  usage: usageWithMetadata,
4550
4821
  safety: safety.report()
@@ -5390,11 +5661,8 @@ async function generateInsightsWithAI(provider, modelId, systemPrompt, userPromp
5390
5661
  maxRetries: 0,
5391
5662
  output: Output5.object({ schema: engagementInsightsSchema }),
5392
5663
  experimental_telemetry: { isEnabled: true },
5664
+ system: systemPrompt,
5393
5665
  messages: [
5394
- {
5395
- role: "system",
5396
- content: systemPrompt
5397
- },
5398
5666
  {
5399
5667
  role: "user",
5400
5668
  content: [
@@ -6697,11 +6965,8 @@ async function analyzeStoryboard2(imageDataUrl, provider, modelId, userPrompt, s
6697
6965
  description: "Structured summary with title, description, and keywords.",
6698
6966
  schema
6699
6967
  }),
6968
+ system: systemPrompt,
6700
6969
  messages: [
6701
- {
6702
- role: "system",
6703
- content: systemPrompt
6704
- },
6705
6970
  {
6706
6971
  role: "user",
6707
6972
  content: [
@@ -6745,11 +7010,8 @@ async function analyzeAudioOnly(provider, modelId, userPrompt, systemPrompt, des
6745
7010
  description: "Structured summary with title, description, and keywords.",
6746
7011
  schema
6747
7012
  }),
7013
+ system: systemPrompt,
6748
7014
  messages: [
6749
- {
6750
- role: "system",
6751
- content: systemPrompt
6752
- },
6753
7015
  {
6754
7016
  role: "user",
6755
7017
  content: userPrompt
@@ -7241,23 +7503,6 @@ async function downloadAndUploadDubTranscript({
7241
7503
  console.warn(`\u2705 Dub transcript uploaded successfully to: ${vttKey}`);
7242
7504
  return presignedUrl;
7243
7505
  }
7244
- async function createAudioTrackOnMux(assetId, languageCode, presignedUrl, credentials) {
7245
- "use step";
7246
- const muxClient = await resolveMuxClient(credentials);
7247
- const mux = await muxClient.createClient();
7248
- const languageName = new Intl.DisplayNames(["en"], { type: "language" }).of(languageCode) || languageCode.toUpperCase();
7249
- const trackName = `${languageName} (auto-dubbed)`;
7250
- const trackResponse = await mux.video.assets.createTrack(assetId, {
7251
- type: "audio",
7252
- language_code: languageCode,
7253
- name: trackName,
7254
- url: presignedUrl
7255
- });
7256
- if (!trackResponse.id) {
7257
- throw new Error("Failed to create audio track: no track ID returned from Mux");
7258
- }
7259
- return trackResponse.id;
7260
- }
7261
7506
  async function translateAudio(assetId, toLanguageCode, options = {}) {
7262
7507
  "use workflow";
7263
7508
  const {
@@ -7270,15 +7515,23 @@ async function translateAudio(assetId, toLanguageCode, options = {}) {
7270
7515
  uploadCaptionsToMux = false,
7271
7516
  staticRenditionCleanup: staticRenditionCleanupOption = "delete",
7272
7517
  storageAdapter,
7273
- credentials: providedCredentials
7518
+ credentials: providedCredentials,
7519
+ replaceExistingTracks = "fail",
7520
+ trackName: providedTrackName,
7521
+ trackPassthrough: providedTrackPassthrough
7274
7522
  } = options;
7275
7523
  if (provider !== "elevenlabs") {
7276
7524
  throw new MuxAiError("Only ElevenLabs provider is currently supported for audio translation.", { type: "validation_error" });
7277
7525
  }
7278
7526
  const credentials = providedCredentials;
7279
7527
  const effectiveStorageAdapter = storageAdapter;
7528
+ const trackPassthrough = validateTrackPassthrough(providedTrackPassthrough) ?? buildMuxAiTrackPassthrough("translate-audio");
7280
7529
  const uploadToMux = uploadToMuxOption !== false;
7281
7530
  const uploadToS3 = uploadToS3Option || uploadToMux || uploadCaptionsToMux;
7531
+ const muxLangCode = toISO639_1(toLanguageCode);
7532
+ const trackName = providedTrackName ?? `${getLanguageName(muxLangCode) ?? muxLangCode.toUpperCase()} (Auto-dubbed)`;
7533
+ const audioTrackTarget = { type: "audio", languageCode: muxLangCode, name: trackName };
7534
+ const captionsTrackTarget = { type: "text", languageCode: muxLangCode, name: trackName };
7282
7535
  const s3Endpoint = options.s3Endpoint ?? env_default.S3_ENDPOINT;
7283
7536
  const s3Region = options.s3Region ?? env_default.S3_REGION ?? "auto";
7284
7537
  const s3Bucket = options.s3Bucket ?? env_default.S3_BUCKET;
@@ -7289,6 +7542,15 @@ async function translateAudio(assetId, toLanguageCode, options = {}) {
7289
7542
  }
7290
7543
  const { asset: initialAsset, playbackId, policy } = await getPlaybackIdForAsset(assetId, credentials, options.assetSnapshot);
7291
7544
  const assetDurationSeconds = getAssetDurationSecondsFromAsset(initialAsset);
7545
+ for (const target of [
7546
+ ...uploadToMux ? [audioTrackTarget] : [],
7547
+ ...uploadCaptionsToMux ? [captionsTrackTarget] : []
7548
+ ]) {
7549
+ const plan = planTextTrackReplacement(initialAsset, target, replaceExistingTracks);
7550
+ if (plan.kind === "blocked") {
7551
+ throw new MuxAiError(plan.reason, { type: "validation_error" });
7552
+ }
7553
+ }
7292
7554
  let currentAsset = initialAsset;
7293
7555
  let createdStaticRenditionId;
7294
7556
  if (!hasReadyAudioStaticRendition(currentAsset)) {
@@ -7304,6 +7566,7 @@ async function translateAudio(assetId, toLanguageCode, options = {}) {
7304
7566
  let dubbingId;
7305
7567
  let presignedUrl;
7306
7568
  let uploadedTrackId;
7569
+ let replacedTracks;
7307
7570
  let captionsPresignedUrl;
7308
7571
  let captionsTrackId;
7309
7572
  try {
@@ -7394,18 +7657,29 @@ async function translateAudio(assetId, toLanguageCode, options = {}) {
7394
7657
  }
7395
7658
  if (uploadToMux) {
7396
7659
  console.warn("\u{1F4F9} Adding dubbed audio track to Mux asset...");
7397
- const muxLangCode = toISO639_1(toLanguageCode);
7660
+ let outcome;
7398
7661
  try {
7399
- uploadedTrackId = await createAudioTrackOnMux(assetId, muxLangCode, presignedUrl, credentials);
7400
- const languageName = new Intl.DisplayNames(["en"], { type: "language" }).of(muxLangCode) || muxLangCode.toUpperCase();
7401
- const trackName = `${languageName} (auto-dubbed)`;
7402
- console.warn(`\u2705 Track added to Mux asset with ID: ${uploadedTrackId}`);
7403
- console.warn(`\u{1F4CB} Track name: "${trackName}"`);
7662
+ outcome = await replaceAndCreateTrack({
7663
+ assetId,
7664
+ target: audioTrackTarget,
7665
+ policy: replaceExistingTracks,
7666
+ presignedUrl,
7667
+ passthrough: trackPassthrough,
7668
+ credentials
7669
+ });
7404
7670
  } catch (error) {
7405
- console.warn(`\u26A0\uFE0F Failed to add audio track to Mux asset: ${error instanceof Error ? error.message : "Unknown error"}`);
7406
- console.warn("\u{1F517} You can manually add the track using this presigned URL:");
7407
- console.warn(presignedUrl);
7671
+ wrapError(error, `Failed to add dubbed audio track to Mux asset (dubbed audio is staged at: ${presignedUrl})`);
7672
+ }
7673
+ if (outcome.kind === "blocked") {
7674
+ throw new MuxAiError(`${outcome.reason} Dubbed audio is staged at: ${presignedUrl}`, { type: "validation_error" });
7408
7675
  }
7676
+ if (outcome.kind === "create_failed") {
7677
+ throw new Error(`Failed to add dubbed audio track to Mux asset: ${outcome.reason}. Dubbed audio is staged at: ${presignedUrl}`);
7678
+ }
7679
+ uploadedTrackId = outcome.trackId;
7680
+ replacedTracks = outcome.deleted;
7681
+ console.warn(`\u2705 Track added to Mux asset with ID: ${uploadedTrackId}`);
7682
+ console.warn(`\u{1F4CB} Track name: "${trackName}"`);
7409
7683
  }
7410
7684
  try {
7411
7685
  console.warn("\u{1F4E5} Downloading dub transcript from ElevenLabs and staging to S3...");
@@ -7423,15 +7697,21 @@ async function translateAudio(assetId, toLanguageCode, options = {}) {
7423
7697
  });
7424
7698
  if (uploadCaptionsToMux) {
7425
7699
  console.warn("\u{1F4F9} Adding dubbed captions track to Mux asset...");
7426
- const muxLangCode = toISO639_1(toLanguageCode);
7427
- const languageName = new Intl.DisplayNames(["en"], { type: "language" }).of(muxLangCode) || muxLangCode.toUpperCase();
7428
- captionsTrackId = await createTextTrackOnMux(
7700
+ const captionsOutcome = await replaceAndCreateTrack({
7429
7701
  assetId,
7430
- muxLangCode,
7431
- `${languageName} (auto-dubbed)`,
7432
- captionsPresignedUrl,
7702
+ target: captionsTrackTarget,
7703
+ policy: replaceExistingTracks,
7704
+ presignedUrl: captionsPresignedUrl,
7705
+ passthrough: trackPassthrough,
7433
7706
  credentials
7434
- );
7707
+ });
7708
+ if (captionsOutcome.deleted.length > 0) {
7709
+ replacedTracks = [...replacedTracks ?? [], ...captionsOutcome.deleted];
7710
+ }
7711
+ if (captionsOutcome.kind !== "created") {
7712
+ throw new Error(captionsOutcome.reason);
7713
+ }
7714
+ captionsTrackId = captionsOutcome.trackId;
7435
7715
  console.warn(`\u2705 Captions track added to Mux asset with ID: ${captionsTrackId}`);
7436
7716
  }
7437
7717
  } catch (error) {
@@ -7462,6 +7742,7 @@ async function translateAudio(assetId, toLanguageCode, options = {}) {
7462
7742
  dubbingId,
7463
7743
  uploadedTrackId,
7464
7744
  presignedUrl,
7745
+ replacedTracks,
7465
7746
  captionsTrackId,
7466
7747
  captionsPresignedUrl,
7467
7748
  createdStaticRenditionId,
@@ -7813,11 +8094,8 @@ async function translateVttWithAI({
7813
8094
  model,
7814
8095
  maxRetries: 0,
7815
8096
  output: Output7.object({ schema: translationSchema }),
8097
+ system: SYSTEM_PROMPT6,
7816
8098
  messages: [
7817
- {
7818
- role: "system",
7819
- content: SYSTEM_PROMPT6
7820
- },
7821
8099
  {
7822
8100
  role: "user",
7823
8101
  content: `Translate from ${fromLanguageCode} to ${toLanguageCode}:${buildNeverTranslateSection(neverTranslate)}
@@ -7881,11 +8159,8 @@ async function translateCueChunkWithAI({
7881
8159
  model,
7882
8160
  maxRetries: 0,
7883
8161
  output: Output7.object({ schema }),
8162
+ system: CUE_TRANSLATION_SYSTEM_PROMPT,
7884
8163
  messages: [
7885
- {
7886
- role: "system",
7887
- content: CUE_TRANSLATION_SYSTEM_PROMPT
7888
- },
7889
8164
  {
7890
8165
  role: "user",
7891
8166
  content: `Translate from ${fromLanguageCode} to ${toLanguageCode}.
@@ -8137,11 +8412,15 @@ async function translateCaptionsInternal(assetId, trackId, toLanguageCode, optio
8137
8412
  storageAdapter,
8138
8413
  credentials: providedCredentials,
8139
8414
  chunking,
8140
- neverTranslate: neverTranslateOption
8415
+ neverTranslate: neverTranslateOption,
8416
+ replaceExistingTracks = "fail",
8417
+ trackName: providedTrackName,
8418
+ trackPassthrough: providedTrackPassthrough
8141
8419
  } = options;
8142
8420
  const credentials = providedCredentials;
8143
8421
  const effectiveStorageAdapter = storageAdapter;
8144
8422
  const neverTranslateTerms = neverTranslateOption ? validateNeverTranslateTerms(neverTranslateOption) : [];
8423
+ const trackPassthrough = validateTrackPassthrough(providedTrackPassthrough) ?? buildMuxAiTrackPassthrough("translate-captions");
8145
8424
  const s3Endpoint = providedS3Endpoint ?? env_default.S3_ENDPOINT;
8146
8425
  const s3Region = providedS3Region ?? env_default.S3_REGION ?? "auto";
8147
8426
  const s3Bucket = providedS3Bucket ?? env_default.S3_BUCKET;
@@ -8182,6 +8461,16 @@ async function translateCaptionsInternal(assetId, trackId, toLanguageCode, optio
8182
8461
  { type: "validation_error" }
8183
8462
  );
8184
8463
  }
8464
+ const outputTrack = {
8465
+ languageCode: toLanguageCode,
8466
+ name: providedTrackName ?? `${getLanguageName(toLanguageCode) ?? toLanguageCode.toUpperCase()} (Auto-translated)`
8467
+ };
8468
+ if (uploadToMux) {
8469
+ const plan = planTextTrackReplacement(assetData, outputTrack, replaceExistingTracks);
8470
+ if (plan.kind === "blocked") {
8471
+ throw new MuxAiError(plan.reason, { type: "validation_error" });
8472
+ }
8473
+ }
8185
8474
  const vttUrl = await buildTranscriptUrl(playbackId, trackId, policy === "signed", credentials);
8186
8475
  let vttContent;
8187
8476
  try {
@@ -8249,6 +8538,7 @@ async function translateCaptionsInternal(assetId, trackId, toLanguageCode, optio
8249
8538
  const targetLanguage = getLanguageCodePair(toLanguageCode);
8250
8539
  let presignedUrl;
8251
8540
  let uploadedTrackId;
8541
+ let replacedTracks;
8252
8542
  if (uploadToS3) {
8253
8543
  try {
8254
8544
  presignedUrl = await uploadVttToS3({
@@ -8266,19 +8556,28 @@ async function translateCaptionsInternal(assetId, trackId, toLanguageCode, optio
8266
8556
  wrapError(error, "Failed to upload VTT to S3");
8267
8557
  }
8268
8558
  if (uploadToMux) {
8559
+ let outcome;
8269
8560
  try {
8270
- const languageName = getLanguageName(toLanguageCode) ?? toLanguageCode.toUpperCase();
8271
- const trackName = `${languageName} (auto-translated)`;
8272
- uploadedTrackId = await createTextTrackOnMux(
8561
+ outcome = await replaceAndCreateTextTrack({
8273
8562
  assetId,
8274
- toLanguageCode,
8275
- trackName,
8563
+ target: outputTrack,
8564
+ policy: replaceExistingTracks,
8276
8565
  presignedUrl,
8566
+ passthrough: trackPassthrough,
8277
8567
  credentials
8278
- );
8568
+ });
8279
8569
  } catch (error) {
8280
- console.warn(`Failed to add track to Mux asset: ${error instanceof Error ? error.message : "Unknown error"}`);
8570
+ wrapError(error, "Failed to add translated track to Mux asset");
8571
+ }
8572
+ if (outcome.kind !== "created") {
8573
+ const deletedNote = outcome.deleted.length > 0 ? ` Tracks already deleted before the failure: ${outcome.deleted.map((track) => `${track.name ?? track.id} (${track.id})`).join(", ")}.` : "";
8574
+ if (outcome.kind === "blocked") {
8575
+ throw new MuxAiError(`${outcome.reason}${deletedNote}`, { type: "validation_error" });
8576
+ }
8577
+ throw new Error(`Failed to add translated track to Mux asset: ${outcome.reason}.${deletedNote}`);
8281
8578
  }
8579
+ uploadedTrackId = outcome.trackId;
8580
+ replacedTracks = outcome.deleted;
8282
8581
  }
8283
8582
  }
8284
8583
  return {
@@ -8291,6 +8590,7 @@ async function translateCaptionsInternal(assetId, trackId, toLanguageCode, optio
8291
8590
  originalVtt: vttContent,
8292
8591
  translatedVtt,
8293
8592
  uploadedTrackId,
8593
+ replacedTracks,
8294
8594
  presignedUrl,
8295
8595
  usage: usageWithMetadata,
8296
8596
  safety,