@mux/ai 0.28.0 → 0.29.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.
@@ -97,6 +97,24 @@ interface MuxAIOptions {
97
97
  /** Optional storage adapter for upload and presigning operations. */
98
98
  storageAdapter?: StorageAdapter;
99
99
  }
100
+ /**
101
+ * Asset-relative time range used to limit workflow analysis.
102
+ *
103
+ * Times are expressed in seconds. The start is inclusive and the end is
104
+ * exclusive. Either boundary may be omitted to analyze from the beginning or
105
+ * through the end of the asset, respectively.
106
+ */
107
+ interface WorkflowScope {
108
+ /** Inclusive start offset from the beginning of the asset, in seconds. */
109
+ startTime?: number;
110
+ /** Exclusive end offset from the beginning of the asset, in seconds. */
111
+ endTime?: number;
112
+ }
113
+ /** Base options for workflows that can analyze a bounded part of an asset. */
114
+ interface ScopedMuxAIOptions extends MuxAIOptions {
115
+ /** Optional asset-relative time range to analyze. */
116
+ scope?: WorkflowScope;
117
+ }
100
118
  /**
101
119
  * Workflow credentials.
102
120
  *
@@ -356,4 +374,4 @@ declare function getShotsForAsset(assetId: string, options?: ShotRequestOptions)
356
374
  */
357
375
  declare function waitForShotsForAsset(assetId: string, options?: WaitForShotsOptions): Promise<CompletedShotsResult>;
358
376
 
359
- export { type AssetTextTrack as A, getShotsForAsset as B, type ChunkEmbedding as C, requestShotsForAsset as D, type Encrypted as E, waitForShotsForAsset as F, type Hotspot as H, type ImageSubmissionMode as I, type MuxAIOptions as M, type PlaybackAsset as P, type StoragePutObjectInput as S, type TextChunk as T, type UsageMetadata as U, type VTTChunkingConfig as V, type WorkflowCredentials as W, type StoragePresignGetObjectInput as a, type ChunkingStrategy as b, type EncryptedPayload as c, type MuxAsset as d, type PlaybackPolicy as e, type StorageAdapter as f, type TokenChunkingConfig as g, type TokenUsage as h, type ToneType as i, type VideoEmbeddingsResult as j, type WorkflowCredentialsInput as k, type WorkflowMuxClient as l, decryptFromWorkflow as m, encryptForWorkflow as n, type CompletedShotsResult as o, type ErroredShotsResult as p, type HotspotOptions as q, type HotspotResponse as r, type PendingShotsResult as s, type Shot as t, type ShotRequestOptions as u, type ShotsResult as v, type WaitForShotsOptions as w, getHotspotsForAsset as x, getHotspotsForPlaybackId as y, getHotspotsForVideo as z };
377
+ export { type AssetTextTrack as A, getHotspotsForPlaybackId as B, type ChunkEmbedding as C, getHotspotsForVideo as D, type Encrypted as E, getShotsForAsset as F, requestShotsForAsset as G, type Hotspot as H, type ImageSubmissionMode as I, waitForShotsForAsset as J, type MuxAIOptions as M, type PlaybackAsset as P, type StoragePutObjectInput as S, type TextChunk as T, type UsageMetadata as U, type VTTChunkingConfig as V, type WorkflowCredentials as W, type StoragePresignGetObjectInput as a, type ChunkingStrategy as b, type EncryptedPayload as c, type MuxAsset as d, type PlaybackPolicy as e, type ScopedMuxAIOptions as f, type StorageAdapter as g, type TokenChunkingConfig as h, type TokenUsage as i, type ToneType as j, type VideoEmbeddingsResult as k, type WorkflowCredentialsInput as l, type WorkflowMuxClient as m, type WorkflowScope as n, decryptFromWorkflow as o, encryptForWorkflow as p, type CompletedShotsResult as q, type ErroredShotsResult as r, type HotspotOptions as s, type HotspotResponse as t, type PendingShotsResult as u, type Shot as v, type ShotRequestOptions as w, type ShotsResult as x, type WaitForShotsOptions as y, getHotspotsForAsset as z };
@@ -1,7 +1,7 @@
1
- export { A as AskQuestionsOptions, j as AskQuestionsResult, k as AskQuestionsType, l as AudioTranslationOptions, m as AudioTranslationResult, n as AutoCensorProfanityOptions, B as BurnedInCaptionsAnalysis, o as BurnedInCaptionsOptions, p as BurnedInCaptionsPromptOverrides, q as BurnedInCaptionsPromptSections, r as BurnedInCaptionsResult, C as CaptionReplacement, s as CensorMode, u as Chapter, v as ChapterSystemPromptSections, w as ChaptersOptions, x as ChaptersPromptOverrides, y as ChaptersPromptSections, z as ChaptersResult, D as ChaptersType, E as DEFAULT_DESCRIPTION_LENGTH, F as DEFAULT_SUMMARY_KEYWORD_LIMIT, G as DEFAULT_TITLE_LENGTH, H as EditCaptionsOptions, J as EditCaptionsResult, K as EmbeddingsOptions, M as EmbeddingsResult, N as EngagementInsightsOptions, O as EngagementInsightsResult, P as GOOGLE_VISION_LIKELIHOOD_TO_SCORE, Q as HIVE_SEXUAL_CATEGORIES, R as HIVE_VIOLENCE_CATEGORIES, T as HiveModerationOutput, U as HiveModerationSource, V as ModerationOptions, W as ModerationProvider, X as ModerationResult, Y as MomentInsight, Z as OverallInsight, _ as ProfanityDetectionPayload, $ as Question, a0 as QuestionAnswer, a1 as QuestionAnswerType, a2 as ReplacementRecord, a3 as SummarizationOptions, a4 as SummarizationPromptOverrides, a5 as SummarizationPromptSections, a6 as SummaryAndTagsResult, a7 as SummaryType, a8 as ThumbnailModerationScore, a9 as TranslationChunkingOptions, aa as TranslationOptions, ab as TranslationPayload, ac as TranslationResult, ad as aggregateTokenUsage, ae as applyOverrideLists, af as applyReplacements, ag as askQuestions, ah as buildReplacementRegex, ai as burnedInCaptionsSchema, aj as censorVttContent, ak as chapterSchema, al as chaptersSchema, am as createReplacer, an as editCaptions, ao as extractTranscriptSegmentsForHotspots, ap as generateChapters, aq as generateEmbeddings, ar as generateEngagementInsights, as as generateVideoEmbeddings, at as getModerationScores, au as getSummaryAndTags, av as hasBurnedInCaptions, aw as mapShotsToHotspots, ax as normalizeTranslatedVtt, ay as profanityDetectionSchema, az as questionAnswerSchema, aA as shouldSplitChunkTranslationError, aB as summarySchema, aC as transformCueText, aD as translateAudio, aE as translateCaptions, aF as translationSchema } from '../index-DUVBpnj5.js';
1
+ export { A as AskQuestionsOptions, j as AskQuestionsResult, k as AskQuestionsType, l as AudioTranslationOptions, m as AudioTranslationResult, n as AutoCensorProfanityOptions, B as BurnedInCaptionsAnalysis, o as BurnedInCaptionsOptions, p as BurnedInCaptionsPromptOverrides, q as BurnedInCaptionsPromptSections, r as BurnedInCaptionsResult, C as CaptionReplacement, s as CensorMode, u as Chapter, v as ChapterSystemPromptSections, w as ChaptersOptions, x as ChaptersPromptOverrides, y as ChaptersPromptSections, z as ChaptersResult, D as ChaptersType, E as DEFAULT_DESCRIPTION_LENGTH, F as DEFAULT_SUMMARY_KEYWORD_LIMIT, G as DEFAULT_TITLE_LENGTH, H as EditCaptionsOptions, J as EditCaptionsResult, K as EmbeddingsOptions, M as EmbeddingsResult, N as EngagementInsightsOptions, O as EngagementInsightsResult, P as GOOGLE_VISION_LIKELIHOOD_TO_SCORE, Q as HIVE_SEXUAL_CATEGORIES, R as HIVE_VIOLENCE_CATEGORIES, T as HiveModerationOutput, U as HiveModerationSource, V as ModerationOptions, W as ModerationProvider, X as ModerationResult, Y as MomentInsight, Z as OverallInsight, _ as ProfanityDetectionPayload, $ as Question, a0 as QuestionAnswer, a1 as QuestionAnswerType, a2 as ReplacementRecord, a3 as SummarizationOptions, a4 as SummarizationPromptOverrides, a5 as SummarizationPromptSections, a6 as SummaryAndTagsResult, a7 as SummaryType, a8 as ThumbnailModerationScore, a9 as TranslationChunkingOptions, aa as TranslationOptions, ab as TranslationPayload, ac as TranslationResult, ad as aggregateTokenUsage, ae as applyOverrideLists, af as applyReplacements, ag as askQuestions, ah as buildReplacementRegex, ai as burnedInCaptionsSchema, aj as censorVttContent, ak as chapterSchema, al as chaptersSchema, am as createReplacer, an as editCaptions, ao as extractTranscriptSegmentsForHotspots, ap as generateChapters, aq as generateEmbeddings, ar as generateEngagementInsights, as as generateVideoEmbeddings, at as getModerationScores, au as getSummaryAndTags, av as hasBurnedInCaptions, aw as mapShotsToHotspots, ax as normalizeTranslatedVtt, ay as profanityDetectionSchema, az as questionAnswerSchema, aA as shouldSplitChunkTranslationError, aB as summarySchema, aC as transformCueText, aD as translateAudio, aE as translateCaptions, aF as translationSchema } from '../index-DJirK4fh.js';
2
2
  import 'zod';
