@mux/ai 0.28.1 → 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.
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.28.1";
8
+ var version = "0.29.0";
9
9
 
10
10
  // src/lib/language-codes.ts
11
11
  var ISO639_1_TO_3 = {
@@ -1372,6 +1372,7 @@ __export(primitives_exports, {
1372
1372
  extractTextFromVTT: () => extractTextFromVTT,
1373
1373
  extractTimestampedTranscript: () => extractTimestampedTranscript,
1374
1374
  fetchTranscriptForAsset: () => fetchTranscriptForAsset,
1375
+ filterVttByScope: () => filterVttByScope,
1375
1376
  findCaptionTrack: () => findCaptionTrack,
1376
1377
  getHeatmapForAsset: () => getHeatmapForAsset,
1377
1378
  getHeatmapForPlaybackId: () => getHeatmapForPlaybackId,
@@ -1908,13 +1909,22 @@ async function signUrl(url, playbackId, type = "video", params, credentials) {
1908
1909
 
1909
1910
  // src/primitives/storyboards.ts
1910
1911
  var DEFAULT_STORYBOARD_WIDTH = 640;
1911
- async function getStoryboardUrl(playbackId, width = DEFAULT_STORYBOARD_WIDTH, shouldSign = false, credentials) {
1912
+ async function getStoryboardUrl(playbackId, width = DEFAULT_STORYBOARD_WIDTH, shouldSign = false, credentials, scope) {
1912
1913
  "use step";
1913
1914
  const baseUrl = getMuxStoryboardBaseUrl(playbackId);
1915
+ const params = { width };
1916
+ if (scope?.startTime !== void 0) {
1917
+ params.asset_start_time = scope.startTime;
1918
+ }
1919
+ if (scope?.endTime !== void 0) {
1920
+ params.asset_end_time = scope.endTime;
1921
+ }
1914
1922
  if (shouldSign) {
1915
- return signUrl(baseUrl, playbackId, "storyboard", { width }, credentials);
1923
+ return signUrl(baseUrl, playbackId, "storyboard", params, credentials);
1916
1924
  }
1917
- return `${baseUrl}?width=${width}`;
1925
+ return `${baseUrl}?${new URLSearchParams(
1926
+ Object.entries(params).map(([key, value]) => [key, String(value)])
1927
+ ).toString()}`;
1918
1928
  }
1919
1929
 
1920
1930
  // src/primitives/text-chunking.ts
@@ -2155,30 +2165,97 @@ function chunkText(text, strategy) {
2155
2165
  }
2156
2166
  }
2157
2167
 
2168
+ // src/lib/workflow-scope.ts
2169
+ function hasWorkflowScopeBoundaries(scope) {
2170
+ return scope?.startTime !== void 0 || scope?.endTime !== void 0;
2171
+ }
2172
+ function resolveWorkflowScope(scope, assetDurationSeconds) {
2173
+ if (assetDurationSeconds === void 0 || !Number.isFinite(assetDurationSeconds) || assetDurationSeconds <= 0) {
2174
+ throw new MuxAiError("Asset has no valid duration.", { type: "validation_error" });
2175
+ }
2176
+ const startTime = scope?.startTime ?? 0;
2177
+ const endTime = scope?.endTime ?? assetDurationSeconds;
2178
+ if (!Number.isFinite(startTime) || startTime < 0) {
2179
+ throw new MuxAiError(
2180
+ `scope.startTime must be a finite, non-negative number (received ${startTime}).`,
2181
+ { type: "validation_error" }
2182
+ );
2183
+ }
2184
+ if (!Number.isFinite(endTime) || endTime < 0) {
2185
+ throw new MuxAiError(
2186
+ `scope.endTime must be a finite, non-negative number (received ${endTime}).`,
2187
+ { type: "validation_error" }
2188
+ );
2189
+ }
2190
+ if (endTime > assetDurationSeconds) {
2191
+ throw new MuxAiError(
2192
+ `scope.endTime (${endTime}) cannot exceed the asset duration (${assetDurationSeconds}).`,
2193
+ { type: "validation_error" }
2194
+ );
2195
+ }
2196
+ if (startTime >= endTime) {
2197
+ throw new MuxAiError(
2198
+ `scope.startTime (${startTime}) must be less than scope.endTime (${endTime}).`,
2199
+ { type: "validation_error" }
2200
+ );
2201
+ }
2202
+ return { startTime, endTime };
2203
+ }
2204
+ function resolveRenderableVideoScope(scope, assetDurationSeconds, videoTrackDurationSeconds) {
2205
+ if (!hasWorkflowScopeBoundaries(scope)) {
2206
+ return void 0;
2207
+ }
2208
+ const resolvedScope = resolveWorkflowScope(scope, assetDurationSeconds);
2209
+ let renderableEndTime = resolvedScope.endTime;
2210
+ if (videoTrackDurationSeconds !== void 0 && Number.isFinite(videoTrackDurationSeconds) && videoTrackDurationSeconds >= 0) {
2211
+ renderableEndTime = Math.min(videoTrackDurationSeconds, resolvedScope.endTime);
2212
+ }
2213
+ const renderableScope = {
2214
+ startTime: Math.min(resolvedScope.startTime, renderableEndTime),
2215
+ endTime: renderableEndTime
2216
+ };
2217
+ if (renderableScope.startTime >= renderableScope.endTime) {
2218
+ throw new MuxAiError("The requested scope does not include any renderable video.", {
2219
+ type: "validation_error"
2220
+ });
2221
+ }
2222
+ return renderableScope;
2223
+ }
2224
+ function timeRangesOverlap(firstStart, firstEnd, second) {
2225
+ const secondStart = second.startTime ?? 0;
2226
+ const secondEnd = second.endTime ?? Number.POSITIVE_INFINITY;
2227
+ return firstStart < secondEnd && firstEnd > secondStart;
2228
+ }
2229
+
2158
2230
  // src/primitives/thumbnails.ts
2159
2231
  async function getThumbnailUrls(playbackId, duration, options = {}) {
2160
2232
  "use step";
2161
- const { interval = 10, width = 640, shouldSign = false, maxSamples, credentials } = options;
2233
+ const { interval = 10, width = 640, shouldSign = false, maxSamples, credentials, scope } = options;
2234
+ const effectiveScope = hasWorkflowScopeBoundaries(scope) ? scope : void 0;
2235
+ const resolvedScope = effectiveScope ? resolveWorkflowScope(effectiveScope, duration) : { startTime: 0, endTime: duration };
2236
+ const rangeDuration = resolvedScope.endTime - resolvedScope.startTime;
2162
2237
  let timestamps = [];
2163
- if (duration <= 50) {
2164
- const spacing = duration / 6;
2238
+ if (rangeDuration <= 50) {
2239
+ const spacing = rangeDuration / 6;
2165
2240
  for (let i = 1; i <= 5; i++) {
2166
- timestamps.push(Math.round(i * spacing));
2241
+ const time = resolvedScope.startTime + i * spacing;
2242
+ timestamps.push(effectiveScope ? Number(time.toFixed(3)) : Math.round(time));
2167
2243
  }
2168
2244
  } else {
2169
- for (let time = 0; time < duration; time += interval) {
2245
+ for (let time = resolvedScope.startTime; time < resolvedScope.endTime; time += interval) {
2170
2246
  timestamps.push(time);
2171
2247
  }
2172
2248
  }
2173
2249
  if (maxSamples !== void 0 && timestamps.length > maxSamples) {
2174
2250
  const newTimestamps = [];
2175
- newTimestamps.push(0);
2251
+ newTimestamps.push(resolvedScope.startTime);
2176
2252
  if (maxSamples >= 2) {
2177
- const spacing = duration / (maxSamples - 1);
2253
+ const lastTime = effectiveScope ? Math.max(resolvedScope.startTime, resolvedScope.endTime - 1e-3) : resolvedScope.endTime;
2254
+ const spacing = (lastTime - resolvedScope.startTime) / (maxSamples - 1);
2178
2255
  for (let i = 1; i < maxSamples - 1; i++) {
2179
- newTimestamps.push(spacing * i);
2256
+ newTimestamps.push(resolvedScope.startTime + spacing * i);
2180
2257
  }
2181
- newTimestamps.push(duration);
2258
+ newTimestamps.push(lastTime);
2182
2259
  }
2183
2260
  timestamps = newTimestamps;
2184
2261
  }
@@ -2512,6 +2589,23 @@ function parseVTTCues(vttContent) {
2512
2589
  }
2513
2590
  return cues;
2514
2591
  }
2592
+ function filterVttByScope(vttContent, scope) {
2593
+ if (!scope || scope.startTime === void 0 && scope.endTime === void 0) {
2594
+ return vttContent;
2595
+ }
2596
+ const { preamble, cueBlocks } = splitVttPreambleAndCueBlocks(vttContent);
2597
+ const scopedCueBlocks = cueBlocks.filter((cueBlock) => {
2598
+ const cue = parseVTTCues(cueBlock)[0];
2599
+ return cue ? timeRangesOverlap(cue.startTime, cue.endTime, scope) : false;
2600
+ }).map((cueBlock, index) => {
2601
+ const lines = cueBlock.split("\n");
2602
+ if (/^\d+$/.test(lines[0]?.trim()) && lines[1]?.includes("-->")) {
2603
+ lines[0] = String(index + 1);
2604
+ }
2605
+ return lines.join("\n");
2606
+ });
2607
+ return buildVttFromCueBlocks(scopedCueBlocks, preamble);
2608
+ }
2515
2609
  function splitVttPreambleAndCueBlocks(vttContent) {
2516
2610
  const normalizedContent = normalizeLineEndings(vttContent).trim();
2517
2611
  if (!normalizedContent) {
@@ -2616,6 +2710,7 @@ async function fetchTranscriptForAsset(asset, playbackId, options = {}) {
2616
2710
  const {
2617
2711
  languageCode,
2618
2712
  cleanTranscript = true,
2713
+ scope,
2619
2714
  shouldSign,
2620
2715
  credentials,
2621
2716
  required = false
@@ -2647,9 +2742,15 @@ async function fetchTranscriptForAsset(asset, playbackId, options = {}) {
2647
2742
  return { transcriptText: "", transcriptUrl, track };
2648
2743
  }
2649
2744
  const rawVtt = await response.text();
2650
- const transcriptText = cleanTranscript ? extractTextFromVTT(rawVtt) : rawVtt;
2651
- if (required && !transcriptText.trim()) {
2652
- throw new MuxAiError("Transcript is empty.", { type: "validation_error" });
2745
+ const effectiveScope = hasWorkflowScopeBoundaries(scope) ? scope : void 0;
2746
+ const scopedVtt = filterVttByScope(rawVtt, effectiveScope);
2747
+ const transcriptText = cleanTranscript ? extractTextFromVTT(scopedVtt) : scopedVtt;
2748
+ const hasScopedCues = !effectiveScope || parseVTTCues(scopedVtt).length > 0;
2749
+ if (required && (!transcriptText.trim() || !hasScopedCues)) {
2750
+ throw new MuxAiError(
2751
+ effectiveScope ? "Transcript has no cues in the requested scope." : "Transcript is empty.",
2752
+ { type: "validation_error" }
2753
+ );
2653
2754
  }
2654
2755
  return { transcriptText, transcriptUrl, track };
2655
2756
  } catch (error) {
@@ -3431,7 +3532,8 @@ async function askQuestionsInternal(assetId, questions, options, collectedUsage)
3431
3532
  imageSubmissionMode = "url",
3432
3533
  imageDownloadOptions,
3433
3534
  storyboardWidth = 640,
3434
- credentials
3535
+ credentials,
3536
+ scope
3435
3537
  } = options ?? {};
3436
3538
  const normalizedQuestions = questions.map((q, idx) => normalizeQuestion(q, idx));
3437
3539
  const hasFreeForm = normalizedQuestions.some((q) => q.mode.kind === "freeForm");
@@ -3443,6 +3545,15 @@ async function askQuestionsInternal(assetId, questions, options, collectedUsage)
3443
3545
  const { asset: assetData, playbackId, policy } = await getPlaybackIdForAsset(assetId, credentials);
3444
3546
  const assetDurationSeconds = getAssetDurationSecondsFromAsset(assetData);
3445
3547
  const isAudioOnly = isAudioOnlyAsset(assetData);
3548
+ const effectiveScope = hasWorkflowScopeBoundaries(scope) ? scope : void 0;
3549
+ const storyboardScope = isAudioOnly ? void 0 : resolveRenderableVideoScope(
3550
+ effectiveScope,
3551
+ assetDurationSeconds,
3552
+ getVideoTrackDurationSecondsFromAsset(assetData)
3553
+ );
3554
+ if (isAudioOnly && effectiveScope) {
3555
+ resolveWorkflowScope(effectiveScope, assetDurationSeconds);
3556
+ }
3446
3557
  if (isAudioOnly && !includeTranscript) {
3447
3558
  throw new MuxAiError(
3448
3559
  "Audio-only assets require a transcript. Set includeTranscript: true and ensure the asset has a ready text track (captions/subtitles).",
@@ -3461,7 +3572,8 @@ async function askQuestionsInternal(assetId, questions, options, collectedUsage)
3461
3572
  cleanTranscript,
3462
3573
  shouldSign: policy === "signed",
3463
3574
  credentials,
3464
- required: isAudioOnly
3575
+ required: isAudioOnly,
3576
+ scope: effectiveScope
3465
3577
  }) : void 0;
3466
3578
  const transcriptText = transcriptResult?.transcriptText ?? "";
3467
3579
  const userPrompt = buildUserPrompt({
@@ -3490,7 +3602,8 @@ async function askQuestionsInternal(assetId, questions, options, collectedUsage)
3490
3602
  playbackId,
3491
3603
  storyboardWidth,
3492
3604
  policy === "signed",
3493
- credentials
3605
+ credentials,
3606
+ storyboardScope
3494
3607
  );
3495
3608
  imageUrl = storyboardUrl;
3496
3609
  if (imageSubmissionMode === "base64") {
@@ -3773,6 +3886,7 @@ async function hasBurnedInCaptionsInternal(assetId, options, collectedUsage) {
3773
3886
  imageDownloadOptions,
3774
3887
  promptOverrides,
3775
3888
  credentials,
3889
+ scope,
3776
3890
  ...config
3777
3891
  } = options;
3778
3892
  const userPrompt = buildUserPrompt2(promptOverrides);
@@ -3783,7 +3897,18 @@ async function hasBurnedInCaptionsInternal(assetId, options, collectedUsage) {
3783
3897
  });
3784
3898
  const { asset: assetData, playbackId, policy } = await getPlaybackIdForAsset(assetId, credentials);
3785
3899
  const assetDurationSeconds = getAssetDurationSecondsFromAsset(assetData);
3786
- const imageUrl = await getStoryboardUrl(playbackId, 640, policy === "signed", credentials);
3900
+ const storyboardScope = resolveRenderableVideoScope(
3901
+ scope,
3902
+ assetDurationSeconds,
3903
+ getVideoTrackDurationSecondsFromAsset(assetData)
3904
+ );
3905
+ const imageUrl = await getStoryboardUrl(
3906
+ playbackId,
3907
+ 640,
3908
+ policy === "signed",
3909
+ credentials,
3910
+ storyboardScope
3911
+ );
3787
3912
  let analysisResponse;
3788
3913
  if (imageSubmissionMode === "base64") {
3789
3914
  const base64Data = await fetchImageAsBase642(imageUrl, imageDownloadOptions);
@@ -3982,7 +4107,8 @@ function buildUserPrompt3({
3982
4107
  promptOverrides,
3983
4108
  minChaptersPerHour = 3,
3984
4109
  maxChaptersPerHour = 8,
3985
- languageName
4110
+ languageName,
4111
+ scope
3986
4112
  }) {
3987
4113
  const contextSections = [
3988
4114
  {
@@ -3997,6 +4123,8 @@ function buildUserPrompt3({
3997
4123
  const dynamicChapterGuidelines = dedent4`
3998
4124
  - Create at least ${minChaptersPerHour} and at most ${maxChaptersPerHour} chapters per hour of content
3999
4125
  - Use start times in seconds (not HH:MM:SS)
4126
+ ${scope ? `- Analyze only the range from ${scope.startTime}s (inclusive) to ${scope.endTime}s (exclusive)
4127
+ - The first chapter must start at ${scope.startTime}s` : ""}
4000
4128
  - Chapter start times should be non-decreasing
4001
4129
  - Do not include text before or after the JSON`;
4002
4130
  const mergedOverrides = {
@@ -4023,7 +4151,8 @@ async function generateChaptersInternal(assetId, options, collectedUsage) {
4023
4151
  minChaptersPerHour,
4024
4152
  maxChaptersPerHour,
4025
4153
  credentials,
4026
- outputLanguageCode
4154
+ outputLanguageCode,
4155
+ scope
4027
4156
  } = options;
4028
4157
  const modelConfig = resolveLanguageModelConfig({
4029
4158
  ...options,
@@ -4032,6 +4161,8 @@ async function generateChaptersInternal(assetId, options, collectedUsage) {
4032
4161
  });
4033
4162
  const { asset: assetData, playbackId, policy } = await getPlaybackIdForAsset(assetId, credentials);
4034
4163
  const assetDurationSeconds = getAssetDurationSecondsFromAsset(assetData);
4164
+ const effectiveScope = hasWorkflowScopeBoundaries(scope) ? scope : void 0;
4165
+ const resolvedScope = effectiveScope ? resolveWorkflowScope(effectiveScope, assetDurationSeconds) : void 0;
4035
4166
  const isAudioOnly = isAudioOnlyAsset(assetData);
4036
4167
  const signingContext = await resolveMuxSigningContext(credentials);
4037
4168
  if (policy === "signed" && !signingContext) {
@@ -4046,7 +4177,8 @@ async function generateChaptersInternal(assetId, options, collectedUsage) {
4046
4177
  cleanTranscript: false,
4047
4178
  // keep timestamps for chapter segmentation
4048
4179
  shouldSign: policy === "signed",
4049
- credentials
4180
+ credentials,
4181
+ scope: effectiveScope
4050
4182
  });
4051
4183
  if (!transcriptResult.track || !transcriptResult.transcriptText) {
4052
4184
  const availableLanguages = readyTextTracks.map((t) => t.language_code).filter(Boolean).join(", ");
@@ -4067,7 +4199,8 @@ async function generateChaptersInternal(assetId, options, collectedUsage) {
4067
4199
  promptOverrides,
4068
4200
  minChaptersPerHour,
4069
4201
  maxChaptersPerHour,
4070
- languageName
4202
+ languageName,
4203
+ scope: resolvedScope
4071
4204
  });
4072
4205
  let chaptersData = null;
4073
4206
  try {
@@ -4089,7 +4222,9 @@ async function generateChaptersInternal(assetId, options, collectedUsage) {
4089
4222
  throw new MuxAiError(`Failed to generate chapters for asset ${assetId}.`);
4090
4223
  }
4091
4224
  const { chapters: chaptersPayload, usage } = chaptersData;
4092
- const validChapters = chaptersPayload.chapters.filter((chapter) => typeof chapter.startTime === "number" && typeof chapter.title === "string").sort((a, b) => a.startTime - b.startTime);
4225
+ const validChapters = chaptersPayload.chapters.filter((chapter) => typeof chapter.startTime === "number" && typeof chapter.title === "string").filter(
4226
+ (chapter) => resolvedScope ? chapter.startTime >= resolvedScope.startTime && chapter.startTime < resolvedScope.endTime : true
4227
+ ).sort((a, b) => a.startTime - b.startTime);
4093
4228
  if (validChapters.length === 0) {
4094
4229
  throw new MuxAiError(`Failed to generate valid chapters for asset ${assetId}.`);
4095
4230
  }
@@ -4115,8 +4250,9 @@ async function generateChaptersInternal(assetId, options, collectedUsage) {
4115
4250
  if (scrubbedChapters.length === 0) {
4116
4251
  throw new MuxAiError(`Failed to generate valid chapters for asset ${assetId}.`);
4117
4252
  }
4118
- if (scrubbedChapters[0].startTime !== 0) {
4119
- scrubbedChapters[0].startTime = 0;
4253
+ const firstChapterStartTime = resolvedScope?.startTime ?? 0;
4254
+ if (scrubbedChapters[0].startTime !== firstChapterStartTime) {
4255
+ scrubbedChapters[0].startTime = firstChapterStartTime;
4120
4256
  }
4121
4257
  const usageWithMetadata = {
4122
4258
  ...usage,
@@ -4695,11 +4831,16 @@ async function generateEmbeddingsInternal(assetId, options, collectedUsage) {
4695
4831
  languageCode,
4696
4832
  chunkingStrategy = { type: "token", maxTokens: 500, overlap: 100 },
4697
4833
  batchSize = 5,
4698
- credentials
4834
+ credentials,
4835
+ scope
4699
4836
  } = options;
4700
4837
  const embeddingModel = resolveEmbeddingModelConfig({ ...options, provider, model });
4701
4838
  const { asset: assetData, playbackId, policy } = await getPlaybackIdForAsset(assetId, credentials);
4702
4839
  const assetDurationSeconds = getAssetDurationSecondsFromAsset(assetData);
4840
+ const effectiveScope = hasWorkflowScopeBoundaries(scope) ? scope : void 0;
4841
+ if (effectiveScope) {
4842
+ resolveWorkflowScope(effectiveScope, assetDurationSeconds);
4843
+ }
4703
4844
  const signingContext = await resolveMuxSigningContext(credentials);
4704
4845
  if (policy === "signed" && !signingContext) {
4705
4846
  throw new Error(
@@ -4712,7 +4853,8 @@ async function generateEmbeddingsInternal(assetId, options, collectedUsage) {
4712
4853
  cleanTranscript: !useVttChunking,
4713
4854
  shouldSign: policy === "signed",
4714
4855
  credentials,
4715
- required: true
4856
+ required: true,
4857
+ scope: effectiveScope
4716
4858
  });
4717
4859
  const transcriptText = transcriptResult.transcriptText;
4718
4860
  const chunks = useVttChunking ? chunkVTTCues(
@@ -5843,12 +5985,24 @@ async function requestGoogleVisionModeration(images, maxConcurrent = 5, submissi
5843
5985
  }));
5844
5986
  return processConcurrently(targets, moderateImageWithGoogleVision, maxConcurrent);
5845
5987
  }
5988
+ function toThumbnailTimeSeconds(timestampMs) {
5989
+ return Number((timestampMs / 1e3).toFixed(2));
5990
+ }
5991
+ function filterTimestampsWithinExclusiveScope(timestampsMs, scope) {
5992
+ if (!scope) {
5993
+ return timestampsMs;
5994
+ }
5995
+ return timestampsMs.filter((timestampMs) => {
5996
+ const time = toThumbnailTimeSeconds(timestampMs);
5997
+ return time >= scope.startTime && time < scope.endTime;
5998
+ });
5999
+ }
5846
6000
  async function getThumbnailUrlsFromTimestamps(playbackId, timestampsMs, options) {
5847
6001
  "use step";
5848
6002
  const { width, shouldSign, credentials } = options;
5849
6003
  const baseUrl = getMuxThumbnailBaseUrl(playbackId);
5850
6004
  const urlPromises = timestampsMs.map(async (tsMs) => {
5851
- const time = Number((tsMs / 1e3).toFixed(2));
6005
+ const time = toThumbnailTimeSeconds(tsMs);
5852
6006
  const url = shouldSign ? await signUrl(baseUrl, playbackId, "thumbnail", { time, width }, credentials) : `${baseUrl}?time=${time}&width=${width}`;
5853
6007
  return { url, time };
5854
6008
  });
@@ -5867,7 +6021,8 @@ async function getModerationScores(assetId, options = {}) {
5867
6021
  maxConcurrent = 5,
5868
6022
  imageSubmissionMode = "url",
5869
6023
  imageDownloadOptions,
5870
- credentials: providedCredentials
6024
+ credentials: providedCredentials,
6025
+ scope
5871
6026
  } = options;
5872
6027
  const credentials = providedCredentials;
5873
6028
  const { asset, playbackId, policy } = await getPlaybackIdForAsset(assetId, credentials);
@@ -5878,7 +6033,16 @@ async function getModerationScores(assetId, options = {}) {
5878
6033
  (d) => d != null
5879
6034
  );
5880
6035
  const duration = candidateDurations.length > 0 ? Math.min(...candidateDurations) : 0;
6036
+ const effectiveScope = hasWorkflowScopeBoundaries(scope) ? scope : void 0;
6037
+ const resolvedScope = effectiveScope ? resolveWorkflowScope(effectiveScope, assetDurationSeconds) : void 0;
5881
6038
  const isAudioOnly = isAudioOnlyAsset(asset);
6039
+ const renderableScope = resolvedScope ? {
6040
+ startTime: Math.min(resolvedScope.startTime, duration),
6041
+ endTime: Math.min(resolvedScope.endTime, duration)
6042
+ } : void 0;
6043
+ if (!isAudioOnly && renderableScope && renderableScope.startTime >= renderableScope.endTime) {
6044
+ throw new Error("The requested scope does not include any renderable video.");
6045
+ }
5882
6046
  const signingContext = await resolveMuxSigningContext(credentials);
5883
6047
  if (policy === "signed" && !signingContext) {
5884
6048
  throw new Error(
@@ -5895,7 +6059,8 @@ async function getModerationScores(assetId, options = {}) {
5895
6059
  cleanTranscript: true,
5896
6060
  shouldSign: policy === "signed",
5897
6061
  credentials,
5898
- required: true
6062
+ required: true,
6063
+ scope: effectiveScope
5899
6064
  });
5900
6065
  if (provider === "openai") {
5901
6066
  thumbnailScores = await requestOpenAITranscriptModeration(
@@ -5913,17 +6078,21 @@ async function getModerationScores(assetId, options = {}) {
5913
6078
  throw new Error(`Unsupported moderation provider: ${exhaustiveCheck}`);
5914
6079
  }
5915
6080
  } else {
5916
- const estimatedIntervalCount = duration <= 50 ? 5 : Math.ceil(duration / thumbnailInterval);
6081
+ const scopedDuration = renderableScope ? renderableScope.endTime - renderableScope.startTime : duration;
6082
+ const estimatedIntervalCount = scopedDuration <= 50 ? 5 : Math.ceil(scopedDuration / thumbnailInterval);
5917
6083
  const thumbnailUrls = maxSamples !== void 0 && estimatedIntervalCount > maxSamples ? await getThumbnailUrlsFromTimestamps(
5918
6084
  playbackId,
5919
- planSamplingTimestamps({
5920
- duration_sec: duration,
5921
- max_candidates: maxSamples,
5922
- trim_start_sec: duration > 2 ? Math.min(5, Math.max(1, duration / 6)) : 0,
5923
- trim_end_sec: duration > 2 ? Math.min(5, Math.max(1, duration / 6)) : 0,
5924
- fps: videoTrackFps,
5925
- base_cadence_hz: thumbnailInterval > 0 ? 1 / thumbnailInterval : void 0
5926
- }),
6085
+ filterTimestampsWithinExclusiveScope(
6086
+ planSamplingTimestamps({
6087
+ duration_sec: duration,
6088
+ max_candidates: maxSamples,
6089
+ trim_start_sec: renderableScope?.startTime ?? (duration > 2 ? Math.min(5, Math.max(1, duration / 6)) : 0),
6090
+ trim_end_sec: renderableScope ? duration - renderableScope.endTime : duration > 2 ? Math.min(5, Math.max(1, duration / 6)) : 0,
6091
+ fps: videoTrackFps,
6092
+ base_cadence_hz: thumbnailInterval > 0 ? 1 / thumbnailInterval : void 0
6093
+ }),
6094
+ renderableScope
6095
+ ),
5927
6096
  {
5928
6097
  width: thumbnailWidth,
5929
6098
  shouldSign: policy === "signed",
@@ -5933,7 +6102,8 @@ async function getModerationScores(assetId, options = {}) {
5933
6102
  interval: thumbnailInterval,
5934
6103
  width: thumbnailWidth,
5935
6104
  shouldSign: policy === "signed",
5936
- credentials
6105
+ credentials,
6106
+ scope: renderableScope
5937
6107
  });
5938
6108
  thumbnailCount = thumbnailUrls.length;
5939
6109
  if (provider === "openai") {
@@ -6448,7 +6618,8 @@ async function getSummaryAndTagsInternal(assetId, options, collectedUsage) {
6448
6618
  titleLength,
6449
6619
  descriptionLength,
6450
6620
  tagCount,
6451
- outputLanguageCode
6621
+ outputLanguageCode,
6622
+ scope
6452
6623
  } = options ?? {};
6453
6624
  if (!VALID_TONES.includes(tone)) {
6454
6625
  throw new MuxAiError(
@@ -6465,6 +6636,15 @@ async function getSummaryAndTagsInternal(assetId, options, collectedUsage) {
6465
6636
  const { asset: assetData, playbackId, policy } = await getPlaybackIdForAsset(assetId, workflowCredentials);
6466
6637
  const assetDurationSeconds = getAssetDurationSecondsFromAsset(assetData);
6467
6638
  const isAudioOnly = isAudioOnlyAsset(assetData);
6639
+ const effectiveScope = hasWorkflowScopeBoundaries(scope) ? scope : void 0;
6640
+ const storyboardScope = isAudioOnly ? void 0 : resolveRenderableVideoScope(
6641
+ effectiveScope,
6642
+ assetDurationSeconds,
6643
+ getVideoTrackDurationSecondsFromAsset(assetData)
6644
+ );
6645
+ if (isAudioOnly && effectiveScope) {
6646
+ resolveWorkflowScope(effectiveScope, assetDurationSeconds);
6647
+ }
6468
6648
  if (isAudioOnly && !includeTranscript) {
6469
6649
  throw new MuxAiError(
6470
6650
  "Audio-only assets require a transcript. Set includeTranscript: true and ensure the asset has a ready text track (captions/subtitles).",
@@ -6483,7 +6663,8 @@ async function getSummaryAndTagsInternal(assetId, options, collectedUsage) {
6483
6663
  cleanTranscript,
6484
6664
  shouldSign: policy === "signed",
6485
6665
  credentials: workflowCredentials,
6486
- required: isAudioOnly
6666
+ required: isAudioOnly,
6667
+ scope: effectiveScope
6487
6668
  }) : void 0;
6488
6669
  const transcriptText = transcriptResult?.transcriptText ?? "";
6489
6670
  const resolvedLanguageCode = outputLanguageCode && outputLanguageCode !== "auto" ? outputLanguageCode : getReliableLanguageCode(transcriptResult?.track) ?? getReliableLanguageCode(getReadyTextTracks(assetData)[0]);
@@ -6514,7 +6695,13 @@ async function getSummaryAndTagsInternal(assetId, options, collectedUsage) {
6514
6695
  workflowCredentials
6515
6696
  );
6516
6697
  } else {
6517
- const storyboardUrl = await getStoryboardUrl(playbackId, 640, policy === "signed", workflowCredentials);
6698
+ const storyboardUrl = await getStoryboardUrl(
6699
+ playbackId,
6700
+ 640,
6701
+ policy === "signed",
6702
+ workflowCredentials,
6703
+ storyboardScope
6704
+ );
6518
6705
  imageUrl = storyboardUrl;
6519
6706
  if (imageSubmissionMode === "base64") {
6520
6707
  const downloadResult = await downloadImageAsBase64(storyboardUrl, imageDownloadOptions);