@mux/ai 0.18.0 → 0.20.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,4 @@
1
- export { A as AskQuestionsOptions, a as AskQuestionsResult, b as AskQuestionsType, c as AudioTranslationOptions, d as AudioTranslationResult, e as AutoCensorProfanityOptions, B as BurnedInCaptionsAnalysis, f as BurnedInCaptionsOptions, g as BurnedInCaptionsPromptOverrides, h as BurnedInCaptionsPromptSections, j as BurnedInCaptionsResult, C as CaptionReplacement, k as CensorMode, l as Chapter, m as ChapterSystemPromptSections, n as ChaptersOptions, o as ChaptersPromptOverrides, p as ChaptersPromptSections, q as ChaptersResult, r as ChaptersType, D as DEFAULT_DESCRIPTION_LENGTH, s as DEFAULT_SUMMARY_KEYWORD_LIMIT, t as DEFAULT_TITLE_LENGTH, E as EditCaptionsOptions, u as EditCaptionsResult, v as EmbeddingsOptions, w as EmbeddingsResult, x as EngagementInsightsOptions, y as EngagementInsightsResult, H as HIVE_SEXUAL_CATEGORIES, z as HIVE_VIOLENCE_CATEGORIES, F as HiveModerationOutput, G as HiveModerationSource, M as ModerationOptions, I as ModerationProvider, J as ModerationResult, K as MomentInsight, O as OverallInsight, P as ProfanityDetectionPayload, Q as Question, L as QuestionAnswer, N as QuestionAnswerType, R as ReplacementRecord, S as SummarizationOptions, T as SummarizationPromptOverrides, U as SummarizationPromptSections, V as SummaryAndTagsResult, W as SummaryType, X as ThumbnailModerationScore, Y as TranslationChunkingOptions, Z as TranslationOptions, _ as TranslationPayload, $ as TranslationResult, a0 as aggregateTokenUsage, a1 as applyOverrideLists, a2 as applyReplacements, a3 as askQuestions, a4 as buildReplacementRegex, a5 as burnedInCaptionsSchema, a6 as censorVttContent, a7 as chapterSchema, a8 as chaptersSchema, a9 as createReplacer, aa as editCaptions, ab as extractTranscriptSegmentsForHotspots, ac as generateChapters, ad as generateEmbeddings, ae as generateEngagementInsights, af as generateVideoEmbeddings, ag as getModerationScores, ah as getSummaryAndTags, ai as hasBurnedInCaptions, aj as mapShotsToHotspots, ak as normalizeTranslatedVtt, al as profanityDetectionSchema, am as questionAnswerSchema, an as shouldSplitChunkTranslationError, ao as summarySchema, ap as transformCueText, aq as translateAudio, ar as translateCaptions, as as translationSchema } from '../index-BjwbvdoQ.js';
1
+ export { A as AskQuestionsOptions, a as AskQuestionsResult, b as AskQuestionsType, c as AudioTranslationOptions, d as AudioTranslationResult, e as AutoCensorProfanityOptions, B as BurnedInCaptionsAnalysis, f as BurnedInCaptionsOptions, g as BurnedInCaptionsPromptOverrides, h as BurnedInCaptionsPromptSections, j as BurnedInCaptionsResult, C as CaptionReplacement, k as CensorMode, l as Chapter, m as ChapterSystemPromptSections, n as ChaptersOptions, o as ChaptersPromptOverrides, p as ChaptersPromptSections, q as ChaptersResult, r as ChaptersType, D as DEFAULT_DESCRIPTION_LENGTH, s as DEFAULT_SUMMARY_KEYWORD_LIMIT, t as DEFAULT_TITLE_LENGTH, E as EditCaptionsOptions, u as EditCaptionsResult, v as EmbeddingsOptions, w as EmbeddingsResult, x as EngagementInsightsOptions, y as EngagementInsightsResult, H as HIVE_SEXUAL_CATEGORIES, z as HIVE_VIOLENCE_CATEGORIES, F as HiveModerationOutput, G as HiveModerationSource, M as ModerationOptions, I as ModerationProvider, J as ModerationResult, K as MomentInsight, O as OverallInsight, P as ProfanityDetectionPayload, Q as Question, L as QuestionAnswer, N as QuestionAnswerType, R as ReplacementRecord, S as SummarizationOptions, T as SummarizationPromptOverrides, U as SummarizationPromptSections, V as SummaryAndTagsResult, W as SummaryType, X as ThumbnailModerationScore, Y as TranslationChunkingOptions, Z as TranslationOptions, _ as TranslationPayload, $ as TranslationResult, a0 as aggregateTokenUsage, a1 as applyOverrideLists, a2 as applyReplacements, a3 as askQuestions, a4 as buildReplacementRegex, a5 as burnedInCaptionsSchema, a6 as censorVttContent, a7 as chapterSchema, a8 as chaptersSchema, a9 as createReplacer, aa as editCaptions, ab as extractTranscriptSegmentsForHotspots, ac as generateChapters, ad as generateEmbeddings, ae as generateEngagementInsights, af as generateVideoEmbeddings, ag as getModerationScores, ah as getSummaryAndTags, ai as hasBurnedInCaptions, aj as mapShotsToHotspots, ak as normalizeTranslatedVtt, al as profanityDetectionSchema, am as questionAnswerSchema, an as shouldSplitChunkTranslationError, ao as summarySchema, ap as transformCueText, aq as translateAudio, ar as translateCaptions, as as translationSchema } from '../index-DXMIJjUa.js';
2
2
  import 'zod';
