@mux/ai 0.7.5 → 0.8.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.
@@ -523,6 +523,12 @@ interface SummarizationOptions extends MuxAIOptions {
523
523
  * Useful for customizing the AI's output for specific use cases (SEO, social media, etc.)
524
524
  */
525
525
  promptOverrides?: SummarizationPromptOverrides;
526
+ /** Desired title length in characters. */
527
+ titleLength?: number;
528
+ /** Desired description length in characters. */
529
+ descriptionLength?: number;
530
+ /** Desired number of tags. */
531
+ tagCount?: number;
526
532
  }
527
533
  declare function getSummaryAndTags(assetId: string, options?: SummarizationOptions): Promise<SummaryAndTagsResult>;
528
534
 
package/dist/index.d.ts CHANGED
@@ -2,14 +2,14 @@ import { W as WorkflowCredentials, S as StoragePutObjectInput, a as StoragePresi
2
2
  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 StorageAdapter, T as TextChunk, g as TokenChunkingConfig, h as TokenUsage, i as ToneType, U as UsageMetadata, V as VTTChunkingConfig, j as VideoEmbeddingsResult, k as WorkflowCredentialsInput, l as WorkflowMuxClient, m as decryptFromWorkflow, n as encryptForWorkflow } from './types-BRbaGW3t.js';
3
3
  import { WORKFLOW_SERIALIZE, WORKFLOW_DESERIALIZE } from '@workflow/serde';
4
4
  export { i as primitives } from './index-Nxf6BaBO.js';
5
- export { i as workflows } from './index-B0U9upb4.js';
5
+ export { i as workflows } from './index-DP02N3iR.js';
6
6
  import '@mux/mux-node';
7
7
  import 'zod';
8
8
  import '@ai-sdk/anthropic';
9
9
  import '@ai-sdk/google';
10
10
  import '@ai-sdk/openai';
11
11
 
12
- var version = "0.7.5";
12
+ var version = "0.8.0";
13
13
 
