@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
package/dist/index.js
CHANGED
|
@@ -5,7 +5,7 @@ var __export = (target, all) => {
|
|
|
5
5
|
};
|
|
6
6
|
|
|
7
7
|
// package.json
|
|
8
|
-
var version = "0.
|
|
8
|
+
var version = "0.41.0";
|
|
9
9
|
|
|
10
10
|
// src/lib/language-codes.ts
|
|
11
11
|
var ISO639_1_TO_3 = {
|
|
@@ -1113,6 +1113,234 @@ async function resolveMuxSigningContext(credentials) {
|
|
|
1113
1113
|
return { keyId, keySecret };
|
|
1114
1114
|
}
|
|
1115
1115
|
|
|
1116
|
+
// src/lib/mux-tracks.ts
|
|
1117
|
+
var MUX_TRACK_PASSTHROUGH_MAX_CHARS = 255;
|
|
1118
|
+
function normalizeTrackLanguageCode(code) {
|
|
1119
|
+
if (!code) {
|
|
1120
|
+
return void 0;
|
|
1121
|
+
}
|
|
1122
|
+
const primarySubtag = code.trim().toLowerCase().split(/[-_]/)[0];
|
|
1123
|
+
if (!primarySubtag || primarySubtag === "auto" || isUndeterminedLanguageCode(primarySubtag)) {
|
|
1124
|
+
return void 0;
|
|
1125
|
+
}
|
|
1126
|
+
return toISO639_1(primarySubtag);
|
|
1127
|
+
}
|
|
1128
|
+
function normalizeTrackName(name) {
|
|
1129
|
+
const normalized = name?.trim().toLowerCase();
|
|
1130
|
+
return normalized || void 0;
|
|
1131
|
+
}
|
|
1132
|
+
function isMuxGeneratedTextTrack(track) {
|
|
1133
|
+
return track.text_source === "generated_vod" || track.text_source === "generated_live" || track.text_source === "generated_live_final";
|
|
1134
|
+
}
|
|
1135
|
+
function targetType(target) {
|
|
1136
|
+
return target.type ?? "text";
|
|
1137
|
+
}
|
|
1138
|
+
function listTracks(asset, type) {
|
|
1139
|
+
return (asset.tracks ?? []).filter((track) => track.type === type && track.status !== "deleted" && !!track.id);
|
|
1140
|
+
}
|
|
1141
|
+
function summarizeTextTrack(track) {
|
|
1142
|
+
return {
|
|
1143
|
+
id: track.id,
|
|
1144
|
+
type: track.type === "audio" ? "audio" : "text",
|
|
1145
|
+
name: track.name,
|
|
1146
|
+
languageCode: track.language_code,
|
|
1147
|
+
status: track.status,
|
|
1148
|
+
textSource: track.text_source,
|
|
1149
|
+
passthrough: track.passthrough,
|
|
1150
|
+
...track.type === "audio" ? { primary: track.primary === true } : {}
|
|
1151
|
+
};
|
|
1152
|
+
}
|
|
1153
|
+
function findSameLanguageTextTracks(asset, target) {
|
|
1154
|
+
const targetLanguage = normalizeTrackLanguageCode(target.languageCode);
|
|
1155
|
+
if (!targetLanguage) {
|
|
1156
|
+
return [];
|
|
1157
|
+
}
|
|
1158
|
+
const type = targetType(target);
|
|
1159
|
+
return listTracks(asset, type).filter(
|
|
1160
|
+
(track) => (type === "audio" || track.text_type === "subtitles") && normalizeTrackLanguageCode(track.language_code) === targetLanguage
|
|
1161
|
+
);
|
|
1162
|
+
}
|
|
1163
|
+
function findNameCollisionTextTracks(asset, target) {
|
|
1164
|
+
const targetName = normalizeTrackName(target.name);
|
|
1165
|
+
if (!targetName) {
|
|
1166
|
+
return [];
|
|
1167
|
+
}
|
|
1168
|
+
return listTracks(asset, targetType(target)).filter((track) => normalizeTrackName(track.name) === targetName);
|
|
1169
|
+
}
|
|
1170
|
+
function describeTracks(tracks) {
|
|
1171
|
+
return tracks.map((track) => `${track.name ?? "(unnamed)"} [${track.language_code ?? "?"}, ${track.type === "audio" ? track.primary ? "primary audio" : "audio" : track.text_source ?? "unknown source"}, ${track.status ?? "ready"}]`).join("; ");
|
|
1172
|
+
}
|
|
1173
|
+
function planTextTrackReplacement(asset, target, policy, options = {}) {
|
|
1174
|
+
const label = targetType(target) === "audio" ? "Audio" : "Text";
|
|
1175
|
+
const keep = new Set(options.keepTrackIds ?? []);
|
|
1176
|
+
const conflicts = /* @__PURE__ */ new Map();
|
|
1177
|
+
for (const track of [...findSameLanguageTextTracks(asset, target), ...findNameCollisionTextTracks(asset, target)]) {
|
|
1178
|
+
if (!keep.has(track.id)) {
|
|
1179
|
+
conflicts.set(track.id, track);
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
const conflicting = [...conflicts.values()];
|
|
1183
|
+
if (conflicting.length === 0) {
|
|
1184
|
+
return { kind: "clear" };
|
|
1185
|
+
}
|
|
1186
|
+
if (policy === "fail") {
|
|
1187
|
+
return {
|
|
1188
|
+
kind: "blocked",
|
|
1189
|
+
reason: `${label} track(s) already exist for language '${target.languageCode}' or name '${target.name}': ${describeTracks(conflicting)}. Set replaceExistingTracks to replace them.`,
|
|
1190
|
+
tracks: conflicting.map(summarizeTextTrack)
|
|
1191
|
+
};
|
|
1192
|
+
}
|
|
1193
|
+
const primary = conflicting.filter((track) => track.type === "audio" && track.primary === true);
|
|
1194
|
+
if (primary.length > 0) {
|
|
1195
|
+
return {
|
|
1196
|
+
kind: "blocked",
|
|
1197
|
+
reason: `The asset's primary audio track is in the way for language '${target.languageCode}' or name '${target.name}': ${describeTracks(primary)}. Primary audio cannot be replaced; choose a different trackName.`,
|
|
1198
|
+
tracks: primary.map(summarizeTextTrack)
|
|
1199
|
+
};
|
|
1200
|
+
}
|
|
1201
|
+
if (policy === "replace_generated") {
|
|
1202
|
+
const notGenerated = conflicting.filter((track) => track.type !== "text" || !isMuxGeneratedTextTrack(track));
|
|
1203
|
+
if (notGenerated.length > 0) {
|
|
1204
|
+
return {
|
|
1205
|
+
kind: "blocked",
|
|
1206
|
+
reason: `${label} track(s) that are not Mux-generated exist for language '${target.languageCode}' or name '${target.name}': ${describeTracks(notGenerated)}. Use replaceExistingTracks: "replace_all" to replace them.`,
|
|
1207
|
+
tracks: notGenerated.map(summarizeTextTrack)
|
|
1208
|
+
};
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
return { kind: "replace", toDelete: conflicting.map(summarizeTextTrack) };
|
|
1212
|
+
}
|
|
1213
|
+
function isDuplicateTrackNameError(error) {
|
|
1214
|
+
if (typeof error !== "object" || error === null) {
|
|
1215
|
+
return false;
|
|
1216
|
+
}
|
|
1217
|
+
const record = error;
|
|
1218
|
+
if (record.status !== 400) {
|
|
1219
|
+
return false;
|
|
1220
|
+
}
|
|
1221
|
+
const messages = Array.isArray(record.error?.messages) ? record.error.messages : [];
|
|
1222
|
+
const haystack = [...messages, record.message].filter((value) => typeof value === "string").map((value) => value.toLowerCase());
|
|
1223
|
+
return haystack.some((value) => value.includes("is not unique"));
|
|
1224
|
+
}
|
|
1225
|
+
function isNotFoundError(error) {
|
|
1226
|
+
return typeof error === "object" && error !== null && error.status === 404;
|
|
1227
|
+
}
|
|
1228
|
+
function buildMuxAiTrackPassthrough(workflow) {
|
|
1229
|
+
return JSON.stringify({ mux_ai: { workflow } });
|
|
1230
|
+
}
|
|
1231
|
+
function validateTrackPassthrough(passthrough) {
|
|
1232
|
+
if (passthrough !== void 0 && passthrough.length > MUX_TRACK_PASSTHROUGH_MAX_CHARS) {
|
|
1233
|
+
throw new MuxAiError(
|
|
1234
|
+
`Track passthrough must be at most ${MUX_TRACK_PASSTHROUGH_MAX_CHARS} characters (received ${passthrough.length}).`,
|
|
1235
|
+
{ type: "validation_error" }
|
|
1236
|
+
);
|
|
1237
|
+
}
|
|
1238
|
+
return passthrough;
|
|
1239
|
+
}
|
|
1240
|
+
async function fetchVttFromMux(vttUrl) {
|
|
1241
|
+
"use step";
|
|
1242
|
+
const vttResponse = await fetch(vttUrl);
|
|
1243
|
+
if (!vttResponse.ok) {
|
|
1244
|
+
throw new Error(`Failed to fetch VTT file: ${vttResponse.statusText}`);
|
|
1245
|
+
}
|
|
1246
|
+
return vttResponse.text();
|
|
1247
|
+
}
|
|
1248
|
+
async function createTextTrackOnMux(assetId, languageCode, trackName, presignedUrl, credentials, options = {}) {
|
|
1249
|
+
"use step";
|
|
1250
|
+
const muxClient = await resolveMuxClient(credentials);
|
|
1251
|
+
const mux = await muxClient.createClient();
|
|
1252
|
+
const trackResponse = await mux.video.assets.createTrack(assetId, {
|
|
1253
|
+
type: "text",
|
|
1254
|
+
text_type: "subtitles",
|
|
1255
|
+
language_code: languageCode,
|
|
1256
|
+
name: trackName,
|
|
1257
|
+
url: presignedUrl,
|
|
1258
|
+
closed_captions: options.closedCaptions,
|
|
1259
|
+
passthrough: options.passthrough
|
|
1260
|
+
});
|
|
1261
|
+
if (!trackResponse.id) {
|
|
1262
|
+
throw new Error("Failed to create text track: no track ID returned from Mux");
|
|
1263
|
+
}
|
|
1264
|
+
return trackResponse.id;
|
|
1265
|
+
}
|
|
1266
|
+
async function replaceAndCreateTrack(input) {
|
|
1267
|
+
"use step";
|
|
1268
|
+
const muxClient = await resolveMuxClient(input.credentials);
|
|
1269
|
+
const mux = await muxClient.createClient();
|
|
1270
|
+
const type = targetType(input.target);
|
|
1271
|
+
const deleted = [];
|
|
1272
|
+
const failed = (error) => ({
|
|
1273
|
+
kind: "create_failed",
|
|
1274
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
1275
|
+
deleted
|
|
1276
|
+
});
|
|
1277
|
+
const clearConflicts = async () => {
|
|
1278
|
+
let asset;
|
|
1279
|
+
try {
|
|
1280
|
+
asset = await mux.video.assets.retrieve(input.assetId);
|
|
1281
|
+
} catch (error) {
|
|
1282
|
+
if (deleted.length === 0) {
|
|
1283
|
+
throw error;
|
|
1284
|
+
}
|
|
1285
|
+
return failed(error);
|
|
1286
|
+
}
|
|
1287
|
+
const plan = planTextTrackReplacement(asset, input.target, input.policy, { keepTrackIds: input.keepTrackIds });
|
|
1288
|
+
if (plan.kind === "blocked") {
|
|
1289
|
+
return { ...plan, deleted };
|
|
1290
|
+
}
|
|
1291
|
+
if (plan.kind === "replace") {
|
|
1292
|
+
for (const track of plan.toDelete) {
|
|
1293
|
+
try {
|
|
1294
|
+
await mux.video.assets.deleteTrack(input.assetId, track.id);
|
|
1295
|
+
deleted.push(track);
|
|
1296
|
+
} catch (error) {
|
|
1297
|
+
if (!isNotFoundError(error)) {
|
|
1298
|
+
return failed(error);
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
return void 0;
|
|
1304
|
+
};
|
|
1305
|
+
const create = async () => {
|
|
1306
|
+
const track = await mux.video.assets.createTrack(input.assetId, {
|
|
1307
|
+
type,
|
|
1308
|
+
...type === "text" ? { text_type: "subtitles", closed_captions: input.closedCaptions } : {},
|
|
1309
|
+
language_code: input.target.languageCode,
|
|
1310
|
+
name: input.target.name.trim(),
|
|
1311
|
+
url: input.presignedUrl,
|
|
1312
|
+
passthrough: input.passthrough
|
|
1313
|
+
});
|
|
1314
|
+
if (!track.id) {
|
|
1315
|
+
throw new Error(`Failed to create ${type} track: no track ID returned from Mux`);
|
|
1316
|
+
}
|
|
1317
|
+
return track.id;
|
|
1318
|
+
};
|
|
1319
|
+
const stopped = await clearConflicts();
|
|
1320
|
+
if (stopped) {
|
|
1321
|
+
return stopped;
|
|
1322
|
+
}
|
|
1323
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
1324
|
+
try {
|
|
1325
|
+
return { kind: "created", trackId: await create(), deleted };
|
|
1326
|
+
} catch (error) {
|
|
1327
|
+
if (isDuplicateTrackNameError(error) && attempt === 0) {
|
|
1328
|
+
const stoppedOnRetry = await clearConflicts();
|
|
1329
|
+
if (stoppedOnRetry) {
|
|
1330
|
+
return stoppedOnRetry;
|
|
1331
|
+
}
|
|
1332
|
+
continue;
|
|
1333
|
+
}
|
|
1334
|
+
return failed(error);
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
return { kind: "create_failed", reason: "Mux rejected the track name as not unique after retrying.", deleted };
|
|
1338
|
+
}
|
|
1339
|
+
async function replaceAndCreateTextTrack(input) {
|
|
1340
|
+
"use step";
|
|
1341
|
+
return replaceAndCreateTrack({ ...input, target: { ...input.target, type: "text" } });
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1116
1344
|
// node_modules/@workflow/serde/dist/index.js
|
|
1117
1345
|
var WORKFLOW_SERIALIZE = /* @__PURE__ */ Symbol.for("workflow-serialize");
|
|
1118
1346
|
var WORKFLOW_DESERIALIZE = /* @__PURE__ */ Symbol.for("workflow-deserialize");
|
|
@@ -4747,32 +4975,6 @@ async function generateChaptersInternal(assetId, options, collectedUsage) {
|
|
|
4747
4975
|
import { generateText as generateText4, Output as Output4 } from "ai";
|
|
4748
4976
|
import { z as z6 } from "zod";
|
|
4749
4977
|
|
|
4750
|
-
// src/lib/mux-tracks.ts
|
|
4751
|
-
async function fetchVttFromMux(vttUrl) {
|
|
4752
|
-
"use step";
|
|
4753
|
-
const vttResponse = await fetch(vttUrl);
|
|
4754
|
-
if (!vttResponse.ok) {
|
|
4755
|
-
throw new Error(`Failed to fetch VTT file: ${vttResponse.statusText}`);
|
|
4756
|
-
}
|
|
4757
|
-
return vttResponse.text();
|
|
4758
|
-
}
|
|
4759
|
-
async function createTextTrackOnMux(assetId, languageCode, trackName, presignedUrl, credentials) {
|
|
4760
|
-
"use step";
|
|
4761
|
-
const muxClient = await resolveMuxClient(credentials);
|
|
4762
|
-
const mux = await muxClient.createClient();
|
|
4763
|
-
const trackResponse = await mux.video.assets.createTrack(assetId, {
|
|
4764
|
-
type: "text",
|
|
4765
|
-
text_type: "subtitles",
|
|
4766
|
-
language_code: languageCode,
|
|
4767
|
-
name: trackName,
|
|
4768
|
-
url: presignedUrl
|
|
4769
|
-
});
|
|
4770
|
-
if (!trackResponse.id) {
|
|
4771
|
-
throw new Error("Failed to create text track: no track ID returned from Mux");
|
|
4772
|
-
}
|
|
4773
|
-
return trackResponse.id;
|
|
4774
|
-
}
|
|
4775
|
-
|
|
4776
4978
|
// src/lib/storage-adapter.ts
|
|
4777
4979
|
function requireCredentials(accessKeyId, secretAccessKey) {
|
|
4778
4980
|
if (!accessKeyId || !secretAccessKey) {
|
|
@@ -5051,6 +5253,7 @@ async function uploadEditedVttToS3({
|
|
|
5051
5253
|
editedVtt,
|
|
5052
5254
|
assetId,
|
|
5053
5255
|
trackId,
|
|
5256
|
+
variant = "edited",
|
|
5054
5257
|
s3Endpoint,
|
|
5055
5258
|
s3Region,
|
|
5056
5259
|
s3Bucket,
|
|
@@ -5060,7 +5263,7 @@ async function uploadEditedVttToS3({
|
|
|
5060
5263
|
"use step";
|
|
5061
5264
|
const s3AccessKeyId = env_default.S3_ACCESS_KEY_ID;
|
|
5062
5265
|
const s3SecretAccessKey = env_default.S3_SECRET_ACCESS_KEY;
|
|
5063
|
-
const vttKey = `edited/${assetId}/${trackId}
|
|
5266
|
+
const vttKey = `edited/${assetId}/${trackId}-${variant}-${Date.now()}.vtt`;
|
|
5064
5267
|
await putObjectWithStorageAdapter({
|
|
5065
5268
|
accessKeyId: s3AccessKeyId,
|
|
5066
5269
|
secretAccessKey: s3SecretAccessKey,
|
|
@@ -5110,6 +5313,9 @@ async function editCaptionsInternal(assetId, trackId, options, collectedUsage) {
|
|
|
5110
5313
|
s3Region: providedS3Region,
|
|
5111
5314
|
s3Bucket: providedS3Bucket,
|
|
5112
5315
|
trackNameSuffix,
|
|
5316
|
+
replaceExistingTracks: replaceExistingTracksOption,
|
|
5317
|
+
trackName: providedTrackName,
|
|
5318
|
+
trackPassthrough: providedTrackPassthrough,
|
|
5113
5319
|
storageAdapter,
|
|
5114
5320
|
credentials
|
|
5115
5321
|
} = options;
|
|
@@ -5122,6 +5328,21 @@ async function editCaptionsInternal(assetId, trackId, options, collectedUsage) {
|
|
|
5122
5328
|
if (autoCensorOption && !provider) {
|
|
5123
5329
|
throw new MuxAiError("provider is required when using autoCensorProfanity.", { type: "validation_error" });
|
|
5124
5330
|
}
|
|
5331
|
+
const legacyTrackNaming = deleteOriginalTrack !== void 0 || trackNameSuffix !== void 0;
|
|
5332
|
+
if (legacyTrackNaming && (replaceExistingTracksOption !== void 0 || providedTrackName !== void 0)) {
|
|
5333
|
+
throw new MuxAiError(
|
|
5334
|
+
"deleteOriginalTrack and trackNameSuffix are deprecated and cannot be combined with replaceExistingTracks or trackName.",
|
|
5335
|
+
{ type: "validation_error" }
|
|
5336
|
+
);
|
|
5337
|
+
}
|
|
5338
|
+
const replaceExistingTracks = replaceExistingTracksOption ?? "replace_all";
|
|
5339
|
+
if (!legacyTrackNaming && replaceExistingTracks === "fail" && !providedTrackName) {
|
|
5340
|
+
throw new MuxAiError(
|
|
5341
|
+
`trackName is required when replaceExistingTracks is "fail": the edited track cannot reuse the source track's name while the source is kept.`,
|
|
5342
|
+
{ type: "validation_error" }
|
|
5343
|
+
);
|
|
5344
|
+
}
|
|
5345
|
+
const trackPassthrough = validateTrackPassthrough(providedTrackPassthrough) ?? buildMuxAiTrackPassthrough("edit-captions");
|
|
5125
5346
|
const deleteOriginal = deleteOriginalTrack !== false;
|
|
5126
5347
|
const uploadToMux = uploadToMuxOption !== false;
|
|
5127
5348
|
const uploadToS3 = uploadToS3Option || uploadToMux;
|
|
@@ -5164,6 +5385,25 @@ async function editCaptionsInternal(assetId, trackId, options, collectedUsage) {
|
|
|
5164
5385
|
{ type: "validation_error" }
|
|
5165
5386
|
);
|
|
5166
5387
|
}
|
|
5388
|
+
const sourceLanguageCode = sourceTrack.language_code || "en";
|
|
5389
|
+
const sourceName = sourceTrack.name || "Subtitles";
|
|
5390
|
+
const outputTrack = {
|
|
5391
|
+
languageCode: sourceLanguageCode,
|
|
5392
|
+
name: providedTrackName ?? sourceName
|
|
5393
|
+
};
|
|
5394
|
+
const keepTrackIds = replaceExistingTracks === "fail" ? [trackId] : [];
|
|
5395
|
+
if (replaceExistingTracks === "fail" && normalizeTrackName(outputTrack.name) === normalizeTrackName(sourceName)) {
|
|
5396
|
+
throw new MuxAiError(
|
|
5397
|
+
`trackName "${outputTrack.name}" matches the source track's name. Mux track names must be unique; choose a different trackName or use replaceExistingTracks: "replace_all" to replace the source.`,
|
|
5398
|
+
{ type: "validation_error" }
|
|
5399
|
+
);
|
|
5400
|
+
}
|
|
5401
|
+
if (uploadToMux && !legacyTrackNaming) {
|
|
5402
|
+
const plan = planTextTrackReplacement(assetData, outputTrack, replaceExistingTracks, { keepTrackIds });
|
|
5403
|
+
if (plan.kind === "blocked") {
|
|
5404
|
+
throw new MuxAiError(plan.reason, { type: "validation_error" });
|
|
5405
|
+
}
|
|
5406
|
+
}
|
|
5167
5407
|
const vttUrl = await buildTranscriptUrl(playbackId, trackId, policy === "signed", credentials);
|
|
5168
5408
|
let vttContent;
|
|
5169
5409
|
try {
|
|
@@ -5241,32 +5481,32 @@ async function editCaptionsInternal(assetId, trackId, options, collectedUsage) {
|
|
|
5241
5481
|
} : void 0;
|
|
5242
5482
|
let presignedUrl;
|
|
5243
5483
|
let uploadedTrackId;
|
|
5484
|
+
let replacedTracks;
|
|
5244
5485
|
if (uploadToS3) {
|
|
5486
|
+
const s3Config = {
|
|
5487
|
+
assetId,
|
|
5488
|
+
trackId,
|
|
5489
|
+
s3Endpoint,
|
|
5490
|
+
s3Region,
|
|
5491
|
+
s3Bucket,
|
|
5492
|
+
storageAdapter,
|
|
5493
|
+
s3SignedUrlExpirySeconds: options.s3SignedUrlExpirySeconds
|
|
5494
|
+
};
|
|
5245
5495
|
try {
|
|
5246
|
-
presignedUrl = await uploadEditedVttToS3({
|
|
5247
|
-
editedVtt,
|
|
5248
|
-
assetId,
|
|
5249
|
-
trackId,
|
|
5250
|
-
s3Endpoint,
|
|
5251
|
-
s3Region,
|
|
5252
|
-
s3Bucket,
|
|
5253
|
-
storageAdapter,
|
|
5254
|
-
s3SignedUrlExpirySeconds: options.s3SignedUrlExpirySeconds
|
|
5255
|
-
});
|
|
5496
|
+
presignedUrl = await uploadEditedVttToS3({ editedVtt, ...s3Config });
|
|
5256
5497
|
} catch (error) {
|
|
5257
5498
|
wrapError(error, "Failed to upload VTT to S3");
|
|
5258
5499
|
}
|
|
5259
|
-
if (uploadToMux) {
|
|
5500
|
+
if (uploadToMux && legacyTrackNaming) {
|
|
5260
5501
|
try {
|
|
5261
|
-
const
|
|
5262
|
-
const suffix = trackNameSuffix ?? "edited";
|
|
5263
|
-
const trackName = `${sourceTrack.name || "Subtitles"} (${suffix})`;
|
|
5502
|
+
const trackName = `${sourceName} (${trackNameSuffix ?? "edited"})`;
|
|
5264
5503
|
uploadedTrackId = await createTextTrackOnMux(
|
|
5265
5504
|
assetId,
|
|
5266
|
-
|
|
5505
|
+
sourceLanguageCode,
|
|
5267
5506
|
trackName,
|
|
5268
5507
|
presignedUrl,
|
|
5269
|
-
credentials
|
|
5508
|
+
credentials,
|
|
5509
|
+
{ closedCaptions: sourceTrack.closed_captions, passthrough: trackPassthrough }
|
|
5270
5510
|
);
|
|
5271
5511
|
} catch (error) {
|
|
5272
5512
|
wrapError(error, "Failed to add edited track to Mux asset");
|
|
@@ -5278,6 +5518,48 @@ async function editCaptionsInternal(assetId, trackId, options, collectedUsage) {
|
|
|
5278
5518
|
wrapError(error, "Failed to delete original track");
|
|
5279
5519
|
}
|
|
5280
5520
|
}
|
|
5521
|
+
} else if (uploadToMux) {
|
|
5522
|
+
let outcome;
|
|
5523
|
+
try {
|
|
5524
|
+
outcome = await replaceAndCreateTextTrack({
|
|
5525
|
+
assetId,
|
|
5526
|
+
target: outputTrack,
|
|
5527
|
+
policy: replaceExistingTracks,
|
|
5528
|
+
presignedUrl,
|
|
5529
|
+
closedCaptions: sourceTrack.closed_captions,
|
|
5530
|
+
passthrough: trackPassthrough,
|
|
5531
|
+
keepTrackIds,
|
|
5532
|
+
credentials
|
|
5533
|
+
});
|
|
5534
|
+
} catch (error) {
|
|
5535
|
+
wrapError(error, "Failed to add edited track to Mux asset");
|
|
5536
|
+
}
|
|
5537
|
+
if (outcome.kind !== "created") {
|
|
5538
|
+
const sourceWasDeleted = outcome.deleted.some((track) => track.id === trackId);
|
|
5539
|
+
let restoreNote = "";
|
|
5540
|
+
if (sourceWasDeleted) {
|
|
5541
|
+
try {
|
|
5542
|
+
const originalUrl = await uploadEditedVttToS3({ editedVtt: vttContent, variant: "original", ...s3Config });
|
|
5543
|
+
const restoredTrackId = await createTextTrackOnMux(
|
|
5544
|
+
assetId,
|
|
5545
|
+
sourceLanguageCode,
|
|
5546
|
+
sourceName,
|
|
5547
|
+
originalUrl,
|
|
5548
|
+
credentials,
|
|
5549
|
+
{ closedCaptions: sourceTrack.closed_captions, passthrough: sourceTrack.passthrough }
|
|
5550
|
+
);
|
|
5551
|
+
restoreNote = ` The source track was restored as ${restoredTrackId}.`;
|
|
5552
|
+
} catch (restoreError) {
|
|
5553
|
+
restoreNote = ` Restoring the source track also failed: ${restoreError instanceof Error ? restoreError.message : String(restoreError)}.`;
|
|
5554
|
+
}
|
|
5555
|
+
}
|
|
5556
|
+
if (outcome.kind === "blocked") {
|
|
5557
|
+
throw new MuxAiError(`${outcome.reason}${restoreNote}`, { type: "validation_error" });
|
|
5558
|
+
}
|
|
5559
|
+
throw new Error(`Failed to add edited track to Mux asset: ${outcome.reason}.${restoreNote}`);
|
|
5560
|
+
}
|
|
5561
|
+
uploadedTrackId = outcome.trackId;
|
|
5562
|
+
replacedTracks = outcome.deleted;
|
|
5281
5563
|
}
|
|
5282
5564
|
}
|
|
5283
5565
|
return {
|
|
@@ -5290,6 +5572,7 @@ async function editCaptionsInternal(assetId, trackId, options, collectedUsage) {
|
|
|
5290
5572
|
replacements: replacementsResult,
|
|
5291
5573
|
speakerReplacements: speakerReplacementsResult,
|
|
5292
5574
|
uploadedTrackId,
|
|
5575
|
+
replacedTracks,
|
|
5293
5576
|
presignedUrl,
|
|
5294
5577
|
usage: usageWithMetadata,
|
|
5295
5578
|
safety: safety.report()
|
|
@@ -7550,23 +7833,6 @@ async function downloadAndUploadDubTranscript({
|
|
|
7550
7833
|
console.warn(`\u2705 Dub transcript uploaded successfully to: ${vttKey}`);
|
|
7551
7834
|
return presignedUrl;
|
|
7552
7835
|
}
|
|
7553
|
-
async function createAudioTrackOnMux(assetId, languageCode, presignedUrl, credentials) {
|
|
7554
|
-
"use step";
|
|
7555
|
-
const muxClient = await resolveMuxClient(credentials);
|
|
7556
|
-
const mux = await muxClient.createClient();
|
|
7557
|
-
const languageName = new Intl.DisplayNames(["en"], { type: "language" }).of(languageCode) || languageCode.toUpperCase();
|
|
7558
|
-
const trackName = `${languageName} (auto-dubbed)`;
|
|
7559
|
-
const trackResponse = await mux.video.assets.createTrack(assetId, {
|
|
7560
|
-
type: "audio",
|
|
7561
|
-
language_code: languageCode,
|
|
7562
|
-
name: trackName,
|
|
7563
|
-
url: presignedUrl
|
|
7564
|
-
});
|
|
7565
|
-
if (!trackResponse.id) {
|
|
7566
|
-
throw new Error("Failed to create audio track: no track ID returned from Mux");
|
|
7567
|
-
}
|
|
7568
|
-
return trackResponse.id;
|
|
7569
|
-
}
|
|
7570
7836
|
async function translateAudio(assetId, toLanguageCode, options = {}) {
|
|
7571
7837
|
"use workflow";
|
|
7572
7838
|
const {
|
|
@@ -7579,15 +7845,23 @@ async function translateAudio(assetId, toLanguageCode, options = {}) {
|
|
|
7579
7845
|
uploadCaptionsToMux = false,
|
|
7580
7846
|
staticRenditionCleanup: staticRenditionCleanupOption = "delete",
|
|
7581
7847
|
storageAdapter,
|
|
7582
|
-
credentials: providedCredentials
|
|
7848
|
+
credentials: providedCredentials,
|
|
7849
|
+
replaceExistingTracks = "fail",
|
|
7850
|
+
trackName: providedTrackName,
|
|
7851
|
+
trackPassthrough: providedTrackPassthrough
|
|
7583
7852
|
} = options;
|
|
7584
7853
|
if (provider !== "elevenlabs") {
|
|
7585
7854
|
throw new MuxAiError("Only ElevenLabs provider is currently supported for audio translation.", { type: "validation_error" });
|
|
7586
7855
|
}
|
|
7587
7856
|
const credentials = providedCredentials;
|
|
7588
7857
|
const effectiveStorageAdapter = storageAdapter;
|
|
7858
|
+
const trackPassthrough = validateTrackPassthrough(providedTrackPassthrough) ?? buildMuxAiTrackPassthrough("translate-audio");
|
|
7589
7859
|
const uploadToMux = uploadToMuxOption !== false;
|
|
7590
7860
|
const uploadToS3 = uploadToS3Option || uploadToMux || uploadCaptionsToMux;
|
|
7861
|
+
const muxLangCode = toISO639_1(toLanguageCode);
|
|
7862
|
+
const trackName = providedTrackName ?? `${getLanguageName(muxLangCode) ?? muxLangCode.toUpperCase()} (Auto-dubbed)`;
|
|
7863
|
+
const audioTrackTarget = { type: "audio", languageCode: muxLangCode, name: trackName };
|
|
7864
|
+
const captionsTrackTarget = { type: "text", languageCode: muxLangCode, name: trackName };
|
|
7591
7865
|
const s3Endpoint = options.s3Endpoint ?? env_default.S3_ENDPOINT;
|
|
7592
7866
|
const s3Region = options.s3Region ?? env_default.S3_REGION ?? "auto";
|
|
7593
7867
|
const s3Bucket = options.s3Bucket ?? env_default.S3_BUCKET;
|
|
@@ -7598,6 +7872,15 @@ async function translateAudio(assetId, toLanguageCode, options = {}) {
|
|
|
7598
7872
|
}
|
|
7599
7873
|
const { asset: initialAsset, playbackId, policy } = await getPlaybackIdForAsset(assetId, credentials, options.assetSnapshot);
|
|
7600
7874
|
const assetDurationSeconds = getAssetDurationSecondsFromAsset(initialAsset);
|
|
7875
|
+
for (const target of [
|
|
7876
|
+
...uploadToMux ? [audioTrackTarget] : [],
|
|
7877
|
+
...uploadCaptionsToMux ? [captionsTrackTarget] : []
|
|
7878
|
+
]) {
|
|
7879
|
+
const plan = planTextTrackReplacement(initialAsset, target, replaceExistingTracks);
|
|
7880
|
+
if (plan.kind === "blocked") {
|
|
7881
|
+
throw new MuxAiError(plan.reason, { type: "validation_error" });
|
|
7882
|
+
}
|
|
7883
|
+
}
|
|
7601
7884
|
let currentAsset = initialAsset;
|
|
7602
7885
|
let createdStaticRenditionId;
|
|
7603
7886
|
if (!hasReadyAudioStaticRendition(currentAsset)) {
|
|
@@ -7613,6 +7896,7 @@ async function translateAudio(assetId, toLanguageCode, options = {}) {
|
|
|
7613
7896
|
let dubbingId;
|
|
7614
7897
|
let presignedUrl;
|
|
7615
7898
|
let uploadedTrackId;
|
|
7899
|
+
let replacedTracks;
|
|
7616
7900
|
let captionsPresignedUrl;
|
|
7617
7901
|
let captionsTrackId;
|
|
7618
7902
|
try {
|
|
@@ -7703,18 +7987,29 @@ async function translateAudio(assetId, toLanguageCode, options = {}) {
|
|
|
7703
7987
|
}
|
|
7704
7988
|
if (uploadToMux) {
|
|
7705
7989
|
console.warn("\u{1F4F9} Adding dubbed audio track to Mux asset...");
|
|
7706
|
-
|
|
7990
|
+
let outcome;
|
|
7707
7991
|
try {
|
|
7708
|
-
|
|
7709
|
-
|
|
7710
|
-
|
|
7711
|
-
|
|
7712
|
-
|
|
7992
|
+
outcome = await replaceAndCreateTrack({
|
|
7993
|
+
assetId,
|
|
7994
|
+
target: audioTrackTarget,
|
|
7995
|
+
policy: replaceExistingTracks,
|
|
7996
|
+
presignedUrl,
|
|
7997
|
+
passthrough: trackPassthrough,
|
|
7998
|
+
credentials
|
|
7999
|
+
});
|
|
7713
8000
|
} catch (error) {
|
|
7714
|
-
|
|
7715
|
-
|
|
7716
|
-
|
|
8001
|
+
wrapError(error, `Failed to add dubbed audio track to Mux asset (dubbed audio is staged at: ${presignedUrl})`);
|
|
8002
|
+
}
|
|
8003
|
+
if (outcome.kind === "blocked") {
|
|
8004
|
+
throw new MuxAiError(`${outcome.reason} Dubbed audio is staged at: ${presignedUrl}`, { type: "validation_error" });
|
|
8005
|
+
}
|
|
8006
|
+
if (outcome.kind === "create_failed") {
|
|
8007
|
+
throw new Error(`Failed to add dubbed audio track to Mux asset: ${outcome.reason}. Dubbed audio is staged at: ${presignedUrl}`);
|
|
7717
8008
|
}
|
|
8009
|
+
uploadedTrackId = outcome.trackId;
|
|
8010
|
+
replacedTracks = outcome.deleted;
|
|
8011
|
+
console.warn(`\u2705 Track added to Mux asset with ID: ${uploadedTrackId}`);
|
|
8012
|
+
console.warn(`\u{1F4CB} Track name: "${trackName}"`);
|
|
7718
8013
|
}
|
|
7719
8014
|
try {
|
|
7720
8015
|
console.warn("\u{1F4E5} Downloading dub transcript from ElevenLabs and staging to S3...");
|
|
@@ -7732,15 +8027,21 @@ async function translateAudio(assetId, toLanguageCode, options = {}) {
|
|
|
7732
8027
|
});
|
|
7733
8028
|
if (uploadCaptionsToMux) {
|
|
7734
8029
|
console.warn("\u{1F4F9} Adding dubbed captions track to Mux asset...");
|
|
7735
|
-
const
|
|
7736
|
-
const languageName = new Intl.DisplayNames(["en"], { type: "language" }).of(muxLangCode) || muxLangCode.toUpperCase();
|
|
7737
|
-
captionsTrackId = await createTextTrackOnMux(
|
|
8030
|
+
const captionsOutcome = await replaceAndCreateTrack({
|
|
7738
8031
|
assetId,
|
|
7739
|
-
|
|
7740
|
-
|
|
7741
|
-
captionsPresignedUrl,
|
|
8032
|
+
target: captionsTrackTarget,
|
|
8033
|
+
policy: replaceExistingTracks,
|
|
8034
|
+
presignedUrl: captionsPresignedUrl,
|
|
8035
|
+
passthrough: trackPassthrough,
|
|
7742
8036
|
credentials
|
|
7743
|
-
);
|
|
8037
|
+
});
|
|
8038
|
+
if (captionsOutcome.deleted.length > 0) {
|
|
8039
|
+
replacedTracks = [...replacedTracks ?? [], ...captionsOutcome.deleted];
|
|
8040
|
+
}
|
|
8041
|
+
if (captionsOutcome.kind !== "created") {
|
|
8042
|
+
throw new Error(captionsOutcome.reason);
|
|
8043
|
+
}
|
|
8044
|
+
captionsTrackId = captionsOutcome.trackId;
|
|
7744
8045
|
console.warn(`\u2705 Captions track added to Mux asset with ID: ${captionsTrackId}`);
|
|
7745
8046
|
}
|
|
7746
8047
|
} catch (error) {
|
|
@@ -7771,6 +8072,7 @@ async function translateAudio(assetId, toLanguageCode, options = {}) {
|
|
|
7771
8072
|
dubbingId,
|
|
7772
8073
|
uploadedTrackId,
|
|
7773
8074
|
presignedUrl,
|
|
8075
|
+
replacedTracks,
|
|
7774
8076
|
captionsTrackId,
|
|
7775
8077
|
captionsPresignedUrl,
|
|
7776
8078
|
createdStaticRenditionId,
|
|
@@ -8440,11 +8742,15 @@ async function translateCaptionsInternal(assetId, trackId, toLanguageCode, optio
|
|
|
8440
8742
|
storageAdapter,
|
|
8441
8743
|
credentials: providedCredentials,
|
|
8442
8744
|
chunking,
|
|
8443
|
-
neverTranslate: neverTranslateOption
|
|
8745
|
+
neverTranslate: neverTranslateOption,
|
|
8746
|
+
replaceExistingTracks = "fail",
|
|
8747
|
+
trackName: providedTrackName,
|
|
8748
|
+
trackPassthrough: providedTrackPassthrough
|
|
8444
8749
|
} = options;
|
|
8445
8750
|
const credentials = providedCredentials;
|
|
8446
8751
|
const effectiveStorageAdapter = storageAdapter;
|
|
8447
8752
|
const neverTranslateTerms = neverTranslateOption ? validateNeverTranslateTerms(neverTranslateOption) : [];
|
|
8753
|
+
const trackPassthrough = validateTrackPassthrough(providedTrackPassthrough) ?? buildMuxAiTrackPassthrough("translate-captions");
|
|
8448
8754
|
const s3Endpoint = providedS3Endpoint ?? env_default.S3_ENDPOINT;
|
|
8449
8755
|
const s3Region = providedS3Region ?? env_default.S3_REGION ?? "auto";
|
|
8450
8756
|
const s3Bucket = providedS3Bucket ?? env_default.S3_BUCKET;
|
|
@@ -8485,6 +8791,16 @@ async function translateCaptionsInternal(assetId, trackId, toLanguageCode, optio
|
|
|
8485
8791
|
{ type: "validation_error" }
|
|
8486
8792
|
);
|
|
8487
8793
|
}
|
|
8794
|
+
const outputTrack = {
|
|
8795
|
+
languageCode: toLanguageCode,
|
|
8796
|
+
name: providedTrackName ?? `${getLanguageName(toLanguageCode) ?? toLanguageCode.toUpperCase()} (Auto-translated)`
|
|
8797
|
+
};
|
|
8798
|
+
if (uploadToMux) {
|
|
8799
|
+
const plan = planTextTrackReplacement(assetData, outputTrack, replaceExistingTracks);
|
|
8800
|
+
if (plan.kind === "blocked") {
|
|
8801
|
+
throw new MuxAiError(plan.reason, { type: "validation_error" });
|
|
8802
|
+
}
|
|
8803
|
+
}
|
|
8488
8804
|
const vttUrl = await buildTranscriptUrl(playbackId, trackId, policy === "signed", credentials);
|
|
8489
8805
|
let vttContent;
|
|
8490
8806
|
try {
|
|
@@ -8552,6 +8868,7 @@ async function translateCaptionsInternal(assetId, trackId, toLanguageCode, optio
|
|
|
8552
8868
|
const targetLanguage = getLanguageCodePair(toLanguageCode);
|
|
8553
8869
|
let presignedUrl;
|
|
8554
8870
|
let uploadedTrackId;
|
|
8871
|
+
let replacedTracks;
|
|
8555
8872
|
if (uploadToS3) {
|
|
8556
8873
|
try {
|
|
8557
8874
|
presignedUrl = await uploadVttToS3({
|
|
@@ -8569,19 +8886,28 @@ async function translateCaptionsInternal(assetId, trackId, toLanguageCode, optio
|
|
|
8569
8886
|
wrapError(error, "Failed to upload VTT to S3");
|
|
8570
8887
|
}
|
|
8571
8888
|
if (uploadToMux) {
|
|
8889
|
+
let outcome;
|
|
8572
8890
|
try {
|
|
8573
|
-
|
|
8574
|
-
const trackName = `${languageName} (auto-translated)`;
|
|
8575
|
-
uploadedTrackId = await createTextTrackOnMux(
|
|
8891
|
+
outcome = await replaceAndCreateTextTrack({
|
|
8576
8892
|
assetId,
|
|
8577
|
-
|
|
8578
|
-
|
|
8893
|
+
target: outputTrack,
|
|
8894
|
+
policy: replaceExistingTracks,
|
|
8579
8895
|
presignedUrl,
|
|
8896
|
+
passthrough: trackPassthrough,
|
|
8580
8897
|
credentials
|
|
8581
|
-
);
|
|
8898
|
+
});
|
|
8582
8899
|
} catch (error) {
|
|
8583
|
-
|
|
8900
|
+
wrapError(error, "Failed to add translated track to Mux asset");
|
|
8901
|
+
}
|
|
8902
|
+
if (outcome.kind !== "created") {
|
|
8903
|
+
const deletedNote = outcome.deleted.length > 0 ? ` Tracks already deleted before the failure: ${outcome.deleted.map((track) => `${track.name ?? track.id} (${track.id})`).join(", ")}.` : "";
|
|
8904
|
+
if (outcome.kind === "blocked") {
|
|
8905
|
+
throw new MuxAiError(`${outcome.reason}${deletedNote}`, { type: "validation_error" });
|
|
8906
|
+
}
|
|
8907
|
+
throw new Error(`Failed to add translated track to Mux asset: ${outcome.reason}.${deletedNote}`);
|
|
8584
8908
|
}
|
|
8909
|
+
uploadedTrackId = outcome.trackId;
|
|
8910
|
+
replacedTracks = outcome.deleted;
|
|
8585
8911
|
}
|
|
8586
8912
|
}
|
|
8587
8913
|
return {
|
|
@@ -8594,6 +8920,7 @@ async function translateCaptionsInternal(assetId, trackId, toLanguageCode, optio
|
|
|
8594
8920
|
originalVtt: vttContent,
|
|
8595
8921
|
translatedVtt,
|
|
8596
8922
|
uploadedTrackId,
|
|
8923
|
+
replacedTracks,
|
|
8597
8924
|
presignedUrl,
|
|
8598
8925
|
usage: usageWithMetadata,
|
|
8599
8926
|
safety,
|
|
@@ -8601,20 +8928,33 @@ async function translateCaptionsInternal(assetId, trackId, toLanguageCode, optio
|
|
|
8601
8928
|
};
|
|
8602
8929
|
}
|
|
8603
8930
|
export {
|
|
8931
|
+
MUX_TRACK_PASSTHROUGH_MAX_CHARS,
|
|
8604
8932
|
MuxAiError,
|
|
8605
8933
|
WorkflowStorageClient,
|
|
8934
|
+
buildMuxAiTrackPassthrough,
|
|
8606
8935
|
createWorkflowStorageClient,
|
|
8607
8936
|
decryptFromWorkflow,
|
|
8608
8937
|
encryptForWorkflow,
|
|
8938
|
+
findNameCollisionTextTracks,
|
|
8939
|
+
findSameLanguageTextTracks,
|
|
8609
8940
|
getLanguageCodePair,
|
|
8610
8941
|
getLanguageName,
|
|
8942
|
+
isDuplicateTrackNameError,
|
|
8943
|
+
isMuxGeneratedTextTrack,
|
|
8611
8944
|
isUndeterminedLanguageCode,
|
|
8612
8945
|
isValidISO639_1,
|
|
8613
8946
|
isValidISO639_3,
|
|
8947
|
+
normalizeTrackLanguageCode,
|
|
8948
|
+
normalizeTrackName,
|
|
8949
|
+
planTextTrackReplacement,
|
|
8614
8950
|
primitives_exports as primitives,
|
|
8951
|
+
replaceAndCreateTextTrack,
|
|
8952
|
+
replaceAndCreateTrack,
|
|
8615
8953
|
setWorkflowCredentialsProvider,
|
|
8954
|
+
summarizeTextTrack,
|
|
8616
8955
|
toISO639_1,
|
|
8617
8956
|
toISO639_3,
|
|
8957
|
+
validateTrackPassthrough,
|
|
8618
8958
|
version,
|
|
8619
8959
|
workflows_exports as workflows,
|
|
8620
8960
|
wrapError
|