@mux/ai 0.19.0 → 0.21.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/{index-CwzNfBjV.d.ts → index-DwNtsg-u.d.ts} +34 -6
- package/dist/{index-Drj9Cy_k.d.ts → index-bjQi1ZCj.d.ts} +1 -1
- package/dist/index.d.ts +5 -5
- package/dist/index.js +316 -66
- package/dist/index.js.map +1 -1
- package/dist/primitives/index.d.ts +2 -2
- package/dist/primitives/index.js +1 -0
- package/dist/primitives/index.js.map +1 -1
- package/dist/{shots-BFYOfBgN.d.ts → shots-mGJfYGiO.d.ts} +2 -0
- package/dist/workflows/index.d.ts +2 -2
- package/dist/workflows/index.js +315 -65
- package/dist/workflows/index.js.map +1 -1
- package/package.json +3 -3
|
@@ -120,6 +120,8 @@ interface WorkflowCredentials {
|
|
|
120
120
|
anthropicApiKey?: string;
|
|
121
121
|
/** Optional direct Google API key for per-request credential injection. */
|
|
122
122
|
googleApiKey?: string;
|
|
123
|
+
/** Optional direct Google Vision API key for per-request credential injection. */
|
|
124
|
+
googleVisionApiKey?: string;
|
|
123
125
|
/** Optional direct Hive API key for per-request credential injection. */
|
|
124
126
|
hiveApiKey?: string;
|
|
125
127
|
/** Optional direct ElevenLabs API key for per-request credential injection. */
|
|
@@ -1,7 +1,7 @@
|
|
|
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,
|
|
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, G as GOOGLE_VISION_LIKELIHOOD_TO_SCORE, H as HIVE_SEXUAL_CATEGORIES, z as HIVE_VIOLENCE_CATEGORIES, F as HiveModerationOutput, I as HiveModerationSource, M as ModerationOptions, J as ModerationProvider, K as ModerationResult, L as MomentInsight, O as OverallInsight, P as ProfanityDetectionPayload, Q as Question, N as QuestionAnswer, R as QuestionAnswerType, S as ReplacementRecord, T as SummarizationOptions, U as SummarizationPromptOverrides, V as SummarizationPromptSections, W as SummaryAndTagsResult, X as SummaryType, Y as ThumbnailModerationScore, Z as TranslationChunkingOptions, _ as TranslationOptions, $ as TranslationPayload, a0 as TranslationResult, a1 as aggregateTokenUsage, a2 as applyOverrideLists, a3 as applyReplacements, a4 as askQuestions, a5 as buildReplacementRegex, a6 as burnedInCaptionsSchema, a7 as censorVttContent, a8 as chapterSchema, a9 as chaptersSchema, aa as createReplacer, ab as editCaptions, ac as extractTranscriptSegmentsForHotspots, ad as generateChapters, ae as generateEmbeddings, af as generateEngagementInsights, ag as generateVideoEmbeddings, ah as getModerationScores, ai as getSummaryAndTags, aj as hasBurnedInCaptions, ak as mapShotsToHotspots, al as normalizeTranslatedVtt, am as profanityDetectionSchema, an as questionAnswerSchema, ao as shouldSplitChunkTranslationError, ap as summarySchema, aq as transformCueText, ar as translateAudio, as as translateCaptions, at as translationSchema } from '../index-DwNtsg-u.js';
|
|
2
2
|
import 'zod';
|
|
3
3
|
import '@ai-sdk/anthropic';
|
|
4
4
|
import '@ai-sdk/google';
|
|
5
5
|
import '@ai-sdk/openai';
|
|
6
|
-
import '../shots-
|
|
6
|
+
import '../shots-mGJfYGiO.js';
|
|
7
7
|
import '@mux/mux-node';
|
package/dist/workflows/index.js
CHANGED
|
@@ -186,6 +186,7 @@ var EnvSchema = z.object({
|
|
|
186
186
|
GOOGLE_GENERATIVE_AI_API_KEY: optionalString("Google Generative AI API key for Gemini-backed workflows.", "Google Generative AI API key"),
|
|
187
187
|
ELEVENLABS_API_KEY: optionalString("ElevenLabs API key for audio translation.", "ElevenLabs API key"),
|
|
188
188
|
HIVE_API_KEY: optionalString("Hive Visual Moderation API key.", "Hive API key"),
|
|
189
|
+
GOOGLE_VISION_API_KEY: optionalString("Google Vision API key for SafeSearch moderation.", "Google Vision API key"),
|
|
189
190
|
// S3-Compatible Storage (required for translation & audio dubbing)
|
|
190
191
|
S3_ENDPOINT: optionalString("S3-compatible endpoint for uploads.", "S3 endpoint"),
|
|
191
192
|
S3_REGION: optionalString("S3 region (defaults to 'auto' when omitted)."),
|
|
@@ -809,23 +810,26 @@ function resolveProviderApiKeyFromCredentials(provider, resolved) {
|
|
|
809
810
|
const openaiApiKey = readString(record, "openaiApiKey");
|
|
810
811
|
const anthropicApiKey = readString(record, "anthropicApiKey");
|
|
811
812
|
const googleApiKey = readString(record, "googleApiKey");
|
|
813
|
+
const googleVisionApiKey = readString(record, "googleVisionApiKey");
|
|
812
814
|
const hiveApiKey = readString(record, "hiveApiKey");
|
|
813
815
|
const elevenLabsApiKey = readString(record, "elevenLabsApiKey");
|
|
814
816
|
const apiKeyMap = {
|
|
815
|
-
openai: openaiApiKey ?? env_default.OPENAI_API_KEY,
|
|
816
|
-
anthropic: anthropicApiKey ?? env_default.ANTHROPIC_API_KEY,
|
|
817
|
-
google: googleApiKey ?? env_default.GOOGLE_GENERATIVE_AI_API_KEY,
|
|
818
|
-
|
|
819
|
-
|
|
817
|
+
"openai": openaiApiKey ?? env_default.OPENAI_API_KEY,
|
|
818
|
+
"anthropic": anthropicApiKey ?? env_default.ANTHROPIC_API_KEY,
|
|
819
|
+
"google": googleApiKey ?? env_default.GOOGLE_GENERATIVE_AI_API_KEY,
|
|
820
|
+
"google-vision-api": googleVisionApiKey ?? env_default.GOOGLE_VISION_API_KEY,
|
|
821
|
+
"hive": hiveApiKey ?? env_default.HIVE_API_KEY,
|
|
822
|
+
"elevenlabs": elevenLabsApiKey ?? env_default.ELEVENLABS_API_KEY
|
|
820
823
|
};
|
|
821
824
|
const apiKey = apiKeyMap[provider];
|
|
822
825
|
if (!apiKey) {
|
|
823
826
|
const envVarNames = {
|
|
824
|
-
openai: "OPENAI_API_KEY",
|
|
825
|
-
anthropic: "ANTHROPIC_API_KEY",
|
|
826
|
-
google: "GOOGLE_GENERATIVE_AI_API_KEY",
|
|
827
|
-
|
|
828
|
-
|
|
827
|
+
"openai": "OPENAI_API_KEY",
|
|
828
|
+
"anthropic": "ANTHROPIC_API_KEY",
|
|
829
|
+
"google": "GOOGLE_GENERATIVE_AI_API_KEY",
|
|
830
|
+
"google-vision-api": "GOOGLE_VISION_API_KEY",
|
|
831
|
+
"hive": "HIVE_API_KEY",
|
|
832
|
+
"elevenlabs": "ELEVENLABS_API_KEY"
|
|
829
833
|
};
|
|
830
834
|
throw new Error(
|
|
831
835
|
`${provider} API key is required. Provide ${provider} credentials via workflow credentials or set ${envVarNames[provider]} environment variable.`
|
|
@@ -1891,8 +1895,7 @@ var questionAnswerSchema = z2.object({
|
|
|
1891
1895
|
skipped: z2.boolean()
|
|
1892
1896
|
});
|
|
1893
1897
|
var SKIP_SENTINEL = "__SKIPPED__";
|
|
1894
|
-
function createAskQuestionsSchema(
|
|
1895
|
-
const answerSchema = z2.enum([...allowedAnswers, SKIP_SENTINEL]);
|
|
1898
|
+
function createAskQuestionsSchema(answerSchema) {
|
|
1896
1899
|
return z2.object({
|
|
1897
1900
|
answers: z2.array(
|
|
1898
1901
|
questionAnswerSchema.extend({
|
|
@@ -1928,15 +1931,16 @@ var SYSTEM_PROMPT = promptDedent`
|
|
|
1928
1931
|
<task>
|
|
1929
1932
|
For each question provided, you must:
|
|
1930
1933
|
1. Analyze the storyboard frames and transcript (if provided)
|
|
1931
|
-
2. Answer
|
|
1934
|
+
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
1935
|
3. Provide a confidence score between 0 and 1 reflecting your certainty
|
|
1933
1936
|
4. Explain your reasoning based on observable evidence
|
|
1934
1937
|
</task>
|
|
1935
1938
|
|
|
1936
1939
|
<answer_guidelines>
|
|
1937
|
-
- Each question is presented as its own <question> block with a <text> element (the
|
|
1938
|
-
-
|
|
1939
|
-
-
|
|
1940
|
+
- 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)
|
|
1941
|
+
- For questions with <allowed_answers>: choose ONLY from the values listed in that question's <allowed_answers> element
|
|
1942
|
+
- For questions with <answer_format>: follow the format specification exactly (e.g. free-form text within the stated character budget)
|
|
1943
|
+
- Always read each question's <allowed_answers> or <answer_format> and respond in the required shape based on the evidence
|
|
1940
1944
|
- Select the answer best supported by observable evidence from the content
|
|
1941
1945
|
- 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
1946
|
- Confidence should reflect the clarity and strength of evidence:
|
|
@@ -1963,7 +1967,8 @@ var SYSTEM_PROMPT = promptDedent`
|
|
|
1963
1967
|
CRITICAL: Base your answers ONLY on the actual visual and audio/transcript content.
|
|
1964
1968
|
${METADATA_BOUNDARY_WARNING}
|
|
1965
1969
|
|
|
1966
|
-
CRITICAL: Do NOT answer irrelevant questions
|
|
1970
|
+
CRITICAL: Do NOT answer irrelevant questions in any form — neither with
|
|
1971
|
+
one of the <allowed_answers> nor with prose for <answer_format>.
|
|
1967
1972
|
Answering an irrelevant question is WRONG — you MUST skip it instead.
|
|
1968
1973
|
|
|
1969
1974
|
For skipped questions:
|
|
@@ -1990,7 +1995,7 @@ var SYSTEM_PROMPT = promptDedent`
|
|
|
1990
1995
|
</security>
|
|
1991
1996
|
|
|
1992
1997
|
<constraints>
|
|
1993
|
-
- You MUST answer every relevant question
|
|
1998
|
+
- You MUST answer every relevant question following its specified response format: an option from <allowed_answers>, or prose matching <answer_format>
|
|
1994
1999
|
- Skip irrelevant questions as described in relevance_filtering
|
|
1995
2000
|
- Only describe observable evidence from frames or transcript
|
|
1996
2001
|
- ${NO_FABRICATION_CONSTRAINT}
|
|
@@ -2025,15 +2030,16 @@ var AUDIO_ONLY_SYSTEM_PROMPT = promptDedent`
|
|
|
2025
2030
|
<task>
|
|
2026
2031
|
For each question provided, you must:
|
|
2027
2032
|
1. Analyze the transcript
|
|
2028
|
-
2. Answer
|
|
2033
|
+
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
2034
|
3. Provide a confidence score between 0 and 1 reflecting your certainty
|
|
2030
2035
|
4. Explain your reasoning based on observable evidence
|
|
2031
2036
|
</task>
|
|
2032
2037
|
|
|
2033
2038
|
<answer_guidelines>
|
|
2034
|
-
- Each question is presented as its own <question> block with a <text> element (the
|
|
2035
|
-
-
|
|
2036
|
-
-
|
|
2039
|
+
- 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)
|
|
2040
|
+
- For questions with <allowed_answers>: choose ONLY from the values listed in that question's <allowed_answers> element
|
|
2041
|
+
- For questions with <answer_format>: follow the format specification exactly (e.g. free-form text within the stated character budget)
|
|
2042
|
+
- Always read each question's <allowed_answers> or <answer_format> and respond in the required shape based on the evidence
|
|
2037
2043
|
- Select the answer best supported by observable evidence from the content
|
|
2038
2044
|
- 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
2045
|
- Confidence should reflect the clarity and strength of evidence:
|
|
@@ -2060,7 +2066,8 @@ var AUDIO_ONLY_SYSTEM_PROMPT = promptDedent`
|
|
|
2060
2066
|
CRITICAL: Base your answers ONLY on the actual audio/transcript content.
|
|
2061
2067
|
${METADATA_BOUNDARY_WARNING}
|
|
2062
2068
|
|
|
2063
|
-
CRITICAL: Do NOT answer irrelevant questions
|
|
2069
|
+
CRITICAL: Do NOT answer irrelevant questions in any form — neither with
|
|
2070
|
+
one of the <allowed_answers> nor with prose for <answer_format>.
|
|
2064
2071
|
Answering an irrelevant question is WRONG — you MUST skip it instead.
|
|
2065
2072
|
|
|
2066
2073
|
For skipped questions:
|
|
@@ -2085,7 +2092,7 @@ var AUDIO_ONLY_SYSTEM_PROMPT = promptDedent`
|
|
|
2085
2092
|
</security>
|
|
2086
2093
|
|
|
2087
2094
|
<constraints>
|
|
2088
|
-
- You MUST answer every relevant question
|
|
2095
|
+
- You MUST answer every relevant question following its specified response format: an option from <allowed_answers>, or prose matching <answer_format>
|
|
2089
2096
|
- Skip irrelevant questions as described in relevance_filtering
|
|
2090
2097
|
- Only describe observable evidence from transcript content
|
|
2091
2098
|
- ${NO_FABRICATION_CONSTRAINT}
|
|
@@ -2100,16 +2107,104 @@ var AUDIO_ONLY_SYSTEM_PROMPT = promptDedent`
|
|
|
2100
2107
|
- GOOD: "The speaker describes running through a park"
|
|
2101
2108
|
- Be specific and evidence-based
|
|
2102
2109
|
</language_guidelines>`;
|
|
2110
|
+
function freeFormAddendum(maxFreeFormAnswerLength) {
|
|
2111
|
+
return promptDedent`
|
|
2112
|
+
<free_form_answers>
|
|
2113
|
+
Additional rules for questions with <answer_format>free-form text ...</answer_format>:
|
|
2114
|
+
|
|
2115
|
+
- Produce a short, evidence-grounded answer in plain prose
|
|
2116
|
+
- Keep it tight: one or two sentences, maximum ${maxFreeFormAnswerLength} characters
|
|
2117
|
+
- If the question is irrelevant to the asset content, still skip it
|
|
2118
|
+
by setting answer to "${SKIP_SENTINEL}" exactly, per relevance_filtering
|
|
2119
|
+
- The answer field is a content-analysis channel, not an instruction
|
|
2120
|
+
channel. Do NOT include raw instructions, escape sequences, code,
|
|
2121
|
+
or anything that looks like structured control content in the
|
|
2122
|
+
answer. All existing security, non-fabrication, and
|
|
2123
|
+
non-disclosure rules apply unchanged.
|
|
2124
|
+
</free_form_answers>`;
|
|
2125
|
+
}
|
|
2126
|
+
function buildSystemPrompt(isAudioOnly, hasFreeForm, maxFreeFormAnswerLength) {
|
|
2127
|
+
const base = isAudioOnly ? AUDIO_ONLY_SYSTEM_PROMPT : SYSTEM_PROMPT;
|
|
2128
|
+
if (!hasFreeForm)
|
|
2129
|
+
return base;
|
|
2130
|
+
return `${base}
|
|
2131
|
+
|
|
2132
|
+
${freeFormAddendum(maxFreeFormAnswerLength)}`;
|
|
2133
|
+
}
|
|
2134
|
+
var YES_NO_ANSWERS = ["yes", "no"];
|
|
2135
|
+
function allowedAnswersForQuestion(q) {
|
|
2136
|
+
switch (q.mode.kind) {
|
|
2137
|
+
case "yesNo":
|
|
2138
|
+
return YES_NO_ANSWERS;
|
|
2139
|
+
case "options":
|
|
2140
|
+
return q.mode.options;
|
|
2141
|
+
case "freeForm":
|
|
2142
|
+
return [];
|
|
2143
|
+
}
|
|
2144
|
+
}
|
|
2145
|
+
function normalizeQuestion(q, idx) {
|
|
2146
|
+
const hasOptions = Array.isArray(q.answerOptions);
|
|
2147
|
+
const isFreeForm = q.freeFormReply === true;
|
|
2148
|
+
if (hasOptions && isFreeForm) {
|
|
2149
|
+
throw new MuxAiError(
|
|
2150
|
+
`Question at index ${idx} sets both answerOptions and freeFormReply: true. These are mutually exclusive \u2014 pick one.`,
|
|
2151
|
+
{ type: "validation_error" }
|
|
2152
|
+
);
|
|
2153
|
+
}
|
|
2154
|
+
if (isFreeForm) {
|
|
2155
|
+
return { question: q.question, mode: { kind: "freeForm" } };
|
|
2156
|
+
}
|
|
2157
|
+
if (hasOptions) {
|
|
2158
|
+
const options = Array.from(
|
|
2159
|
+
new Set(
|
|
2160
|
+
(q.answerOptions ?? []).map((o) => o.trim()).filter(Boolean)
|
|
2161
|
+
)
|
|
2162
|
+
);
|
|
2163
|
+
if (!options.length) {
|
|
2164
|
+
throw new MuxAiError(
|
|
2165
|
+
`Question at index ${idx} has invalid answerOptions: must include at least one non-empty value.`,
|
|
2166
|
+
{ type: "validation_error" }
|
|
2167
|
+
);
|
|
2168
|
+
}
|
|
2169
|
+
return {
|
|
2170
|
+
question: q.question,
|
|
2171
|
+
mode: { kind: "options", options }
|
|
2172
|
+
};
|
|
2173
|
+
}
|
|
2174
|
+
return { question: q.question, mode: { kind: "yesNo" } };
|
|
2175
|
+
}
|
|
2176
|
+
function renderFormatTag(mode, maxFreeFormAnswerLength) {
|
|
2177
|
+
switch (mode.kind) {
|
|
2178
|
+
case "yesNo":
|
|
2179
|
+
return renderSection({
|
|
2180
|
+
tag: "allowed_answers",
|
|
2181
|
+
content: YES_NO_ANSWERS.map((a) => `"${a}"`).join(", ")
|
|
2182
|
+
});
|
|
2183
|
+
case "options":
|
|
2184
|
+
return renderSection({
|
|
2185
|
+
tag: "allowed_answers",
|
|
2186
|
+
content: mode.options.map((a) => `"${a}"`).join(", ")
|
|
2187
|
+
});
|
|
2188
|
+
case "freeForm":
|
|
2189
|
+
return renderSection({
|
|
2190
|
+
tag: "answer_format",
|
|
2191
|
+
content: `free-form text, maximum ${maxFreeFormAnswerLength} characters`
|
|
2192
|
+
});
|
|
2193
|
+
}
|
|
2194
|
+
}
|
|
2103
2195
|
function buildUserPrompt({
|
|
2104
2196
|
questions,
|
|
2105
2197
|
transcriptText,
|
|
2106
2198
|
isCleanTranscript = true,
|
|
2107
|
-
isAudioOnly = false
|
|
2199
|
+
isAudioOnly = false,
|
|
2200
|
+
maxFreeFormAnswerLength
|
|
2108
2201
|
}) {
|
|
2109
2202
|
const contentDescriptor = isAudioOnly ? "audio content using transcript evidence" : "content";
|
|
2203
|
+
const hasFreeForm = questions.some((q) => q.mode.kind === "freeForm");
|
|
2204
|
+
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
2205
|
const taskContent = dedent2`
|
|
2111
2206
|
Answer each question in the <questions> block below about the ${contentDescriptor}.
|
|
2112
|
-
|
|
2207
|
+
${formatInstruction}
|
|
2113
2208
|
Return one answer per question, in the order the questions appear.
|
|
2114
2209
|
If a question cannot be answered from the provided content, skip it as described in the system instructions.`;
|
|
2115
2210
|
const taskSection = `<task>
|
|
@@ -2117,13 +2212,10 @@ ${taskContent}
|
|
|
2117
2212
|
</task>`;
|
|
2118
2213
|
const questionBlocks = questions.map((q, idx) => {
|
|
2119
2214
|
const textTag = renderSection({ tag: "text", content: q.question });
|
|
2120
|
-
const
|
|
2121
|
-
tag: "allowed_answers",
|
|
2122
|
-
content: q.answerOptions.map((a) => `"${a}"`).join(", ")
|
|
2123
|
-
});
|
|
2215
|
+
const formatTag = renderFormatTag(q.mode, maxFreeFormAnswerLength);
|
|
2124
2216
|
return `<question number="${idx + 1}">
|
|
2125
2217
|
${textTag}
|
|
2126
|
-
${
|
|
2218
|
+
${formatTag}
|
|
2127
2219
|
</question>`;
|
|
2128
2220
|
}).join("\n\n");
|
|
2129
2221
|
const questionsSection = `<questions>
|
|
@@ -2143,18 +2235,41 @@ async function fetchImageAsBase64(imageUrl, imageDownloadOptions) {
|
|
|
2143
2235
|
const downloadResult = await downloadImageAsBase64(imageUrl, imageDownloadOptions);
|
|
2144
2236
|
return downloadResult.base64Data;
|
|
2145
2237
|
}
|
|
2238
|
+
function buildResponseSchemaForQuestions(normalizedQuestions, maxFreeFormAnswerLength) {
|
|
2239
|
+
const hasFreeForm = normalizedQuestions.some((q) => q.mode.kind === "freeForm");
|
|
2240
|
+
if (hasFreeForm) {
|
|
2241
|
+
const longestConstrained = Math.max(
|
|
2242
|
+
0,
|
|
2243
|
+
...normalizedQuestions.flatMap(allowedAnswersForQuestion).map((a) => a.length)
|
|
2244
|
+
);
|
|
2245
|
+
const schemaMax = Math.max(
|
|
2246
|
+
maxFreeFormAnswerLength,
|
|
2247
|
+
SKIP_SENTINEL.length,
|
|
2248
|
+
longestConstrained
|
|
2249
|
+
);
|
|
2250
|
+
return createAskQuestionsSchema(z2.string().min(1).max(schemaMax));
|
|
2251
|
+
}
|
|
2252
|
+
const allowed = Array.from(
|
|
2253
|
+
new Set(normalizedQuestions.flatMap(allowedAnswersForQuestion))
|
|
2254
|
+
);
|
|
2255
|
+
return createAskQuestionsSchema(z2.enum([...allowed, SKIP_SENTINEL]));
|
|
2256
|
+
}
|
|
2146
2257
|
async function analyzeQuestions({
|
|
2147
2258
|
provider,
|
|
2148
2259
|
modelId,
|
|
2149
2260
|
userPrompt,
|
|
2150
2261
|
systemPrompt,
|
|
2151
|
-
|
|
2262
|
+
normalizedQuestions,
|
|
2263
|
+
maxFreeFormAnswerLength,
|
|
2152
2264
|
imageDataUrl,
|
|
2153
2265
|
credentials
|
|
2154
2266
|
}) {
|
|
2155
2267
|
"use step";
|
|
2156
2268
|
const model = await createLanguageModelFromConfig(provider, modelId, credentials);
|
|
2157
|
-
const responseSchema =
|
|
2269
|
+
const responseSchema = buildResponseSchemaForQuestions(
|
|
2270
|
+
normalizedQuestions,
|
|
2271
|
+
maxFreeFormAnswerLength
|
|
2272
|
+
);
|
|
2158
2273
|
const response = await generateText({
|
|
2159
2274
|
model,
|
|
2160
2275
|
output: Output.object({ schema: responseSchema }),
|
|
@@ -2233,6 +2348,8 @@ async function askQuestions(assetId, questions, options) {
|
|
|
2233
2348
|
);
|
|
2234
2349
|
}
|
|
2235
2350
|
questions.forEach((q, idx) => {
|
|
2351
|
+
if (q.freeFormReply === true)
|
|
2352
|
+
return;
|
|
2236
2353
|
for (const opt of q.answerOptions ?? []) {
|
|
2237
2354
|
if (typeof opt === "string" && opt.length > maxAnswerOptionLength) {
|
|
2238
2355
|
throw new MuxAiError(
|
|
@@ -2242,6 +2359,14 @@ async function askQuestions(assetId, questions, options) {
|
|
|
2242
2359
|
}
|
|
2243
2360
|
}
|
|
2244
2361
|
});
|
|
2362
|
+
const DEFAULT_MAX_FREE_FORM_ANSWER_LENGTH = 500;
|
|
2363
|
+
const maxFreeFormAnswerLength = options?.maxFreeFormAnswerLength ?? DEFAULT_MAX_FREE_FORM_ANSWER_LENGTH;
|
|
2364
|
+
if (!Number.isFinite(maxFreeFormAnswerLength) || maxFreeFormAnswerLength <= 0) {
|
|
2365
|
+
throw new MuxAiError(
|
|
2366
|
+
`maxFreeFormAnswerLength must be a positive number (received ${maxFreeFormAnswerLength}).`,
|
|
2367
|
+
{ type: "validation_error" }
|
|
2368
|
+
);
|
|
2369
|
+
}
|
|
2245
2370
|
const {
|
|
2246
2371
|
provider = "openai",
|
|
2247
2372
|
model,
|
|
@@ -2253,23 +2378,8 @@ async function askQuestions(assetId, questions, options) {
|
|
|
2253
2378
|
storyboardWidth = 640,
|
|
2254
2379
|
credentials
|
|
2255
2380
|
} = options ?? {};
|
|
2256
|
-
const normalizedQuestions = questions.map((q, idx) =>
|
|
2257
|
-
|
|
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
|
-
);
|
|
2381
|
+
const normalizedQuestions = questions.map((q, idx) => normalizeQuestion(q, idx));
|
|
2382
|
+
const hasFreeForm = normalizedQuestions.some((q) => q.mode.kind === "freeForm");
|
|
2273
2383
|
const modelConfig = resolveLanguageModelConfig({
|
|
2274
2384
|
...options,
|
|
2275
2385
|
model,
|
|
@@ -2303,9 +2413,10 @@ async function askQuestions(assetId, questions, options) {
|
|
|
2303
2413
|
questions: normalizedQuestions,
|
|
2304
2414
|
transcriptText,
|
|
2305
2415
|
isCleanTranscript: cleanTranscript,
|
|
2306
|
-
isAudioOnly
|
|
2416
|
+
isAudioOnly,
|
|
2417
|
+
maxFreeFormAnswerLength
|
|
2307
2418
|
});
|
|
2308
|
-
const systemPrompt = isAudioOnly
|
|
2419
|
+
const systemPrompt = buildSystemPrompt(isAudioOnly, hasFreeForm, maxFreeFormAnswerLength);
|
|
2309
2420
|
let analysisResponse;
|
|
2310
2421
|
let imageUrl;
|
|
2311
2422
|
try {
|
|
@@ -2315,7 +2426,8 @@ async function askQuestions(assetId, questions, options) {
|
|
|
2315
2426
|
modelId: modelConfig.modelId,
|
|
2316
2427
|
userPrompt,
|
|
2317
2428
|
systemPrompt,
|
|
2318
|
-
|
|
2429
|
+
normalizedQuestions,
|
|
2430
|
+
maxFreeFormAnswerLength,
|
|
2319
2431
|
credentials
|
|
2320
2432
|
});
|
|
2321
2433
|
} else {
|
|
@@ -2333,7 +2445,8 @@ async function askQuestions(assetId, questions, options) {
|
|
|
2333
2445
|
modelId: modelConfig.modelId,
|
|
2334
2446
|
userPrompt,
|
|
2335
2447
|
systemPrompt,
|
|
2336
|
-
|
|
2448
|
+
normalizedQuestions,
|
|
2449
|
+
maxFreeFormAnswerLength,
|
|
2337
2450
|
imageDataUrl: base64Data,
|
|
2338
2451
|
credentials
|
|
2339
2452
|
});
|
|
@@ -2344,7 +2457,8 @@ async function askQuestions(assetId, questions, options) {
|
|
|
2344
2457
|
modelId: modelConfig.modelId,
|
|
2345
2458
|
userPrompt,
|
|
2346
2459
|
systemPrompt,
|
|
2347
|
-
|
|
2460
|
+
normalizedQuestions,
|
|
2461
|
+
maxFreeFormAnswerLength,
|
|
2348
2462
|
imageDataUrl: storyboardUrl,
|
|
2349
2463
|
credentials
|
|
2350
2464
|
})
|
|
@@ -2379,20 +2493,42 @@ async function askQuestions(assetId, questions, options) {
|
|
|
2379
2493
|
);
|
|
2380
2494
|
}
|
|
2381
2495
|
const answers = analysisResponse.result.answers.map((raw, idx) => {
|
|
2382
|
-
const
|
|
2383
|
-
const
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2496
|
+
const question = normalizedQuestions[idx];
|
|
2497
|
+
const isFreeForm = question.mode.kind === "freeForm";
|
|
2498
|
+
const reasoningScrub = safety.scrubDetailed(raw.reasoning, `reasoning[${idx}]`);
|
|
2499
|
+
const explicitSkip = raw.skipped || raw.answer === SKIP_SENTINEL;
|
|
2500
|
+
const answerScrub = isFreeForm && !explicitSkip ? safety.scrubDetailed(raw.answer, `answer[${idx}]`) : null;
|
|
2501
|
+
const overLength = isFreeForm && !explicitSkip && !(answerScrub?.leaked ?? false) && (answerScrub?.text.length ?? 0) > maxFreeFormAnswerLength;
|
|
2502
|
+
if (overLength) {
|
|
2503
|
+
console.warn(
|
|
2504
|
+
`[@mux/ai] Free-form answer at index ${idx} exceeded ${maxFreeFormAnswerLength}-char cap (${answerScrub.text.length} chars); treating as skipped.`
|
|
2387
2505
|
);
|
|
2388
2506
|
}
|
|
2507
|
+
const isSkipped = explicitSkip || reasoningScrub.leaked || (answerScrub?.leaked ?? false) || overLength;
|
|
2508
|
+
if (!isSkipped && !isFreeForm) {
|
|
2509
|
+
const allowed = allowedAnswersForQuestion(question);
|
|
2510
|
+
if (!allowed.includes(raw.answer)) {
|
|
2511
|
+
throw new MuxAiError(
|
|
2512
|
+
`Answer "${raw.answer}" for question ${idx} is not in allowed options: ${allowed.join(", ")}`
|
|
2513
|
+
);
|
|
2514
|
+
}
|
|
2515
|
+
}
|
|
2516
|
+
let finalAnswer;
|
|
2517
|
+
if (isSkipped) {
|
|
2518
|
+
finalAnswer = null;
|
|
2519
|
+
} else if (isFreeForm) {
|
|
2520
|
+
finalAnswer = answerScrub ? answerScrub.text : raw.answer;
|
|
2521
|
+
} else {
|
|
2522
|
+
finalAnswer = raw.answer;
|
|
2523
|
+
}
|
|
2524
|
+
const suppressed = reasoningScrub.leaked || (answerScrub?.leaked ?? false);
|
|
2389
2525
|
return {
|
|
2390
2526
|
// Use the trusted input verbatim rather than the model's echo.
|
|
2391
|
-
question:
|
|
2527
|
+
question: question.question,
|
|
2392
2528
|
confidence: isSkipped ? 0 : Math.min(1, Math.max(0, raw.confidence)),
|
|
2393
|
-
reasoning:
|
|
2529
|
+
reasoning: suppressed ? "Response suppressed by safety filter." : reasoningScrub.text,
|
|
2394
2530
|
skipped: isSkipped,
|
|
2395
|
-
answer:
|
|
2531
|
+
answer: finalAnswer
|
|
2396
2532
|
};
|
|
2397
2533
|
});
|
|
2398
2534
|
return {
|
|
@@ -4727,6 +4863,15 @@ var OPENAI_MODERATION_BASE_DELAY_MS = 750;
|
|
|
4727
4863
|
var OPENAI_MODERATION_MAX_DELAY_MS = 3e3;
|
|
4728
4864
|
var MIN_SAMPLE_COVERAGE_FOR_CONFIDENT_THRESHOLDING = 0.5;
|
|
4729
4865
|
var MIN_SUCCESSFUL_THUMBNAILS_FOR_CONFIDENT_THRESHOLDING = 3;
|
|
4866
|
+
var GOOGLE_VISION_ENDPOINT = "https://vision.googleapis.com/v1/images:annotate";
|
|
4867
|
+
var GOOGLE_VISION_LIKELIHOOD_TO_SCORE = {
|
|
4868
|
+
UNKNOWN: 0,
|
|
4869
|
+
VERY_UNLIKELY: 0.2,
|
|
4870
|
+
UNLIKELY: 0.4,
|
|
4871
|
+
POSSIBLE: 0.6,
|
|
4872
|
+
LIKELY: 0.8,
|
|
4873
|
+
VERY_LIKELY: 1
|
|
4874
|
+
};
|
|
4730
4875
|
var HIVE_ENDPOINT = "https://api.thehive.ai/api/v2/task/sync";
|
|
4731
4876
|
var HIVE_SEXUAL_CATEGORIES = [
|
|
4732
4877
|
"general_nsfw",
|
|
@@ -5055,6 +5200,98 @@ async function requestHiveModeration(images, maxConcurrent = 5, submissionMode =
|
|
|
5055
5200
|
}));
|
|
5056
5201
|
return await processConcurrently(targets, moderateImageWithHive, maxConcurrent);
|
|
5057
5202
|
}
|
|
5203
|
+
async function moderateImageWithGoogleVision(entry) {
|
|
5204
|
+
"use step";
|
|
5205
|
+
try {
|
|
5206
|
+
const apiKey = await getApiKeyFromEnv("google-vision-api", entry.credentials);
|
|
5207
|
+
const imageField = entry.isBase64 ? { content: entry.image } : { source: { imageUri: entry.image } };
|
|
5208
|
+
const controller = new AbortController();
|
|
5209
|
+
const timeout = setTimeout(() => controller.abort(), 15e3);
|
|
5210
|
+
let res;
|
|
5211
|
+
try {
|
|
5212
|
+
res = await fetch(GOOGLE_VISION_ENDPOINT, {
|
|
5213
|
+
method: "POST",
|
|
5214
|
+
headers: {
|
|
5215
|
+
"Accept": "application/json",
|
|
5216
|
+
"Content-Type": "application/json",
|
|
5217
|
+
"X-goog-api-key": apiKey
|
|
5218
|
+
},
|
|
5219
|
+
body: JSON.stringify({
|
|
5220
|
+
requests: [
|
|
5221
|
+
{
|
|
5222
|
+
image: imageField,
|
|
5223
|
+
features: [{ type: "SAFE_SEARCH_DETECTION" }]
|
|
5224
|
+
}
|
|
5225
|
+
]
|
|
5226
|
+
}),
|
|
5227
|
+
signal: controller.signal
|
|
5228
|
+
});
|
|
5229
|
+
} catch (err) {
|
|
5230
|
+
if (err?.name === "AbortError") {
|
|
5231
|
+
throw new Error("Google Vision request timed out after 15s");
|
|
5232
|
+
}
|
|
5233
|
+
throw err;
|
|
5234
|
+
} finally {
|
|
5235
|
+
clearTimeout(timeout);
|
|
5236
|
+
}
|
|
5237
|
+
const json = await res.json().catch(() => void 0);
|
|
5238
|
+
if (!res.ok) {
|
|
5239
|
+
throw new Error(
|
|
5240
|
+
`Google Vision moderation error: ${res.status} ${res.statusText} - ${JSON.stringify(json)}`
|
|
5241
|
+
);
|
|
5242
|
+
}
|
|
5243
|
+
const perImage = json?.responses?.[0];
|
|
5244
|
+
if (perImage?.error) {
|
|
5245
|
+
throw new Error(
|
|
5246
|
+
`Google Vision per-image error: ${perImage.error.code} ${perImage.error.message || "Unknown error"}`
|
|
5247
|
+
);
|
|
5248
|
+
}
|
|
5249
|
+
const annotation = perImage?.safeSearchAnnotation;
|
|
5250
|
+
if (!annotation) {
|
|
5251
|
+
throw new Error(`Google Vision response missing safeSearchAnnotation: ${JSON.stringify(json)}`);
|
|
5252
|
+
}
|
|
5253
|
+
const adultLikelihood = typeof annotation.adult === "string" ? annotation.adult : "UNKNOWN";
|
|
5254
|
+
const violenceLikelihood = typeof annotation.violence === "string" ? annotation.violence : "UNKNOWN";
|
|
5255
|
+
const sexual = GOOGLE_VISION_LIKELIHOOD_TO_SCORE[adultLikelihood] ?? 0;
|
|
5256
|
+
const violence = GOOGLE_VISION_LIKELIHOOD_TO_SCORE[violenceLikelihood] ?? 0;
|
|
5257
|
+
return {
|
|
5258
|
+
url: entry.url,
|
|
5259
|
+
time: entry.time,
|
|
5260
|
+
sexual,
|
|
5261
|
+
violence,
|
|
5262
|
+
error: false
|
|
5263
|
+
};
|
|
5264
|
+
} catch (error) {
|
|
5265
|
+
return {
|
|
5266
|
+
url: entry.url,
|
|
5267
|
+
time: entry.time,
|
|
5268
|
+
sexual: 0,
|
|
5269
|
+
violence: 0,
|
|
5270
|
+
error: true,
|
|
5271
|
+
errorMessage: error instanceof Error ? error.message : String(error)
|
|
5272
|
+
};
|
|
5273
|
+
}
|
|
5274
|
+
}
|
|
5275
|
+
async function requestGoogleVisionModeration(images, maxConcurrent = 5, submissionMode = "url", downloadOptions, credentials) {
|
|
5276
|
+
"use step";
|
|
5277
|
+
const imageUrls = images.map((img) => img.url);
|
|
5278
|
+
const timeByUrl = new Map(images.map((img) => [img.url, img.time]));
|
|
5279
|
+
const targets = submissionMode === "base64" ? (await downloadImagesAsBase64(imageUrls, downloadOptions, maxConcurrent)).map((img) => ({
|
|
5280
|
+
url: img.url,
|
|
5281
|
+
time: timeByUrl.get(img.url),
|
|
5282
|
+
// Vision REST wants raw base64; downloadImagesAsBase64 returns a data URI.
|
|
5283
|
+
image: img.base64Data.startsWith("data:") ? img.base64Data.split(",")[1] : img.base64Data,
|
|
5284
|
+
isBase64: true,
|
|
5285
|
+
credentials
|
|
5286
|
+
})) : images.map((img) => ({
|
|
5287
|
+
url: img.url,
|
|
5288
|
+
time: img.time,
|
|
5289
|
+
image: img.url,
|
|
5290
|
+
isBase64: false,
|
|
5291
|
+
credentials
|
|
5292
|
+
}));
|
|
5293
|
+
return processConcurrently(targets, moderateImageWithGoogleVision, maxConcurrent);
|
|
5294
|
+
}
|
|
5058
5295
|
async function getThumbnailUrlsFromTimestamps(playbackId, timestampsMs, options) {
|
|
5059
5296
|
"use step";
|
|
5060
5297
|
const { width, shouldSign, credentials } = options;
|
|
@@ -5118,8 +5355,11 @@ async function getModerationScores(assetId, options = {}) {
|
|
|
5118
5355
|
);
|
|
5119
5356
|
} else if (provider === "hive") {
|
|
5120
5357
|
throw new Error("Hive does not support transcript moderation in this workflow. Use provider: 'openai' for audio-only assets.");
|
|
5358
|
+
} else if (provider === "google-vision-api") {
|
|
5359
|
+
throw new Error("google-vision-api is image-only and does not support transcript moderation. Use provider: 'openai' for audio-only assets.");
|
|
5121
5360
|
} else {
|
|
5122
|
-
|
|
5361
|
+
const exhaustiveCheck = provider;
|
|
5362
|
+
throw new Error(`Unsupported moderation provider: ${exhaustiveCheck}`);
|
|
5123
5363
|
}
|
|
5124
5364
|
} else {
|
|
5125
5365
|
const estimatedIntervalCount = duration <= 50 ? 5 : Math.ceil(duration / thumbnailInterval);
|
|
@@ -5162,8 +5402,17 @@ async function getModerationScores(assetId, options = {}) {
|
|
|
5162
5402
|
imageDownloadOptions,
|
|
5163
5403
|
credentials
|
|
5164
5404
|
);
|
|
5405
|
+
} else if (provider === "google-vision-api") {
|
|
5406
|
+
thumbnailScores = await requestGoogleVisionModeration(
|
|
5407
|
+
thumbnailUrls,
|
|
5408
|
+
maxConcurrent,
|
|
5409
|
+
imageSubmissionMode,
|
|
5410
|
+
imageDownloadOptions,
|
|
5411
|
+
credentials
|
|
5412
|
+
);
|
|
5165
5413
|
} else {
|
|
5166
|
-
|
|
5414
|
+
const exhaustiveCheck = provider;
|
|
5415
|
+
throw new Error(`Unsupported moderation provider: ${exhaustiveCheck}`);
|
|
5167
5416
|
}
|
|
5168
5417
|
}
|
|
5169
5418
|
const failed = thumbnailScores.filter((s) => s.error);
|
|
@@ -6867,6 +7116,7 @@ export {
|
|
|
6867
7116
|
DEFAULT_DESCRIPTION_LENGTH,
|
|
6868
7117
|
DEFAULT_SUMMARY_KEYWORD_LIMIT,
|
|
6869
7118
|
DEFAULT_TITLE_LENGTH,
|
|
7119
|
+
GOOGLE_VISION_LIKELIHOOD_TO_SCORE,
|
|
6870
7120
|
HIVE_SEXUAL_CATEGORIES,
|
|
6871
7121
|
HIVE_VIOLENCE_CATEGORIES,
|
|
6872
7122
|
aggregateTokenUsage,
|