@mux/ai 0.41.0 → 0.42.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.
@@ -1,4 +1,5 @@
1
1
  import { z } from 'zod';
2
+ import { SafetyReport, PromptOverrides, SceneContextItemV1 } from './prompts/index.js';
2
3
  import { createAnthropic } from '@ai-sdk/anthropic';
3
4
  import { createGoogleGenerativeAI } from '@ai-sdk/google';
4
5
  import { createOpenAI } from '@ai-sdk/openai';
@@ -314,56 +315,6 @@ interface ImageDownloadOptions {
314
315
  exponentialBackoff?: boolean;
315
316
  }
316
317
 
317
- /**
318
- * Which detector fired on a leak, or `null` when the text is clean.
319
- *
320
- * - `canary` — the system-prompt canary token appeared in the output.
321
- * Near-zero false-positive rate; treat as a confirmed leak.
322
- * - `prompt_tag` — an XML-like tag taken from our prompt structure
323
- * appeared in the output. Low false-positive rate.
324
- * - `encoded_blob` — a long run of base64- or hex-shaped characters
325
- * appeared in a field expected to hold prose. Heuristic; some false
326
- * positives on hashes or tokens in tech-content transcripts.
327
- * - `unexpected_key` — the model emitted a JSON field not declared in
328
- * the schema (see {@link detectUnexpectedKeys}). Stripped silently
329
- * by zod's default `.strip()` mode; this signal surfaces the
330
- * smuggling attempt rather than letting it go unseen.
331
- */
332
- type LeakReason = "canary" | "prompt_tag" | "encoded_blob" | "unexpected_key" | null;
333
- /**
334
- * Per-field record of a detected leak. Collected across every scrub
335
- * performed inside a single workflow invocation and returned to the
336
- * caller in the workflow result's `safety` field.
337
- */
338
- interface ScrubbedFieldReport {
339
- /**
340
- * Identifier for the field that leaked. Format is workflow-specific
341
- * (e.g. "reasoning[0]", "moment_insight[2]", "description"). Intended
342
- * for debugging and observability; do not parse programmatically.
343
- */
344
- field: string;
345
- /** Which detector fired — see {@link LeakReason}. */
346
- reason: Exclude<LeakReason, null>;
347
- }
348
- /**
349
- * Aggregate safety report returned from workflows that perform
350
- * output-side scrubbing.
351
- *
352
- * Operators can use this to alert on suspected prompt-injection traffic
353
- * without inspecting log warnings. Shape is stable across workflows so a
354
- * single observer can consume it uniformly.
355
- */
356
- interface SafetyReport {
357
- /** `true` when at least one field was suppressed by the scrubber. */
358
- leaksDetected: boolean;
359
- /**
360
- * One entry per suppressed field. Empty (but present) when no leaks
361
- * were detected. Iteration order matches the order scrubs ran, so
362
- * callers can correlate entries with workflow structure if they wish.
363
- */
364
- scrubbedFields: ScrubbedFieldReport[];
365
- }
366
-
367
318
  type SupportedProvider = "openai" | "anthropic" | "google" | "baseten" | "openai-compatible";
368
319
  type SupportedEmbeddingProvider = "openai" | "google" | "baseten" | "openai-compatible";
369
320
  type OpenAIModelId = Parameters<ReturnType<typeof createOpenAI>["chat"]>[0];
@@ -629,28 +580,6 @@ type AskQuestionsType = z.infer<AskQuestionsSchema>;
629
580
  */
630
581
  declare function askQuestions(assetId: string, questions: Question[], options?: AskQuestionsOptions): Promise<AskQuestionsResult>;
631
582
 
632
- /**
633
- * A single section of a prompt, rendered as an XML-like tag.
634
- */
635
- interface PromptSection {
636
- /** The XML tag name for this section */
637
- tag: string;
638
- /** The content inside the tag */
639
- content: string;
640
- /** Optional attributes to add to the tag (e.g., { format: "plain text" }) */
641
- attributes?: Record<string, string>;
642
- }
643
- /**
644
- * Configuration for building a prompt section.
645
- * Can be a full PromptSection object, just a string (content only), or undefined to use default.
646
- */
647
- type SectionOverride = string | PromptSection | undefined;
648
- /**
649
- * User-provided overrides for prompt sections.
650
- * Each key can override the corresponding section's content or full definition.
651
- */
652
- type PromptOverrides<TSections extends string> = Partial<Record<TSections, SectionOverride>>;
653
-
654
583
  /** Structured payload returned from `hasBurnedInCaptions`. */