3
3
  import '@ai-sdk/anthropic';
4
4
  import '@ai-sdk/google';
@@ -1891,8 +1891,7 @@ var questionAnswerSchema = z2.object({
1891
1891
  skipped: z2.boolean()
1892
1892
  });
1893
1893
  var SKIP_SENTINEL = "__SKIPPED__";
1894
- function createAskQuestionsSchema(allowedAnswers) {
1895
- const answerSchema = z2.enum([...allowedAnswers, SKIP_SENTINEL]);
1894
+ function createAskQuestionsSchema(answerSchema) {
1896
1895
  return z2.object({
1897
1896
  answers: z2.array(
1898
1897
  questionAnswerSchema.extend({
@@ -1928,15 +1927,16 @@ var SYSTEM_PROMPT = promptDedent`
1928
1927
  <task>
1929
1928
  For each question provided, you must:
1930
1929
  1. Analyze the storyboard frames and transcript (if provided)
1931
- 2. Answer with ONLY the allowed response options listed for that question - no other values are acceptable
1930
+ 2. Answer each question according to its specified response format: for questions with <allowed_answers>, select ONLY from the listed values; for questions with <answer_format>, follow the format specification exactly
1932
1931
  3. Provide a confidence score between 0 and 1 reflecting your certainty
1933
1932
  4. Explain your reasoning based on observable evidence
1934
1933
  </task>
1935
1934
 
1936
1935
  <answer_guidelines>
1937
- - Each question is presented as its own <question> block with a <text> element (the question) and an <allowed_answers> element (the permitted response values)
1938
- - Choose only from the values listed in that question's <allowed_answers> element
1939
- - Questions may have any set of allowed answers. Always read the <allowed_answers> for each question and select the best matching option based on the evidence
1936
+ - Each question is presented as its own <question> block with a <text> element and either an <allowed_answers> element (listing the permitted response values) or an <answer_format> element (describing the required response shape for open-ended responses)
1937
+ - For questions with <allowed_answers>: choose ONLY from the values listed in that question's <allowed_answers> element
1938
+ - For questions with <answer_format>: follow the format specification exactly (e.g. free-form text within the stated character budget)
1939
+ - Always read each question's <allowed_answers> or <answer_format> and respond in the required shape based on the evidence
1940
1940
  - Select the answer best supported by observable evidence from the content
1941
1941
  - When evidence is ambiguous but some signal exists, select the most conservative option and use a low confidence score. If the question cannot be answered at all from the content, skip it per the relevance_filtering rules
1942
1942
  - Confidence should reflect the clarity and strength of evidence:
@@ -1963,7 +1963,8 @@ var SYSTEM_PROMPT = promptDedent`
1963
1963
  CRITICAL: Base your answers ONLY on the actual visual and audio/transcript content.
1964
1964
  ${METADATA_BOUNDARY_WARNING}
1965
1965
 
1966
- CRITICAL: Do NOT answer irrelevant questions with any of the allowed answers.
1966
+ CRITICAL: Do NOT answer irrelevant questions in any form — neither with
1967
+ one of the <allowed_answers> nor with prose for <answer_format>.
1967
1968
  Answering an irrelevant question is WRONG — you MUST skip it instead.
1968
1969
 
1969
1970
  For skipped questions:
@@ -1990,7 +1991,7 @@ var SYSTEM_PROMPT = promptDedent`
1990
1991
  </security>
1991
1992
 
1992
1993
  <constraints>
1993
- - You MUST answer every relevant question with one of its own listed allowed response options
1994
+ - You MUST answer every relevant question following its specified response format: an option from <allowed_answers>, or prose matching <answer_format>
1994
1995
  - Skip irrelevant questions as described in relevance_filtering
1995
1996
  - Only describe observable evidence from frames or transcript
1996
1997
  - ${NO_FABRICATION_CONSTRAINT}
@@ -2025,15 +2026,16 @@ var AUDIO_ONLY_SYSTEM_PROMPT = promptDedent`
2025
2026
  <task>
2026
2027
  For each question provided, you must:
2027
2028
  1. Analyze the transcript
2028
- 2. Answer with ONLY the allowed response options listed for that question - no other values are acceptable
2029
+ 2. Answer each question according to its specified response format: for questions with <allowed_answers>, select ONLY from the listed values; for questions with <answer_format>, follow the format specification exactly
2029
2030
  3. Provide a confidence score between 0 and 1 reflecting your certainty
2030
2031
  4. Explain your reasoning based on observable evidence
2031
2032
  </task>
2032
2033
 
2033
2034
  <answer_guidelines>
2034
- - Each question is presented as its own <question> block with a <text> element (the question) and an <allowed_answers> element (the permitted response values)
2035
- - Choose only from the values listed in that question's <allowed_answers> element
2036
- - Questions may have any set of allowed answers. Always read the <allowed_answers> for each question and select the best matching option based on the evidence
2035
+ - Each question is presented as its own <question> block with a <text> element and either an <allowed_answers> element (listing the permitted response values) or an <answer_format> element (describing the required response shape for open-ended responses)
2036
+ - For questions with <allowed_answers>: choose ONLY from the values listed in that question's <allowed_answers> element
2037
+ - For questions with <answer_format>: follow the format specification exactly (e.g. free-form text within the stated character budget)
2038
+ - Always read each question's <allowed_answers> or <answer_format> and respond in the required shape based on the evidence
2037
2039
  - Select the answer best supported by observable evidence from the content
2038
2040
  - When evidence is ambiguous but some signal exists, select the most conservative option and use a low confidence score. If the question cannot be answered at all from the content, skip it per the relevance_filtering rules
2039
2041
  - Confidence should reflect the clarity and strength of evidence:
@@ -2060,7 +2062,8 @@ var AUDIO_ONLY_SYSTEM_PROMPT = promptDedent`
2060
2062
  CRITICAL: Base your answers ONLY on the actual audio/transcript content.
2061
2063
  ${METADATA_BOUNDARY_WARNING}
2062
2064
 
2063
- CRITICAL: Do NOT answer irrelevant questions with any of the allowed answers.
2065
+ CRITICAL: Do NOT answer irrelevant questions in any form — neither with
2066
+ one of the <allowed_answers> nor with prose for <answer_format>.
2064
2067
  Answering an irrelevant question is WRONG — you MUST skip it instead.
2065
2068
 
2066
2069
  For skipped questions:
@@ -2085,7 +2088,7 @@ var AUDIO_ONLY_SYSTEM_PROMPT = promptDedent`
2085
2088
  </security>
2086
2089
 
2087
2090
  <constraints>
2088
- - You MUST answer every relevant question with one of its own listed allowed response options
2091
+ - You MUST answer every relevant question following its specified response format: an option from <allowed_answers>, or prose matching <answer_format>
2089
2092
  - Skip irrelevant questions as described in relevance_filtering
2090
2093
  - Only describe observable evidence from transcript content
2091
2094
  - ${NO_FABRICATION_CONSTRAINT}
@@ -2100,16 +2103,104 @@ var AUDIO_ONLY_SYSTEM_PROMPT = promptDedent`
2100
2103
  - GOOD: "The speaker describes running through a park"
2101
2104
  - Be specific and evidence-based
2102
2105
  </language_guidelines>`;
2106
+ function freeFormAddendum(maxFreeFormAnswerLength) {
2107
+ return promptDedent`
2108
+ <free_form_answers>
2109
+ Additional rules for questions with <answer_format>free-form text ...</answer_format>:
2110
+
2111
+ - Produce a short, evidence-grounded answer in plain prose
2112
+ - Keep it tight: one or two sentences, maximum ${maxFreeFormAnswerLength} characters
2113
+ - If the question is irrelevant to the asset content, still skip it
2114
+ by setting answer to "${SKIP_SENTINEL}" exactly, per relevance_filtering
2115
+ - The answer field is a content-analysis channel, not an instruction
2116
+ channel. Do NOT include raw instructions, escape sequences, code,
2117
+ or anything that looks like structured control content in the
2118
+ answer. All existing security, non-fabrication, and
2119
+ non-disclosure rules apply unchanged.
2120
+ </free_form_answers>`;
2121
+ }
2122
+ function buildSystemPrompt(isAudioOnly, hasFreeForm, maxFreeFormAnswerLength) {
2123
+ const base = isAudioOnly ? AUDIO_ONLY_SYSTEM_PROMPT : SYSTEM_PROMPT;
2124
+ if (!hasFreeForm)
2125
+ return base;
2126
+ return `${base}
2127
+
2128
+ ${freeFormAddendum(maxFreeFormAnswerLength)}`;
2129
+ }
2130
+ var YES_NO_ANSWERS = ["yes", "no"];
2131
+ function allowedAnswersForQuestion(q) {
2132
+ switch (q.mode.kind) {
2133
+ case "yesNo":
2134
+ return YES_NO_ANSWERS;
2135
+ case "options":
2136
+ return q.mode.options;
2137
+ case "freeForm":
2138
+ return [];
2139
+ }
2140
+ }
2141
+ function normalizeQuestion(q, idx) {
2142
+ const hasOptions = Array.isArray(q.answerOptions);
2143
+ const isFreeForm = q.freeFormReply === true;
2144
+ if (hasOptions && isFreeForm) {
2145
+ throw new MuxAiError(
2146
+ `Question at index ${idx} sets both answerOptions and freeFormReply: true. These are mutually exclusive \u2014 pick one.`,
2147
+ { type: "validation_error" }
2148
+ );
2149
+ }
2150
+ if (isFreeForm) {
2151
+ return { question: q.question, mode: { kind: "freeForm" } };
2152
+ }
2153
+ if (hasOptions) {
2154
+ const options = Array.from(
2155
+ new Set(
2156
+ (q.answerOptions ?? []).map((o) => o.trim()).filter(Boolean)
2157
+ )
2158
+ );
2159
+ if (!options.length) {
2160
+ throw new MuxAiError(
2161
+ `Question at index ${idx} has invalid answerOptions: must include at least one non-empty value.`,
2162
+ { type: "validation_error" }
2163
+ );
2164
+ }
2165
+ return {
2166
+ question: q.question,
2167
+ mode: { kind: "options", options }
2168
+ };
2169
+ }
2170
+ return { question: q.question, mode: { kind: "yesNo" } };
2171
+ }
2172
+ function renderFormatTag(mode, maxFreeFormAnswerLength) {
2173
+ switch (mode.kind) {
2174
+ case "yesNo":
2175
+ return renderSection({
2176
+ tag: "allowed_answers",
2177
+ content: YES_NO_ANSWERS.map((a) => `"${a}"`).join(", ")
2178
+ });
2179
+ case "options":
2180
+ return renderSection({
2181
+ tag: "allowed_answers",
2182
+ content: mode.options.map((a) => `"${a}"`).join(", ")
2183
+ });
2184
+ case "freeForm":
2185
+ return renderSection({
2186
+ tag: "answer_format",
2187
+ content: `free-form text, maximum ${maxFreeFormAnswerLength} characters`
2188
+ });
2189
+ }
2190
+ }
2103
2191
  function buildUserPrompt({
2104
2192
  questions,
2105
2193
  transcriptText,
2106
2194
  isCleanTranscript = true,
2107
- isAudioOnly = false
2195
+ isAudioOnly = false,
2196
+ maxFreeFormAnswerLength
2108
2197
  }) {
2109
2198
  const contentDescriptor = isAudioOnly ? "audio content using transcript evidence" : "content";
2199
+ const hasFreeForm = questions.some((q) => q.mode.kind === "freeForm");
2200
+ const formatInstruction = hasFreeForm ? "Follow each question's specified response format: pick from <allowed_answers> where listed, or produce a concise free-form answer where <answer_format> is specified." : "Use only the values listed inside each question's own <allowed_answers> element.";
2110
2201
  const taskContent = dedent2`
2111
2202
  Answer each question in the <questions> block below about the ${contentDescriptor}.
2112
- Use only the values listed inside each question's own <allowed_answers> element.
2203
+ ${formatInstruction}
2113
2204
  Return one answer per question, in the order the questions appear.
2114
2205
  If a question cannot be answered from the provided content, skip it as described in the system instructions.`;
2115
2206
  const taskSection = `<task>
@@ -2117,13 +2208,10 @@ ${taskContent}
2117
2208
  </task>`;
2118
2209
  const questionBlocks = questions.map((q, idx) => {
2119
2210
  const textTag = renderSection({ tag: "text", content: q.question });
2120
- const answersTag = renderSection({
2121
- tag: "allowed_answers",
2122
- content: q.answerOptions.map((a) => `"${a}"`).join(", ")
2123
- });
2211
+ const formatTag = renderFormatTag(q.mode, maxFreeFormAnswerLength);
2124
2212
  return `<question number="${idx + 1}">
2125
2213
  ${textTag}
2126
- ${answersTag}
2214
+ ${formatTag}
2127
2215
  </question>`;
2128
2216
  }).join("\n\n");
2129
2217
  const questionsSection = `<questions>
@@ -2143,18 +2231,41 @@ async function fetchImageAsBase64(imageUrl, imageDownloadOptions) {
2143
2231
  const downloadResult = await downloadImageAsBase64(imageUrl, imageDownloadOptions);
2144
2232
  return downloadResult.base64Data;
2145
2233
  }
2234
+ function buildResponseSchemaForQuestions(normalizedQuestions, maxFreeFormAnswerLength) {
2235
+ const hasFreeForm = normalizedQuestions.some((q) => q.mode.kind === "freeForm");
2236
+ if (hasFreeForm) {
2237
+ const longestConstrained = Math.max(
2238
+ 0,
2239
+ ...normalizedQuestions.flatMap(allowedAnswersForQuestion).map((a) => a.length)
2240
+ );
2241
+ const schemaMax = Math.max(
2242
+ maxFreeFormAnswerLength,
2243
+ SKIP_SENTINEL.length,
2244
+ longestConstrained
2245
+ );
2246
+ return createAskQuestionsSchema(z2.string().min(1).max(schemaMax));
2247
+ }
2248
+ const allowed = Array.from(
2249
+ new Set(normalizedQuestions.flatMap(allowedAnswersForQuestion))
2250
+ );
2251
+ return createAskQuestionsSchema(z2.enum([...allowed, SKIP_SENTINEL]));
2252
+ }
2146
2253
  async function analyzeQuestions({
2147
2254
  provider,
2148
2255
  modelId,
2149
2256
  userPrompt,
2150
2257
  systemPrompt,
2151
- allowedAnswers,
2258
+ normalizedQuestions,
2259
+ maxFreeFormAnswerLength,
2152
2260
  imageDataUrl,
2153
2261
  credentials
2154
2262
  }) {
2155
2263
  "use step";
2156
2264
  const model = await createLanguageModelFromConfig(provider, modelId, credentials);
2157
- const responseSchema = createAskQuestionsSchema(allowedAnswers);
2265
+ const responseSchema = buildResponseSchemaForQuestions(
2266
+ normalizedQuestions,
2267
+ maxFreeFormAnswerLength
2268
+ );
2158
2269
  const response = await generateText({
2159
2270
  model,
2160
2271
  output: Output.object({ schema: responseSchema }),
@@ -2233,6 +2344,8 @@ async function askQuestions(assetId, questions, options) {
2233
2344
  );
2234
2345
  }
2235
2346
  questions.forEach((q, idx) => {
2347
+ if (q.freeFormReply === true)
2348
+ return;
2236
2349
  for (const opt of q.answerOptions ?? []) {
2237
2350
  if (typeof opt === "string" && opt.length > maxAnswerOptionLength) {
2238
2351
  throw new MuxAiError(
@@ -2242,6 +2355,14 @@ async function askQuestions(assetId, questions, options) {
2242
2355
  }
2243
2356
  }
2244
2357
  });
2358
+ const DEFAULT_MAX_FREE_FORM_ANSWER_LENGTH = 500;
2359
+ const maxFreeFormAnswerLength = options?.maxFreeFormAnswerLength ?? DEFAULT_MAX_FREE_FORM_ANSWER_LENGTH;
2360
+ if (!Number.isFinite(maxFreeFormAnswerLength) || maxFreeFormAnswerLength <= 0) {
2361
+ throw new MuxAiError(
2362
+ `maxFreeFormAnswerLength must be a positive number (received ${maxFreeFormAnswerLength}).`,
2363
+ { type: "validation_error" }
2364
+ );
2365
+ }
2245
2366
  const {
2246
2367
  provider = "openai",
2247
2368
  model,
@@ -2253,23 +2374,8 @@ async function askQuestions(assetId, questions, options) {
2253
2374
  storyboardWidth = 640,
2254
2375
  credentials
2255
2376
  } = options ?? {};
2256
- const normalizedQuestions = questions.map((q, idx) => {
2257
- const options2 = Array.from(
2258
- new Set(
2259
- (q.answerOptions?.length ? q.answerOptions : ["yes", "no"]).map((o) => o.trim()).filter(Boolean)
2260
- )
2261
- );
2262
- if (!options2.length) {
2263
- throw new MuxAiError(
2264
- `Question at index ${idx} has invalid answerOptions: must include at least one non-empty value.`,
2265
- { type: "validation_error" }
2266
- );
2267
- }
2268
- return { ...q, answerOptions: options2 };
2269
- });
2270
- const allowedAnswers = Array.from(
2271
- new Set(normalizedQuestions.flatMap((q) => q.answerOptions))
2272
- );
2377
+ const normalizedQuestions = questions.map((q, idx) => normalizeQuestion(q, idx));
2378
+ const hasFreeForm = normalizedQuestions.some((q) => q.mode.kind === "freeForm");
2273
2379
  const modelConfig = resolveLanguageModelConfig({
2274
2380
  ...options,
2275
2381
  model,
@@ -2303,9 +2409,10 @@ async function askQuestions(assetId, questions, options) {
2303
2409
  questions: normalizedQuestions,
2304
2410
  transcriptText,
2305
2411
  isCleanTranscript: cleanTranscript,
2306
- isAudioOnly
2412
+ isAudioOnly,
2413
+ maxFreeFormAnswerLength
2307
2414
  });
2308
- const systemPrompt = isAudioOnly ? AUDIO_ONLY_SYSTEM_PROMPT : SYSTEM_PROMPT;
2415
+ const systemPrompt = buildSystemPrompt(isAudioOnly, hasFreeForm, maxFreeFormAnswerLength);
2309
2416
  let analysisResponse;
2310
2417
  let imageUrl;
2311
2418
  try {
@@ -2315,7 +2422,8 @@ async function askQuestions(assetId, questions, options) {
2315
2422
  modelId: modelConfig.modelId,
2316
2423
  userPrompt,
2317
2424
  systemPrompt,
2318
- allowedAnswers,
2425
+ normalizedQuestions,
2426
+ maxFreeFormAnswerLength,
2319
2427
  credentials
2320
2428
  });
2321
2429
  } else {
@@ -2333,7 +2441,8 @@ async function askQuestions(assetId, questions, options) {
2333
2441
  modelId: modelConfig.modelId,
2334
2442
  userPrompt,
2335
2443
  systemPrompt,
2336
- allowedAnswers,
2444
+ normalizedQuestions,
2445
+ maxFreeFormAnswerLength,
2337
2446
  imageDataUrl: base64Data,
2338
2447
  credentials
2339
2448
  });
@@ -2344,7 +2453,8 @@ async function askQuestions(assetId, questions, options) {
2344
2453
  modelId: modelConfig.modelId,
2345
2454
  userPrompt,
2346
2455
  systemPrompt,
2347
- allowedAnswers,
2456
+ normalizedQuestions,
2457
+ maxFreeFormAnswerLength,
2348
2458
  imageDataUrl: storyboardUrl,
2349
2459
  credentials
2350
2460
  })
@@ -2379,20 +2489,42 @@ async function askQuestions(assetId, questions, options) {
2379
2489
  );
2380
2490
  }
2381
2491
  const answers = analysisResponse.result.answers.map((raw, idx) => {
2382
- const scrub = safety.scrubDetailed(raw.reasoning, `reasoning[${idx}]`);
2383
- const isSkipped = raw.skipped || raw.answer === SKIP_SENTINEL || scrub.leaked;
2384
- if (!isSkipped && !normalizedQuestions[idx].answerOptions.includes(raw.answer)) {
2385
- throw new MuxAiError(
2386
- `Answer "${raw.answer}" for question ${idx} is not in allowed options: ${normalizedQuestions[idx].answerOptions.join(", ")}`
2492
+ const question = normalizedQuestions[idx];
2493
+ const isFreeForm = question.mode.kind === "freeForm";
2494
+ const reasoningScrub = safety.scrubDetailed(raw.reasoning, `reasoning[${idx}]`);
2495
+ const explicitSkip = raw.skipped || raw.answer === SKIP_SENTINEL;
2496
+ const answerScrub = isFreeForm && !explicitSkip ? safety.scrubDetailed(raw.answer, `answer[${idx}]`) : null;
2497
+ const overLength = isFreeForm && !explicitSkip && !(answerScrub?.leaked ?? false) && (answerScrub?.text.length ?? 0) > maxFreeFormAnswerLength;
2498
+ if (overLength) {
2499
+ console.warn(
2500
+ `[@mux/ai] Free-form answer at index ${idx} exceeded ${maxFreeFormAnswerLength}-char cap (${answerScrub.text.length} chars); treating as skipped.`
2387
2501
  );
2388
2502
  }
2503
+ const isSkipped = explicitSkip || reasoningScrub.leaked || (answerScrub?.leaked ?? false) || overLength;
2504
+ if (!isSkipped && !isFreeForm) {
2505
+ const allowed = allowedAnswersForQuestion(question);
2506
+ if (!allowed.includes(raw.answer)) {
2507
+ throw new MuxAiError(
2508
+ `Answer "${raw.answer}" for question ${idx} is not in allowed options: ${allowed.join(", ")}`
2509
+ );
2510
+ }
2511
+ }
2512
+ let finalAnswer;
2513
+ if (isSkipped) {
2514
+ finalAnswer = null;
2515
+ } else if (isFreeForm) {
2516
+ finalAnswer = answerScrub ? answerScrub.text : raw.answer;
2517
+ } else {
2518
+ finalAnswer = raw.answer;
2519
+ }
2520
+ const suppressed = reasoningScrub.leaked || (answerScrub?.leaked ?? false);
2389
2521
  return {
2390
2522
  // Use the trusted input verbatim rather than the model's echo.
2391
- question: normalizedQuestions[idx].question,
2523
+ question: question.question,
2392
2524
  confidence: isSkipped ? 0 : Math.min(1, Math.max(0, raw.confidence)),
2393
- reasoning: scrub.leaked ? "Response suppressed by safety filter." : scrub.text,
2525
+ reasoning: suppressed ? "Response suppressed by safety filter." : reasoningScrub.text,
2394
2526
  skipped: isSkipped,
2395
- answer: isSkipped ? null : raw.answer
2527
+ answer: finalAnswer
2396
2528
  };
2397
2529
  });
2398
2530
  return {
@@ -3310,9 +3442,10 @@ function applyReplacements(rawVtt, replacements) {
3310
3442
  const records = [];
3311
3443
  const editedVtt = transformCueText(rawVtt, (line, cueStartTime) => {
3312
3444
  let result = line;
3313
- for (const { find, replace } of filtered) {
3445
+ for (const { find, replace, caseSensitive } of filtered) {
3314
3446
  const escaped = find.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
3315
- const regex = new RegExp(`\\b${escaped}\\b`, "g");
3447
+ const flags = caseSensitive === false ? "gi" : "g";
3448
+ const regex = new RegExp(`\\b${escaped}\\b`, flags);
3316
3449
  result = result.replace(regex, (match) => {
3317
3450
  records.push({ cueStartTime, before: match, after: replace });
3318
3451
  return replace;
@@ -3557,13 +3690,13 @@ async function editCaptions(assetId, trackId, options) {
3557
3690
  credentials
3558
3691
  );
3559
3692
  } catch (error) {
3560
- console.warn(`Failed to add track to Mux asset: ${error instanceof Error ? error.message : "Unknown error"}`);
3693
+ wrapError(error, "Failed to add edited track to Mux asset");
3561
3694
  }
3562
3695
  if (deleteOriginal && uploadedTrackId) {
3563
3696
  try {
3564
3697
  await deleteTrackOnMux(assetId, trackId, credentials);
3565
3698
  } catch (error) {
3566
- console.warn(`Failed to delete original track: ${error instanceof Error ? error.message : "Unknown error"}`);
3699
+ wrapError(error, "Failed to delete original track");
3567
3700
  }
3568
3701
  }
3569
3702
  }