3
3
  import '@ai-sdk/anthropic';
4
4
  import '@ai-sdk/google';
5
5
  import '@ai-sdk/openai';
6
- import '../shots-mGJfYGiO.js';
6
+ import '../shots-BKd7TMFN.js';
7
7
  import '@mux/mux-node';
@@ -1298,6 +1298,68 @@ async function withRetry(fn, {
1298
1298
  throw lastError || new Error("Retry failed with unknown error");
1299
1299
  }
1300
1300
 
1301
+ // src/lib/workflow-scope.ts
1302
+ function hasWorkflowScopeBoundaries(scope) {
1303
+ return scope?.startTime !== void 0 || scope?.endTime !== void 0;
1304
+ }
1305
+ function resolveWorkflowScope(scope, assetDurationSeconds) {
1306
+ if (assetDurationSeconds === void 0 || !Number.isFinite(assetDurationSeconds) || assetDurationSeconds <= 0) {
1307
+ throw new MuxAiError("Asset has no valid duration.", { type: "validation_error" });
1308
+ }
1309
+ const startTime = scope?.startTime ?? 0;
1310
+ const endTime = scope?.endTime ?? assetDurationSeconds;
1311
+ if (!Number.isFinite(startTime) || startTime < 0) {
1312
+ throw new MuxAiError(
1313
+ `scope.startTime must be a finite, non-negative number (received ${startTime}).`,
1314
+ { type: "validation_error" }
1315
+ );
1316
+ }
1317
+ if (!Number.isFinite(endTime) || endTime < 0) {
1318
+ throw new MuxAiError(
1319
+ `scope.endTime must be a finite, non-negative number (received ${endTime}).`,
1320
+ { type: "validation_error" }
1321
+ );
1322
+ }
1323
+ if (endTime > assetDurationSeconds) {
1324
+ throw new MuxAiError(
1325
+ `scope.endTime (${endTime}) cannot exceed the asset duration (${assetDurationSeconds}).`,
1326
+ { type: "validation_error" }
1327
+ );
1328
+ }
1329
+ if (startTime >= endTime) {
1330
+ throw new MuxAiError(
1331
+ `scope.startTime (${startTime}) must be less than scope.endTime (${endTime}).`,
1332
+ { type: "validation_error" }
1333
+ );
1334
+ }
1335
+ return { startTime, endTime };
1336
+ }
1337
+ function resolveRenderableVideoScope(scope, assetDurationSeconds, videoTrackDurationSeconds) {
1338
+ if (!hasWorkflowScopeBoundaries(scope)) {
1339
+ return void 0;
1340
+ }
1341
+ const resolvedScope = resolveWorkflowScope(scope, assetDurationSeconds);
1342
+ let renderableEndTime = resolvedScope.endTime;
1343
+ if (videoTrackDurationSeconds !== void 0 && Number.isFinite(videoTrackDurationSeconds) && videoTrackDurationSeconds >= 0) {
1344
+ renderableEndTime = Math.min(videoTrackDurationSeconds, resolvedScope.endTime);
1345
+ }
1346
+ const renderableScope = {
1347
+ startTime: Math.min(resolvedScope.startTime, renderableEndTime),
1348
+ endTime: renderableEndTime
1349
+ };
1350
+ if (renderableScope.startTime >= renderableScope.endTime) {
1351
+ throw new MuxAiError("The requested scope does not include any renderable video.", {
1352
+ type: "validation_error"
1353
+ });
1354
+ }
1355
+ return renderableScope;
1356
+ }
1357
+ function timeRangesOverlap(firstStart, firstEnd, second) {
1358
+ const secondStart = second.startTime ?? 0;
1359
+ const secondEnd = second.endTime ?? Number.POSITIVE_INFINITY;
1360
+ return firstStart < secondEnd && firstEnd > secondStart;
1361
+ }
1362
+
1301
1363
  // src/lib/mux-url.ts
1302
1364
  var DEFAULT_MUX_IMAGE_ORIGIN = "https://image.mux.com";
1303
1365
  var DEFAULT_MUX_STREAM_ORIGIN = "https://stream.mux.com";
@@ -1384,13 +1446,22 @@ async function signUrl(url, playbackId, type = "video", params, credentials) {
1384
1446
 
1385
1447
  // src/primitives/storyboards.ts
1386
1448
  var DEFAULT_STORYBOARD_WIDTH = 640;
1387
- async function getStoryboardUrl(playbackId, width = DEFAULT_STORYBOARD_WIDTH, shouldSign = false, credentials) {
1449
+ async function getStoryboardUrl(playbackId, width = DEFAULT_STORYBOARD_WIDTH, shouldSign = false, credentials, scope) {
1388
1450
  "use step";
1389
1451
  const baseUrl = getMuxStoryboardBaseUrl(playbackId);
1452
+ const params = { width };
1453
+ if (scope?.startTime !== void 0) {
1454
+ params.asset_start_time = scope.startTime;
1455
+ }
1456
+ if (scope?.endTime !== void 0) {
1457
+ params.asset_end_time = scope.endTime;
1458
+ }
1390
1459
  if (shouldSign) {
1391
- return signUrl(baseUrl, playbackId, "storyboard", { width }, credentials);
1460
+ return signUrl(baseUrl, playbackId, "storyboard", params, credentials);
1392
1461
  }
1393
- return `${baseUrl}?width=${width}`;
1462
+ return `${baseUrl}?${new URLSearchParams(
1463
+ Object.entries(params).map(([key, value]) => [key, String(value)])
1464
+ ).toString()}`;
1394
1465
  }
1395
1466
 
1396
1467
  // src/lib/language-codes.ts
@@ -1829,6 +1900,23 @@ function parseVTTCues(vttContent) {
1829
1900
  }
1830
1901
  return cues;
1831
1902
  }
1903
+ function filterVttByScope(vttContent, scope) {
1904
+ if (!scope || scope.startTime === void 0 && scope.endTime === void 0) {
1905
+ return vttContent;
1906
+ }
1907
+ const { preamble, cueBlocks } = splitVttPreambleAndCueBlocks(vttContent);
1908
+ const scopedCueBlocks = cueBlocks.filter((cueBlock) => {
1909
+ const cue = parseVTTCues(cueBlock)[0];
1910
+ return cue ? timeRangesOverlap(cue.startTime, cue.endTime, scope) : false;
1911
+ }).map((cueBlock, index) => {
1912
+ const lines = cueBlock.split("\n");
1913
+ if (/^\d+$/.test(lines[0]?.trim()) && lines[1]?.includes("-->")) {
1914
+ lines[0] = String(index + 1);
1915
+ }
1916
+ return lines.join("\n");
1917
+ });
1918
+ return buildVttFromCueBlocks(scopedCueBlocks, preamble);
1919
+ }
1832
1920
  function splitVttPreambleAndCueBlocks(vttContent) {
1833
1921
  const normalizedContent = normalizeLineEndings(vttContent).trim();
1834
1922
  if (!normalizedContent) {
@@ -1933,6 +2021,7 @@ async function fetchTranscriptForAsset(asset, playbackId, options = {}) {
1933
2021
  const {
1934
2022
  languageCode,
1935
2023
  cleanTranscript = true,
2024
+ scope,
1936
2025
  shouldSign,
1937
2026
  credentials,
1938
2027
  required = false
@@ -1964,9 +2053,15 @@ async function fetchTranscriptForAsset(asset, playbackId, options = {}) {
1964
2053
  return { transcriptText: "", transcriptUrl, track };
1965
2054
  }
1966
2055
  const rawVtt = await response.text();
1967
- const transcriptText = cleanTranscript ? extractTextFromVTT(rawVtt) : rawVtt;
1968
- if (required && !transcriptText.trim()) {
1969
- throw new MuxAiError("Transcript is empty.", { type: "validation_error" });
2056
+ const effectiveScope = hasWorkflowScopeBoundaries(scope) ? scope : void 0;
2057
+ const scopedVtt = filterVttByScope(rawVtt, effectiveScope);
2058
+ const transcriptText = cleanTranscript ? extractTextFromVTT(scopedVtt) : scopedVtt;
2059
+ const hasScopedCues = !effectiveScope || parseVTTCues(scopedVtt).length > 0;
2060
+ if (required && (!transcriptText.trim() || !hasScopedCues)) {
2061
+ throw new MuxAiError(
2062
+ effectiveScope ? "Transcript has no cues in the requested scope." : "Transcript is empty.",
2063
+ { type: "validation_error" }
2064
+ );
1970
2065
  }
1971
2066
  return { transcriptText, transcriptUrl, track };
1972
2067
  } catch (error) {
@@ -2476,7 +2571,8 @@ async function askQuestionsInternal(assetId, questions, options, collectedUsage)
2476
2571
  imageSubmissionMode = "url",
2477
2572
  imageDownloadOptions,
2478
2573
  storyboardWidth = 640,
2479
- credentials
2574
+ credentials,
2575
+ scope
2480
2576
  } = options ?? {};
2481
2577
  const normalizedQuestions = questions.map((q, idx) => normalizeQuestion(q, idx));
2482
2578
  const hasFreeForm = normalizedQuestions.some((q) => q.mode.kind === "freeForm");
@@ -2488,6 +2584,15 @@ async function askQuestionsInternal(assetId, questions, options, collectedUsage)
2488
2584
  const { asset: assetData, playbackId, policy } = await getPlaybackIdForAsset(assetId, credentials);
2489
2585
  const assetDurationSeconds = getAssetDurationSecondsFromAsset(assetData);
2490
2586
  const isAudioOnly = isAudioOnlyAsset(assetData);
2587
+ const effectiveScope = hasWorkflowScopeBoundaries(scope) ? scope : void 0;
2588
+ const storyboardScope = isAudioOnly ? void 0 : resolveRenderableVideoScope(
2589
+ effectiveScope,
2590
+ assetDurationSeconds,
2591
+ getVideoTrackDurationSecondsFromAsset(assetData)
2592
+ );
2593
+ if (isAudioOnly && effectiveScope) {
2594
+ resolveWorkflowScope(effectiveScope, assetDurationSeconds);
2595
+ }
2491
2596
  if (isAudioOnly && !includeTranscript) {
2492
2597
  throw new MuxAiError(
2493
2598
  "Audio-only assets require a transcript. Set includeTranscript: true and ensure the asset has a ready text track (captions/subtitles).",
@@ -2506,7 +2611,8 @@ async function askQuestionsInternal(assetId, questions, options, collectedUsage)
2506
2611
  cleanTranscript,
2507
2612
  shouldSign: policy === "signed",
2508
2613
  credentials,
2509
- required: isAudioOnly
2614
+ required: isAudioOnly,
2615
+ scope: effectiveScope
2510
2616
  }) : void 0;
2511
2617
  const transcriptText = transcriptResult?.transcriptText ?? "";
2512
2618
  const userPrompt = buildUserPrompt({
@@ -2535,7 +2641,8 @@ async function askQuestionsInternal(assetId, questions, options, collectedUsage)
2535
2641
  playbackId,
2536
2642
  storyboardWidth,
2537
2643
  policy === "signed",
2538
- credentials
2644
+ credentials,
2645
+ storyboardScope
2539
2646
  );
2540
2647
  imageUrl = storyboardUrl;
2541
2648
  if (imageSubmissionMode === "base64") {
@@ -2818,6 +2925,7 @@ async function hasBurnedInCaptionsInternal(assetId, options, collectedUsage) {
2818
2925
  imageDownloadOptions,
2819
2926
  promptOverrides,
2820
2927
  credentials,
2928
+ scope,
2821
2929
  ...config
2822
2930
  } = options;
2823
2931
  const userPrompt = buildUserPrompt2(promptOverrides);
@@ -2828,7 +2936,18 @@ async function hasBurnedInCaptionsInternal(assetId, options, collectedUsage) {
2828
2936
  });
2829
2937
  const { asset: assetData, playbackId, policy } = await getPlaybackIdForAsset(assetId, credentials);
2830
2938
  const assetDurationSeconds = getAssetDurationSecondsFromAsset(assetData);
2831
- const imageUrl = await getStoryboardUrl(playbackId, 640, policy === "signed", credentials);
2939
+ const storyboardScope = resolveRenderableVideoScope(
2940
+ scope,
2941
+ assetDurationSeconds,
2942
+ getVideoTrackDurationSecondsFromAsset(assetData)
2943
+ );
2944
+ const imageUrl = await getStoryboardUrl(
2945
+ playbackId,
2946
+ 640,
2947
+ policy === "signed",
2948
+ credentials,
2949
+ storyboardScope
2950
+ );
2832
2951
  let analysisResponse;
2833
2952
  if (imageSubmissionMode === "base64") {
2834
2953
  const base64Data = await fetchImageAsBase642(imageUrl, imageDownloadOptions);
@@ -3027,7 +3146,8 @@ function buildUserPrompt3({
3027
3146
  promptOverrides,
3028
3147
  minChaptersPerHour = 3,
3029
3148
  maxChaptersPerHour = 8,
3030
- languageName
3149
+ languageName,
3150
+ scope
3031
3151
  }) {
3032
3152
  const contextSections = [
3033
3153
  {
@@ -3042,6 +3162,8 @@ function buildUserPrompt3({
3042
3162
  const dynamicChapterGuidelines = dedent4`
3043
3163
  - Create at least ${minChaptersPerHour} and at most ${maxChaptersPerHour} chapters per hour of content
3044
3164
  - Use start times in seconds (not HH:MM:SS)
3165
+ ${scope ? `- Analyze only the range from ${scope.startTime}s (inclusive) to ${scope.endTime}s (exclusive)
3166
+ - The first chapter must start at ${scope.startTime}s` : ""}
3045
3167
  - Chapter start times should be non-decreasing
3046
3168
  - Do not include text before or after the JSON`;
3047
3169
  const mergedOverrides = {
@@ -3068,7 +3190,8 @@ async function generateChaptersInternal(assetId, options, collectedUsage) {
3068
3190
  minChaptersPerHour,
3069
3191
  maxChaptersPerHour,
3070
3192
  credentials,
3071
- outputLanguageCode
3193
+ outputLanguageCode,
3194
+ scope
3072
3195
  } = options;
3073
3196
  const modelConfig = resolveLanguageModelConfig({
3074
3197
  ...options,
@@ -3077,6 +3200,8 @@ async function generateChaptersInternal(assetId, options, collectedUsage) {
3077
3200
  });
3078
3201
  const { asset: assetData, playbackId, policy } = await getPlaybackIdForAsset(assetId, credentials);
3079
3202
  const assetDurationSeconds = getAssetDurationSecondsFromAsset(assetData);
3203
+ const effectiveScope = hasWorkflowScopeBoundaries(scope) ? scope : void 0;
3204
+ const resolvedScope = effectiveScope ? resolveWorkflowScope(effectiveScope, assetDurationSeconds) : void 0;
3080
3205
  const isAudioOnly = isAudioOnlyAsset(assetData);
3081
3206
  const signingContext = await resolveMuxSigningContext(credentials);
3082
3207
  if (policy === "signed" && !signingContext) {
@@ -3091,7 +3216,8 @@ async function generateChaptersInternal(assetId, options, collectedUsage) {
3091
3216
  cleanTranscript: false,
3092
3217
  // keep timestamps for chapter segmentation
3093
3218
  shouldSign: policy === "signed",
3094
- credentials
3219
+ credentials,
3220
+ scope: effectiveScope
3095
3221
  });
3096
3222
  if (!transcriptResult.track || !transcriptResult.transcriptText) {
3097
3223
  const availableLanguages = readyTextTracks.map((t) => t.language_code).filter(Boolean).join(", ");
@@ -3112,7 +3238,8 @@ async function generateChaptersInternal(assetId, options, collectedUsage) {
3112
3238
  promptOverrides,
3113
3239
  minChaptersPerHour,
3114
3240
  maxChaptersPerHour,
3115
- languageName
3241
+ languageName,
3242
+ scope: resolvedScope
3116
3243
  });
3117
3244
  let chaptersData = null;
3118
3245
  try {
@@ -3134,7 +3261,9 @@ async function generateChaptersInternal(assetId, options, collectedUsage) {
3134
3261
  throw new MuxAiError(`Failed to generate chapters for asset ${assetId}.`);
3135
3262
  }
3136
3263
  const { chapters: chaptersPayload, usage } = chaptersData;
3137
- const validChapters = chaptersPayload.chapters.filter((chapter) => typeof chapter.startTime === "number" && typeof chapter.title === "string").sort((a, b) => a.startTime - b.startTime);
3264
+ const validChapters = chaptersPayload.chapters.filter((chapter) => typeof chapter.startTime === "number" && typeof chapter.title === "string").filter(
3265
+ (chapter) => resolvedScope ? chapter.startTime >= resolvedScope.startTime && chapter.startTime < resolvedScope.endTime : true
3266
+ ).sort((a, b) => a.startTime - b.startTime);
3138
3267
  if (validChapters.length === 0) {
3139
3268
  throw new MuxAiError(`Failed to generate valid chapters for asset ${assetId}.`);
3140
3269
  }
@@ -3160,8 +3289,9 @@ async function generateChaptersInternal(assetId, options, collectedUsage) {
3160
3289
  if (scrubbedChapters.length === 0) {
3161
3290
  throw new MuxAiError(`Failed to generate valid chapters for asset ${assetId}.`);
3162
3291
  }
3163
- if (scrubbedChapters[0].startTime !== 0) {
3164
- scrubbedChapters[0].startTime = 0;
3292
+ const firstChapterStartTime = resolvedScope?.startTime ?? 0;
3293
+ if (scrubbedChapters[0].startTime !== firstChapterStartTime) {
3294
+ scrubbedChapters[0].startTime = firstChapterStartTime;
3165
3295
  }
3166
3296
  const usageWithMetadata = {
3167
3297
  ...usage,
@@ -4192,11 +4322,16 @@ async function generateEmbeddingsInternal(assetId, options, collectedUsage) {
4192
4322
  languageCode,
4193
4323
  chunkingStrategy = { type: "token", maxTokens: 500, overlap: 100 },
4194
4324
  batchSize = 5,
4195
- credentials
4325
+ credentials,
4326
+ scope
4196
4327
  } = options;
4197
4328
  const embeddingModel = resolveEmbeddingModelConfig({ ...options, provider, model });
4198
4329
  const { asset: assetData, playbackId, policy } = await getPlaybackIdForAsset(assetId, credentials);
4199
4330
  const assetDurationSeconds = getAssetDurationSecondsFromAsset(assetData);
4331
+ const effectiveScope = hasWorkflowScopeBoundaries(scope) ? scope : void 0;
4332
+ if (effectiveScope) {
4333
+ resolveWorkflowScope(effectiveScope, assetDurationSeconds);
4334
+ }
4200
4335
  const signingContext = await resolveMuxSigningContext(credentials);
4201
4336
  if (policy === "signed" && !signingContext) {
4202
4337
  throw new Error(
@@ -4209,7 +4344,8 @@ async function generateEmbeddingsInternal(assetId, options, collectedUsage) {
4209
4344
  cleanTranscript: !useVttChunking,
4210
4345
  shouldSign: policy === "signed",
4211
4346
  credentials,
4212
- required: true
4347
+ required: true,
4348
+ scope: effectiveScope
4213
4349
  });
4214
4350
  const transcriptText = transcriptResult.transcriptText;
4215
4351
  const chunks = useVttChunking ? chunkVTTCues(
@@ -5048,27 +5184,32 @@ function planSamplingTimestamps(options) {
5048
5184
  // src/primitives/thumbnails.ts
5049
5185
  async function getThumbnailUrls(playbackId, duration, options = {}) {
5050
5186
  "use step";
5051
- const { interval = 10, width = 640, shouldSign = false, maxSamples, credentials } = options;
5187
+ const { interval = 10, width = 640, shouldSign = false, maxSamples, credentials, scope } = options;
5188
+ const effectiveScope = hasWorkflowScopeBoundaries(scope) ? scope : void 0;
5189
+ const resolvedScope = effectiveScope ? resolveWorkflowScope(effectiveScope, duration) : { startTime: 0, endTime: duration };
5190
+ const rangeDuration = resolvedScope.endTime - resolvedScope.startTime;
5052
5191
  let timestamps = [];
5053
- if (duration <= 50) {
5054
- const spacing = duration / 6;
5192
+ if (rangeDuration <= 50) {
5193
+ const spacing = rangeDuration / 6;
5055
5194
  for (let i = 1; i <= 5; i++) {
5056
- timestamps.push(Math.round(i * spacing));
5195
+ const time = resolvedScope.startTime + i * spacing;
5196
+ timestamps.push(effectiveScope ? Number(time.toFixed(3)) : Math.round(time));
5057
5197
  }
5058
5198
  } else {
5059
- for (let time = 0; time < duration; time += interval) {
5199
+ for (let time = resolvedScope.startTime; time < resolvedScope.endTime; time += interval) {
5060
5200
  timestamps.push(time);
5061
5201
  }
5062
5202
  }
5063
5203
  if (maxSamples !== void 0 && timestamps.length > maxSamples) {
5064
5204
  const newTimestamps = [];
5065
- newTimestamps.push(0);
5205
+ newTimestamps.push(resolvedScope.startTime);
5066
5206
  if (maxSamples >= 2) {
5067
- const spacing = duration / (maxSamples - 1);
5207
+ const lastTime = effectiveScope ? Math.max(resolvedScope.startTime, resolvedScope.endTime - 1e-3) : resolvedScope.endTime;
5208
+ const spacing = (lastTime - resolvedScope.startTime) / (maxSamples - 1);
5068
5209
  for (let i = 1; i < maxSamples - 1; i++) {
5069
- newTimestamps.push(spacing * i);
5210
+ newTimestamps.push(resolvedScope.startTime + spacing * i);
5070
5211
  }
5071
- newTimestamps.push(duration);
5212
+ newTimestamps.push(lastTime);
5072
5213
  }
5073
5214
  timestamps = newTimestamps;
5074
5215
  }
@@ -5521,12 +5662,24 @@ async function requestGoogleVisionModeration(images, maxConcurrent = 5, submissi
5521
5662
  }));
5522
5663
  return processConcurrently(targets, moderateImageWithGoogleVision, maxConcurrent);
5523
5664
  }
5665
+ function toThumbnailTimeSeconds(timestampMs) {
5666
+ return Number((timestampMs / 1e3).toFixed(2));
5667
+ }
5668
+ function filterTimestampsWithinExclusiveScope(timestampsMs, scope) {
5669
+ if (!scope) {
5670
+ return timestampsMs;
5671
+ }
5672
+ return timestampsMs.filter((timestampMs) => {
5673
+ const time = toThumbnailTimeSeconds(timestampMs);
5674
+ return time >= scope.startTime && time < scope.endTime;
5675
+ });
5676
+ }
5524
5677
  async function getThumbnailUrlsFromTimestamps(playbackId, timestampsMs, options) {
5525
5678
  "use step";
5526
5679
  const { width, shouldSign, credentials } = options;
5527
5680
  const baseUrl = getMuxThumbnailBaseUrl(playbackId);
5528
5681
  const urlPromises = timestampsMs.map(async (tsMs) => {
5529
- const time = Number((tsMs / 1e3).toFixed(2));
5682
+ const time = toThumbnailTimeSeconds(tsMs);
5530
5683
  const url = shouldSign ? await signUrl(baseUrl, playbackId, "thumbnail", { time, width }, credentials) : `${baseUrl}?time=${time}&width=${width}`;
5531
5684
  return { url, time };
5532
5685
  });
@@ -5545,7 +5698,8 @@ async function getModerationScores(assetId, options = {}) {
5545
5698
  maxConcurrent = 5,
5546
5699
  imageSubmissionMode = "url",
5547
5700
  imageDownloadOptions,
5548
- credentials: providedCredentials
5701
+ credentials: providedCredentials,
5702
+ scope
5549
5703
  } = options;
5550
5704
  const credentials = providedCredentials;
5551
5705
  const { asset, playbackId, policy } = await getPlaybackIdForAsset(assetId, credentials);
@@ -5556,7 +5710,16 @@ async function getModerationScores(assetId, options = {}) {
5556
5710
  (d) => d != null
5557
5711
  );
5558
5712
  const duration = candidateDurations.length > 0 ? Math.min(...candidateDurations) : 0;
5713
+ const effectiveScope = hasWorkflowScopeBoundaries(scope) ? scope : void 0;
5714
+ const resolvedScope = effectiveScope ? resolveWorkflowScope(effectiveScope, assetDurationSeconds) : void 0;
5559
5715
  const isAudioOnly = isAudioOnlyAsset(asset);
5716
+ const renderableScope = resolvedScope ? {
5717
+ startTime: Math.min(resolvedScope.startTime, duration),
5718
+ endTime: Math.min(resolvedScope.endTime, duration)
5719
+ } : void 0;
5720
+ if (!isAudioOnly && renderableScope && renderableScope.startTime >= renderableScope.endTime) {
5721
+ throw new Error("The requested scope does not include any renderable video.");
5722
+ }
5560
5723
  const signingContext = await resolveMuxSigningContext(credentials);
5561
5724
  if (policy === "signed" && !signingContext) {
5562
5725
  throw new Error(
@@ -5573,7 +5736,8 @@ async function getModerationScores(assetId, options = {}) {
5573
5736
  cleanTranscript: true,
5574
5737
  shouldSign: policy === "signed",
5575
5738
  credentials,
5576
- required: true
5739
+ required: true,
5740
+ scope: effectiveScope
5577
5741
  });
5578
5742
  if (provider === "openai") {
5579
5743
  thumbnailScores = await requestOpenAITranscriptModeration(
@@ -5591,17 +5755,21 @@ async function getModerationScores(assetId, options = {}) {
5591
5755
  throw new Error(`Unsupported moderation provider: ${exhaustiveCheck}`);
5592
5756
  }
5593
5757
  } else {
5594
- const estimatedIntervalCount = duration <= 50 ? 5 : Math.ceil(duration / thumbnailInterval);
5758
+ const scopedDuration = renderableScope ? renderableScope.endTime - renderableScope.startTime : duration;
5759
+ const estimatedIntervalCount = scopedDuration <= 50 ? 5 : Math.ceil(scopedDuration / thumbnailInterval);
5595
5760
  const thumbnailUrls = maxSamples !== void 0 && estimatedIntervalCount > maxSamples ? await getThumbnailUrlsFromTimestamps(
5596
5761
  playbackId,
5597
- planSamplingTimestamps({
5598
- duration_sec: duration,
5599
- max_candidates: maxSamples,
5600
- trim_start_sec: duration > 2 ? Math.min(5, Math.max(1, duration / 6)) : 0,
5601
- trim_end_sec: duration > 2 ? Math.min(5, Math.max(1, duration / 6)) : 0,
5602
- fps: videoTrackFps,
5603
- base_cadence_hz: thumbnailInterval > 0 ? 1 / thumbnailInterval : void 0
5604
- }),
5762
+ filterTimestampsWithinExclusiveScope(
5763
+ planSamplingTimestamps({
5764
+ duration_sec: duration,
5765
+ max_candidates: maxSamples,
5766
+ trim_start_sec: renderableScope?.startTime ?? (duration > 2 ? Math.min(5, Math.max(1, duration / 6)) : 0),
5767
+ trim_end_sec: renderableScope ? duration - renderableScope.endTime : duration > 2 ? Math.min(5, Math.max(1, duration / 6)) : 0,
5768
+ fps: videoTrackFps,
5769
+ base_cadence_hz: thumbnailInterval > 0 ? 1 / thumbnailInterval : void 0
5770
+ }),
5771
+ renderableScope
5772
+ ),
5605
5773
  {
5606
5774
  width: thumbnailWidth,
5607
5775
  shouldSign: policy === "signed",
@@ -5611,7 +5779,8 @@ async function getModerationScores(assetId, options = {}) {
5611
5779
  interval: thumbnailInterval,
5612
5780
  width: thumbnailWidth,
5613
5781
  shouldSign: policy === "signed",
5614
- credentials
5782
+ credentials,
5783
+ scope: renderableScope
5615
5784
  });
5616
5785
  thumbnailCount = thumbnailUrls.length;
5617
5786
  if (provider === "openai") {
@@ -6126,7 +6295,8 @@ async function getSummaryAndTagsInternal(assetId, options, collectedUsage) {
6126
6295
  titleLength,
6127
6296
  descriptionLength,
6128
6297
  tagCount,
6129
- outputLanguageCode
6298
+ outputLanguageCode,
6299
+ scope
6130
6300
  } = options ?? {};
6131
6301
  if (!VALID_TONES.includes(tone)) {
6132
6302
  throw new MuxAiError(
@@ -6143,6 +6313,15 @@ async function getSummaryAndTagsInternal(assetId, options, collectedUsage) {
6143
6313
  const { asset: assetData, playbackId, policy } = await getPlaybackIdForAsset(assetId, workflowCredentials);
6144
6314
  const assetDurationSeconds = getAssetDurationSecondsFromAsset(assetData);
6145
6315
  const isAudioOnly = isAudioOnlyAsset(assetData);
6316
+ const effectiveScope = hasWorkflowScopeBoundaries(scope) ? scope : void 0;
6317
+ const storyboardScope = isAudioOnly ? void 0 : resolveRenderableVideoScope(
6318
+ effectiveScope,
6319
+ assetDurationSeconds,
6320
+ getVideoTrackDurationSecondsFromAsset(assetData)
6321
+ );
6322
+ if (isAudioOnly && effectiveScope) {
6323
+ resolveWorkflowScope(effectiveScope, assetDurationSeconds);
6324
+ }
6146
6325
  if (isAudioOnly && !includeTranscript) {
6147
6326
  throw new MuxAiError(
6148
6327
  "Audio-only assets require a transcript. Set includeTranscript: true and ensure the asset has a ready text track (captions/subtitles).",
@@ -6161,7 +6340,8 @@ async function getSummaryAndTagsInternal(assetId, options, collectedUsage) {
6161
6340
  cleanTranscript,
6162
6341
  shouldSign: policy === "signed",
6163
6342
  credentials: workflowCredentials,
6164
- required: isAudioOnly
6343
+ required: isAudioOnly,
6344
+ scope: effectiveScope
6165
6345
  }) : void 0;
6166
6346
  const transcriptText = transcriptResult?.transcriptText ?? "";
6167
6347
  const resolvedLanguageCode = outputLanguageCode && outputLanguageCode !== "auto" ? outputLanguageCode : getReliableLanguageCode(transcriptResult?.track) ?? getReliableLanguageCode(getReadyTextTracks(assetData)[0]);
@@ -6192,7 +6372,13 @@ async function getSummaryAndTagsInternal(assetId, options, collectedUsage) {
6192
6372
  workflowCredentials
6193
6373
  );
6194
6374
  } else {
6195
- const storyboardUrl = await getStoryboardUrl(playbackId, 640, policy === "signed", workflowCredentials);
6375
+ const storyboardUrl = await getStoryboardUrl(
6376
+ playbackId,
6377
+ 640,
6378
+ policy === "signed",
6379
+ workflowCredentials,
6380
+ storyboardScope
6381
+ );
6196
6382
  imageUrl = storyboardUrl;
6197
6383
  if (imageSubmissionMode === "base64") {
6198
6384
  const downloadResult = await downloadImageAsBase64(storyboardUrl, imageDownloadOptions);
@@ -6407,9 +6593,16 @@ async function checkElevenLabsDubbingStatus({
6407
6593
  targetLanguages: statusData.target_languages ?? []
6408
6594
  };
6409
6595
  }
6410
- async function downloadDubbedAudioFromElevenLabs({
6596
+ async function downloadAndUploadDubbedAudio({
6411
6597
  dubbingId,
6412
6598
  languageCode,
6599
+ assetId,
6600
+ toLanguageCode,
6601
+ s3Endpoint,
6602
+ s3Region,
6603
+ s3Bucket,
6604
+ storageAdapter,
6605
+ s3SignedUrlExpirySeconds,
6413
6606
  credentials
6414
6607
  }) {
6415
6608
  "use step";
@@ -6423,19 +6616,7 @@ async function downloadDubbedAudioFromElevenLabs({
6423
6616
  if (!audioResponse.ok) {
6424
6617
  throw new Error(`Failed to fetch dubbed audio: ${audioResponse.statusText}`);
6425
6618
  }
6426
- return audioResponse.arrayBuffer();
6427
- }
6428
- async function uploadDubbedAudioToS3({
6429
- dubbedAudioBuffer,
6430
- assetId,
6431
- toLanguageCode,
6432
- s3Endpoint,
6433
- s3Region,
6434
- s3Bucket,
6435
- storageAdapter,
6436
- s3SignedUrlExpirySeconds
6437
- }) {
6438
- "use step";
6619
+ const dubbedAudio = new Uint8Array(await audioResponse.arrayBuffer());
6439
6620
  const s3AccessKeyId = env_default.S3_ACCESS_KEY_ID;
6440
6621
  const s3SecretAccessKey = env_default.S3_SECRET_ACCESS_KEY;
6441
6622
  const audioKey = `audio-translations/${assetId}/auto-to-${toLanguageCode}-${Date.now()}.m4a`;
@@ -6446,7 +6627,7 @@ async function uploadDubbedAudioToS3({
6446
6627
  region: s3Region,
6447
6628
  bucket: s3Bucket,
6448
6629
  key: audioKey,
6449
- body: new Uint8Array(dubbedAudioBuffer),
6630
+ body: dubbedAudio,
6450
6631
  contentType: "audio/mp4"
6451
6632
  }, storageAdapter);
6452
6633
  const presignedUrl = await createPresignedGetUrlWithStorageAdapter({
@@ -6580,46 +6761,24 @@ async function translateAudio(assetId, toLanguageCode, options = {}) {
6580
6761
  let presignedUrl;
6581
6762
  let uploadedTrackId;
6582
6763
  if (uploadToS3) {
6583
- console.warn("\u{1F4E5} Downloading dubbed audio from ElevenLabs...");
6584
- let dubbedAudioBuffer;
6764
+ console.warn("\u{1F4E5} Downloading dubbed audio from ElevenLabs and staging to S3...");
6585
6765
  try {
6586
- const requestedLangCode = toISO639_3(toLanguageCode);
6587
- let downloadLangCode = targetLanguages.find(
6588
- (lang) => lang === requestedLangCode
6589
- ) ?? targetLanguages.find(
6590
- (lang) => lang.toLowerCase() === requestedLangCode.toLowerCase()
6591
- );
6592
- if (!downloadLangCode && targetLanguages.length > 0) {
6593
- downloadLangCode = targetLanguages[0];
6594
- console.warn(`\u26A0\uFE0F Requested language "${requestedLangCode}" not found in target_languages. Using "${downloadLangCode}" instead.`);
6595
- }
6596
- if (!downloadLangCode) {
6597
- downloadLangCode = requestedLangCode;
6598
- console.warn(`\u26A0\uFE0F No target_languages available from ElevenLabs status. Using requested language code: ${requestedLangCode}`);
6599
- }
6600
- dubbedAudioBuffer = await downloadDubbedAudioFromElevenLabs({
6766
+ const downloadLangCode = targetLanguages[0] ?? toISO639_1(toLanguageCode);
6767
+ presignedUrl = await downloadAndUploadDubbedAudio({
6601
6768
  dubbingId,
6602
6769
  languageCode: downloadLangCode,
6603
- credentials
6604
- });
6605
- console.warn("\u2705 Dubbed audio downloaded successfully!");
6606
- } catch (error) {
6607
- wrapError(error, "Failed to download dubbed audio");
6608
- }
6609
- console.warn("\u{1F4E4} Uploading dubbed audio to S3-compatible storage...");
6610
- try {
6611
- presignedUrl = await uploadDubbedAudioToS3({
6612
- dubbedAudioBuffer,
6613
6770
  assetId,
6614
6771
  toLanguageCode,
6615
6772
  s3Endpoint,
6616
6773
  s3Region,
6617
6774
  s3Bucket,
6618
6775
  storageAdapter: effectiveStorageAdapter,
6619
- s3SignedUrlExpirySeconds: options.s3SignedUrlExpirySeconds
6776
+ s3SignedUrlExpirySeconds: options.s3SignedUrlExpirySeconds,
6777
+ credentials
6620
6778
  });
6779
+ console.warn("\u2705 Dubbed audio staged to S3 successfully!");
6621
6780
  } catch (error) {
6622
- wrapError(error, "Failed to upload audio to S3");
6781
+ wrapError(error, "Failed to download and upload dubbed audio");
6623
6782
  }
6624
6783
  if (uploadToMux) {
6625
6784
  console.warn("\u{1F4F9} Adding dubbed audio track to Mux asset...");