655
584
  interface BurnedInCaptionsResult {
656
585
  assetId: string;
@@ -859,6 +788,11 @@ interface ChaptersOptions extends ScopedMuxAIOptions {
859
788
  * Falls back to unconstrained (LLM decides) if no language metadata is available.
860
789
  */
861
790
  outputLanguageCode?: string;
791
+ /**
792
+ * Ordered scene context used to align chapter starts with scene boundaries.
793
+ * When omitted or empty, chapter generation retains its existing behavior.
794
+ */
795
+ sceneContext?: readonly SceneContextItemV1[];
862
796
  }
863
797
  /**
864
798
  * Sections of the chaptering system prompt that can be overridden.
@@ -1409,6 +1343,11 @@ interface SummarizationOptions extends ScopedMuxAIOptions {
1409
1343
  imageSubmissionMode?: ImageSubmissionMode;
1410
1344
  /** Fine-tune storyboard downloads when `imageSubmissionMode` === 'base64'. */
1411
1345
  imageDownloadOptions?: ImageDownloadOptions;
1346
+ /**
1347
+ * Ordered, normalized scenes that augment the storyboard and any transcript
1348
+ * with whole-asset progression. Scene context is treated as untrusted evidence.
1349
+ */
1350
+ sceneContext?: readonly SceneContextItemV1[];
1412
1351
  /**
1413
1352
  * Override specific sections of the user prompt.
1414
1353
  * Useful for customizing the AI's output for specific use cases (SEO, social media, etc.)
package/dist/index.d.ts CHANGED
@@ -1,15 +1,16 @@
1
- export { I as ISO639_1, a as ISO639_3, L as LanguageCodePair, M as MUX_TRACK_PASSTHROUGH_MAX_CHARS, R as ReplaceAndCreateTextTrackInput, b as ReplaceAndCreateTextTrackResult, c as ReplaceAndCreateTrackInput, d as ReplaceExistingTracksPolicy, e as ReplaceableTrackType, S as SupportedISO639_1, f as SupportedISO639_3, T as TextTrackReplacementPlan, g as TextTrackSummary, h as TextTrackTarget, i as TrackSummary, j as TrackTarget, k as buildMuxAiTrackPassthrough, l as findNameCollisionTextTracks, m as findSameLanguageTextTracks, n as getLanguageCodePair, o as getLanguageName, p as isDuplicateTrackNameError, q as isMuxGeneratedTextTrack, r as isUndeterminedLanguageCode, s as isValidISO639_1, t as isValidISO639_3, u as normalizeTrackLanguageCode, v as normalizeTrackName, w as planTextTrackReplacement, x as replaceAndCreateTextTrack, y as replaceAndCreateTrack, z as summarizeTextTrack, A as toISO639_1, B as toISO639_3, C as validateTrackPassthrough, D as workflows } from './index-C3PW73qH.js';
1
+ export { I as ISO639_1, a as ISO639_3, L as LanguageCodePair, M as MUX_TRACK_PASSTHROUGH_MAX_CHARS, R as ReplaceAndCreateTextTrackInput, b as ReplaceAndCreateTextTrackResult, c as ReplaceAndCreateTrackInput, d as ReplaceExistingTracksPolicy, e as ReplaceableTrackType, S as SupportedISO639_1, f as SupportedISO639_3, T as TextTrackReplacementPlan, g as TextTrackSummary, h as TextTrackTarget, i as TrackSummary, j as TrackTarget, k as buildMuxAiTrackPassthrough, l as findNameCollisionTextTracks, m as findSameLanguageTextTracks, n as getLanguageCodePair, o as getLanguageName, p as isDuplicateTrackNameError, q as isMuxGeneratedTextTrack, r as isUndeterminedLanguageCode, s as isValidISO639_1, t as isValidISO639_3, u as normalizeTrackLanguageCode, v as normalizeTrackName, w as planTextTrackReplacement, x as replaceAndCreateTextTrack, y as replaceAndCreateTrack, z as summarizeTextTrack, A as toISO639_1, B as toISO639_3, C as validateTrackPassthrough, D as workflows } from './index-NkSOF92U.js';
2
2
  import { W as WorkflowCredentials, S as StoragePutObjectInput, a as StoragePresignGetObjectInput } from './shots-CUI09Ohe.js';
3
3
  export { A as AssetTextTrack, C as ChunkEmbedding, b as ChunkingStrategy, E as Encrypted, c as EncryptedPayload, I as ImageSubmissionMode, M as MuxAIOptions, d as MuxAsset, P as PlaybackAsset, e as PlaybackPolicy, f as ScopedMuxAIOptions, g as StorageAdapter, T as TextChunk, h as TokenChunkingConfig, i as TokenUsage, j as ToneType, U as UsageMetadata, V as VTTChunkingConfig, k as VideoEmbeddingsResult, l as WorkflowCredentialsInput, m as WorkflowMuxClient, n as WorkflowScope, o as decryptFromWorkflow, p as encryptForWorkflow } from './shots-CUI09Ohe.js';
4
4
  import { WORKFLOW_SERIALIZE, WORKFLOW_DESERIALIZE } from '@workflow/serde';
5
5
  export { i as primitives } from './index-C2Pk7bbF.js';
6
6
  import 'zod';
7
+ import './prompts/index.js';
7
8
  import '@ai-sdk/anthropic';
8
9
  import '@ai-sdk/google';
9
10
  import '@ai-sdk/openai';
10
11
  import '@mux/mux-node';
11
12
 
12
- var version = "0.41.0";
13
+ var version = "0.42.0";
13
14
 
14
15
  type MuxAiErrorType = "validation_error" | "processing_error" | "timeout_error" | "content_policy_error";
15
16
  /**
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.41.0";
8
+ var version = "0.42.0";
9
9
 
10
10
  // src/lib/language-codes.ts
11
11
  var ISO639_1_TO_3 = {
@@ -446,6 +446,7 @@ var PROMPT_TAG_NAMES = [
446
446
  "answer_guidelines",
447
447
  "relevance_filtering",
448
448
  "transcript_guidance",
449
+ "scene_guidance",
449
450
  "capabilities",
450
451
  "language_guidelines",
451
452
  "tone_guidance",
@@ -4768,6 +4769,17 @@ var AUDIO_ONLY_SYSTEM_PROMPT_OVERRIDES = {
4768
4769
  You receive a timestamped transcript from audio-only content with lines in the form "[12s] Transcript text".
4769
4770
  Use those timestamps as anchors to determine chapter start times in seconds.`
4770
4771
  };
4772
+ var SCENE_CONTEXT_SYSTEM_PROMPT_OVERRIDES = {
4773
+ context: dedent4`
4774
+ You receive a timestamped transcript with lines in the form "[12s] Caption text" and an ordered scene context.
4775
+ Transcript timestamps are in seconds. Scene start and end timestamps are in milliseconds.
4776
+ Use the scene progression to choose logical chapter groupings and scene starts as chapter boundaries.`,
4777
+ constraints: dedent4`
4778
+ - Only use information present in the transcript and scene context
4779
+ - Return structured data that matches the requested JSON schema
4780
+ - Do not add commentary or extra text outside the JSON
4781
+ - When a <language> section is provided, all chapter titles MUST be written in that language`
4782
+ };
4771
4783
  var chaptersPromptBuilder = createPromptBuilder({
4772
4784
  template: {
4773
4785
  task: {
@@ -4805,6 +4817,7 @@ var chaptersPromptBuilder = createPromptBuilder({
4805
4817
  });
4806
4818
  function buildUserPrompt3({
4807
4819
  timestampedTranscript,
4820
+ sceneContext,
4808
4821
  promptOverrides,
4809
4822
  minChaptersPerHour = 3,
4810
4823
  maxChaptersPerHour = 8,
@@ -4818,14 +4831,31 @@ function buildUserPrompt3({
4818
4831
  attributes: { format: "seconds" }
4819
4832
  }
4820
4833
  ];
4834
+ if (sceneContext?.length) {
4835
+ contextSections.push({
4836
+ tag: "scene_context",
4837
+ content: JSON.stringify(sceneContext, null, 2),
4838
+ attributes: { timestamps: "milliseconds", trust: "untrusted_evidence" }
4839
+ });
4840
+ }
4821
4841
  if (languageName) {
4822
4842
  contextSections.push(createLanguageSection(languageName));
4823
4843
  }
4844
+ const sceneBoundarySeconds = getSceneBoundarySeconds(sceneContext).filter(
4845
+ (boundary) => scope ? boundary >= scope.startTime && boundary < scope.endTime : true
4846
+ );
4847
+ let firstChapterGuideline = "";
4848
+ if (sceneBoundarySeconds.length > 0) {
4849
+ firstChapterGuideline = `- The first chapter must start at ${sceneBoundarySeconds[0]}s, the first scene boundary inside the analyzed range`;
4850
+ } else if (scope) {
4851
+ firstChapterGuideline = `- The first chapter must start at ${scope.startTime}s`;
4852
+ }
4824
4853
  const dynamicChapterGuidelines = dedent4`
4825
4854
  - Create at least ${minChaptersPerHour} and at most ${maxChaptersPerHour} chapters per hour of content
4826
4855
  - Use start times in seconds (not HH:MM:SS)
4827
- ${scope ? `- Analyze only the range from ${scope.startTime}s (inclusive) to ${scope.endTime}s (exclusive)
4828
- - The first chapter must start at ${scope.startTime}s` : ""}
4856
+ ${scope ? `- Analyze only the range from ${scope.startTime}s (inclusive) to ${scope.endTime}s (exclusive)` : ""}
4857
+ ${firstChapterGuideline}
4858
+ ${sceneContext?.length ? "- Use the ordered scene context to identify chapter-sized runs of consecutive scenes\n- Every chapter startTime must exactly equal a scene start_ms divided by 1000; never split a scene" : ""}
4829
4859
  - Chapter start times should be non-decreasing
4830
4860
  - Do not include text before or after the JSON`;
4831
4861
  const mergedOverrides = {
@@ -4834,6 +4864,32 @@ function buildUserPrompt3({
4834
4864
  };
4835
4865
  return chaptersPromptBuilder.buildWithContext(mergedOverrides, contextSections);
4836
4866
  }
4867
+ function getSceneBoundarySeconds(sceneContext) {
4868
+ if (!sceneContext?.length) {
4869
+ return [];
4870
+ }
4871
+ return [...new Set(sceneContext.map((scene) => scene.start_ms / 1e3))].sort((a, b) => a - b);
4872
+ }
4873
+ function alignChaptersToSceneBoundaries(chapters, sceneBoundarySeconds) {
4874
+ if (sceneBoundarySeconds.length === 0) {
4875
+ return chapters;
4876
+ }
4877
+ const chapterByBoundary = /* @__PURE__ */ new Map();
4878
+ for (const chapter of chapters) {
4879
+ const startTime = sceneBoundarySeconds.reduce(
4880
+ (closest, boundary) => Math.abs(boundary - chapter.startTime) < Math.abs(closest - chapter.startTime) ? boundary : closest
4881
+ );
4882
+ const distance = Math.abs(startTime - chapter.startTime);
4883
+ const existing = chapterByBoundary.get(startTime);
4884
+ if (!existing || distance === 0 && existing.distance !== 0) {
4885
+ chapterByBoundary.set(startTime, {
4886
+ chapter: { ...chapter, startTime },
4887
+ distance
4888
+ });
4889
+ }
4890
+ }
4891
+ return [...chapterByBoundary.values()].map(({ chapter }) => chapter).sort((a, b) => a.startTime - b.startTime);
4892
+ }
4837
4893
  async function generateChapters(assetId, options = {}) {
4838
4894
  "use workflow";
4839
4895
  const collectedUsage = [];
@@ -4853,7 +4909,8 @@ async function generateChaptersInternal(assetId, options, collectedUsage) {
4853
4909
  maxChaptersPerHour,
4854
4910
  credentials,
4855
4911
  outputLanguageCode,
4856
- scope
4912
+ scope,
4913
+ sceneContext
4857
4914
  } = options;
4858
4915
  const modelConfig = resolveLanguageModelConfig({
4859
4916
  ...options,
@@ -4895,8 +4952,18 @@ async function generateChaptersInternal(assetId, options, collectedUsage) {
4895
4952
  }
4896
4953
  const resolvedLanguageCode = outputLanguageCode && outputLanguageCode !== "auto" ? outputLanguageCode : getReliableLanguageCode(transcriptResult.track) ?? languageCode;
4897
4954
  const languageName = resolvedLanguageCode ? getLanguageName(resolvedLanguageCode) : void 0;
4955
+ const sceneBoundarySeconds = getSceneBoundarySeconds(sceneContext).filter(
4956
+ (boundary) => resolvedScope ? boundary >= resolvedScope.startTime && boundary < resolvedScope.endTime : true
4957
+ );
4958
+ if (sceneContext?.length && sceneBoundarySeconds.length === 0) {
4959
+ throw new MuxAiError(
4960
+ "Scene context has no chapter boundaries within the requested scope.",
4961
+ { type: "validation_error" }
4962
+ );
4963
+ }
4898
4964
  const userPrompt = buildUserPrompt3({
4899
4965
  timestampedTranscript,
4966
+ sceneContext,
4900
4967
  promptOverrides,
4901
4968
  minChaptersPerHour,
4902
4969
  maxChaptersPerHour,
@@ -4905,7 +4972,11 @@ async function generateChaptersInternal(assetId, options, collectedUsage) {
4905
4972
  });
4906
4973
  let chaptersData = null;
4907
4974
  try {
4908
- const systemPrompt = isAudioOnly ? chapterSystemPromptBuilder.build(AUDIO_ONLY_SYSTEM_PROMPT_OVERRIDES) : chapterSystemPromptBuilder.build();
4975
+ const systemPromptOverrides = {
4976
+ ...isAudioOnly ? AUDIO_ONLY_SYSTEM_PROMPT_OVERRIDES : {},
4977
+ ...sceneContext?.length ? SCENE_CONTEXT_SYSTEM_PROMPT_OVERRIDES : {}
4978
+ };
4979
+ const systemPrompt = chapterSystemPromptBuilder.build(systemPromptOverrides);
4909
4980
  chaptersData = await generateChaptersWithAI({
4910
4981
  provider: modelConfig.provider,
4911
4982
  modelId: modelConfig.modelId,
@@ -4923,9 +4994,13 @@ async function generateChaptersInternal(assetId, options, collectedUsage) {
4923
4994
  throw new MuxAiError(`Failed to generate chapters for asset ${assetId}.`);
4924
4995
  }
4925
4996
  const { chapters: chaptersPayload, usage } = chaptersData;
4926
- const validChapters = chaptersPayload.chapters.filter((chapter) => typeof chapter.startTime === "number" && typeof chapter.title === "string").filter(
4997
+ const scopedChapters = chaptersPayload.chapters.filter((chapter) => typeof chapter.startTime === "number" && typeof chapter.title === "string").filter(
4927
4998
  (chapter) => resolvedScope ? chapter.startTime >= resolvedScope.startTime && chapter.startTime < resolvedScope.endTime : true
4928
4999
  ).sort((a, b) => a.startTime - b.startTime);
5000
+ const validChapters = alignChaptersToSceneBoundaries(
5001
+ scopedChapters,
5002
+ sceneBoundarySeconds
5003
+ );
4929
5004
  if (validChapters.length === 0) {
4930
5005
  throw new MuxAiError(`Failed to generate valid chapters for asset ${assetId}.`);
4931
5006
  }
@@ -4951,9 +5026,11 @@ async function generateChaptersInternal(assetId, options, collectedUsage) {
4951
5026
  if (scrubbedChapters.length === 0) {
4952
5027
  throw new MuxAiError(`Failed to generate valid chapters for asset ${assetId}.`);
4953
5028
  }
4954
- const firstChapterStartTime = resolvedScope?.startTime ?? 0;
4955
- if (scrubbedChapters[0].startTime !== firstChapterStartTime) {
4956
- scrubbedChapters[0].startTime = firstChapterStartTime;
5029
+ if (sceneBoundarySeconds.length === 0) {
5030
+ const firstChapterStartTime = resolvedScope?.startTime ?? 0;
5031
+ if (scrubbedChapters[0].startTime !== firstChapterStartTime) {
5032
+ scrubbedChapters[0].startTime = firstChapterStartTime;
5033
+ }
4957
5034
  }
4958
5035
  const usageWithMetadata = {
4959
5036
  ...usage,
@@ -7060,14 +7137,44 @@ function buildDescriptionGuidance(wordCount, contentType) {
7060
7137
  Write in present tense. Be specific about what is discussed or presented rather than making assumptions.
7061
7138
  Focus on the spoken content and any key insights, dialogue, or narrative elements.`;
7062
7139
  }
7063
- function createSummarizationBuilder({ titleLength, descriptionLength, tagCount } = {}) {
7140
+ function buildSceneAwareDescriptionGuidance(wordCount) {
7141
+ if (wordCount < DESCRIPTION_LENGTH_THRESHOLD_SMALL) {
7142
+ return dedent6`A brief summary of the video in no more than ${wordCount} words. Shorter is fine.
7143
+ Focus on the single most important subject or action across the ordered scenes.
7144
+ Use the storyboard as direct visual evidence and the scene context to preserve progression.
7145
+ Write in present tense.`;
7146
+ }
7147
+ if (wordCount > DESCRIPTION_LENGTH_THRESHOLD_LARGE) {
7148
+ return dedent6`A detailed summary that describes what happens across the ordered scenes.
7149
+ Never exceed ${wordCount} words, but shorter is perfectly fine. You may use multiple sentences.
7150
+ Be thorough: cover subjects, actions, setting, progression, and notable details supported across scenes.
7151
+ Use the storyboard as direct visual evidence and the scene context to preserve progression.
7152
+ Write in present tense. If the transcript provides dialogue or narration, incorporate key points while preserving scene progression.`;
7153
+ }
7154
+ return dedent6`A summary that describes what happens across the ordered scenes.
7155
+ Never exceed ${wordCount} words, but shorter is perfectly fine. You may use multiple sentences.
7156
+ Cover the main subjects, actions, setting, and notable progression supported across scenes.
7157
+ Use the storyboard as direct visual evidence and the scene context to preserve progression.
7158
+ Write in present tense. If the transcript provides dialogue or narration, incorporate key points while preserving scene progression.`;
7159
+ }
7160
+ function createSummarizationBuilder({ titleLength, descriptionLength, tagCount } = {}, { hasSceneContext = false } = {}) {
7064
7161
  const titleLimit = titleLength ?? DEFAULT_TITLE_LENGTH;
7065
7162
  const keywordLimit = tagCount ?? DEFAULT_SUMMARY_KEYWORD_LIMIT;
7163
+ const task = hasSceneContext ? "Analyze the storyboard frames and ordered scene context together, then generate metadata that captures the full progression of the video content." : "Analyze the storyboard frames and generate metadata that captures the essence of the video content.";
7164
+ const qualityGuidelines = hasSceneContext ? dedent6`
7165
+ - Follow scene_index order to understand what begins, develops, and concludes
7166
+ - Use scene narratives and concepts as grounded evidence, preserving consistent terminology across the output
7167
+ - Use the storyboard as direct visual evidence for whole-asset details
7168
+ - Balance brevity with informativeness` : dedent6`
7169
+ - Examine all frames to understand the full context and progression
7170
+ - Be precise: "golden retriever" is better than "dog" when identifiable
7171
+ - Capture the narrative: what begins, develops, and concludes
7172
+ - Balance brevity with informativeness`;
7066
7173
  return createPromptBuilder({
7067
7174
  template: {
7068
7175
  task: {
7069
7176
  tag: "task",
7070
- content: "Analyze the storyboard frames and generate metadata that captures the essence of the video content."
7177
+ content: task
7071
7178
  },
7072
7179
  title: {
7073
7180
  tag: "title_requirements",
@@ -7080,7 +7187,7 @@ function createSummarizationBuilder({ titleLength, descriptionLength, tagCount }
7080
7187
  },
7081
7188
  description: {
7082
7189
  tag: "description_requirements",
7083
- content: buildDescriptionGuidance(descriptionLength ?? DEFAULT_DESCRIPTION_LENGTH, "video")
7190
+ content: hasSceneContext ? buildSceneAwareDescriptionGuidance(descriptionLength ?? DEFAULT_DESCRIPTION_LENGTH) : buildDescriptionGuidance(descriptionLength ?? DEFAULT_DESCRIPTION_LENGTH, "video")
7084
7191
  },
7085
7192
  keywords: {
7086
7193
  tag: "keywords_requirements",
@@ -7096,11 +7203,7 @@ function createSummarizationBuilder({ titleLength, descriptionLength, tagCount }
7096
7203
  },
7097
7204
  qualityGuidelines: {
7098
7205
  tag: "quality_guidelines",
7099
- content: dedent6`
7100
- - Examine all frames to understand the full context and progression
7101
- - Be precise: "golden retriever" is better than "dog" when identifiable
7102
- - Capture the narrative: what begins, develops, and concludes
7103
- - Balance brevity with informativeness`
7206
+ content: qualityGuidelines
7104
7207
  }
7105
7208
  },
7106
7209
  sectionOrder: ["task", "title", "description", "keywords", "qualityGuidelines"]
@@ -7152,59 +7255,83 @@ function createAudioOnlyBuilder({ titleLength, descriptionLength, tagCount } = {
7152
7255
  sectionOrder: ["task", "title", "description", "keywords", "qualityGuidelines"]
7153
7256
  });
7154
7257
  }
7155
- var SYSTEM_PROMPT5 = promptDedent`
7156
- <role>
7157
- You are a video content analyst specializing in storyboard interpretation and multimodal analysis.
7158
- </role>
7159
-
7160
- <context>
7161
- You receive storyboard images containing multiple sequential frames extracted from a video.
7162
- ${STORYBOARD_FRAME_INSTRUCTIONS}
7163
- </context>
7164
-
7165
- <transcript_guidance>
7166
- When a transcript is provided alongside the storyboard:
7167
- - Use it to understand spoken content, dialogue, narration, and audio context
7168
- - Correlate transcript content with visual frames to build a complete picture
7169
- - Extract key terminology, names, and specific language used by speakers
7170
- - Let the transcript inform keyword selection, especially for topics not visually obvious
7171
- - Prioritize visual content for the description, but enrich it with transcript insights
7172
- - If transcript and visuals conflict, trust the visual evidence
7173
- </transcript_guidance>
7174
-
7175
- <capabilities>
7176
- - Extract meaning from visual sequences
7177
- - Identify subjects, actions, settings, and narrative arcs
7178
- - Generate accurate, searchable metadata
7179
- - Synthesize visual and transcript information when provided
7180
- </capabilities>
7181
-
7182
- <security>
7183
- ${NON_DISCLOSURE_CONSTRAINT}
7184
-
7185
- ${UNTRUSTED_USER_INPUT_NOTICE}
7186
-
7187
- ${VISUAL_TEXT_AS_CONTENT}
7188
-
7189
- ${CANARY_TRIPWIRE}
7190
- </security>
7191
-
7192
- <constraints>
7193
- - Only describe what is clearly observable in the frames or explicitly stated in the transcript
7194
- - ${NO_FABRICATION_CONSTRAINT}
7195
- - ${METADATA_BOUNDARY_WARNING}
7196
- - ${STRUCTURED_DATA_CONSTRAINT}
7197
- - Output only the JSON object; no markdown or extra text
7198
- - When a <language> section is provided, all output text MUST be written in that language
7199
- </constraints>
7200
-
7201
- <tone_guidance>
7202
- ${TONE_GUIDANCE}
7203
- </tone_guidance>
7204
-
7205
- <language_guidelines>
7206
- ${createLanguageGuidelines("video")}
7207
- </language_guidelines>`;
7258
+ function createVideoSystemPrompt(hasSceneContext) {
7259
+ let sceneContext = "";
7260
+ let sceneGuidance = "";
7261
+ if (hasSceneContext) {
7262
+ sceneContext = promptDedent`
7263
+ Ordered scene context is also provided as structured evidence of the video's progression.
7264
+ Entries use stable, zero-based scene_index values and absolute millisecond timestamps.
7265
+ Read scenes in scene_index order to understand what begins, develops, and concludes.`;
7266
+ sceneGuidance = promptDedent`
7267
+ <scene_guidance>
7268
+ - Use scene titles, narratives, and concepts to understand temporal progression
7269
+ - Use storyboard frames as direct visual evidence for subjects, actions, settings, and details
7270
+ - Preserve consistent terminology from the scene context across the title, description, and keywords
7271
+ - Treat optional scene fields as evidence only when present; do not infer omitted details
7272
+ - Consider all provided evidence together; if derived scene context conflicts with storyboard visuals, trust the visuals
7273
+ </scene_guidance>`;
7274
+ }
7275
+ const transcriptEvidence = hasSceneContext ? "storyboard frames and ordered scenes" : "visual frames";
7276
+ const descriptionPriority = hasSceneContext ? "Use the scene context for temporal structure and the storyboard for direct visual details, enriching both with transcript insights" : "Prioritize visual content for the description, but enrich it with transcript insights";
7277
+ const groundingConstraint = hasSceneContext ? "Use all provided evidence together. Ground each claim in the storyboard, scene context, or transcript" : "Only describe what is clearly observable in the frames or explicitly stated in the transcript";
7278
+ return promptDedent`
7279
+ <role>
7280
+ You are a video content analyst specializing in storyboard interpretation and multimodal analysis.
7281
+ </role>
7282
+
7283
+ <context>
7284
+ You receive storyboard images containing multiple sequential frames extracted from a video.
7285
+ ${STORYBOARD_FRAME_INSTRUCTIONS}
7286
+ ${sceneContext}
7287
+ </context>
7288
+
7289
+ ${sceneGuidance}
7290
+
7291
+ <transcript_guidance>
7292
+ When a transcript is provided alongside the storyboard:
7293
+ - Use it to understand spoken content, dialogue, narration, and audio context
7294
+ - Correlate transcript content with ${transcriptEvidence} to build a complete picture
7295
+ - Extract key terminology, names, and specific language used by speakers
7296
+ - Let the transcript inform keyword selection, especially for topics not visually obvious
7297
+ - ${descriptionPriority}
7298
+ - If transcript and visuals conflict, trust the visual evidence
7299
+ </transcript_guidance>
7300
+
7301
+ <capabilities>
7302
+ - Extract meaning from visual sequences
7303
+ - Identify subjects, actions, settings, and narrative arcs
7304
+ - Generate accurate, searchable metadata
7305
+ - Synthesize visual and transcript information when provided
7306
+ </capabilities>
7307
+
7308
+ <security>
7309
+ ${NON_DISCLOSURE_CONSTRAINT}
7310
+
7311
+ ${UNTRUSTED_USER_INPUT_NOTICE}
7312
+
7313
+ ${VISUAL_TEXT_AS_CONTENT}
7314
+
7315
+ ${CANARY_TRIPWIRE}
7316
+ </security>
7317
+
7318
+ <constraints>
7319
+ - ${groundingConstraint}
7320
+ - ${NO_FABRICATION_CONSTRAINT}
7321
+ - ${METADATA_BOUNDARY_WARNING}
7322
+ - ${STRUCTURED_DATA_CONSTRAINT}
7323
+ - Output only the JSON object; no markdown or extra text
7324
+ - When a <language> section is provided, all output text MUST be written in that language
7325
+ </constraints>
7326
+
7327
+ <tone_guidance>
7328
+ ${TONE_GUIDANCE}
7329
+ </tone_guidance>
7330
+
7331
+ <language_guidelines>
7332
+ ${createLanguageGuidelines("video")}
7333
+ </language_guidelines>`;
7334
+ }
7208
7335
  var AUDIO_ONLY_SYSTEM_PROMPT3 = promptDedent`
7209
7336
  <role>
7210
7337
  You are an audio content analyst specializing in transcript analysis and metadata generation.
@@ -7255,6 +7382,16 @@ var AUDIO_ONLY_SYSTEM_PROMPT3 = promptDedent`
7255
7382
  <language_guidelines>
7256
7383
  ${createLanguageGuidelines("audio")}
7257
7384
  </language_guidelines>`;
7385
+ function createSceneContextSection(sceneContext) {
7386
+ return {
7387
+ tag: "scene_context",
7388
+ content: JSON.stringify(sceneContext),
7389
+ attributes: {
7390
+ format: "json",
7391
+ order: "scene_index_ascending"
7392
+ }
7393
+ };
7394
+ }
7258
7395
  function buildUserPrompt5({
7259
7396
  tone,
7260
7397
  transcriptText,
@@ -7264,7 +7401,8 @@ function buildUserPrompt5({
7264
7401
  titleLength,
7265
7402
  descriptionLength,
7266
7403
  tagCount,
7267
- languageName
7404
+ languageName,
7405
+ sceneContext
7268
7406
  }) {
7269
7407
  const contextSections = [createToneSection(TONE_INSTRUCTIONS[tone])];
7270
7408
  if (languageName) {
@@ -7279,11 +7417,15 @@ function buildUserPrompt5({
7279
7417
  const format = isCleanTranscript ? "plain text" : "WebVTT";
7280
7418
  contextSections.push(createTranscriptSection(transcriptText, format));
7281
7419
  }
7420
+ const hasSceneContext = !isAudioOnly && Boolean(sceneContext?.length);
7421
+ if (hasSceneContext && sceneContext) {
7422
+ contextSections.push(createSceneContextSection(sceneContext));
7423
+ }
7282
7424
  const constraints = { titleLength, descriptionLength, tagCount };
7283
- const promptBuilder = isAudioOnly ? createAudioOnlyBuilder(constraints) : createSummarizationBuilder(constraints);
7425
+ const promptBuilder = isAudioOnly ? createAudioOnlyBuilder(constraints) : createSummarizationBuilder(constraints, { hasSceneContext });
7284
7426
  return promptBuilder.buildWithContext(promptOverrides, contextSections);
7285
7427
  }
7286
- async function analyzeStoryboard2(imageDataUrl, provider, modelId, userPrompt, systemPrompt, descriptionLength, credentials) {
7428
+ async function analyzeVideo(imageDataUrl, provider, modelId, userPrompt, systemPrompt, descriptionLength, credentials) {
7287
7429
  "use step";
7288
7430
  const model = await createLanguageModelFromConfig(provider, modelId, credentials);
7289
7431
  const schema = buildSummarySchema(descriptionLength);
@@ -7299,10 +7441,10 @@ async function analyzeStoryboard2(imageDataUrl, provider, modelId, userPrompt, s
7299
7441
  messages: [
7300
7442
  {
7301
7443
  role: "user",
7302
- content: [
7444
+ content: imageDataUrl ? [
7303
7445
  { type: "text", text: userPrompt },
7304
7446
  { type: "image", image: imageDataUrl }
7305
- ]
7447
+ ] : userPrompt
7306
7448
  }
7307
7449
  ]
7308
7450
  }));
@@ -7412,6 +7554,7 @@ async function getSummaryAndTagsInternal(assetId, options, collectedUsage) {
7412
7554
  cleanTranscript = true,
7413
7555
  imageSubmissionMode = "url",
7414
7556
  imageDownloadOptions,
7557
+ sceneContext,
7415
7558
  promptOverrides,
7416
7559
  credentials,
7417
7560
  titleLength,
@@ -7435,6 +7578,7 @@ async function getSummaryAndTagsInternal(assetId, options, collectedUsage) {
7435
7578
  const { asset: assetData, playbackId, policy } = await getPlaybackIdForAsset(assetId, workflowCredentials, options?.assetSnapshot);
7436
7579
  const assetDurationSeconds = getAssetDurationSecondsFromAsset(assetData);
7437
7580
  const isAudioOnly = isAudioOnlyAsset(assetData);
7581
+ const hasSceneContext = !isAudioOnly && Boolean(sceneContext?.length);
7438
7582
  const effectiveScope = hasWorkflowScopeBoundaries(scope) ? scope : void 0;
7439
7583
  const storyboardScope = isAudioOnly ? void 0 : resolveRenderableVideoScope(
7440
7584
  effectiveScope,
@@ -7451,7 +7595,8 @@ async function getSummaryAndTagsInternal(assetId, options, collectedUsage) {
7451
7595
  );
7452
7596
  }
7453
7597
  const signingContext = await resolveMuxSigningContext(workflowCredentials);
7454
- if (policy === "signed" && !signingContext) {
7598
+ const requiresSignedPlaybackAccess = includeTranscript || !isAudioOnly;
7599
+ if (policy === "signed" && requiresSignedPlaybackAccess && !signingContext) {
7455
7600
  throw new MuxAiError(
7456
7601
  "Signed playback ID requires signing credentials. Set MUX_SIGNING_KEY and MUX_PRIVATE_KEY environment variables.",
7457
7602
  { type: "validation_error" }
@@ -7477,11 +7622,15 @@ async function getSummaryAndTagsInternal(assetId, options, collectedUsage) {
7477
7622
  titleLength,
7478
7623
  descriptionLength,
7479
7624
  tagCount,
7480
- languageName
7625
+ languageName,
7626
+ sceneContext: hasSceneContext ? sceneContext : void 0
7481
7627
  });
7482
7628
  let analysisResponse;
7483
7629
  let imageUrl;
7484
- const systemPrompt = isAudioOnly ? AUDIO_ONLY_SYSTEM_PROMPT3 : SYSTEM_PROMPT5;
7630
+ let systemPrompt = AUDIO_ONLY_SYSTEM_PROMPT3;
7631
+ if (!isAudioOnly) {
7632
+ systemPrompt = createVideoSystemPrompt(hasSceneContext);
7633
+ }
7485
7634
  const effectiveDescriptionLength = descriptionLength ?? DEFAULT_DESCRIPTION_LENGTH;
7486
7635
  try {
7487
7636
  if (isAudioOnly) {
@@ -7504,7 +7653,7 @@ async function getSummaryAndTagsInternal(assetId, options, collectedUsage) {
7504
7653
  imageUrl = storyboardUrl;
7505
7654
  if (imageSubmissionMode === "base64") {
7506
7655
  const downloadResult = await downloadImageAsBase64(storyboardUrl, imageDownloadOptions);
7507
- analysisResponse = await analyzeStoryboard2(
7656
+ analysisResponse = await analyzeVideo(
7508
7657
  downloadResult.base64Data,
7509
7658
  modelConfig.provider,
7510
7659
  modelConfig.modelId,
@@ -7514,7 +7663,7 @@ async function getSummaryAndTagsInternal(assetId, options, collectedUsage) {
7514
7663
  workflowCredentials
7515
7664
  );
7516
7665
  } else {
7517
- analysisResponse = await analyzeStoryboard2(
7666
+ analysisResponse = await analyzeVideo(
7518
7667
  storyboardUrl,
7519
7668
  modelConfig.provider,
7520
7669
  modelConfig.modelId,
@@ -8099,7 +8248,7 @@ import { z as z9 } from "zod";
8099
8248
  var translationSchema = z9.object({
8100
8249
  translation: z9.string()
8101
8250
  });
8102
- var SYSTEM_PROMPT6 = promptDedent`
8251
+ var SYSTEM_PROMPT5 = promptDedent`
8103
8252
  You are a subtitle translation expert. Translate VTT subtitle files to the target language specified by the user.
8104
8253
  You may receive either a full VTT file or a chunk from a larger VTT.
8105
8254
  Preserve all timestamps, cue ordering, and VTT formatting exactly as they appear.
@@ -8424,7 +8573,7 @@ async function translateVttWithAI({
8424
8573
  model,
8425
8574
  maxRetries: 0,
8426
8575
  output: Output7.object({ schema: translationSchema }),
8427
- system: SYSTEM_PROMPT6,
8576
+ system: SYSTEM_PROMPT5,
8428
8577
  messages: [
8429
8578
  {
8430
8579
  role: "user",