14
14
  /**
15
15
  * A function that returns workflow credentials, either synchronously or asynchronously.
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.7.5";
8
+ var version = "0.8.0";
9
9
 
10
10
  // src/env.ts
11
11
  import { z } from "zod";
@@ -30,6 +30,10 @@ var EnvSchema = z.object({
30
30
  ),
31
31
  MUX_SIGNING_KEY: optionalString("Mux signing key ID for signed playback URLs.", "Used to sign playback URLs"),
32
32
  MUX_PRIVATE_KEY: optionalString("Mux signing private key for signed playback URLs.", "Used to sign playback URLs"),
33
+ MUX_IMAGE_URL_OVERRIDE: optionalString(
34
+ "Override for Mux image base URL (defaults to https://image.mux.com).",
35
+ "Mux image URL override"
36
+ ),
33
37
  // Test-only helpers (used by this repo's integration tests)
34
38
  MUX_TEST_ASSET_ID: optionalString("Mux asset ID used by integration tests.", "Mux test asset id"),
35
39
  MUX_TEST_ASSET_ID_CHAPTERS: optionalString("Mux asset ID used by integration tests for chapters.", "Mux test asset id for chapters"),
@@ -1032,6 +1036,44 @@ async function fetchHotspots(identifierType, id, options) {
1032
1036
  return transformHotspotResponse(response);
1033
1037
  }
1034
1038
 
1039
+ // src/lib/mux-image-url.ts
1040
+ var DEFAULT_MUX_IMAGE_ORIGIN = "https://image.mux.com";
1041
+ function normalizeMuxImageOrigin(value) {
1042
+ const trimmed = value.trim();
1043
+ const candidate = trimmed.includes("://") ? trimmed : `https://${trimmed}`;
1044
+ let parsed;
1045
+ try {
1046
+ parsed = new URL(candidate);
1047
+ } catch {
1048
+ throw new Error(
1049
+ `Invalid MUX_IMAGE_URL_OVERRIDE. Provide a hostname like "image.example.mux.com" (or a URL origin such as "https://image.example.mux.com").`
1050
+ );
1051
+ }
1052
+ if (parsed.username || parsed.password || parsed.search || parsed.hash || parsed.pathname && parsed.pathname !== "/") {
1053
+ throw new Error(
1054
+ "Invalid MUX_IMAGE_URL_OVERRIDE. Only a hostname/origin is allowed (no credentials, query params, hash fragments, or path)."
1055
+ );
1056
+ }
1057
+ return parsed.origin;
1058
+ }
1059
+ function getMuxImageOrigin() {
1060
+ const override = env_default.MUX_IMAGE_URL_OVERRIDE;
1061
+ if (!override) {
1062
+ return DEFAULT_MUX_IMAGE_ORIGIN;
1063
+ }
1064
+ return normalizeMuxImageOrigin(override);
1065
+ }
1066
+ function getMuxImageBaseUrl(playbackId, assetType) {
1067
+ const origin = getMuxImageOrigin();
1068
+ return `${origin}/${playbackId}/${assetType}.png`;
1069
+ }
1070
+ function getMuxStoryboardBaseUrl(playbackId) {
1071
+ return getMuxImageBaseUrl(playbackId, "storyboard");
1072
+ }
1073
+ function getMuxThumbnailBaseUrl(playbackId) {
1074
+ return getMuxImageBaseUrl(playbackId, "thumbnail");
1075
+ }
1076
+
1035
1077
  // src/lib/url-signing.ts
1036
1078
  async function createSigningClient(context) {
1037
1079
  const { default: MuxClient } = await import("@mux/mux-node");
@@ -1073,7 +1115,7 @@ async function signUrl(url, playbackId, type = "video", params, credentials) {
1073
1115
  var DEFAULT_STORYBOARD_WIDTH = 640;
1074
1116
  async function getStoryboardUrl(playbackId, width = DEFAULT_STORYBOARD_WIDTH, shouldSign = false, credentials) {
1075
1117
  "use step";
1076
- const baseUrl = `https://image.mux.com/${playbackId}/storyboard.png`;
1118
+ const baseUrl = getMuxStoryboardBaseUrl(playbackId);
1077
1119
  if (shouldSign) {
1078
1120
  return signUrl(baseUrl, playbackId, "storyboard", { width }, credentials);
1079
1121
  }
@@ -1192,7 +1234,7 @@ async function getThumbnailUrls(playbackId, duration, options = {}) {
1192
1234
  }
1193
1235
  timestamps = newTimestamps;
1194
1236
  }
1195
- const baseUrl = `https://image.mux.com/${playbackId}/thumbnail.png`;
1237
+ const baseUrl = getMuxThumbnailBaseUrl(playbackId);
1196
1238
  const urlPromises = timestamps.map(async (time) => {
1197
1239
  if (shouldSign) {
1198
1240
  return signUrl(baseUrl, playbackId, "thumbnail", { time, width }, credentials);
@@ -1756,16 +1798,6 @@ var SYSTEM_PROMPT = dedent`
1756
1798
  - GOOD: "A person runs through a park"
1757
1799
  - Be specific and evidence-based
1758
1800
  </language_guidelines>`;
1759
- function buildSystemPrompt(allowedAnswers) {
1760
- const answerList = allowedAnswers.map((answer) => `"${answer}"`).join(", ");
1761
- return `${SYSTEM_PROMPT}
1762
-
1763
- ${dedent`
1764
- <response_options>
1765
- Allowed answers: ${answerList}
1766
- </response_options>
1767
- `}`;
1768
- }
1769
1801
  var askQuestionsPromptBuilder = createPromptBuilder({
1770
1802
  template: {
1771
1803
  questions: {
@@ -1775,21 +1807,30 @@ var askQuestionsPromptBuilder = createPromptBuilder({
1775
1807
  },
1776
1808
  sectionOrder: ["questions"]
1777
1809
  });
1778
- function buildUserPrompt(questions, transcriptText, isCleanTranscript = true) {
1810
+ function buildUserPrompt(questions, allowedAnswers, transcriptText, isCleanTranscript = true) {
1779
1811
  const questionsList = questions.map((q, idx) => `${idx + 1}. ${q.question}`).join("\n");
1780
1812
  const questionsContent = dedent`
1781
1813
  Please answer the following yes/no questions about this video:
1782
1814
 
1783
1815
  ${questionsList}`;
1816
+ const answerList = allowedAnswers.map((answer) => `"${answer}"`).join(", ");
1817
+ const responseOptions = dedent`
1818
+ <response_options>
1819
+ Allowed answers: ${answerList}
1820
+ </response_options>`;
1821
+ const questionsSection = askQuestionsPromptBuilder.build({ questions: questionsContent });
1784
1822
  if (!transcriptText) {
1785
- return askQuestionsPromptBuilder.build({ questions: questionsContent });
1823
+ return `${questionsSection}
1824
+
1825
+ ${responseOptions}`;
1786
1826
  }
1787
1827
  const format = isCleanTranscript ? "plain text" : "WebVTT";
1788
- const transcriptSection = createTranscriptSection(transcriptText, format);
1789
- return askQuestionsPromptBuilder.buildWithContext(
1790
- { questions: questionsContent },
1791
- [transcriptSection]
1792
- );
1828
+ const transcriptSection = renderSection(createTranscriptSection(transcriptText, format));
1829
+ return `${transcriptSection}
1830
+
1831
+ ${questionsSection}
1832
+
1833
+ ${responseOptions}`;
1793
1834
  }
1794
1835
  async function fetchImageAsBase64(imageUrl, imageDownloadOptions) {
1795
1836
  "use step";
@@ -1885,8 +1926,8 @@ async function askQuestions(assetId, questions, options) {
1885
1926
  cleanTranscript,
1886
1927
  shouldSign: policy === "signed"
1887
1928
  })).transcriptText : "";
1888
- const userPrompt = buildUserPrompt(questions, transcriptText, cleanTranscript);
1889
- const systemPrompt = buildSystemPrompt(normalizedAnswerOptions);
1929
+ const userPrompt = buildUserPrompt(questions, allowedAnswers, transcriptText, cleanTranscript);
1930
+ const systemPrompt = SYSTEM_PROMPT;
1890
1931
  const imageUrl = await getStoryboardUrl(
1891
1932
  playbackId,
1892
1933
  storyboardWidth,
@@ -2601,7 +2642,7 @@ function planSamplingTimestamps(options) {
2601
2642
 
2602
2643
  // src/workflows/moderation.ts
2603
2644
  var DEFAULT_THRESHOLDS = {
2604
- sexual: 0.7,
2645
+ sexual: 0.8,
2605
2646
  violence: 0.8
2606
2647
  };
2607
2648
  var DEFAULT_PROVIDER2 = "openai";
@@ -2866,7 +2907,7 @@ async function requestHiveModeration(imageUrls, maxConcurrent = 5, submissionMod
2866
2907
  async function getThumbnailUrlsFromTimestamps(playbackId, timestampsMs, options) {
2867
2908
  "use step";
2868
2909
  const { width, shouldSign, credentials } = options;
2869
- const baseUrl = `https://image.mux.com/${playbackId}/thumbnail.png`;
2910
+ const baseUrl = getMuxThumbnailBaseUrl(playbackId);
2870
2911
  const urlPromises = timestampsMs.map(async (tsMs) => {
2871
2912
  const time = Number((tsMs / 1e3).toFixed(2));
2872
2913
  if (shouldSign) {
@@ -3043,96 +3084,106 @@ var TONE_INSTRUCTIONS = {
3043
3084
  playful: "Channel your inner diva! Answer with maximum sass, wit, and playful attitude. Don't hold back - be cheeky, clever, and delightfully snarky. Make it pop!",
3044
3085
  professional: "Provide a professional, executive-level analysis suitable for business reporting."
3045
3086
  };
3046
- var summarizationPromptBuilder = createPromptBuilder({
3047
- template: {
3048
- task: {
3049
- tag: "task",
3050
- content: "Analyze the storyboard frames and generate metadata that captures the essence of the video content."
3051
- },
3052
- title: {
3053
- tag: "title_requirements",
3054
- content: dedent4`
3055
- A short, compelling headline that immediately communicates the subject or action.
3056
- Aim for brevity - typically under 10 words. Think of how a news headline or video card title would read.
3057
- Start with the primary subject, action, or topic - never begin with "A video of" or similar phrasing.
3058
- Use active, specific language.`
3059
- },
3060
- description: {
3061
- tag: "description_requirements",
3062
- content: dedent4`
3063
- A concise summary (2-4 sentences) that describes what happens across the video.
3064
- Cover the main subjects, actions, setting, and any notable progression visible across frames.
3065
- Write in present tense. Be specific about observable details rather than making assumptions.
3066
- If the transcript provides dialogue or narration, incorporate key points but prioritize visual content.`
3067
- },
3068
- keywords: {
3069
- tag: "keywords_requirements",
3070
- content: dedent4`
3071
- Specific, searchable terms (up to ${SUMMARY_KEYWORD_LIMIT}) that capture:
3072
- - Primary subjects (people, animals, objects)
3073
- - Actions and activities being performed
3074
- - Setting and environment
3075
- - Notable objects or tools
3076
- - Style or genre (if applicable)
3077
- Prefer concrete nouns and action verbs over abstract concepts.
3078
- Use lowercase. Avoid redundant or overly generic terms like "video" or "content".`
3079
- },
3080
- qualityGuidelines: {
3081
- tag: "quality_guidelines",
3082
- content: dedent4`
3083
- - Examine all frames to understand the full context and progression
3084
- - Be precise: "golden retriever" is better than "dog" when identifiable
3085
- - Capture the narrative: what begins, develops, and concludes
3086
- - Balance brevity with informativeness`
3087
- }
3088
- },
3089
- sectionOrder: ["task", "title", "description", "keywords", "qualityGuidelines"]
3090
- });
3091
- var audioOnlyPromptBuilder = createPromptBuilder({
3092
- template: {
3093
- task: {
3094
- tag: "task",
3095
- content: "Analyze the transcript and generate metadata that captures the essence of the audio content."
3096
- },
3097
- title: {
3098
- tag: "title_requirements",
3099
- content: dedent4`
3100
- A short, compelling headline that immediately communicates the subject or topic.
3101
- Aim for brevity - typically under 10 words. Think of how a podcast title or audio description would read.
3102
- Start with the primary subject, action, or topic - never begin with "An audio of" or similar phrasing.
3103
- Use active, specific language.`
3104
- },
3105
- description: {
3106
- tag: "description_requirements",
3107
- content: dedent4`
3108
- A concise summary (2-4 sentences) that describes the audio content.
3109
- Cover the main topics, speakers, themes, and any notable progression in the discussion or narration.
3110
- Write in present tense. Be specific about what is discussed or presented rather than making assumptions.
3111
- Focus on the spoken content and any key insights, dialogue, or narrative elements.`
3087
+ function createSummarizationBuilder({ titleLength, descriptionLength, tagCount } = {}) {
3088
+ const titleBrevity = titleLength != null ? `Aim for approximately ${titleLength} characters.` : "Aim for brevity - typically under 10 words.";
3089
+ const descConstraint = descriptionLength != null ? `approximately ${descriptionLength} characters` : "2-4 sentences";
3090
+ const keywordLimit = tagCount ?? SUMMARY_KEYWORD_LIMIT;
3091
+ return createPromptBuilder({
3092
+ template: {
3093
+ task: {
3094
+ tag: "task",
3095
+ content: "Analyze the storyboard frames and generate metadata that captures the essence of the video content."
3096
+ },
3097
+ title: {
3098
+ tag: "title_requirements",
3099
+ content: dedent4`
3100
+ A short, compelling headline that immediately communicates the subject or action.
3101
+ ${titleBrevity} Think of how a news headline or video card title would read.
3102
+ Start with the primary subject, action, or topic - never begin with "A video of" or similar phrasing.
3103
+ Use active, specific language.`
3104
+ },
3105
+ description: {
3106
+ tag: "description_requirements",
3107
+ content: dedent4`
3108
+ A concise summary (${descConstraint}) that describes what happens across the video.
3109
+ Cover the main subjects, actions, setting, and any notable progression visible across frames.
3110
+ Write in present tense. Be specific about observable details rather than making assumptions.
3111
+ If the transcript provides dialogue or narration, incorporate key points but prioritize visual content.`
3112
+ },
3113
+ keywords: {
3114
+ tag: "keywords_requirements",
3115
+ content: dedent4`
3116
+ Specific, searchable terms (up to ${keywordLimit}) that capture:
3117
+ - Primary subjects (people, animals, objects)
3118
+ - Actions and activities being performed
3119
+ - Setting and environment
3120
+ - Notable objects or tools
3121
+ - Style or genre (if applicable)
3122
+ Prefer concrete nouns and action verbs over abstract concepts.
3123
+ Use lowercase. Avoid redundant or overly generic terms like "video" or "content".`
3124
+ },
3125
+ qualityGuidelines: {
3126
+ tag: "quality_guidelines",
3127
+ content: dedent4`
3128
+ - Examine all frames to understand the full context and progression
3129
+ - Be precise: "golden retriever" is better than "dog" when identifiable
3130
+ - Capture the narrative: what begins, develops, and concludes
3131
+ - Balance brevity with informativeness`
3132
+ }
3112
3133
  },
3113
- keywords: {
3114
- tag: "keywords_requirements",
3115
- content: dedent4`
3116
- Specific, searchable terms (up to ${SUMMARY_KEYWORD_LIMIT}) that capture:
3117
- - Primary topics and themes
3118
- - Speakers or presenters (if named)
3119
- - Key concepts and terminology
3120
- - Content type (interview, lecture, music, etc.)
3121
- - Genre or style (if applicable)
3122
- Prefer concrete nouns and relevant terms over abstract concepts.
3123
- Use lowercase. Avoid redundant or overly generic terms like "audio" or "content".`
3134
+ sectionOrder: ["task", "title", "description", "keywords", "qualityGuidelines"]
3135
+ });
3136
+ }
3137
+ function createAudioOnlyBuilder({ titleLength, descriptionLength, tagCount } = {}) {
3138
+ const titleBrevity = titleLength != null ? `Aim for approximately ${titleLength} characters.` : "Aim for brevity - typically under 10 words.";
3139
+ const descConstraint = descriptionLength != null ? `approximately ${descriptionLength} characters` : "2-4 sentences";
3140
+ const keywordLimit = tagCount ?? SUMMARY_KEYWORD_LIMIT;
3141
+ return createPromptBuilder({
3142
+ template: {
3143
+ task: {
3144
+ tag: "task",
3145
+ content: "Analyze the transcript and generate metadata that captures the essence of the audio content."
3146
+ },
3147
+ title: {
3148
+ tag: "title_requirements",
3149
+ content: dedent4`
3150
+ A short, compelling headline that immediately communicates the subject or topic.
3151
+ ${titleBrevity} Think of how a podcast title or audio description would read.
3152
+ Start with the primary subject, action, or topic - never begin with "An audio of" or similar phrasing.
3153
+ Use active, specific language.`
3154
+ },
3155
+ description: {
3156
+ tag: "description_requirements",
3157
+ content: dedent4`
3158
+ A concise summary (${descConstraint}) that describes the audio content.
3159
+ Cover the main topics, speakers, themes, and any notable progression in the discussion or narration.
3160
+ Write in present tense. Be specific about what is discussed or presented rather than making assumptions.
3161
+ Focus on the spoken content and any key insights, dialogue, or narrative elements.`
3162
+ },
3163
+ keywords: {
3164
+ tag: "keywords_requirements",
3165
+ content: dedent4`
3166
+ Specific, searchable terms (up to ${keywordLimit}) that capture:
3167
+ - Primary topics and themes
3168
+ - Speakers or presenters (if named)
3169
+ - Key concepts and terminology
3170
+ - Content type (interview, lecture, music, etc.)
3171
+ - Genre or style (if applicable)
3172
+ Prefer concrete nouns and relevant terms over abstract concepts.
3173
+ Use lowercase. Avoid redundant or overly generic terms like "audio" or "content".`
3174
+ },
3175
+ qualityGuidelines: {
3176
+ tag: "quality_guidelines",
3177
+ content: dedent4`
3178
+ - Analyze the full transcript to understand context and themes
3179
+ - Be precise: use specific terminology when mentioned
3180
+ - Capture the narrative: what is introduced, discussed, and concluded
3181
+ - Balance brevity with informativeness`
3182
+ }
3124
3183
  },
3125
- qualityGuidelines: {
3126
- tag: "quality_guidelines",
3127
- content: dedent4`
3128
- - Analyze the full transcript to understand context and themes
3129
- - Be precise: use specific terminology when mentioned
3130
- - Capture the narrative: what is introduced, discussed, and concluded
3131
- - Balance brevity with informativeness`
3132
- }
3133
- },
3134
- sectionOrder: ["task", "title", "description", "keywords", "qualityGuidelines"]
3135
- });
3184
+ sectionOrder: ["task", "title", "description", "keywords", "qualityGuidelines"]
3185
+ });
3186
+ }
3136
3187
  var SYSTEM_PROMPT3 = dedent4`
3137
3188
  <role>
3138
3189
  You are a video content analyst specializing in storyboard interpretation and multimodal analysis.
@@ -3247,14 +3298,18 @@ function buildUserPrompt4({
3247
3298
  transcriptText,
3248
3299
  isCleanTranscript = true,
3249
3300
  promptOverrides,
3250
- isAudioOnly = false
3301
+ isAudioOnly = false,
3302
+ titleLength,
3303
+ descriptionLength,
3304
+ tagCount
3251
3305
  }) {
3252
3306
  const contextSections = [createToneSection(TONE_INSTRUCTIONS[tone])];
3253
3307
  if (transcriptText) {
3254
3308
  const format = isCleanTranscript ? "plain text" : "WebVTT";
3255
3309
  contextSections.push(createTranscriptSection(transcriptText, format));
3256
3310
  }
3257
- const promptBuilder = isAudioOnly ? audioOnlyPromptBuilder : summarizationPromptBuilder;
3311
+ const constraints = { titleLength, descriptionLength, tagCount };
3312
+ const promptBuilder = isAudioOnly ? createAudioOnlyBuilder(constraints) : createSummarizationBuilder(constraints);
3258
3313
  return promptBuilder.buildWithContext(promptOverrides, contextSections);
3259
3314
  }
3260
3315
  async function analyzeStoryboard2(imageDataUrl, provider, modelId, userPrompt, systemPrompt, credentials) {
@@ -3324,7 +3379,7 @@ async function analyzeAudioOnly(provider, modelId, userPrompt, systemPrompt, cre
3324
3379
  }
3325
3380
  };
3326
3381
  }
3327
- function normalizeKeywords(keywords) {
3382
+ function normalizeKeywords(keywords, limit = SUMMARY_KEYWORD_LIMIT) {
3328
3383
  if (!Array.isArray(keywords) || keywords.length === 0) {
3329
3384
  return [];
3330
3385
  }
@@ -3341,7 +3396,7 @@ function normalizeKeywords(keywords) {
3341
3396
  }
3342
3397
  uniqueLowercase.add(lower);
3343
3398
  normalized.push(trimmed);
3344
- if (normalized.length === SUMMARY_KEYWORD_LIMIT) {
3399
+ if (normalized.length === limit) {
3345
3400
  break;
3346
3401
  }
3347
3402
  }
@@ -3358,7 +3413,10 @@ async function getSummaryAndTags(assetId, options) {
3358
3413
  imageSubmissionMode = "url",
3359
3414
  imageDownloadOptions,
3360
3415
  promptOverrides,
3361
- credentials
3416
+ credentials,
3417
+ titleLength,
3418
+ descriptionLength,
3419
+ tagCount
3362
3420
  } = options ?? {};
3363
3421
  if (!VALID_TONES.includes(tone)) {
3364
3422
  throw new Error(
@@ -3396,7 +3454,10 @@ async function getSummaryAndTags(assetId, options) {
3396
3454
  transcriptText,
3397
3455
  isCleanTranscript: cleanTranscript,
3398
3456
  promptOverrides,
3399
- isAudioOnly
3457
+ isAudioOnly,
3458
+ titleLength,
3459
+ descriptionLength,
3460
+ tagCount
3400
3461
  });
3401
3462
  let analysisResponse;
3402
3463
  let imageUrl;
@@ -3453,7 +3514,7 @@ async function getSummaryAndTags(assetId, options) {
3453
3514
  assetId,
3454
3515
  title: analysisResponse.result.title,
3455
3516
  description: analysisResponse.result.description,
3456
- tags: normalizeKeywords(analysisResponse.result.keywords),
3517
+ tags: normalizeKeywords(analysisResponse.result.keywords, tagCount ?? SUMMARY_KEYWORD_LIMIT),
3457
3518
  storyboardUrl: imageUrl,
3458
3519
  // undefined for audio-only assets
3459
3520
  usage: {
@@ -4073,6 +4134,7 @@ import { z as z6 } from "zod";
4073
4134
  var translationSchema = z6.object({
4074
4135
  translation: z6.string()
4075
4136
  });
4137
+ var SYSTEM_PROMPT4 = 'You are a subtitle translation expert. Translate VTT subtitle files to the target language specified by the user. Preserve all timestamps and VTT formatting exactly as they appear. Return JSON with a single key "translation" containing the translated VTT content.';
4076
4138
  async function fetchVttFromMux(vttUrl) {
4077
4139
  "use step";
4078
4140
  const vttResponse = await fetch(vttUrl);
@@ -4095,9 +4157,13 @@ async function translateVttWithAI({
4095
4157
  model,
4096
4158
  output: Output5.object({ schema: translationSchema }),
4097
4159
  messages: [
4160
+ {
4161
+ role: "system",
4162
+ content: SYSTEM_PROMPT4
4163
+ },
4098
4164
  {
4099
4165
  role: "user",
4100
- content: `Translate the following VTT subtitle file from ${fromLanguageCode} to ${toLanguageCode}. Preserve all timestamps and VTT formatting exactly as they appear. Return JSON with a single key "translation" containing the translated VTT.
4166
+ content: `Translate from ${fromLanguageCode} to ${toLanguageCode}:
4101
4167
 
4102
4168
  ${vttContent}`
4103
4169
  }