@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-UhRs9t1h.d.ts → index-C3PW73qH.d.ts} +200 -6
- package/dist/index.d.ts +2 -2
- package/dist/index.js +426 -86
- package/dist/index.js.map +1 -1
- package/dist/workflows/index.d.ts +1 -1
- package/dist/workflows/index.js +388 -61
- package/dist/workflows/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export {
|
|
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';
|
package/dist/workflows/index.js
CHANGED
|
@@ -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}
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
7660
|
+
let outcome;
|
|
7377
7661
|
try {
|
|
7378
|
-
|
|
7379
|
-
|
|
7380
|
-
|
|
7381
|
-
|
|
7382
|
-
|
|
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
|
-
|
|
7385
|
-
|
|
7386
|
-
|
|
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
|
|
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
|
-
|
|
7410
|
-
|
|
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
|
-
|
|
8244
|
-
const trackName = `${languageName} (auto-translated)`;
|
|
8245
|
-
uploadedTrackId = await createTextTrackOnMux(
|
|
8561
|
+
outcome = await replaceAndCreateTextTrack({
|
|
8246
8562
|
assetId,
|
|
8247
|
-
|
|
8248
|
-
|
|
8563
|
+
target: outputTrack,
|
|
8564
|
+
policy: replaceExistingTracks,
|
|
8249
8565
|
presignedUrl,
|
|
8566
|
+
passthrough: trackPassthrough,
|
|
8250
8567
|
credentials
|
|
8251
|
-
);
|
|
8568
|
+
});
|
|
8252
8569
|
} catch (error) {
|
|
8253
|
-
|
|
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,
|