@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.
@@ -1,4 +1,4 @@
1
- export { A as ASK_QUESTIONS_DEFAULT_MAX_ANSWER_OPTION_LENGTH, j as ASK_QUESTIONS_DEFAULT_MAX_FREE_FORM_ANSWER_LENGTH, k as ASK_QUESTIONS_MAX_QUESTIONS_PER_CALL, l as ASK_QUESTIONS_MAX_QUESTION_LENGTH, m as AskQuestionsOptions, n as AskQuestionsResult, o as AskQuestionsType, p as AudioTranslationOptions, q as AudioTranslationResult, r as AutoCensorProfanityOptions, B as BurnedInCaptionsAnalysis, s as BurnedInCaptionsOptions, u as BurnedInCaptionsPromptOverrides, v as BurnedInCaptionsPromptSections, w as BurnedInCaptionsResult, C as CaptionReplacement, x as CensorMode, y as Chapter, z as ChapterSystemPromptSections, D as ChaptersOptions, E as ChaptersPromptOverrides, F as ChaptersPromptSections, G as ChaptersResult, H as ChaptersType, J as DEFAULT_DESCRIPTION_LENGTH, K as DEFAULT_SUMMARY_KEYWORD_LIMIT, M as DEFAULT_TITLE_LENGTH, N as EditCaptionsOptions, O as EditCaptionsResult, P as EmbeddingsOptions, Q as EmbeddingsResult, R as EngagementInsightsOptions, T as EngagementInsightsResult, U as GOOGLE_VISION_LIKELIHOOD_TO_SCORE, V as HIVE_SEXUAL_CATEGORIES, W as HIVE_VIOLENCE_CATEGORIES, X as HiveModerationOutput, Y as HiveModerationSource, Z as ModerationOptions, _ as ModerationProvider, $ as ModerationResult, a0 as MomentInsight, a1 as OverallInsight, a2 as ProfanityDetectionPayload, a3 as Question, a4 as QuestionAnswer, a5 as QuestionAnswerType, a6 as ReplacementRecord, a7 as SpeakerReplacement, a8 as StaticRenditionCleanupOutcome, a9 as SummarizationOptions, aa as SummarizationPromptOverrides, ab as SummarizationPromptSections, ac as SummaryAndTagsResult, ad as SummaryType, ae as ThumbnailModerationScore, af as TranslationChunkingOptions, ag as TranslationOptions, ah as TranslationPayload, ai as TranslationResult, aj as aggregateTokenUsage, ak as applyOverrideLists, al as applyReplacements, am as applySpeakerReplacements, an as askQuestions, ao as buildReplacementRegex, ap as burnedInCaptionsSchema, aq as censorVttContent, ar as chapterSchema, as as chaptersSchema, at as createReplacer, au as editCaptions, av as extractTranscriptSegmentsForHotspots, aw as generateChapters, ax as generateEmbeddings, ay as generateEngagementInsights, az as generateVideoEmbeddings, aA as getModerationScores, aB as getSummaryAndTags, aC as hasBurnedInCaptions, aD as mapShotsToHotspots, aE as normalizeTranslatedVtt, aF as profanityDetectionSchema, aG as questionAnswerSchema, aH as shouldSplitChunkTranslationError, aI as summarySchema, aJ as transformCueText, aK as translateAudio, aL as translateCaptions, aM as translationSchema, aN as validateNeverTranslateTerms, aO as verifyNeverTranslateTerms } from '../index-UhRs9t1h.js';
1
+ export { E as ASK_QUESTIONS_DEFAULT_MAX_ANSWER_OPTION_LENGTH, F as ASK_QUESTIONS_DEFAULT_MAX_FREE_FORM_ANSWER_LENGTH, G as ASK_QUESTIONS_MAX_QUESTIONS_PER_CALL, H as ASK_QUESTIONS_MAX_QUESTION_LENGTH, J as AskQuestionsOptions, K as AskQuestionsResult, N as AskQuestionsType, O as AudioTranslationOptions, P as AudioTranslationResult, Q as AutoCensorProfanityOptions, U as BurnedInCaptionsAnalysis, V as BurnedInCaptionsOptions, W as BurnedInCaptionsPromptOverrides, X as BurnedInCaptionsPromptSections, Y as BurnedInCaptionsResult, Z as CaptionReplacement, _ as CensorMode, $ as Chapter, a0 as ChapterSystemPromptSections, a1 as ChaptersOptions, a2 as ChaptersPromptOverrides, a3 as ChaptersPromptSections, a4 as ChaptersResult, a5 as ChaptersType, a6 as DEFAULT_DESCRIPTION_LENGTH, a7 as DEFAULT_SUMMARY_KEYWORD_LIMIT, a8 as DEFAULT_TITLE_LENGTH, a9 as EditCaptionsOptions, aa as EditCaptionsResult, ab as EmbeddingsOptions, ac as EmbeddingsResult, ad as EngagementInsightsOptions, ae as EngagementInsightsResult, af as GOOGLE_VISION_LIKELIHOOD_TO_SCORE, ag as HIVE_SEXUAL_CATEGORIES, ah as HIVE_VIOLENCE_CATEGORIES, ai as HiveModerationOutput, aj as HiveModerationSource, ak as ModerationOptions, al as ModerationProvider, am as ModerationResult, an as MomentInsight, ao as OverallInsight, ap as ProfanityDetectionPayload, aq as Question, ar as QuestionAnswer, as as QuestionAnswerType, at as ReplacementRecord, au as SpeakerReplacement, av as StaticRenditionCleanupOutcome, aw as SummarizationOptions, ax as SummarizationPromptOverrides, ay as SummarizationPromptSections, az as SummaryAndTagsResult, aA as SummaryType, aB as ThumbnailModerationScore, aC as TranslationChunkingOptions, aD as TranslationOptions, aE as TranslationPayload, aF as TranslationResult, aG as aggregateTokenUsage, aH as applyOverrideLists, aI as applyReplacements, aJ as applySpeakerReplacements, aK as askQuestions, aL as buildReplacementRegex, aM as burnedInCaptionsSchema, aN as censorVttContent, aO as chapterSchema, aP as chaptersSchema, aQ as createReplacer, aR as editCaptions, aS as extractTranscriptSegmentsForHotspots, aT as generateChapters, aU as generateEmbeddings, aV as generateEngagementInsights, aW as generateVideoEmbeddings, aX as getModerationScores, aY as getSummaryAndTags, aZ as hasBurnedInCaptions, a_ as mapShotsToHotspots, a$ as normalizeTranslatedVtt, b0 as profanityDetectionSchema, b1 as questionAnswerSchema, b2 as shouldSplitChunkTranslationError, b3 as summarySchema, b4 as transformCueText, b5 as translateAudio, b6 as translateCaptions, b7 as translationSchema, b8 as validateNeverTranslateTerms, b9 as verifyNeverTranslateTerms } from '../index-C3PW73qH.js';
2
2
  import 'zod';
