@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.js CHANGED
@@ -5,7 +5,7 @@ var __export = (target, all) => {
5
5
  };
6
6
 
7
7
  // package.json
8
- var version = "0.9.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
- if (shouldSign) {
1279
- return signUrl(baseUrl, playbackId, "thumbnail", { time, width }, credentials);
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
- if (!line)
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
- if (line.includes("-->"))
1530
+ }
1531
+ if (isTimingLine(line))
1318
1532
  continue;
1319
- if (/^[\w-]+$/.test(line) && !line.includes(" "))
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.includes("-->")) {
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 textLines = [];
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].includes("-->")) {
1388
- const cleanLine = lines[j].trim().replace(/<[^>]*>/g, "");
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(imageUrls, model, maxConcurrent = 5, submissionMode = "url", downloadOptions, credentials) {
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
- ) : imageUrls.map((url) => ({ url, image: url, model, credentials }));
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(imageUrls, maxConcurrent = 5, submissionMode = "url", downloadOptions, credentials) {
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
- })) : imageUrls.map((url) => ({
3116
- url,
3117
- source: { kind: "url", value: url },
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
- if (shouldSign) {
3129
- return signUrl(baseUrl, playbackId, "thumbnail", { time, width }, credentials);
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 { generateText as generateText5, Output as Output5 } from "ai";
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 = 'You are a subtitle translation expert. Translate VTT subtitle files to the target language specified by the user. Preserve all timestamps and VTT formatting exactly as they appear. Return JSON with a single key "translation" containing the translated VTT content.';
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 translateVttWithAI({
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;