@mux/ai 0.9.0 → 0.10.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-Nxf6BaBO.d.ts → index-C8-E3VR9.d.ts} +59 -4
- package/dist/{index-CkJStzYO.d.ts → index-CA7bG50u.d.ts} +29 -2
- package/dist/index.d.ts +3 -3
- package/dist/index.js +711 -31
- package/dist/index.js.map +1 -1
- package/dist/primitives/index.d.ts +1 -1
- package/dist/primitives/index.js +336 -14
- package/dist/primitives/index.js.map +1 -1
- package/dist/workflows/index.d.ts +1 -1
- package/dist/workflows/index.js +703 -30
- 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.10.0";
|
|
9
9
|
|
|
10
10
|
// src/env.ts
|
|
11
11
|
import { z } from "zod";
|
|
@@ -783,9 +783,14 @@ var primitives_exports = {};
|
|
|
783
783
|
__export(primitives_exports, {
|
|
784
784
|
DEFAULT_STORYBOARD_WIDTH: () => DEFAULT_STORYBOARD_WIDTH,
|
|
785
785
|
buildTranscriptUrl: () => buildTranscriptUrl,
|
|
786
|
+
buildVttFromCueBlocks: () => buildVttFromCueBlocks,
|
|
787
|
+
buildVttFromTranslatedCueBlocks: () => buildVttFromTranslatedCueBlocks,
|
|
786
788
|
chunkByTokens: () => chunkByTokens,
|
|
787
789
|
chunkText: () => chunkText,
|
|
788
790
|
chunkVTTCues: () => chunkVTTCues,
|
|
791
|
+
chunkVTTCuesByBudget: () => chunkVTTCuesByBudget,
|
|
792
|
+
chunkVTTCuesByDuration: () => chunkVTTCuesByDuration,
|
|
793
|
+
concatenateVttSegments: () => concatenateVttSegments,
|
|
789
794
|
estimateTokenCount: () => estimateTokenCount,
|
|
790
795
|
extractTextFromVTT: () => extractTextFromVTT,
|
|
791
796
|
extractTimestampedTranscript: () => extractTimestampedTranscript,
|
|
@@ -801,7 +806,9 @@ __export(primitives_exports, {
|
|
|
801
806
|
getStoryboardUrl: () => getStoryboardUrl,
|
|
802
807
|
getThumbnailUrls: () => getThumbnailUrls,
|
|
803
808
|
parseVTTCues: () => parseVTTCues,
|
|
809
|
+
replaceCueText: () => replaceCueText,
|
|
804
810
|
secondsToTimestamp: () => secondsToTimestamp,
|
|
811
|
+
splitVttPreambleAndCueBlocks: () => splitVttPreambleAndCueBlocks,
|
|
805
812
|
vttTimestampToSeconds: () => vttTimestampToSeconds
|
|
806
813
|
});
|
|
807
814
|
|
|
@@ -1162,6 +1169,14 @@ async function getStoryboardUrl(playbackId, width = DEFAULT_STORYBOARD_WIDTH, sh
|
|
|
1162
1169
|
}
|
|
1163
1170
|
|
|
1164
1171
|
// src/primitives/text-chunking.ts
|
|
1172
|
+
var DEFAULT_MIN_CHUNK_DURATION_RATIO = 2 / 3;
|
|
1173
|
+
var DEFAULT_BOUNDARY_LOOKAHEAD_CUES = 12;
|
|
1174
|
+
var DEFAULT_BOUNDARY_PAUSE_SECONDS = 1.25;
|
|
1175
|
+
var STRONG_BOUNDARY_SCORE = 4;
|
|
1176
|
+
var PREFERRED_BOUNDARY_WINDOW_SECONDS = 5 * 60;
|
|
1177
|
+
var SENTENCE_BOUNDARY_REGEX = /[.!?]["')\]]*$/;
|
|
1178
|
+
var CLAUSE_BOUNDARY_REGEX = /[,;:]["')\]]*$/;
|
|
1179
|
+
var NEXT_SENTENCE_START_REGEX = /^[A-Z0-9"'([{]/;
|
|
1165
1180
|
function estimateTokenCount(text) {
|
|
1166
1181
|
const words = text.trim().split(/\s+/).length;
|
|
1167
1182
|
return Math.ceil(words / 0.75);
|
|
@@ -1234,6 +1249,151 @@ function chunkVTTCues(cues, maxTokens, overlapCues = 2) {
|
|
|
1234
1249
|
}
|
|
1235
1250
|
return chunks;
|
|
1236
1251
|
}
|
|
1252
|
+
function scoreCueBoundary(cues, index, boundaryPauseSeconds) {
|
|
1253
|
+
const cue = cues[index];
|
|
1254
|
+
const nextCue = cues[index + 1];
|
|
1255
|
+
if (!nextCue) {
|
|
1256
|
+
return Number.POSITIVE_INFINITY;
|
|
1257
|
+
}
|
|
1258
|
+
const trimmedText = cue.text.trim();
|
|
1259
|
+
let score = 0;
|
|
1260
|
+
if (SENTENCE_BOUNDARY_REGEX.test(trimmedText)) {
|
|
1261
|
+
score += 4;
|
|
1262
|
+
} else if (CLAUSE_BOUNDARY_REGEX.test(trimmedText)) {
|
|
1263
|
+
score += 2;
|
|
1264
|
+
}
|
|
1265
|
+
if (nextCue.startTime - cue.endTime >= boundaryPauseSeconds) {
|
|
1266
|
+
score += 2;
|
|
1267
|
+
}
|
|
1268
|
+
if (NEXT_SENTENCE_START_REGEX.test(nextCue.text.trim())) {
|
|
1269
|
+
score += 1;
|
|
1270
|
+
}
|
|
1271
|
+
return score;
|
|
1272
|
+
}
|
|
1273
|
+
function chunkVTTCuesByBudget(cues, options) {
|
|
1274
|
+
if (cues.length === 0) {
|
|
1275
|
+
return [];
|
|
1276
|
+
}
|
|
1277
|
+
const maxCuesPerChunk = Math.max(1, options.maxCuesPerChunk);
|
|
1278
|
+
let maxTextTokensPerChunk = Number.POSITIVE_INFINITY;
|
|
1279
|
+
if (options.maxTextTokensPerChunk) {
|
|
1280
|
+
maxTextTokensPerChunk = Math.max(1, options.maxTextTokensPerChunk);
|
|
1281
|
+
}
|
|
1282
|
+
const chunks = [];
|
|
1283
|
+
let chunkIndex = 0;
|
|
1284
|
+
let cueStartIndex = 0;
|
|
1285
|
+
let currentTokenCount = 0;
|
|
1286
|
+
for (let cueIndex = 0; cueIndex < cues.length; cueIndex++) {
|
|
1287
|
+
const cue = cues[cueIndex];
|
|
1288
|
+
const cueTokenCount = estimateTokenCount(cue.text);
|
|
1289
|
+
const currentCueCount = cueIndex - cueStartIndex;
|
|
1290
|
+
const wouldExceedCueCount = currentCueCount >= maxCuesPerChunk;
|
|
1291
|
+
const wouldExceedTokenCount = currentCueCount > 0 && currentTokenCount + cueTokenCount > maxTextTokensPerChunk;
|
|
1292
|
+
if (wouldExceedCueCount || wouldExceedTokenCount) {
|
|
1293
|
+
chunks.push({
|
|
1294
|
+
id: `chunk-${chunkIndex}`,
|
|
1295
|
+
cueStartIndex,
|
|
1296
|
+
cueEndIndex: cueIndex - 1,
|
|
1297
|
+
cueCount: cueIndex - cueStartIndex,
|
|
1298
|
+
startTime: cues[cueStartIndex].startTime,
|
|
1299
|
+
endTime: cues[cueIndex - 1].endTime
|
|
1300
|
+
});
|
|
1301
|
+
cueStartIndex = cueIndex;
|
|
1302
|
+
currentTokenCount = 0;
|
|
1303
|
+
chunkIndex++;
|
|
1304
|
+
}
|
|
1305
|
+
currentTokenCount += cueTokenCount;
|
|
1306
|
+
}
|
|
1307
|
+
chunks.push({
|
|
1308
|
+
id: `chunk-${chunkIndex}`,
|
|
1309
|
+
cueStartIndex,
|
|
1310
|
+
cueEndIndex: cues.length - 1,
|
|
1311
|
+
cueCount: cues.length - cueStartIndex,
|
|
1312
|
+
startTime: cues[cueStartIndex].startTime,
|
|
1313
|
+
endTime: cues[cues.length - 1].endTime
|
|
1314
|
+
});
|
|
1315
|
+
return chunks;
|
|
1316
|
+
}
|
|
1317
|
+
function chunkVTTCuesByDuration(cues, options) {
|
|
1318
|
+
if (cues.length === 0) {
|
|
1319
|
+
return [];
|
|
1320
|
+
}
|
|
1321
|
+
const targetChunkDurationSeconds = Math.max(1, options.targetChunkDurationSeconds);
|
|
1322
|
+
const maxChunkDurationSeconds = Math.max(targetChunkDurationSeconds, options.maxChunkDurationSeconds);
|
|
1323
|
+
const minChunkDurationSeconds = Math.min(
|
|
1324
|
+
targetChunkDurationSeconds,
|
|
1325
|
+
Math.max(
|
|
1326
|
+
1,
|
|
1327
|
+
options.minChunkDurationSeconds ?? Math.floor(targetChunkDurationSeconds * DEFAULT_MIN_CHUNK_DURATION_RATIO)
|
|
1328
|
+
)
|
|
1329
|
+
);
|
|
1330
|
+
const boundaryLookaheadCues = Math.max(1, options.boundaryLookaheadCues ?? DEFAULT_BOUNDARY_LOOKAHEAD_CUES);
|
|
1331
|
+
const boundaryPauseSeconds = options.boundaryPauseSeconds ?? DEFAULT_BOUNDARY_PAUSE_SECONDS;
|
|
1332
|
+
const preferredBoundaryStartSeconds = Math.max(
|
|
1333
|
+
minChunkDurationSeconds,
|
|
1334
|
+
targetChunkDurationSeconds - Math.min(PREFERRED_BOUNDARY_WINDOW_SECONDS, targetChunkDurationSeconds / 6)
|
|
1335
|
+
);
|
|
1336
|
+
const chunks = [];
|
|
1337
|
+
let chunkIndex = 0;
|
|
1338
|
+
let cueStartIndex = 0;
|
|
1339
|
+
while (cueStartIndex < cues.length) {
|
|
1340
|
+
const chunkStartTime = cues[cueStartIndex].startTime;
|
|
1341
|
+
let cueEndIndex = cueStartIndex;
|
|
1342
|
+
let bestBoundaryIndex = -1;
|
|
1343
|
+
let bestBoundaryScore = -1;
|
|
1344
|
+
let bestPreferredBoundaryIndex = -1;
|
|
1345
|
+
let bestPreferredBoundaryScore = -1;
|
|
1346
|
+
while (cueEndIndex < cues.length) {
|
|
1347
|
+
const cue = cues[cueEndIndex];
|
|
1348
|
+
const currentDuration = cue.endTime - chunkStartTime;
|
|
1349
|
+
if (currentDuration >= minChunkDurationSeconds) {
|
|
1350
|
+
const boundaryScore = scoreCueBoundary(cues, cueEndIndex, boundaryPauseSeconds);
|
|
1351
|
+
if (boundaryScore >= bestBoundaryScore) {
|
|
1352
|
+
bestBoundaryIndex = cueEndIndex;
|
|
1353
|
+
bestBoundaryScore = boundaryScore;
|
|
1354
|
+
}
|
|
1355
|
+
if (currentDuration >= preferredBoundaryStartSeconds && boundaryScore >= bestPreferredBoundaryScore) {
|
|
1356
|
+
bestPreferredBoundaryIndex = cueEndIndex;
|
|
1357
|
+
bestPreferredBoundaryScore = boundaryScore;
|
|
1358
|
+
}
|
|
1359
|
+
}
|
|
1360
|
+
const nextCue = cues[cueEndIndex + 1];
|
|
1361
|
+
if (!nextCue) {
|
|
1362
|
+
break;
|
|
1363
|
+
}
|
|
1364
|
+
const nextDuration = nextCue.endTime - chunkStartTime;
|
|
1365
|
+
const lookaheadExceeded = cueEndIndex - cueStartIndex >= boundaryLookaheadCues;
|
|
1366
|
+
const preferredBoundaryIndex = bestPreferredBoundaryIndex >= cueStartIndex ? bestPreferredBoundaryIndex : bestBoundaryIndex;
|
|
1367
|
+
const preferredBoundaryScore = bestPreferredBoundaryIndex >= cueStartIndex ? bestPreferredBoundaryScore : bestBoundaryScore;
|
|
1368
|
+
if (currentDuration >= targetChunkDurationSeconds) {
|
|
1369
|
+
if (preferredBoundaryIndex >= cueStartIndex && preferredBoundaryScore >= STRONG_BOUNDARY_SCORE) {
|
|
1370
|
+
cueEndIndex = preferredBoundaryIndex;
|
|
1371
|
+
break;
|
|
1372
|
+
}
|
|
1373
|
+
if (nextDuration > maxChunkDurationSeconds || lookaheadExceeded) {
|
|
1374
|
+
cueEndIndex = preferredBoundaryIndex >= cueStartIndex ? preferredBoundaryIndex : cueEndIndex;
|
|
1375
|
+
break;
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
if (nextDuration > maxChunkDurationSeconds) {
|
|
1379
|
+
cueEndIndex = preferredBoundaryIndex >= cueStartIndex ? preferredBoundaryIndex : cueEndIndex;
|
|
1380
|
+
break;
|
|
1381
|
+
}
|
|
1382
|
+
cueEndIndex++;
|
|
1383
|
+
}
|
|
1384
|
+
chunks.push({
|
|
1385
|
+
id: `chunk-${chunkIndex}`,
|
|
1386
|
+
cueStartIndex,
|
|
1387
|
+
cueEndIndex,
|
|
1388
|
+
cueCount: cueEndIndex - cueStartIndex + 1,
|
|
1389
|
+
startTime: cues[cueStartIndex].startTime,
|
|
1390
|
+
endTime: cues[cueEndIndex].endTime
|
|
1391
|
+
});
|
|
1392
|
+
cueStartIndex = cueEndIndex + 1;
|
|
1393
|
+
chunkIndex++;
|
|
1394
|
+
}
|
|
1395
|
+
return chunks;
|
|
1396
|
+
}
|
|
1237
1397
|
function chunkText(text, strategy) {
|
|
1238
1398
|
switch (strategy.type) {
|
|
1239
1399
|
case "token": {
|
|
@@ -1275,10 +1435,8 @@ async function getThumbnailUrls(playbackId, duration, options = {}) {
|
|
|
1275
1435
|
}
|
|
1276
1436
|
const baseUrl = getMuxThumbnailBaseUrl(playbackId);
|
|
1277
1437
|
const urlPromises = timestamps.map(async (time) => {
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
}
|
|
1281
|
-
return `${baseUrl}?time=${time}&width=${width}`;
|
|
1438
|
+
const url = shouldSign ? await signUrl(baseUrl, playbackId, "thumbnail", { time, width }, credentials) : `${baseUrl}?time=${time}&width=${width}`;
|
|
1439
|
+
return { url, time };
|
|
1282
1440
|
});
|
|
1283
1441
|
return Promise.all(urlPromises);
|
|
1284
1442
|
}
|
|
@@ -1300,24 +1458,82 @@ function findCaptionTrack(asset, languageCode) {
|
|
|
1300
1458
|
(track) => track.text_type === "subtitles" && track.language_code === languageCode
|
|
1301
1459
|
);
|
|
1302
1460
|
}
|
|
1461
|
+
function normalizeLineEndings(value) {
|
|
1462
|
+
return value.replace(/\r\n/g, "\n");
|
|
1463
|
+
}
|
|
1464
|
+
function isTimingLine(line) {
|
|
1465
|
+
return line.includes("-->");
|
|
1466
|
+
}
|
|
1467
|
+
function parseNumericCueIdentifier(line) {
|
|
1468
|
+
if (!/^\d+$/.test(line)) {
|
|
1469
|
+
return null;
|
|
1470
|
+
}
|
|
1471
|
+
return Number.parseInt(line, 10);
|
|
1472
|
+
}
|
|
1473
|
+
function isLikelyTitledCueIdentifier(line) {
|
|
1474
|
+
return /^\d+\s+-\s+\S.*$/.test(line);
|
|
1475
|
+
}
|
|
1476
|
+
function isLikelyCueIdentifier({
|
|
1477
|
+
line,
|
|
1478
|
+
nextLine,
|
|
1479
|
+
previousCueIdentifier
|
|
1480
|
+
}) {
|
|
1481
|
+
if (!line || !nextLine || !isTimingLine(nextLine)) {
|
|
1482
|
+
return false;
|
|
1483
|
+
}
|
|
1484
|
+
const numericIdentifier = parseNumericCueIdentifier(line);
|
|
1485
|
+
if (numericIdentifier !== null) {
|
|
1486
|
+
if (previousCueIdentifier === null || previousCueIdentifier === void 0) {
|
|
1487
|
+
return numericIdentifier === 1;
|
|
1488
|
+
}
|
|
1489
|
+
return numericIdentifier === previousCueIdentifier + 1;
|
|
1490
|
+
}
|
|
1491
|
+
return isLikelyTitledCueIdentifier(line);
|
|
1492
|
+
}
|
|
1493
|
+
function getCueIdentifierLineIndex(lines, timingLineIndex, previousCueIdentifier) {
|
|
1494
|
+
const identifierIndex = timingLineIndex - 1;
|
|
1495
|
+
if (identifierIndex < 0) {
|
|
1496
|
+
return -1;
|
|
1497
|
+
}
|
|
1498
|
+
const candidate = lines[identifierIndex].trim();
|
|
1499
|
+
if (!candidate || isTimingLine(candidate)) {
|
|
1500
|
+
return -1;
|
|
1501
|
+
}
|
|
1502
|
+
return isLikelyCueIdentifier({
|
|
1503
|
+
line: candidate,
|
|
1504
|
+
nextLine: lines[timingLineIndex]?.trim(),
|
|
1505
|
+
previousCueIdentifier
|
|
1506
|
+
}) ? identifierIndex : -1;
|
|
1507
|
+
}
|
|
1303
1508
|
function extractTextFromVTT(vttContent) {
|
|
1304
1509
|
if (!vttContent.trim()) {
|
|
1305
1510
|
return "";
|
|
1306
1511
|
}
|
|
1307
1512
|
const lines = vttContent.split("\n");
|
|
1308
1513
|
const textLines = [];
|
|
1514
|
+
let previousCueIdentifier = null;
|
|
1515
|
+
let isInsideNoteBlock = false;
|
|
1309
1516
|
for (let i = 0; i < lines.length; i++) {
|
|
1310
1517
|
const line = lines[i].trim();
|
|
1311
|
-
|
|
1518
|
+
const nextLine = lines[i + 1]?.trim();
|
|
1519
|
+
if (!line) {
|
|
1520
|
+
isInsideNoteBlock = false;
|
|
1521
|
+
continue;
|
|
1522
|
+
}
|
|
1523
|
+
if (isInsideNoteBlock)
|
|
1312
1524
|
continue;
|
|
1313
1525
|
if (line === "WEBVTT")
|
|
1314
1526
|
continue;
|
|
1315
|
-
if (line.startsWith("NOTE "))
|
|
1527
|
+
if (line === "NOTE" || line.startsWith("NOTE ")) {
|
|
1528
|
+
isInsideNoteBlock = true;
|
|
1316
1529
|
continue;
|
|
1317
|
-
|
|
1530
|
+
}
|
|
1531
|
+
if (isTimingLine(line))
|
|
1318
1532
|
continue;
|
|
1319
|
-
if (
|
|
1533
|
+
if (isLikelyCueIdentifier({ line, nextLine, previousCueIdentifier })) {
|
|
1534
|
+
previousCueIdentifier = parseNumericCueIdentifier(line);
|
|
1320
1535
|
continue;
|
|
1536
|
+
}
|
|
1321
1537
|
if (line.startsWith("STYLE") || line.startsWith("REGION"))
|
|
1322
1538
|
continue;
|
|
1323
1539
|
const cleanLine = line.replace(/<[^>]*>/g, "").trim();
|
|
@@ -1376,20 +1592,34 @@ function parseVTTCues(vttContent) {
|
|
|
1376
1592
|
return [];
|
|
1377
1593
|
const lines = vttContent.split("\n");
|
|
1378
1594
|
const cues = [];
|
|
1595
|
+
let previousCueIdentifier = null;
|
|
1379
1596
|
for (let i = 0; i < lines.length; i++) {
|
|
1380
1597
|
const line = lines[i].trim();
|
|
1381
|
-
if (line
|
|
1598
|
+
if (isTimingLine(line)) {
|
|
1382
1599
|
const [startStr, endStr] = line.split(" --> ").map((s) => s.trim());
|
|
1383
1600
|
const startTime = vttTimestampToSeconds(startStr);
|
|
1384
1601
|
const endTime = vttTimestampToSeconds(endStr.split(" ")[0]);
|
|
1385
|
-
const
|
|
1602
|
+
const currentCueIdentifierLine = lines[i - 1]?.trim() ?? "";
|
|
1603
|
+
const currentCueIdentifier = isLikelyCueIdentifier({
|
|
1604
|
+
line: currentCueIdentifierLine,
|
|
1605
|
+
nextLine: line,
|
|
1606
|
+
previousCueIdentifier
|
|
1607
|
+
}) ? parseNumericCueIdentifier(currentCueIdentifierLine) : null;
|
|
1608
|
+
const rawTextLines = [];
|
|
1386
1609
|
let j = i + 1;
|
|
1387
|
-
while (j < lines.length && lines[j].trim() && !lines[j].
|
|
1388
|
-
|
|
1389
|
-
if (cleanLine)
|
|
1390
|
-
textLines.push(cleanLine);
|
|
1610
|
+
while (j < lines.length && lines[j].trim() && !isTimingLine(lines[j].trim())) {
|
|
1611
|
+
rawTextLines.push(lines[j].trim());
|
|
1391
1612
|
j++;
|
|
1392
1613
|
}
|
|
1614
|
+
const trailingNumericLine = parseNumericCueIdentifier(rawTextLines.at(-1) ?? "");
|
|
1615
|
+
if (trailingNumericLine !== null && isLikelyCueIdentifier({
|
|
1616
|
+
line: rawTextLines.at(-1) ?? "",
|
|
1617
|
+
nextLine: lines[j]?.trim(),
|
|
1618
|
+
previousCueIdentifier: currentCueIdentifier
|
|
1619
|
+
}) && rawTextLines.length > 1) {
|
|
1620
|
+
rawTextLines.pop();
|
|
1621
|
+
}
|
|
1622
|
+
const textLines = rawTextLines.map((textLine) => textLine.replace(/<[^>]*>/g, "")).filter(Boolean);
|
|
1393
1623
|
if (textLines.length > 0) {
|
|
1394
1624
|
cues.push({
|
|
1395
1625
|
startTime,
|
|
@@ -1397,10 +1627,102 @@ function parseVTTCues(vttContent) {
|
|
|
1397
1627
|
text: textLines.join(" ")
|
|
1398
1628
|
});
|
|
1399
1629
|
}
|
|
1630
|
+
previousCueIdentifier = currentCueIdentifier;
|
|
1400
1631
|
}
|
|
1401
1632
|
}
|
|
1402
1633
|
return cues;
|
|
1403
1634
|
}
|
|
1635
|
+
function splitVttPreambleAndCueBlocks(vttContent) {
|
|
1636
|
+
const normalizedContent = normalizeLineEndings(vttContent).trim();
|
|
1637
|
+
if (!normalizedContent) {
|
|
1638
|
+
return {
|
|
1639
|
+
preamble: "WEBVTT",
|
|
1640
|
+
cueBlocks: []
|
|
1641
|
+
};
|
|
1642
|
+
}
|
|
1643
|
+
const rawBlocks = normalizedContent.split(/\n{2,}/).map((block) => block.trim()).filter(Boolean);
|
|
1644
|
+
const cueBlockStartIndex = rawBlocks.findIndex((block) => block.includes("-->"));
|
|
1645
|
+
if (cueBlockStartIndex === -1) {
|
|
1646
|
+
return {
|
|
1647
|
+
preamble: normalizedContent.startsWith("WEBVTT") ? normalizedContent : `WEBVTT
|
|
1648
|
+
|
|
1649
|
+
${normalizedContent}`,
|
|
1650
|
+
cueBlocks: []
|
|
1651
|
+
};
|
|
1652
|
+
}
|
|
1653
|
+
const hasMergedCueBlocks = rawBlocks.slice(cueBlockStartIndex).some((block) => (block.match(/-->/g) ?? []).length > 1);
|
|
1654
|
+
if (hasMergedCueBlocks) {
|
|
1655
|
+
const lines = normalizedContent.split("\n");
|
|
1656
|
+
const timingLineIndices = lines.map((line, index) => isTimingLine(line.trim()) ? index : -1).filter((index) => index >= 0);
|
|
1657
|
+
let previousCueIdentifier = null;
|
|
1658
|
+
const firstCueStartIndex = getCueIdentifierLineIndex(lines, timingLineIndices[0], previousCueIdentifier);
|
|
1659
|
+
const preambleEndIndex = firstCueStartIndex >= 0 ? firstCueStartIndex : timingLineIndices[0];
|
|
1660
|
+
const preamble2 = lines.slice(0, preambleEndIndex).join("\n").trim() || "WEBVTT";
|
|
1661
|
+
const cueBlocks2 = timingLineIndices.map((timingLineIndex, index) => {
|
|
1662
|
+
const cueIdentifierLineIndex = getCueIdentifierLineIndex(lines, timingLineIndex, previousCueIdentifier);
|
|
1663
|
+
const cueStartIndex = cueIdentifierLineIndex >= 0 ? cueIdentifierLineIndex : timingLineIndex;
|
|
1664
|
+
const currentCueIdentifier = cueIdentifierLineIndex >= 0 ? parseNumericCueIdentifier(lines[cueIdentifierLineIndex].trim()) : null;
|
|
1665
|
+
const nextTimingLineIndex = timingLineIndices[index + 1] ?? lines.length;
|
|
1666
|
+
let cueEndIndex = nextTimingLineIndex - 1;
|
|
1667
|
+
while (cueEndIndex > timingLineIndex && !lines[cueEndIndex].trim()) {
|
|
1668
|
+
cueEndIndex--;
|
|
1669
|
+
}
|
|
1670
|
+
const nextCueIdentifierLineIndex = index < timingLineIndices.length - 1 ? getCueIdentifierLineIndex(lines, nextTimingLineIndex, currentCueIdentifier) : -1;
|
|
1671
|
+
if (nextCueIdentifierLineIndex === cueEndIndex) {
|
|
1672
|
+
cueEndIndex--;
|
|
1673
|
+
}
|
|
1674
|
+
while (cueEndIndex > timingLineIndex && !lines[cueEndIndex].trim()) {
|
|
1675
|
+
cueEndIndex--;
|
|
1676
|
+
}
|
|
1677
|
+
previousCueIdentifier = currentCueIdentifier;
|
|
1678
|
+
return lines.slice(cueStartIndex, cueEndIndex + 1).join("\n").trim();
|
|
1679
|
+
});
|
|
1680
|
+
return {
|
|
1681
|
+
preamble: preamble2,
|
|
1682
|
+
cueBlocks: cueBlocks2
|
|
1683
|
+
};
|
|
1684
|
+
}
|
|
1685
|
+
const preambleBlocks = rawBlocks.slice(0, cueBlockStartIndex);
|
|
1686
|
+
const cueBlocks = rawBlocks.slice(cueBlockStartIndex);
|
|
1687
|
+
const preamble = preambleBlocks.length > 0 ? preambleBlocks.join("\n\n") : "WEBVTT";
|
|
1688
|
+
return {
|
|
1689
|
+
preamble,
|
|
1690
|
+
cueBlocks
|
|
1691
|
+
};
|
|
1692
|
+
}
|
|
1693
|
+
function buildVttFromCueBlocks(cueBlocks, preamble = "WEBVTT") {
|
|
1694
|
+
if (cueBlocks.length === 0) {
|
|
1695
|
+
return `${preamble.trim()}
|
|
1696
|
+
`;
|
|
1697
|
+
}
|
|
1698
|
+
return `${preamble.trim()}
|
|
1699
|
+
|
|
1700
|
+
${cueBlocks.map((block) => block.trim()).join("\n\n")}
|
|
1701
|
+
`;
|
|
1702
|
+
}
|
|
1703
|
+
function replaceCueText(cueBlock, translatedText) {
|
|
1704
|
+
const lines = normalizeLineEndings(cueBlock).split("\n").map((line) => line.trim()).filter(Boolean);
|
|
1705
|
+
const timingLineIndex = lines.findIndex((line) => line.includes("-->"));
|
|
1706
|
+
if (timingLineIndex === -1) {
|
|
1707
|
+
throw new Error("Cue block is missing a timestamp line");
|
|
1708
|
+
}
|
|
1709
|
+
const headerLines = lines.slice(0, timingLineIndex + 1);
|
|
1710
|
+
const translatedLines = normalizeLineEndings(translatedText).split("\n").map((line) => line.trim()).filter(Boolean);
|
|
1711
|
+
return [...headerLines, ...translatedLines].join("\n");
|
|
1712
|
+
}
|
|
1713
|
+
function buildVttFromTranslatedCueBlocks(cueBlocks, translatedTexts, preamble = "WEBVTT") {
|
|
1714
|
+
if (cueBlocks.length !== translatedTexts.length) {
|
|
1715
|
+
throw new Error(`Expected ${cueBlocks.length} translated cues, received ${translatedTexts.length}`);
|
|
1716
|
+
}
|
|
1717
|
+
return buildVttFromCueBlocks(
|
|
1718
|
+
cueBlocks.map((cueBlock, index) => replaceCueText(cueBlock, translatedTexts[index])),
|
|
1719
|
+
preamble
|
|
1720
|
+
);
|
|
1721
|
+
}
|
|
1722
|
+
function concatenateVttSegments(segments, preamble = "WEBVTT") {
|
|
1723
|
+
const cueBlocks = segments.flatMap((segment) => splitVttPreambleAndCueBlocks(segment).cueBlocks);
|
|
1724
|
+
return buildVttFromCueBlocks(cueBlocks, preamble);
|
|
1725
|
+
}
|
|
1404
1726
|
async function buildTranscriptUrl(playbackId, trackId, shouldSign = false, credentials) {
|
|
1405
1727
|
"use step";
|
|
1406
1728
|
const baseUrl = `https://stream.mux.com/${playbackId}/text/${trackId}.vtt`;
|
|
@@ -1466,6 +1788,7 @@ __export(workflows_exports, {
|
|
|
1466
1788
|
HIVE_SEXUAL_CATEGORIES: () => HIVE_SEXUAL_CATEGORIES,
|
|
1467
1789
|
HIVE_VIOLENCE_CATEGORIES: () => HIVE_VIOLENCE_CATEGORIES,
|
|
1468
1790
|
SUMMARY_KEYWORD_LIMIT: () => SUMMARY_KEYWORD_LIMIT,
|
|
1791
|
+
aggregateTokenUsage: () => aggregateTokenUsage,
|
|
1469
1792
|
askQuestions: () => askQuestions,
|
|
1470
1793
|
burnedInCaptionsSchema: () => burnedInCaptionsSchema,
|
|
1471
1794
|
chapterSchema: () => chapterSchema,
|
|
@@ -1477,6 +1800,7 @@ __export(workflows_exports, {
|
|
|
1477
1800
|
getSummaryAndTags: () => getSummaryAndTags,
|
|
1478
1801
|
hasBurnedInCaptions: () => hasBurnedInCaptions,
|
|
1479
1802
|
questionAnswerSchema: () => questionAnswerSchema,
|
|
1803
|
+
shouldSplitChunkTranslationError: () => shouldSplitChunkTranslationError,
|
|
1480
1804
|
summarySchema: () => summarySchema,
|
|
1481
1805
|
translateAudio: () => translateAudio,
|
|
1482
1806
|
translateCaptions: () => translateCaptions,
|
|
@@ -2924,6 +3248,7 @@ async function moderateImageWithOpenAI(entry) {
|
|
|
2924
3248
|
const categoryScores = json.results?.[0]?.category_scores || {};
|
|
2925
3249
|
return {
|
|
2926
3250
|
url: entry.url,
|
|
3251
|
+
time: entry.time,
|
|
2927
3252
|
sexual: categoryScores.sexual || 0,
|
|
2928
3253
|
violence: categoryScores.violence || 0,
|
|
2929
3254
|
error: false
|
|
@@ -2932,6 +3257,7 @@ async function moderateImageWithOpenAI(entry) {
|
|
|
2932
3257
|
console.error("OpenAI moderation failed:", error);
|
|
2933
3258
|
return {
|
|
2934
3259
|
url: entry.url,
|
|
3260
|
+
time: entry.time,
|
|
2935
3261
|
sexual: 0,
|
|
2936
3262
|
violence: 0,
|
|
2937
3263
|
error: true,
|
|
@@ -2939,11 +3265,13 @@ async function moderateImageWithOpenAI(entry) {
|
|
|
2939
3265
|
};
|
|
2940
3266
|
}
|
|
2941
3267
|
}
|
|
2942
|
-
async function requestOpenAIModeration(
|
|
3268
|
+
async function requestOpenAIModeration(images, model, maxConcurrent = 5, submissionMode = "url", downloadOptions, credentials) {
|
|
2943
3269
|
"use step";
|
|
3270
|
+
const imageUrls = images.map((img) => img.url);
|
|
3271
|
+
const timeByUrl = new Map(images.map((img) => [img.url, img.time]));
|
|
2944
3272
|
const targetUrls = submissionMode === "base64" ? (await downloadImagesAsBase64(imageUrls, downloadOptions, maxConcurrent)).map(
|
|
2945
|
-
(img) => ({ url: img.url, image: img.base64Data, model, credentials })
|
|
2946
|
-
) :
|
|
3273
|
+
(img) => ({ url: img.url, time: timeByUrl.get(img.url), image: img.base64Data, model, credentials })
|
|
3274
|
+
) : images.map((img) => ({ url: img.url, time: img.time, image: img.url, model, credentials }));
|
|
2947
3275
|
return processConcurrently(targetUrls, moderateImageWithOpenAI, maxConcurrent);
|
|
2948
3276
|
}
|
|
2949
3277
|
async function requestOpenAITextModeration(text, model, url, credentials) {
|
|
@@ -3088,6 +3416,7 @@ async function moderateImageWithHive(entry) {
|
|
|
3088
3416
|
const violence = getHiveCategoryScores(classes, HIVE_VIOLENCE_CATEGORIES);
|
|
3089
3417
|
return {
|
|
3090
3418
|
url: entry.url,
|
|
3419
|
+
time: entry.time,
|
|
3091
3420
|
sexual,
|
|
3092
3421
|
violence,
|
|
3093
3422
|
error: false
|
|
@@ -3095,6 +3424,7 @@ async function moderateImageWithHive(entry) {
|
|
|
3095
3424
|
} catch (error) {
|
|
3096
3425
|
return {
|
|
3097
3426
|
url: entry.url,
|
|
3427
|
+
time: entry.time,
|
|
3098
3428
|
sexual: 0,
|
|
3099
3429
|
violence: 0,
|
|
3100
3430
|
error: true,
|
|
@@ -3102,19 +3432,23 @@ async function moderateImageWithHive(entry) {
|
|
|
3102
3432
|
};
|
|
3103
3433
|
}
|
|
3104
3434
|
}
|
|
3105
|
-
async function requestHiveModeration(
|
|
3435
|
+
async function requestHiveModeration(images, maxConcurrent = 5, submissionMode = "url", downloadOptions, credentials) {
|
|
3106
3436
|
"use step";
|
|
3437
|
+
const imageUrls = images.map((img) => img.url);
|
|
3438
|
+
const timeByUrl = new Map(images.map((img) => [img.url, img.time]));
|
|
3107
3439
|
const targets = submissionMode === "base64" ? (await downloadImagesAsBase64(imageUrls, downloadOptions, maxConcurrent)).map((img) => ({
|
|
3108
3440
|
url: img.url,
|
|
3441
|
+
time: timeByUrl.get(img.url),
|
|
3109
3442
|
source: {
|
|
3110
3443
|
kind: "file",
|
|
3111
3444
|
buffer: img.buffer,
|
|
3112
3445
|
contentType: img.contentType
|
|
3113
3446
|
},
|
|
3114
3447
|
credentials
|
|
3115
|
-
})) :
|
|
3116
|
-
url,
|
|
3117
|
-
|
|
3448
|
+
})) : images.map((img) => ({
|
|
3449
|
+
url: img.url,
|
|
3450
|
+
time: img.time,
|
|
3451
|
+
source: { kind: "url", value: img.url },
|
|
3118
3452
|
credentials
|
|
3119
3453
|
}));
|
|
3120
3454
|
return await processConcurrently(targets, moderateImageWithHive, maxConcurrent);
|
|
@@ -3125,10 +3459,8 @@ async function getThumbnailUrlsFromTimestamps(playbackId, timestampsMs, options)
|
|
|
3125
3459
|
const baseUrl = getMuxThumbnailBaseUrl(playbackId);
|
|
3126
3460
|
const urlPromises = timestampsMs.map(async (tsMs) => {
|
|
3127
3461
|
const time = Number((tsMs / 1e3).toFixed(2));
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
}
|
|
3131
|
-
return `${baseUrl}?time=${time}&width=${width}`;
|
|
3462
|
+
const url = shouldSign ? await signUrl(baseUrl, playbackId, "thumbnail", { time, width }, credentials) : `${baseUrl}?time=${time}&width=${width}`;
|
|
3463
|
+
return { url, time };
|
|
3132
3464
|
});
|
|
3133
3465
|
return Promise.all(urlPromises);
|
|
3134
3466
|
}
|
|
@@ -4197,12 +4529,187 @@ async function translateAudio(assetId, toLanguageCode, options = {}) {
|
|
|
4197
4529
|
}
|
|
4198
4530
|
|
|
4199
4531
|
// src/workflows/translate-captions.ts
|
|
4200
|
-
import {
|
|
4532
|
+
import {
|
|
4533
|
+
APICallError,
|
|
4534
|
+
generateText as generateText5,
|
|
4535
|
+
NoObjectGeneratedError,
|
|
4536
|
+
Output as Output5,
|
|
4537
|
+
RetryError,
|
|
4538
|
+
TypeValidationError
|
|
4539
|
+
} from "ai";
|
|
4540
|
+
import dedent5 from "dedent";
|
|
4201
4541
|
import { z as z6 } from "zod";
|
|
4202
4542
|
var translationSchema = z6.object({
|
|
4203
4543
|
translation: z6.string()
|
|
4204
4544
|
});
|
|
4205
|
-
var SYSTEM_PROMPT4 =
|
|
4545
|
+
var SYSTEM_PROMPT4 = dedent5`
|
|
4546
|
+
You are a subtitle translation expert. Translate VTT subtitle files to the target language specified by the user.
|
|
4547
|
+
You may receive either a full VTT file or a chunk from a larger VTT.
|
|
4548
|
+
Preserve all timestamps, cue ordering, and VTT formatting exactly as they appear.
|
|
4549
|
+
Return JSON with a single key "translation" containing the translated VTT content.
|
|
4550
|
+
`;
|
|
4551
|
+
var CUE_TRANSLATION_SYSTEM_PROMPT = dedent5`
|
|
4552
|
+
You are a subtitle translation expert.
|
|
4553
|
+
You will receive a sequence of subtitle cues extracted from a VTT file.
|
|
4554
|
+
Translate the cues to the requested target language while preserving their original order.
|
|
4555
|
+
Treat the cue list as continuous context so the translation reads naturally across adjacent lines.
|
|
4556
|
+
Return JSON with a single key "translations" containing exactly one translated string for each input cue.
|
|
4557
|
+
Do not merge, split, omit, reorder, or add cues.
|
|
4558
|
+
`;
|
|
4559
|
+
var DEFAULT_TRANSLATION_CHUNKING = {
|
|
4560
|
+
enabled: true,
|
|
4561
|
+
minimumAssetDurationSeconds: 30 * 60,
|
|
4562
|
+
targetChunkDurationSeconds: 30 * 60,
|
|
4563
|
+
maxConcurrentTranslations: 4,
|
|
4564
|
+
maxCuesPerChunk: 80,
|
|
4565
|
+
maxCueTextTokensPerChunk: 2e3
|
|
4566
|
+
};
|
|
4567
|
+
var TOKEN_USAGE_FIELDS = [
|
|
4568
|
+
"inputTokens",
|
|
4569
|
+
"outputTokens",
|
|
4570
|
+
"totalTokens",
|
|
4571
|
+
"reasoningTokens",
|
|
4572
|
+
"cachedInputTokens"
|
|
4573
|
+
];
|
|
4574
|
+
var TranslationChunkValidationError = class extends Error {
|
|
4575
|
+
constructor(message) {
|
|
4576
|
+
super(message);
|
|
4577
|
+
this.name = "TranslationChunkValidationError";
|
|
4578
|
+
}
|
|
4579
|
+
};
|
|
4580
|
+
function isTranslationChunkValidationError(error) {
|
|
4581
|
+
return error instanceof TranslationChunkValidationError;
|
|
4582
|
+
}
|
|
4583
|
+
function isProviderServiceError(error) {
|
|
4584
|
+
if (!error) {
|
|
4585
|
+
return false;
|
|
4586
|
+
}
|
|
4587
|
+
if (RetryError.isInstance(error)) {
|
|
4588
|
+
return isProviderServiceError(error.lastError);
|
|
4589
|
+
}
|
|
4590
|
+
if (APICallError.isInstance(error)) {
|
|
4591
|
+
return true;
|
|
4592
|
+
}
|
|
4593
|
+
if (error instanceof Error && "cause" in error) {
|
|
4594
|
+
return isProviderServiceError(error.cause);
|
|
4595
|
+
}
|
|
4596
|
+
return false;
|
|
4597
|
+
}
|
|
4598
|
+
function shouldSplitChunkTranslationError(error) {
|
|
4599
|
+
if (isProviderServiceError(error)) {
|
|
4600
|
+
return false;
|
|
4601
|
+
}
|
|
4602
|
+
return NoObjectGeneratedError.isInstance(error) || TypeValidationError.isInstance(error) || isTranslationChunkValidationError(error);
|
|
4603
|
+
}
|
|
4604
|
+
function isDefinedTokenUsageValue(value) {
|
|
4605
|
+
return typeof value === "number";
|
|
4606
|
+
}
|
|
4607
|
+
function resolveTranslationChunkingOptions(options) {
|
|
4608
|
+
const targetChunkDurationSeconds = Math.max(
|
|
4609
|
+
1,
|
|
4610
|
+
options?.targetChunkDurationSeconds ?? DEFAULT_TRANSLATION_CHUNKING.targetChunkDurationSeconds
|
|
4611
|
+
);
|
|
4612
|
+
return {
|
|
4613
|
+
enabled: options?.enabled ?? DEFAULT_TRANSLATION_CHUNKING.enabled,
|
|
4614
|
+
minimumAssetDurationSeconds: Math.max(
|
|
4615
|
+
1,
|
|
4616
|
+
options?.minimumAssetDurationSeconds ?? DEFAULT_TRANSLATION_CHUNKING.minimumAssetDurationSeconds
|
|
4617
|
+
),
|
|
4618
|
+
targetChunkDurationSeconds,
|
|
4619
|
+
maxConcurrentTranslations: Math.max(
|
|
4620
|
+
1,
|
|
4621
|
+
options?.maxConcurrentTranslations ?? DEFAULT_TRANSLATION_CHUNKING.maxConcurrentTranslations
|
|
4622
|
+
),
|
|
4623
|
+
maxCuesPerChunk: Math.max(
|
|
4624
|
+
1,
|
|
4625
|
+
options?.maxCuesPerChunk ?? DEFAULT_TRANSLATION_CHUNKING.maxCuesPerChunk
|
|
4626
|
+
),
|
|
4627
|
+
maxCueTextTokensPerChunk: Math.max(
|
|
4628
|
+
1,
|
|
4629
|
+
options?.maxCueTextTokensPerChunk ?? DEFAULT_TRANSLATION_CHUNKING.maxCueTextTokensPerChunk
|
|
4630
|
+
)
|
|
4631
|
+
};
|
|
4632
|
+
}
|
|
4633
|
+
function aggregateTokenUsage(usages) {
|
|
4634
|
+
return TOKEN_USAGE_FIELDS.reduce((aggregate, field) => {
|
|
4635
|
+
const values = usages.map((usage) => usage[field]).filter(isDefinedTokenUsageValue);
|
|
4636
|
+
if (values.length > 0) {
|
|
4637
|
+
aggregate[field] = values.reduce((total, value) => total + value, 0);
|
|
4638
|
+
}
|
|
4639
|
+
return aggregate;
|
|
4640
|
+
}, {});
|
|
4641
|
+
}
|
|
4642
|
+
function createTranslationChunkRequest(id, cues, cueBlocks) {
|
|
4643
|
+
return {
|
|
4644
|
+
id,
|
|
4645
|
+
cueCount: cues.length,
|
|
4646
|
+
startTime: cues[0].startTime,
|
|
4647
|
+
endTime: cues[cues.length - 1].endTime,
|
|
4648
|
+
cues,
|
|
4649
|
+
cueBlocks
|
|
4650
|
+
};
|
|
4651
|
+
}
|
|
4652
|
+
function splitTranslationChunkRequestByBudget(id, cues, cueBlocks, maxCuesPerChunk, maxCueTextTokensPerChunk) {
|
|
4653
|
+
const chunks = chunkVTTCuesByBudget(cues, {
|
|
4654
|
+
maxCuesPerChunk,
|
|
4655
|
+
maxTextTokensPerChunk: maxCueTextTokensPerChunk
|
|
4656
|
+
});
|
|
4657
|
+
return chunks.map(
|
|
4658
|
+
(chunk, index) => createTranslationChunkRequest(
|
|
4659
|
+
chunks.length === 1 ? id : `${id}-part-${index}`,
|
|
4660
|
+
cues.slice(chunk.cueStartIndex, chunk.cueEndIndex + 1),
|
|
4661
|
+
cueBlocks.slice(chunk.cueStartIndex, chunk.cueEndIndex + 1)
|
|
4662
|
+
)
|
|
4663
|
+
);
|
|
4664
|
+
}
|
|
4665
|
+
function buildTranslationChunkRequests(vttContent, assetDurationSeconds, chunkingOptions) {
|
|
4666
|
+
const resolvedChunking = resolveTranslationChunkingOptions(chunkingOptions);
|
|
4667
|
+
const cues = parseVTTCues(vttContent);
|
|
4668
|
+
if (cues.length === 0) {
|
|
4669
|
+
return null;
|
|
4670
|
+
}
|
|
4671
|
+
const { preamble, cueBlocks } = splitVttPreambleAndCueBlocks(vttContent);
|
|
4672
|
+
if (cueBlocks.length !== cues.length) {
|
|
4673
|
+
console.warn(
|
|
4674
|
+
`Falling back to full-VTT caption translation because cue block count (${cueBlocks.length}) does not match parsed cue count (${cues.length}).`
|
|
4675
|
+
);
|
|
4676
|
+
return null;
|
|
4677
|
+
}
|
|
4678
|
+
if (!resolvedChunking.enabled) {
|
|
4679
|
+
return {
|
|
4680
|
+
preamble,
|
|
4681
|
+
chunks: [
|
|
4682
|
+
createTranslationChunkRequest("chunk-0", cues, cueBlocks)
|
|
4683
|
+
]
|
|
4684
|
+
};
|
|
4685
|
+
}
|
|
4686
|
+
if (typeof assetDurationSeconds !== "number" || assetDurationSeconds < resolvedChunking.minimumAssetDurationSeconds) {
|
|
4687
|
+
return {
|
|
4688
|
+
preamble,
|
|
4689
|
+
chunks: [
|
|
4690
|
+
createTranslationChunkRequest("chunk-0", cues, cueBlocks)
|
|
4691
|
+
]
|
|
4692
|
+
};
|
|
4693
|
+
}
|
|
4694
|
+
const targetChunkDurationSeconds = resolvedChunking.targetChunkDurationSeconds;
|
|
4695
|
+
const durationChunks = chunkVTTCuesByDuration(cues, {
|
|
4696
|
+
targetChunkDurationSeconds,
|
|
4697
|
+
maxChunkDurationSeconds: Math.max(targetChunkDurationSeconds, Math.round(targetChunkDurationSeconds * (7 / 6))),
|
|
4698
|
+
minChunkDurationSeconds: Math.max(1, Math.round(targetChunkDurationSeconds * (2 / 3)))
|
|
4699
|
+
});
|
|
4700
|
+
return {
|
|
4701
|
+
preamble,
|
|
4702
|
+
chunks: durationChunks.flatMap(
|
|
4703
|
+
(chunk) => splitTranslationChunkRequestByBudget(
|
|
4704
|
+
chunk.id,
|
|
4705
|
+
cues.slice(chunk.cueStartIndex, chunk.cueEndIndex + 1),
|
|
4706
|
+
cueBlocks.slice(chunk.cueStartIndex, chunk.cueEndIndex + 1),
|
|
4707
|
+
resolvedChunking.maxCuesPerChunk,
|
|
4708
|
+
resolvedChunking.maxCueTextTokensPerChunk
|
|
4709
|
+
)
|
|
4710
|
+
)
|
|
4711
|
+
};
|
|
4712
|
+
}
|
|
4206
4713
|
async function fetchVttFromMux(vttUrl) {
|
|
4207
4714
|
"use step";
|
|
4208
4715
|
const vttResponse = await fetch(vttUrl);
|
|
@@ -4248,6 +4755,176 @@ ${vttContent}`
|
|
|
4248
4755
|
}
|
|
4249
4756
|
};
|
|
4250
4757
|
}
|
|
4758
|
+
async function translateCueChunkWithAI({
|
|
4759
|
+
cues,
|
|
4760
|
+
fromLanguageCode,
|
|
4761
|
+
toLanguageCode,
|
|
4762
|
+
provider,
|
|
4763
|
+
modelId,
|
|
4764
|
+
credentials
|
|
4765
|
+
}) {
|
|
4766
|
+
"use step";
|
|
4767
|
+
const model = await createLanguageModelFromConfig(provider, modelId, credentials);
|
|
4768
|
+
const schema = z6.object({
|
|
4769
|
+
translations: z6.array(z6.string().min(1)).length(cues.length)
|
|
4770
|
+
});
|
|
4771
|
+
const cuePayload = cues.map((cue, index) => ({
|
|
4772
|
+
index,
|
|
4773
|
+
startTime: cue.startTime,
|
|
4774
|
+
endTime: cue.endTime,
|
|
4775
|
+
text: cue.text
|
|
4776
|
+
}));
|
|
4777
|
+
const response = await generateText5({
|
|
4778
|
+
model,
|
|
4779
|
+
output: Output5.object({ schema }),
|
|
4780
|
+
messages: [
|
|
4781
|
+
{
|
|
4782
|
+
role: "system",
|
|
4783
|
+
content: CUE_TRANSLATION_SYSTEM_PROMPT
|
|
4784
|
+
},
|
|
4785
|
+
{
|
|
4786
|
+
role: "user",
|
|
4787
|
+
content: `Translate from ${fromLanguageCode} to ${toLanguageCode}.
|
|
4788
|
+
Return exactly ${cues.length} translated cues in the same order as the input.
|
|
4789
|
+
|
|
4790
|
+
${JSON.stringify(cuePayload, null, 2)}`
|
|
4791
|
+
}
|
|
4792
|
+
]
|
|
4793
|
+
});
|
|
4794
|
+
return {
|
|
4795
|
+
translations: response.output.translations,
|
|
4796
|
+
usage: {
|
|
4797
|
+
inputTokens: response.usage.inputTokens,
|
|
4798
|
+
outputTokens: response.usage.outputTokens,
|
|
4799
|
+
totalTokens: response.usage.totalTokens,
|
|
4800
|
+
reasoningTokens: response.usage.reasoningTokens,
|
|
4801
|
+
cachedInputTokens: response.usage.cachedInputTokens
|
|
4802
|
+
}
|
|
4803
|
+
};
|
|
4804
|
+
}
|
|
4805
|
+
function splitTranslationChunkAtMidpoint(chunk) {
|
|
4806
|
+
const midpoint = Math.floor(chunk.cueCount / 2);
|
|
4807
|
+
if (midpoint <= 0 || midpoint >= chunk.cueCount) {
|
|
4808
|
+
throw new Error(`Cannot split chunk ${chunk.id} with cueCount=${chunk.cueCount}`);
|
|
4809
|
+
}
|
|
4810
|
+
return [
|
|
4811
|
+
createTranslationChunkRequest(
|
|
4812
|
+
`${chunk.id}-a`,
|
|
4813
|
+
chunk.cues.slice(0, midpoint),
|
|
4814
|
+
chunk.cueBlocks.slice(0, midpoint)
|
|
4815
|
+
),
|
|
4816
|
+
createTranslationChunkRequest(
|
|
4817
|
+
`${chunk.id}-b`,
|
|
4818
|
+
chunk.cues.slice(midpoint),
|
|
4819
|
+
chunk.cueBlocks.slice(midpoint)
|
|
4820
|
+
)
|
|
4821
|
+
];
|
|
4822
|
+
}
|
|
4823
|
+
async function translateChunkWithFallback({
|
|
4824
|
+
chunk,
|
|
4825
|
+
fromLanguageCode,
|
|
4826
|
+
toLanguageCode,
|
|
4827
|
+
provider,
|
|
4828
|
+
modelId,
|
|
4829
|
+
credentials
|
|
4830
|
+
}) {
|
|
4831
|
+
"use step";
|
|
4832
|
+
try {
|
|
4833
|
+
const result = await translateCueChunkWithAI({
|
|
4834
|
+
cues: chunk.cues,
|
|
4835
|
+
fromLanguageCode,
|
|
4836
|
+
toLanguageCode,
|
|
4837
|
+
provider,
|
|
4838
|
+
modelId,
|
|
4839
|
+
credentials
|
|
4840
|
+
});
|
|
4841
|
+
if (result.translations.length !== chunk.cueCount) {
|
|
4842
|
+
throw new TranslationChunkValidationError(
|
|
4843
|
+
`Chunk ${chunk.id} returned ${result.translations.length} cues, expected ${chunk.cueCount} for ${Math.round(chunk.startTime)}s-${Math.round(chunk.endTime)}s`
|
|
4844
|
+
);
|
|
4845
|
+
}
|
|
4846
|
+
return {
|
|
4847
|
+
translatedVtt: buildVttFromTranslatedCueBlocks(chunk.cueBlocks, result.translations),
|
|
4848
|
+
usage: result.usage
|
|
4849
|
+
};
|
|
4850
|
+
} catch (error) {
|
|
4851
|
+
if (!shouldSplitChunkTranslationError(error) || chunk.cueCount <= 1) {
|
|
4852
|
+
throw new Error(
|
|
4853
|
+
`Chunk ${chunk.id} failed for ${Math.round(chunk.startTime)}s-${Math.round(chunk.endTime)}s: ${error instanceof Error ? error.message : "Unknown error"}`
|
|
4854
|
+
);
|
|
4855
|
+
}
|
|
4856
|
+
const [leftChunk, rightChunk] = splitTranslationChunkAtMidpoint(chunk);
|
|
4857
|
+
const [leftResult, rightResult] = await Promise.all([
|
|
4858
|
+
translateChunkWithFallback({
|
|
4859
|
+
chunk: leftChunk,
|
|
4860
|
+
fromLanguageCode,
|
|
4861
|
+
toLanguageCode,
|
|
4862
|
+
provider,
|
|
4863
|
+
modelId,
|
|
4864
|
+
credentials
|
|
4865
|
+
}),
|
|
4866
|
+
translateChunkWithFallback({
|
|
4867
|
+
chunk: rightChunk,
|
|
4868
|
+
fromLanguageCode,
|
|
4869
|
+
toLanguageCode,
|
|
4870
|
+
provider,
|
|
4871
|
+
modelId,
|
|
4872
|
+
credentials
|
|
4873
|
+
})
|
|
4874
|
+
]);
|
|
4875
|
+
return {
|
|
4876
|
+
translatedVtt: concatenateVttSegments([leftResult.translatedVtt, rightResult.translatedVtt]),
|
|
4877
|
+
usage: aggregateTokenUsage([leftResult.usage, rightResult.usage])
|
|
4878
|
+
};
|
|
4879
|
+
}
|
|
4880
|
+
}
|
|
4881
|
+
async function translateCaptionTrack({
|
|
4882
|
+
vttContent,
|
|
4883
|
+
assetDurationSeconds,
|
|
4884
|
+
fromLanguageCode,
|
|
4885
|
+
toLanguageCode,
|
|
4886
|
+
provider,
|
|
4887
|
+
modelId,
|
|
4888
|
+
credentials,
|
|
4889
|
+
chunking
|
|
4890
|
+
}) {
|
|
4891
|
+
"use step";
|
|
4892
|
+
const chunkPlan = buildTranslationChunkRequests(vttContent, assetDurationSeconds, chunking);
|
|
4893
|
+
if (!chunkPlan) {
|
|
4894
|
+
return translateVttWithAI({
|
|
4895
|
+
vttContent,
|
|
4896
|
+
fromLanguageCode,
|
|
4897
|
+
toLanguageCode,
|
|
4898
|
+
provider,
|
|
4899
|
+
modelId,
|
|
4900
|
+
credentials
|
|
4901
|
+
});
|
|
4902
|
+
}
|
|
4903
|
+
const resolvedChunking = resolveTranslationChunkingOptions(chunking);
|
|
4904
|
+
const translatedSegments = [];
|
|
4905
|
+
const usageByChunk = [];
|
|
4906
|
+
for (let index = 0; index < chunkPlan.chunks.length; index += resolvedChunking.maxConcurrentTranslations) {
|
|
4907
|
+
const batch = chunkPlan.chunks.slice(index, index + resolvedChunking.maxConcurrentTranslations);
|
|
4908
|
+
const batchResults = await Promise.all(
|
|
4909
|
+
batch.map(
|
|
4910
|
+
(chunk) => translateChunkWithFallback({
|
|
4911
|
+
chunk,
|
|
4912
|
+
fromLanguageCode,
|
|
4913
|
+
toLanguageCode,
|
|
4914
|
+
provider,
|
|
4915
|
+
modelId,
|
|
4916
|
+
credentials
|
|
4917
|
+
})
|
|
4918
|
+
)
|
|
4919
|
+
);
|
|
4920
|
+
translatedSegments.push(...batchResults.map((result) => result.translatedVtt));
|
|
4921
|
+
usageByChunk.push(...batchResults.map((result) => result.usage));
|
|
4922
|
+
}
|
|
4923
|
+
return {
|
|
4924
|
+
translatedVtt: concatenateVttSegments(translatedSegments, chunkPlan.preamble),
|
|
4925
|
+
usage: aggregateTokenUsage(usageByChunk)
|
|
4926
|
+
};
|
|
4927
|
+
}
|
|
4251
4928
|
async function uploadVttToS3({
|
|
4252
4929
|
translatedVtt,
|
|
4253
4930
|
assetId,
|
|
@@ -4308,7 +4985,8 @@ async function translateCaptions(assetId, fromLanguageCode, toLanguageCode, opti
|
|
|
4308
4985
|
s3Bucket: providedS3Bucket,
|
|
4309
4986
|
uploadToMux: uploadToMuxOption,
|
|
4310
4987
|
storageAdapter,
|
|
4311
|
-
credentials: providedCredentials
|
|
4988
|
+
credentials: providedCredentials,
|
|
4989
|
+
chunking
|
|
4312
4990
|
} = options;
|
|
4313
4991
|
const credentials = providedCredentials;
|
|
4314
4992
|
const effectiveStorageAdapter = storageAdapter;
|
|
@@ -4369,13 +5047,15 @@ async function translateCaptions(assetId, fromLanguageCode, toLanguageCode, opti
|
|
|
4369
5047
|
let translatedVtt;
|
|
4370
5048
|
let usage;
|
|
4371
5049
|
try {
|
|
4372
|
-
const result = await
|
|
5050
|
+
const result = await translateCaptionTrack({
|
|
4373
5051
|
vttContent,
|
|
5052
|
+
assetDurationSeconds,
|
|
4374
5053
|
fromLanguageCode,
|
|
4375
5054
|
toLanguageCode,
|
|
4376
5055
|
provider: modelConfig.provider,
|
|
4377
5056
|
modelId: modelConfig.modelId,
|
|
4378
|
-
credentials
|
|
5057
|
+
credentials,
|
|
5058
|
+
chunking
|
|
4379
5059
|
});
|
|
4380
5060
|
translatedVtt = result.translatedVtt;
|
|
4381
5061
|
usage = result.usage;
|