3
3
  import '@ai-sdk/anthropic';
4
4
  import '@ai-sdk/google';
@@ -3779,6 +3779,129 @@ import { generateText as generateText4, Output as Output4 } from "ai";
3779
3779
  import { z as z6 } from "zod";
3780
3780
 
3781
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
+ }
3782
3905
  async function fetchVttFromMux(vttUrl) {
3783
3906
  "use step";
3784
3907
  const vttResponse = await fetch(vttUrl);
@@ -3787,7 +3910,7 @@ async function fetchVttFromMux(vttUrl) {
3787
3910
  }
3788
3911
  return vttResponse.text();
3789
3912
  }
3790
- async function createTextTrackOnMux(assetId, languageCode, trackName, presignedUrl, credentials) {
3913
+ async function createTextTrackOnMux(assetId, languageCode, trackName, presignedUrl, credentials, options = {}) {
3791
3914
  "use step";
3792
3915
  const muxClient = await resolveMuxClient(credentials);
3793
3916
  const mux = await muxClient.createClient();
@@ -3796,13 +3919,92 @@ async function createTextTrackOnMux(assetId, languageCode, trackName, presignedU
3796
3919
  text_type: "subtitles",
3797
3920
  language_code: languageCode,
3798
3921
  name: trackName,
3799
- url: presignedUrl
3922
+ url: presignedUrl,
3923
+ closed_captions: options.closedCaptions,
3924
+ passthrough: options.passthrough
3800
3925
  });
3801
3926
  if (!trackResponse.id) {
3802
3927
  throw new Error("Failed to create text track: no track ID returned from Mux");
3803
3928
  }
3804
3929
  return trackResponse.id;
3805
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
+ }
3806
4008
 
