@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,5 +1,6 @@
1
- export { E as ASK_QUESTIONS_DEFAULT_MAX_ANSWER_OPTION_LENGTH, F as ASK_QUESTIONS_DEFAULT_MAX_FREE_FORM_ANSWER_LENGTH, G as ASK_QUESTIONS_MAX_QUESTIONS_PER_CALL, H as ASK_QUESTIONS_MAX_QUESTION_LENGTH, J as AskQuestionsOptions, K as AskQuestionsResult, N as AskQuestionsType, O as AudioTranslationOptions, P as AudioTranslationResult, Q as AutoCensorProfanityOptions, U as BurnedInCaptionsAnalysis, V as BurnedInCaptionsOptions, W as BurnedInCaptionsPromptOverrides, X as BurnedInCaptionsPromptSections, Y as BurnedInCaptionsResult, Z as CaptionReplacement, _ as CensorMode, $ as Chapter, a0 as ChapterSystemPromptSections, a1 as ChaptersOptions, a2 as ChaptersPromptOverrides, a3 as ChaptersPromptSections, a4 as ChaptersResult, a5 as ChaptersType, a6 as DEFAULT_DESCRIPTION_LENGTH, a7 as DEFAULT_SUMMARY_KEYWORD_LIMIT, a8 as DEFAULT_TITLE_LENGTH, a9 as EditCaptionsOptions, aa as EditCaptionsResult, ab as EmbeddingsOptions, ac as EmbeddingsResult, ad as EngagementInsightsOptions, ae as EngagementInsightsResult, af as GOOGLE_VISION_LIKELIHOOD_TO_SCORE, ag as HIVE_SEXUAL_CATEGORIES, ah as HIVE_VIOLENCE_CATEGORIES, ai as HiveModerationOutput, aj as HiveModerationSource, ak as ModerationOptions, al as ModerationProvider, am as ModerationResult, an as MomentInsight, ao as OverallInsight, ap as ProfanityDetectionPayload, aq as Question, ar as QuestionAnswer, as as QuestionAnswerType, at as ReplacementRecord, au as SpeakerReplacement, av as StaticRenditionCleanupOutcome, aw as SummarizationOptions, ax as SummarizationPromptOverrides, ay as SummarizationPromptSections, az as SummaryAndTagsResult, aA as SummaryType, aB as ThumbnailModerationScore, aC as TranslationChunkingOptions, aD as TranslationOptions, aE as TranslationPayload, aF as TranslationResult, aG as aggregateTokenUsage, aH as applyOverrideLists, aI as applyReplacements, aJ as applySpeakerReplacements, aK as askQuestions, aL as buildReplacementRegex, aM as burnedInCaptionsSchema, aN as censorVttContent, aO as chapterSchema, aP as chaptersSchema, aQ as createReplacer, aR as editCaptions, aS as extractTranscriptSegmentsForHotspots, aT as generateChapters, aU as generateEmbeddings, aV as generateEngagementInsights, aW as generateVideoEmbeddings, aX as getModerationScores, aY as getSummaryAndTags, aZ as hasBurnedInCaptions, a_ as mapShotsToHotspots, a$ as normalizeTranslatedVtt, b0 as profanityDetectionSchema, b1 as questionAnswerSchema, b2 as shouldSplitChunkTranslationError, b3 as summarySchema, b4 as transformCueText, b5 as translateAudio, b6 as translateCaptions, b7 as translationSchema, b8 as validateNeverTranslateTerms, b9 as verifyNeverTranslateTerms } from '../index-C3PW73qH.js';
1
+ export { E as ASK_QUESTIONS_DEFAULT_MAX_ANSWER_OPTION_LENGTH, F as ASK_QUESTIONS_DEFAULT_MAX_FREE_FORM_ANSWER_LENGTH, G as ASK_QUESTIONS_MAX_QUESTIONS_PER_CALL, H as ASK_QUESTIONS_MAX_QUESTION_LENGTH, J as AskQuestionsOptions, K as AskQuestionsResult, N as AskQuestionsType, O as AudioTranslationOptions, P as AudioTranslationResult, Q as AutoCensorProfanityOptions, U as BurnedInCaptionsAnalysis, V as BurnedInCaptionsOptions, W as BurnedInCaptionsPromptOverrides, X as BurnedInCaptionsPromptSections, Y as BurnedInCaptionsResult, Z as CaptionReplacement, _ as CensorMode, $ as Chapter, a0 as ChapterSystemPromptSections, a1 as ChaptersOptions, a2 as ChaptersPromptOverrides, a3 as ChaptersPromptSections, a4 as ChaptersResult, a5 as ChaptersType, a6 as DEFAULT_DESCRIPTION_LENGTH, a7 as DEFAULT_SUMMARY_KEYWORD_LIMIT, a8 as DEFAULT_TITLE_LENGTH, a9 as EditCaptionsOptions, aa as EditCaptionsResult, ab as EmbeddingsOptions, ac as EmbeddingsResult, ad as EngagementInsightsOptions, ae as EngagementInsightsResult, af as GOOGLE_VISION_LIKELIHOOD_TO_SCORE, ag as HIVE_SEXUAL_CATEGORIES, ah as HIVE_VIOLENCE_CATEGORIES, ai as HiveModerationOutput, aj as HiveModerationSource, ak as ModerationOptions, al as ModerationProvider, am as ModerationResult, an as MomentInsight, ao as OverallInsight, ap as ProfanityDetectionPayload, aq as Question, ar as QuestionAnswer, as as QuestionAnswerType, at as ReplacementRecord, au as SpeakerReplacement, av as StaticRenditionCleanupOutcome, aw as SummarizationOptions, ax as SummarizationPromptOverrides, ay as SummarizationPromptSections, az as SummaryAndTagsResult, aA as SummaryType, aB as ThumbnailModerationScore, aC as TranslationChunkingOptions, aD as TranslationOptions, aE as TranslationPayload, aF as TranslationResult, aG as aggregateTokenUsage, aH as applyOverrideLists, aI as applyReplacements, aJ as applySpeakerReplacements, aK as askQuestions, aL as buildReplacementRegex, aM as burnedInCaptionsSchema, aN as censorVttContent, aO as chapterSchema, aP as chaptersSchema, aQ as createReplacer, aR as editCaptions, aS as extractTranscriptSegmentsForHotspots, aT as generateChapters, aU as generateEmbeddings, aV as generateEngagementInsights, aW as generateVideoEmbeddings, aX as getModerationScores, aY as getSummaryAndTags, aZ as hasBurnedInCaptions, a_ as mapShotsToHotspots, a$ as normalizeTranslatedVtt, b0 as profanityDetectionSchema, b1 as questionAnswerSchema, b2 as shouldSplitChunkTranslationError, b3 as summarySchema, b4 as transformCueText, b5 as translateAudio, b6 as translateCaptions, b7 as translationSchema, b8 as validateNeverTranslateTerms, b9 as verifyNeverTranslateTerms } from '../index-NkSOF92U.js';
2
2
  import 'zod';
3
+ import '../prompts/index.js';
3
4
  import '@ai-sdk/anthropic';
4
5
  import '@ai-sdk/google';
5
6
  import '@ai-sdk/openai';
@@ -275,6 +275,7 @@ var PROMPT_TAG_NAMES = [
275
275
  "answer_guidelines",
276
276
  "relevance_filtering",
277
277
  "transcript_guidance",
278
+ "scene_guidance",
278
279
  "capabilities",
279
280
  "language_guidelines",
280
281
  "tone_guidance",
@@ -3571,6 +3572,17 @@ var AUDIO_ONLY_SYSTEM_PROMPT_OVERRIDES = {
3571
3572
  You receive a timestamped transcript from audio-only content with lines in the form "[12s] Transcript text".
3572
3573
  Use those timestamps as anchors to determine chapter start times in seconds.`
3573
3574
  };
3575
+ var SCENE_CONTEXT_SYSTEM_PROMPT_OVERRIDES = {
3576
+ context: dedent4`
3577
+ You receive a timestamped transcript with lines in the form "[12s] Caption text" and an ordered scene context.
3578
+ Transcript timestamps are in seconds. Scene start and end timestamps are in milliseconds.
3579
+ Use the scene progression to choose logical chapter groupings and scene starts as chapter boundaries.`,
3580
+ constraints: dedent4`
3581
+ - Only use information present in the transcript and scene context
3582
+ - Return structured data that matches the requested JSON schema
3583
+ - Do not add commentary or extra text outside the JSON
3584
+ - When a <language> section is provided, all chapter titles MUST be written in that language`
3585
+ };
3574
3586
  var chaptersPromptBuilder = createPromptBuilder({
3575
3587
  template: {
3576
3588
  task: {
@@ -3608,6 +3620,7 @@ var chaptersPromptBuilder = createPromptBuilder({
3608
3620
  });
3609
3621
  function buildUserPrompt3({
3610
3622
  timestampedTranscript,
3623
+ sceneContext,
3611
3624
  promptOverrides,
3612
3625
  minChaptersPerHour = 3,
3613
3626
  maxChaptersPerHour = 8,
@@ -3621,14 +3634,31 @@ function buildUserPrompt3({
3621
3634
  attributes: { format: "seconds" }
3622
3635
  }
3623
3636
  ];
3637
+ if (sceneContext?.length) {
3638
+ contextSections.push({
3639
+ tag: "scene_context",
3640
+ content: JSON.stringify(sceneContext, null, 2),
3641
+ attributes: { timestamps: "milliseconds", trust: "untrusted_evidence" }
3642
+ });
3643
+ }
3624
3644
  if (languageName) {
3625
3645
  contextSections.push(createLanguageSection(languageName));
3626
3646
  }
3647
+ const sceneBoundarySeconds = getSceneBoundarySeconds(sceneContext).filter(
3648
+ (boundary) => scope ? boundary >= scope.startTime && boundary < scope.endTime : true
3649
+ );
3650
+ let firstChapterGuideline = "";
3651
+ if (sceneBoundarySeconds.length > 0) {
3652
+ firstChapterGuideline = `- The first chapter must start at ${sceneBoundarySeconds[0]}s, the first scene boundary inside the analyzed range`;
3653
+ } else if (scope) {
3654
+ firstChapterGuideline = `- The first chapter must start at ${scope.startTime}s`;
3655
+ }
3627
3656
  const dynamicChapterGuidelines = dedent4`
3628
3657
  - Create at least ${minChaptersPerHour} and at most ${maxChaptersPerHour} chapters per hour of content
3629
3658
  - Use start times in seconds (not HH:MM:SS)
3630
- ${scope ? `- Analyze only the range from ${scope.startTime}s (inclusive) to ${scope.endTime}s (exclusive)
3631
- - The first chapter must start at ${scope.startTime}s` : ""}
3659
+ ${scope ? `- Analyze only the range from ${scope.startTime}s (inclusive) to ${scope.endTime}s (exclusive)` : ""}
3660
+ ${firstChapterGuideline}
3661
+ ${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" : ""}
3632
3662
  - Chapter start times should be non-decreasing
3633
3663
  - Do not include text before or after the JSON`;
3634
3664
  const mergedOverrides = {
@@ -3637,6 +3667,32 @@ function buildUserPrompt3({
3637
3667
  };
3638
3668
  return chaptersPromptBuilder.buildWithContext(mergedOverrides, contextSections);
3639
3669
  }
3670
+ function getSceneBoundarySeconds(sceneContext) {
3671
+ if (!sceneContext?.length) {
3672
+ return [];
3673
+ }
3674
+ return [...new Set(sceneContext.map((scene) => scene.start_ms / 1e3))].sort((a, b) => a - b);
3675
+ }
3676
+ function alignChaptersToSceneBoundaries(chapters, sceneBoundarySeconds) {
3677
+ if (sceneBoundarySeconds.length === 0) {
3678
+ return chapters;
3679
+ }
3680
+ const chapterByBoundary = /* @__PURE__ */ new Map();
3681
+ for (const chapter of chapters) {
3682
+ const startTime = sceneBoundarySeconds.reduce(
3683
+ (closest, boundary) => Math.abs(boundary - chapter.startTime) < Math.abs(closest - chapter.startTime) ? boundary : closest
3684
+ );
3685
+ const distance = Math.abs(startTime - chapter.startTime);
3686
+ const existing = chapterByBoundary.get(startTime);
3687
+ if (!existing || distance === 0 && existing.distance !== 0) {
3688
+ chapterByBoundary.set(startTime, {
3689
+ chapter: { ...chapter, startTime },
3690
+ distance
3691
+ });
3692
+ }
3693
+ }
3694
+ return [...chapterByBoundary.values()].map(({ chapter }) => chapter).sort((a, b) => a.startTime - b.startTime);
3695
+ }
3640
3696
  async function generateChapters(assetId, options = {}) {
3641
3697
  "use workflow";
3642
3698
  const collectedUsage = [];
@@ -3656,7 +3712,8 @@ async function generateChaptersInternal(assetId, options, collectedUsage) {
3656
3712
  maxChaptersPerHour,
3657
3713
  credentials,
3658
3714
  outputLanguageCode,
3659
- scope
3715
+ scope,
3716
+ sceneContext
3660
3717
  } = options;
3661
3718
  const modelConfig = resolveLanguageModelConfig({
3662
3719
  ...options,
@@ -3698,8 +3755,18 @@ async function generateChaptersInternal(assetId, options, collectedUsage) {
3698
3755
  }
3699
3756
  const resolvedLanguageCode = outputLanguageCode && outputLanguageCode !== "auto" ? outputLanguageCode : getReliableLanguageCode(transcriptResult.track) ?? languageCode;
3700
3757
  const languageName = resolvedLanguageCode ? getLanguageName(resolvedLanguageCode) : void 0;
3758
+ const sceneBoundarySeconds = getSceneBoundarySeconds(sceneContext).filter(
3759
+ (boundary) => resolvedScope ? boundary >= resolvedScope.startTime && boundary < resolvedScope.endTime : true
3760
+ );
3761
+ if (sceneContext?.length && sceneBoundarySeconds.length === 0) {
3762
+ throw new MuxAiError(
3763
+ "Scene context has no chapter boundaries within the requested scope.",
3764
+ { type: "validation_error" }
3765
+ );
3766
+ }
3701
3767
  const userPrompt = buildUserPrompt3({
3702
3768
  timestampedTranscript,
3769
+ sceneContext,
3703
3770
  promptOverrides,
3704
3771
  minChaptersPerHour,
3705
3772
  maxChaptersPerHour,
@@ -3708,7 +3775,11 @@ async function generateChaptersInternal(assetId, options, collectedUsage) {
3708
3775
  });
3709
3776
  let chaptersData = null;
3710
3777
  try {
3711
- const systemPrompt = isAudioOnly ? chapterSystemPromptBuilder.build(AUDIO_ONLY_SYSTEM_PROMPT_OVERRIDES) : chapterSystemPromptBuilder.build();
3778
+ const systemPromptOverrides = {
3779
+ ...isAudioOnly ? AUDIO_ONLY_SYSTEM_PROMPT_OVERRIDES : {},
3780
+ ...sceneContext?.length ? SCENE_CONTEXT_SYSTEM_PROMPT_OVERRIDES : {}
3781
+ };
3782
+ const systemPrompt = chapterSystemPromptBuilder.build(systemPromptOverrides);
3712
3783
  chaptersData = await generateChaptersWithAI({
3713
3784
  provider: modelConfig.provider,
3714
3785
  modelId: modelConfig.modelId,
@@ -3726,9 +3797,13 @@ async function generateChaptersInternal(assetId, options, collectedUsage) {
3726
3797
  throw new MuxAiError(`Failed to generate chapters for asset ${assetId}.`);
3727
3798
  }
3728
3799
  const { chapters: chaptersPayload, usage } = chaptersData;
3729
- const validChapters = chaptersPayload.chapters.filter((chapter) => typeof chapter.startTime === "number" && typeof chapter.title === "string").filter(
3800
+ const scopedChapters = chaptersPayload.chapters.filter((chapter) => typeof chapter.startTime === "number" && typeof chapter.title === "string").filter(
3730
3801
  (chapter) => resolvedScope ? chapter.startTime >= resolvedScope.startTime && chapter.startTime < resolvedScope.endTime : true
3731
3802
  ).sort((a, b) => a.startTime - b.startTime);
3803
+ const validChapters = alignChaptersToSceneBoundaries(
3804
+ scopedChapters,
3805
+ sceneBoundarySeconds
3806
+ );
3732
3807
  if (validChapters.length === 0) {
3733
3808
  throw new MuxAiError(`Failed to generate valid chapters for asset ${assetId}.`);
3734
3809
  }
@@ -3754,9 +3829,11 @@ async function generateChaptersInternal(assetId, options, collectedUsage) {
3754
3829
  if (scrubbedChapters.length === 0) {
3755
3830
  throw new MuxAiError(`Failed to generate valid chapters for asset ${assetId}.`);
3756
3831
  }
3757
- const firstChapterStartTime = resolvedScope?.startTime ?? 0;
3758
- if (scrubbedChapters[0].startTime !== firstChapterStartTime) {
3759
- scrubbedChapters[0].startTime = firstChapterStartTime;
3832
+ if (sceneBoundarySeconds.length === 0) {
3833
+ const firstChapterStartTime = resolvedScope?.startTime ?? 0;
3834
+ if (scrubbedChapters[0].startTime !== firstChapterStartTime) {
3835
+ scrubbedChapters[0].startTime = firstChapterStartTime;
3836
+ }
3760
3837
  }
3761
3838
  const usageWithMetadata = {
3762
3839
  ...usage,
@@ -6730,14 +6807,44 @@ function buildDescriptionGuidance(wordCount, contentType) {
6730
6807
  Write in present tense. Be specific about what is discussed or presented rather than making assumptions.
6731
6808
  Focus on the spoken content and any key insights, dialogue, or narrative elements.`;
6732
6809
  }
6733
- function createSummarizationBuilder({ titleLength, descriptionLength, tagCount } = {}) {
6810
+ function buildSceneAwareDescriptionGuidance(wordCount) {
6811
+ if (wordCount < DESCRIPTION_LENGTH_THRESHOLD_SMALL) {
6812
+ return dedent6`A brief summary of the video in no more than ${wordCount} words. Shorter is fine.
6813
+ Focus on the single most important subject or action across the ordered scenes.
6814
+ Use the storyboard as direct visual evidence and the scene context to preserve progression.
6815
+ Write in present tense.`;
6816
+ }
6817
+ if (wordCount > DESCRIPTION_LENGTH_THRESHOLD_LARGE) {
6818
+ return dedent6`A detailed summary that describes what happens across the ordered scenes.
6819
+ Never exceed ${wordCount} words, but shorter is perfectly fine. You may use multiple sentences.
6820
+ Be thorough: cover subjects, actions, setting, progression, and notable details supported across scenes.
6821
+ Use the storyboard as direct visual evidence and the scene context to preserve progression.
6822
+ Write in present tense. If the transcript provides dialogue or narration, incorporate key points while preserving scene progression.`;
6823
+ }
6824
+ return dedent6`A summary that describes what happens across the ordered scenes.
6825
+ Never exceed ${wordCount} words, but shorter is perfectly fine. You may use multiple sentences.
6826
+ Cover the main subjects, actions, setting, and notable progression supported across scenes.
6827
+ Use the storyboard as direct visual evidence and the scene context to preserve progression.
6828
+ Write in present tense. If the transcript provides dialogue or narration, incorporate key points while preserving scene progression.`;
6829
+ }
6830
+ function createSummarizationBuilder({ titleLength, descriptionLength, tagCount } = {}, { hasSceneContext = false } = {}) {
6734
6831
  const titleLimit = titleLength ?? DEFAULT_TITLE_LENGTH;
6735
6832
  const keywordLimit = tagCount ?? DEFAULT_SUMMARY_KEYWORD_LIMIT;
6833
+ 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.";
6834
+ const qualityGuidelines = hasSceneContext ? dedent6`
6835
+ - Follow scene_index order to understand what begins, develops, and concludes
6836
+ - Use scene narratives and concepts as grounded evidence, preserving consistent terminology across the output
6837
+ - Use the storyboard as direct visual evidence for whole-asset details
6838
+ - Balance brevity with informativeness` : dedent6`
6839
+ - Examine all frames to understand the full context and progression
6840
+ - Be precise: "golden retriever" is better than "dog" when identifiable
6841
+ - Capture the narrative: what begins, develops, and concludes
6842
+ - Balance brevity with informativeness`;
6736
6843
  return createPromptBuilder({
6737
6844
  template: {
6738
6845
  task: {
6739
6846
  tag: "task",
6740
- content: "Analyze the storyboard frames and generate metadata that captures the essence of the video content."
6847
+ content: task
6741
6848
  },
6742
6849
  title: {
6743
6850
  tag: "title_requirements",
@@ -6750,7 +6857,7 @@ function createSummarizationBuilder({ titleLength, descriptionLength, tagCount }
6750
6857
  },
6751
6858
  description: {
6752
6859
  tag: "description_requirements",
6753
- content: buildDescriptionGuidance(descriptionLength ?? DEFAULT_DESCRIPTION_LENGTH, "video")
6860
+ content: hasSceneContext ? buildSceneAwareDescriptionGuidance(descriptionLength ?? DEFAULT_DESCRIPTION_LENGTH) : buildDescriptionGuidance(descriptionLength ?? DEFAULT_DESCRIPTION_LENGTH, "video")
6754
6861
  },
6755
6862
  keywords: {
6756
6863
  tag: "keywords_requirements",
@@ -6766,11 +6873,7 @@ function createSummarizationBuilder({ titleLength, descriptionLength, tagCount }
6766
6873
  },
6767
6874
  qualityGuidelines: {
6768
6875
  tag: "quality_guidelines",
6769
- content: dedent6`
6770
- - Examine all frames to understand the full context and progression
6771
- - Be precise: "golden retriever" is better than "dog" when identifiable
6772
- - Capture the narrative: what begins, develops, and concludes
6773
- - Balance brevity with informativeness`
6876
+ content: qualityGuidelines
6774
6877
  }
6775
6878
  },
6776
6879
  sectionOrder: ["task", "title", "description", "keywords", "qualityGuidelines"]
@@ -6822,59 +6925,83 @@ function createAudioOnlyBuilder({ titleLength, descriptionLength, tagCount } = {
6822
6925
  sectionOrder: ["task", "title", "description", "keywords", "qualityGuidelines"]
6823
6926
  });
6824
6927
  }
6825
- var SYSTEM_PROMPT5 = promptDedent`
6826
- <role>
6827
- You are a video content analyst specializing in storyboard interpretation and multimodal analysis.
6828
- </role>
6829
-
6830
- <context>
6831
- You receive storyboard images containing multiple sequential frames extracted from a video.
6832
- ${STORYBOARD_FRAME_INSTRUCTIONS}
6833
- </context>
6834
-
6835
- <transcript_guidance>
6836
- When a transcript is provided alongside the storyboard:
6837
- - Use it to understand spoken content, dialogue, narration, and audio context
6838
- - Correlate transcript content with visual frames to build a complete picture
6839
- - Extract key terminology, names, and specific language used by speakers
6840
- - Let the transcript inform keyword selection, especially for topics not visually obvious
6841
- - Prioritize visual content for the description, but enrich it with transcript insights
6842
- - If transcript and visuals conflict, trust the visual evidence
6843
- </transcript_guidance>
6844
-
6845
- <capabilities>
6846
- - Extract meaning from visual sequences
6847
- - Identify subjects, actions, settings, and narrative arcs
6848
- - Generate accurate, searchable metadata
6849
- - Synthesize visual and transcript information when provided
6850
- </capabilities>
6851
-
6852
- <security>
6853
- ${NON_DISCLOSURE_CONSTRAINT}
6854
-
6855
- ${UNTRUSTED_USER_INPUT_NOTICE}
6856
-
6857
- ${VISUAL_TEXT_AS_CONTENT}
6858
-
6859
- ${CANARY_TRIPWIRE}
6860
- </security>
6861
-
6862
- <constraints>
6863
- - Only describe what is clearly observable in the frames or explicitly stated in the transcript
6864
- - ${NO_FABRICATION_CONSTRAINT}
6865
- - ${METADATA_BOUNDARY_WARNING}
6866
- - ${STRUCTURED_DATA_CONSTRAINT}
6867
- - Output only the JSON object; no markdown or extra text
6868
- - When a <language> section is provided, all output text MUST be written in that language
6869
- </constraints>
6870
-
6871
- <tone_guidance>
6872
- ${TONE_GUIDANCE}
6873
- </tone_guidance>
6874
-
6875
- <language_guidelines>
6876
- ${createLanguageGuidelines("video")}
6877
- </language_guidelines>`;
6928
+ function createVideoSystemPrompt(hasSceneContext) {
6929
+ let sceneContext = "";
6930
+ let sceneGuidance = "";
6931
+ if (hasSceneContext) {
6932
+ sceneContext = promptDedent`
6933
+ Ordered scene context is also provided as structured evidence of the video's progression.
6934
+ Entries use stable, zero-based scene_index values and absolute millisecond timestamps.
6935
+ Read scenes in scene_index order to understand what begins, develops, and concludes.`;
6936
+ sceneGuidance = promptDedent`
6937
+ <scene_guidance>
6938
+ - Use scene titles, narratives, and concepts to understand temporal progression
6939
+ - Use storyboard frames as direct visual evidence for subjects, actions, settings, and details
6940
+ - Preserve consistent terminology from the scene context across the title, description, and keywords
6941
+ - Treat optional scene fields as evidence only when present; do not infer omitted details
6942
+ - Consider all provided evidence together; if derived scene context conflicts with storyboard visuals, trust the visuals
6943
+ </scene_guidance>`;
6944
+ }
6945
+ const transcriptEvidence = hasSceneContext ? "storyboard frames and ordered scenes" : "visual frames";
6946
+ 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";
6947
+ 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";
6948
+ return promptDedent`
6949
+ <role>
6950
+ You are a video content analyst specializing in storyboard interpretation and multimodal analysis.
6951
+ </role>
6952
+
6953
+ <context>
6954
+ You receive storyboard images containing multiple sequential frames extracted from a video.
6955
+ ${STORYBOARD_FRAME_INSTRUCTIONS}
6956
+ ${sceneContext}
6957
+ </context>
6958
+
6959
+ ${sceneGuidance}
6960
+
6961
+ <transcript_guidance>
6962
+ When a transcript is provided alongside the storyboard:
6963
+ - Use it to understand spoken content, dialogue, narration, and audio context
6964
+ - Correlate transcript content with ${transcriptEvidence} to build a complete picture
6965
+ - Extract key terminology, names, and specific language used by speakers
6966
+ - Let the transcript inform keyword selection, especially for topics not visually obvious
6967
+ - ${descriptionPriority}
6968
+ - If transcript and visuals conflict, trust the visual evidence
6969
+ </transcript_guidance>
6970
+
6971
+ <capabilities>
6972
+ - Extract meaning from visual sequences
6973
+ - Identify subjects, actions, settings, and narrative arcs
6974
+ - Generate accurate, searchable metadata
6975
+ - Synthesize visual and transcript information when provided
6976
+ </capabilities>
6977
+
6978
+ <security>
6979
+ ${NON_DISCLOSURE_CONSTRAINT}
6980
+
6981
+ ${UNTRUSTED_USER_INPUT_NOTICE}
6982
+
6983
+ ${VISUAL_TEXT_AS_CONTENT}
6984
+
6985
+ ${CANARY_TRIPWIRE}
6986
+ </security>
6987
+
6988
+ <constraints>
6989
+ - ${groundingConstraint}
6990
+ - ${NO_FABRICATION_CONSTRAINT}
6991
+ - ${METADATA_BOUNDARY_WARNING}
6992
+ - ${STRUCTURED_DATA_CONSTRAINT}
6993
+ - Output only the JSON object; no markdown or extra text
6994
+ - When a <language> section is provided, all output text MUST be written in that language
6995
+ </constraints>
6996
+
6997
+ <tone_guidance>
6998
+ ${TONE_GUIDANCE}
6999
+ </tone_guidance>
7000
+
7001
+ <language_guidelines>
7002
+ ${createLanguageGuidelines("video")}
7003
+ </language_guidelines>`;
7004
+ }
6878
7005
  var AUDIO_ONLY_SYSTEM_PROMPT3 = promptDedent`
6879
7006
  <role>
6880
7007
  You are an audio content analyst specializing in transcript analysis and metadata generation.
@@ -6925,6 +7052,16 @@ var AUDIO_ONLY_SYSTEM_PROMPT3 = promptDedent`
6925
7052
  <language_guidelines>
6926
7053
  ${createLanguageGuidelines("audio")}
6927
7054
  </language_guidelines>`;
7055
+ function createSceneContextSection(sceneContext) {
7056
+ return {
7057
+ tag: "scene_context",
7058
+ content: JSON.stringify(sceneContext),
7059
+ attributes: {
7060
+ format: "json",
7061
+ order: "scene_index_ascending"
7062
+ }
7063
+ };
7064
+ }
6928
7065
  function buildUserPrompt5({
6929
7066
  tone,
6930
7067
  transcriptText,
@@ -6934,7 +7071,8 @@ function buildUserPrompt5({
6934
7071
  titleLength,
6935
7072
  descriptionLength,
6936
7073
  tagCount,
6937
- languageName
7074
+ languageName,
7075
+ sceneContext
6938
7076
  }) {
6939
7077
  const contextSections = [createToneSection(TONE_INSTRUCTIONS[tone])];
6940
7078
  if (languageName) {
@@ -6949,11 +7087,15 @@ function buildUserPrompt5({
6949
7087
  const format = isCleanTranscript ? "plain text" : "WebVTT";
6950
7088
  contextSections.push(createTranscriptSection(transcriptText, format));
6951
7089
  }
7090
+ const hasSceneContext = !isAudioOnly && Boolean(sceneContext?.length);
7091
+ if (hasSceneContext && sceneContext) {
7092
+ contextSections.push(createSceneContextSection(sceneContext));
7093
+ }
6952
7094
  const constraints = { titleLength, descriptionLength, tagCount };
6953
- const promptBuilder = isAudioOnly ? createAudioOnlyBuilder(constraints) : createSummarizationBuilder(constraints);
7095
+ const promptBuilder = isAudioOnly ? createAudioOnlyBuilder(constraints) : createSummarizationBuilder(constraints, { hasSceneContext });
6954
7096
  return promptBuilder.buildWithContext(promptOverrides, contextSections);
6955
7097
  }
6956
- async function analyzeStoryboard2(imageDataUrl, provider, modelId, userPrompt, systemPrompt, descriptionLength, credentials) {
7098
+ async function analyzeVideo(imageDataUrl, provider, modelId, userPrompt, systemPrompt, descriptionLength, credentials) {
6957
7099
  "use step";
6958
7100
  const model = await createLanguageModelFromConfig(provider, modelId, credentials);
6959
7101
  const schema = buildSummarySchema(descriptionLength);
@@ -6969,10 +7111,10 @@ async function analyzeStoryboard2(imageDataUrl, provider, modelId, userPrompt, s
6969
7111
  messages: [
6970
7112
  {
6971
7113
  role: "user",
6972
- content: [
7114
+ content: imageDataUrl ? [
6973
7115
  { type: "text", text: userPrompt },
6974
7116
  { type: "image", image: imageDataUrl }
6975
- ]
7117
+ ] : userPrompt
6976
7118
  }
6977
7119
  ]
6978
7120
  }));
@@ -7082,6 +7224,7 @@ async function getSummaryAndTagsInternal(assetId, options, collectedUsage) {
7082
7224
  cleanTranscript = true,
7083
7225
  imageSubmissionMode = "url",
7084
7226
  imageDownloadOptions,
7227
+ sceneContext,
7085
7228
  promptOverrides,
7086
7229
  credentials,
7087
7230
  titleLength,
@@ -7105,6 +7248,7 @@ async function getSummaryAndTagsInternal(assetId, options, collectedUsage) {
7105
7248
  const { asset: assetData, playbackId, policy } = await getPlaybackIdForAsset(assetId, workflowCredentials, options?.assetSnapshot);
7106
7249
  const assetDurationSeconds = getAssetDurationSecondsFromAsset(assetData);
7107
7250
  const isAudioOnly = isAudioOnlyAsset(assetData);
7251
+ const hasSceneContext = !isAudioOnly && Boolean(sceneContext?.length);
7108
7252
  const effectiveScope = hasWorkflowScopeBoundaries(scope) ? scope : void 0;
7109
7253
  const storyboardScope = isAudioOnly ? void 0 : resolveRenderableVideoScope(
7110
7254
  effectiveScope,
@@ -7121,7 +7265,8 @@ async function getSummaryAndTagsInternal(assetId, options, collectedUsage) {
7121
7265
  );
7122
7266
  }
7123
7267
  const signingContext = await resolveMuxSigningContext(workflowCredentials);
7124
- if (policy === "signed" && !signingContext) {
7268
+ const requiresSignedPlaybackAccess = includeTranscript || !isAudioOnly;
7269
+ if (policy === "signed" && requiresSignedPlaybackAccess && !signingContext) {
7125
7270
  throw new MuxAiError(
7126
7271
  "Signed playback ID requires signing credentials. Set MUX_SIGNING_KEY and MUX_PRIVATE_KEY environment variables.",
7127
7272
  { type: "validation_error" }
@@ -7147,11 +7292,15 @@ async function getSummaryAndTagsInternal(assetId, options, collectedUsage) {
7147
7292
  titleLength,
7148
7293
  descriptionLength,
7149
7294
  tagCount,
7150
- languageName
7295
+ languageName,
7296
+ sceneContext: hasSceneContext ? sceneContext : void 0
7151
7297
  });
7152
7298
  let analysisResponse;
7153
7299
  let imageUrl;
7154
- const systemPrompt = isAudioOnly ? AUDIO_ONLY_SYSTEM_PROMPT3 : SYSTEM_PROMPT5;
7300
+ let systemPrompt = AUDIO_ONLY_SYSTEM_PROMPT3;
7301
+ if (!isAudioOnly) {
7302
+ systemPrompt = createVideoSystemPrompt(hasSceneContext);
7303
+ }
7155
7304
  const effectiveDescriptionLength = descriptionLength ?? DEFAULT_DESCRIPTION_LENGTH;
7156
7305
  try {
7157
7306
  if (isAudioOnly) {
@@ -7174,7 +7323,7 @@ async function getSummaryAndTagsInternal(assetId, options, collectedUsage) {
7174
7323
  imageUrl = storyboardUrl;
7175
7324
  if (imageSubmissionMode === "base64") {
7176
7325
  const downloadResult = await downloadImageAsBase64(storyboardUrl, imageDownloadOptions);
7177
- analysisResponse = await analyzeStoryboard2(
7326
+ analysisResponse = await analyzeVideo(
7178
7327
  downloadResult.base64Data,
7179
7328
  modelConfig.provider,
7180
7329
  modelConfig.modelId,
@@ -7184,7 +7333,7 @@ async function getSummaryAndTagsInternal(assetId, options, collectedUsage) {
7184
7333
  workflowCredentials
7185
7334
  );
7186
7335
  } else {
7187
- analysisResponse = await analyzeStoryboard2(
7336
+ analysisResponse = await analyzeVideo(
7188
7337
  storyboardUrl,
7189
7338
  modelConfig.provider,
7190
7339
  modelConfig.modelId,
@@ -7769,7 +7918,7 @@ import { z as z9 } from "zod";
7769
7918
  var translationSchema = z9.object({
7770
7919
  translation: z9.string()
7771
7920
  });
7772
- var SYSTEM_PROMPT6 = promptDedent`
7921
+ var SYSTEM_PROMPT5 = promptDedent`
7773
7922
  You are a subtitle translation expert. Translate VTT subtitle files to the target language specified by the user.
7774
7923
  You may receive either a full VTT file or a chunk from a larger VTT.
7775
7924
  Preserve all timestamps, cue ordering, and VTT formatting exactly as they appear.
@@ -8094,7 +8243,7 @@ async function translateVttWithAI({
8094
8243
  model,
8095
8244
  maxRetries: 0,
8096
8245
  output: Output7.object({ schema: translationSchema }),
8097
- system: SYSTEM_PROMPT6,
8246
+ system: SYSTEM_PROMPT5,
8098
8247
  messages: [
8099
8248
  {
8100
8249
  role: "user",