3807
4009
  // src/lib/s3-sigv4.ts
3808
4010
  var AWS4_ALGORITHM = "AWS4-HMAC-SHA256";
@@ -4294,6 +4496,7 @@ async function uploadEditedVttToS3({
4294
4496
  editedVtt,
4295
4497
  assetId,
4296
4498
  trackId,
4499
+ variant = "edited",
4297
4500
  s3Endpoint,
4298
4501
  s3Region,
4299
4502
  s3Bucket,
@@ -4303,7 +4506,7 @@ async function uploadEditedVttToS3({
4303
4506
  "use step";
4304
4507
  const s3AccessKeyId = env_default.S3_ACCESS_KEY_ID;
4305
4508
  const s3SecretAccessKey = env_default.S3_SECRET_ACCESS_KEY;
4306
- const vttKey = `edited/${assetId}/${trackId}-edited-${Date.now()}.vtt`;
4509
+ const vttKey = `edited/${assetId}/${trackId}-${variant}-${Date.now()}.vtt`;
4307
4510
  await putObjectWithStorageAdapter({
4308
4511
  accessKeyId: s3AccessKeyId,
4309
4512
  secretAccessKey: s3SecretAccessKey,
@@ -4353,6 +4556,9 @@ async function editCaptionsInternal(assetId, trackId, options, collectedUsage) {
4353
4556
  s3Region: providedS3Region,
4354
4557
  s3Bucket: providedS3Bucket,
4355
4558
  trackNameSuffix,
4559
+ replaceExistingTracks: replaceExistingTracksOption,
4560
+ trackName: providedTrackName,
4561
+ trackPassthrough: providedTrackPassthrough,
4356
4562
  storageAdapter,
4357
4563
  credentials
4358
4564
  } = options;
@@ -4365,6 +4571,21 @@ async function editCaptionsInternal(assetId, trackId, options, collectedUsage) {
4365
4571
  if (autoCensorOption && !provider) {
4366
4572
  throw new MuxAiError("provider is required when using autoCensorProfanity.", { type: "validation_error" });
4367
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");
4368
4589
  const deleteOriginal = deleteOriginalTrack !== false;
4369
4590
  const uploadToMux = uploadToMuxOption !== false;
4370
4591
  const uploadToS3 = uploadToS3Option || uploadToMux;
@@ -4407,6 +4628,25 @@ async function editCaptionsInternal(assetId, trackId, options, collectedUsage) {
4407
4628
  { type: "validation_error" }
4408
4629
  );
4409
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
+ }
4410
4650
  const vttUrl = await buildTranscriptUrl(playbackId, trackId, policy === "signed", credentials);
4411
4651
  let vttContent;
4412
4652
  try {
@@ -4484,32 +4724,32 @@ async function editCaptionsInternal(assetId, trackId, options, collectedUsage) {
4484
4724
  } : void 0;
4485
4725
  let presignedUrl;
4486
4726
  let uploadedTrackId;
4727
+ let replacedTracks;
4487
4728
  if (uploadToS3) {
4729
+ const s3Config = {
4730
+ assetId,
4731
+ trackId,
4732
+ s3Endpoint,
4733
+ s3Region,
4734
+ s3Bucket,
4735
+ storageAdapter,
4736
+ s3SignedUrlExpirySeconds: options.s3SignedUrlExpirySeconds
4737
+ };
4488
4738
  try {
4489
- presignedUrl = await uploadEditedVttToS3({
4490
- editedVtt,
4491
- assetId,
4492
- trackId,
4493
- s3Endpoint,
4494
- s3Region,
4495
- s3Bucket,
4496
- storageAdapter,
4497
- s3SignedUrlExpirySeconds: options.s3SignedUrlExpirySeconds
4498
- });
4739
+ presignedUrl = await uploadEditedVttToS3({ editedVtt, ...s3Config });
4499
4740
  } catch (error) {
4500
4741
  wrapError(error, "Failed to upload VTT to S3");
4501
4742
  }
4502
- if (uploadToMux) {
4743
+ if (uploadToMux && legacyTrackNaming) {
4503
4744
  try {
4504
- const languageCode = sourceTrack.language_code || "en";
4505
- const suffix = trackNameSuffix ?? "edited";
4506
- const trackName = `${sourceTrack.name || "Subtitles"} (${suffix})`;
4745
+ const trackName = `${sourceName} (${trackNameSuffix ?? "edited"})`;
4507
4746
  uploadedTrackId = await createTextTrackOnMux(
4508
4747
  assetId,
4509
- languageCode,
4748
+ sourceLanguageCode,
4510
4749
  trackName,
4511
4750
  presignedUrl,
4512
- credentials
4751
+ credentials,
4752
+ { closedCaptions: sourceTrack.closed_captions, passthrough: trackPassthrough }
4513
4753
  );
4514
4754
  } catch (error) {
4515
4755
  wrapError(error, "Failed to add edited track to Mux asset");
@@ -4521,6 +4761,48 @@ async function editCaptionsInternal(assetId, trackId, options, collectedUsage) {
4521
4761
  wrapError(error, "Failed to delete original track");
4522
4762
  }
4523
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;
4524
4806
  }
4525
4807
  }
4526
4808
  return {
@@ -4533,6 +4815,7 @@ async function editCaptionsInternal(assetId, trackId, options, collectedUsage) {
4533
4815
  replacements: replacementsResult,
4534
4816
  speakerReplacements: speakerReplacementsResult,
4535
4817
  uploadedTrackId,
4818
+ replacedTracks,
4536
4819
  presignedUrl,
4537
4820
  usage: usageWithMetadata,
4538
4821
  safety: safety.report()
@@ -7220,23 +7503,6 @@ async function downloadAndUploadDubTranscript({
7220
7503
  console.warn(`\u2705 Dub transcript uploaded successfully to: ${vttKey}`);
7221
7504
  return presignedUrl;
7222
7505
  }
7223
- async function createAudioTrackOnMux(assetId, languageCode, presignedUrl, credentials) {
7224
- "use step";
7225
- const muxClient = await resolveMuxClient(credentials);
7226
- const mux = await muxClient.createClient();
7227
- const languageName = new Intl.DisplayNames(["en"], { type: "language" }).of(languageCode) || languageCode.toUpperCase();
7228
- const trackName = `${languageName} (auto-dubbed)`;
7229
- const trackResponse = await mux.video.assets.createTrack(assetId, {
7230
- type: "audio",
7231
- language_code: languageCode,
7232
- name: trackName,
7233
- url: presignedUrl
7234
- });
7235
- if (!trackResponse.id) {
7236
- throw new Error("Failed to create audio track: no track ID returned from Mux");
7237
- }
7238
- return trackResponse.id;
7239
- }
7240
7506
  async function translateAudio(assetId, toLanguageCode, options = {}) {
7241
7507
  "use workflow";
7242
7508
  const {
@@ -7249,15 +7515,23 @@ async function translateAudio(assetId, toLanguageCode, options = {}) {
7249
7515
  uploadCaptionsToMux = false,
7250
7516
  staticRenditionCleanup: staticRenditionCleanupOption = "delete",
7251
7517
  storageAdapter,
7252
- credentials: providedCredentials
7518
+ credentials: providedCredentials,
7519
+ replaceExistingTracks = "fail",
7520
+ trackName: providedTrackName,
7521
+ trackPassthrough: providedTrackPassthrough
7253
7522
  } = options;
7254
7523
  if (provider !== "elevenlabs") {
7255
7524
  throw new MuxAiError("Only ElevenLabs provider is currently supported for audio translation.", { type: "validation_error" });
7256
7525
  }
7257
7526
  const credentials = providedCredentials;
7258
7527
  const effectiveStorageAdapter = storageAdapter;
7528
+ const trackPassthrough = validateTrackPassthrough(providedTrackPassthrough) ?? buildMuxAiTrackPassthrough("translate-audio");
7259
7529
  const uploadToMux = uploadToMuxOption !== false;
7260
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 };
7261
7535
  const s3Endpoint = options.s3Endpoint ?? env_default.S3_ENDPOINT;
7262
7536
  const s3Region = options.s3Region ?? env_default.S3_REGION ?? "auto";
7263
7537
  const s3Bucket = options.s3Bucket ?? env_default.S3_BUCKET;
@@ -7268,6 +7542,15 @@ async function translateAudio(assetId, toLanguageCode, options = {}) {
7268
7542
  }
7269
7543
  const { asset: initialAsset, playbackId, policy } = await getPlaybackIdForAsset(assetId, credentials, options.assetSnapshot);
7270
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
+ }
7271
7554
  let currentAsset = initialAsset;
7272
7555
  let createdStaticRenditionId;
7273
7556
  if (!hasReadyAudioStaticRendition(currentAsset)) {
@@ -7283,6 +7566,7 @@ async function translateAudio(assetId, toLanguageCode, options = {}) {
7283
7566
  let dubbingId;
7284
7567
  let presignedUrl;
7285
7568
  let uploadedTrackId;
7569
+ let replacedTracks;
7286
7570
  let captionsPresignedUrl;
7287
7571
  let captionsTrackId;
7288
7572
  try {
@@ -7373,18 +7657,29 @@ async function translateAudio(assetId, toLanguageCode, options = {}) {
7373
7657
  }
7374
7658
  if (uploadToMux) {
7375
7659
  console.warn("\u{1F4F9} Adding dubbed audio track to Mux asset...");
7376
- const muxLangCode = toISO639_1(toLanguageCode);
7660
+ let outcome;
7377
7661
  try {
7378
- uploadedTrackId = await createAudioTrackOnMux(assetId, muxLangCode, presignedUrl, credentials);
7379
- const languageName = new Intl.DisplayNames(["en"], { type: "language" }).of(muxLangCode) || muxLangCode.toUpperCase();
7380
- const trackName = `${languageName} (auto-dubbed)`;
7381
- console.warn(`\u2705 Track added to Mux asset with ID: ${uploadedTrackId}`);
7382
- 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
+ });
7383
7670
  } catch (error) {
7384
- console.warn(`\u26A0\uFE0F Failed to add audio track to Mux asset: ${error instanceof Error ? error.message : "Unknown error"}`);
7385
- console.warn("\u{1F517} You can manually add the track using this presigned URL:");
7386
- 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" });
7387
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}"`);
7388
7683
  }
7389
7684
  try {
7390
7685
  console.warn("\u{1F4E5} Downloading dub transcript from ElevenLabs and staging to S3...");
@@ -7402,15 +7697,21 @@ async function translateAudio(assetId, toLanguageCode, options = {}) {
7402
7697
  });
7403
7698
  if (uploadCaptionsToMux) {
7404
7699
  console.warn("\u{1F4F9} Adding dubbed captions track to Mux asset...");
7405
- const muxLangCode = toISO639_1(toLanguageCode);
7406
- const languageName = new Intl.DisplayNames(["en"], { type: "language" }).of(muxLangCode) || muxLangCode.toUpperCase();
7407
- captionsTrackId = await createTextTrackOnMux(
7700
+ const captionsOutcome = await replaceAndCreateTrack({
7408
7701
  assetId,
7409
- muxLangCode,
7410
- `${languageName} (auto-dubbed)`,
7411
- captionsPresignedUrl,
7702
+ target: captionsTrackTarget,
7703
+ policy: replaceExistingTracks,
7704
+ presignedUrl: captionsPresignedUrl,
7705
+ passthrough: trackPassthrough,
7412
7706
  credentials
7413
- );
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;
7414
7715
  console.warn(`\u2705 Captions track added to Mux asset with ID: ${captionsTrackId}`);
7415
7716
  }
7416
7717
  } catch (error) {
@@ -7441,6 +7742,7 @@ async function translateAudio(assetId, toLanguageCode, options = {}) {
7441
7742
  dubbingId,
7442
7743
  uploadedTrackId,
7443
7744
  presignedUrl,
7745
+ replacedTracks,
7444
7746
  captionsTrackId,
7445
7747
  captionsPresignedUrl,
7446
7748
  createdStaticRenditionId,
@@ -8110,11 +8412,15 @@ async function translateCaptionsInternal(assetId, trackId, toLanguageCode, optio
8110
8412
  storageAdapter,
8111
8413
  credentials: providedCredentials,
8112
8414
  chunking,
8113
- neverTranslate: neverTranslateOption
8415
+ neverTranslate: neverTranslateOption,
8416
+ replaceExistingTracks = "fail",
8417
+ trackName: providedTrackName,
8418
+ trackPassthrough: providedTrackPassthrough
8114
8419
  } = options;
8115
8420
  const credentials = providedCredentials;
8116
8421
  const effectiveStorageAdapter = storageAdapter;
8117
8422
  const neverTranslateTerms = neverTranslateOption ? validateNeverTranslateTerms(neverTranslateOption) : [];
8423
+ const trackPassthrough = validateTrackPassthrough(providedTrackPassthrough) ?? buildMuxAiTrackPassthrough("translate-captions");
8118
8424
  const s3Endpoint = providedS3Endpoint ?? env_default.S3_ENDPOINT;
8119
8425
  const s3Region = providedS3Region ?? env_default.S3_REGION ?? "auto";
8120
8426
  const s3Bucket = providedS3Bucket ?? env_default.S3_BUCKET;
@@ -8155,6 +8461,16 @@ async function translateCaptionsInternal(assetId, trackId, toLanguageCode, optio
8155
8461
  { type: "validation_error" }
8156
8462
  );
8157
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
+ }
8158
8474
  const vttUrl = await buildTranscriptUrl(playbackId, trackId, policy === "signed", credentials);
8159
8475
  let vttContent;
8160
8476
  try {
@@ -8222,6 +8538,7 @@ async function translateCaptionsInternal(assetId, trackId, toLanguageCode, optio
8222
8538
  const targetLanguage = getLanguageCodePair(toLanguageCode);
8223
8539
  let presignedUrl;
8224
8540
  let uploadedTrackId;
8541
+ let replacedTracks;
8225
8542
  if (uploadToS3) {
8226
8543
  try {
8227
8544
  presignedUrl = await uploadVttToS3({
@@ -8239,19 +8556,28 @@ async function translateCaptionsInternal(assetId, trackId, toLanguageCode, optio
8239
8556
  wrapError(error, "Failed to upload VTT to S3");
8240
8557
  }
8241
8558
  if (uploadToMux) {
8559
+ let outcome;
8242
8560
  try {
8243
- const languageName = getLanguageName(toLanguageCode) ?? toLanguageCode.toUpperCase();
8244
- const trackName = `${languageName} (auto-translated)`;
8245
- uploadedTrackId = await createTextTrackOnMux(
8561
+ outcome = await replaceAndCreateTextTrack({
8246
8562
  assetId,
8247
- toLanguageCode,
8248
- trackName,
8563
+ target: outputTrack,
8564
+ policy: replaceExistingTracks,
8249
8565
  presignedUrl,
8566
+ passthrough: trackPassthrough,
8250
8567
  credentials
8251
- );
8568
+ });
8252
8569
  } catch (error) {
8253
- 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}`);
8254
8578
  }
8579
+ uploadedTrackId = outcome.trackId;
8580
+ replacedTracks = outcome.deleted;
8255
8581
  }
8256
8582
  }
8257
8583
  return {
@@ -8264,6 +8590,7 @@ async function translateCaptionsInternal(assetId, trackId, toLanguageCode, optio
8264
8590
  originalVtt: vttContent,
8265
8591
  translatedVtt,
8266
8592
  uploadedTrackId,
8593
+ replacedTracks,
8267
8594
  presignedUrl,
8268
8595
  usage: usageWithMetadata,
8269
8596
  safety,