All workflows accept an optional credentials object for runtime credential injection. This is inherited from the base MuxAIOptions interface and is not repeated for each workflow below.
Workflows that analyze asset content accept an optional asset-relative scope:
interface WorkflowScope {
startTime?: number; // Inclusive, in seconds
endTime?: number; // Exclusive, in seconds
}Both boundaries are optional. { startTime: 30 } analyzes from 30 seconds
through the end of the asset, while { endTime: 90 } analyzes from the
beginning up to 90 seconds. Times remain relative to the original asset in
workflow outputs.
scope is supported by getSummaryAndTags, getModerationScores,
hasBurnedInCaptions, askQuestions, generateChapters,
generateEmbeddings, and generateText. It bounds storyboard or thumbnail selection and
transcript cues as applicable. The workflow rejects negative, non-finite,
empty, reversed, or out-of-asset ranges.
Analyzes a Mux video or audio asset and returns AI-generated metadata.
Parameters:
assetId(string) - Mux asset ID (video or audio-only)options(optional) - Configuration options
Options:
provider?: 'openai' | 'anthropic' | 'google' | 'baseten' | 'openai-compatible'- AI provider (default: 'openai')tone?: 'neutral' | 'playful' | 'professional'- Analysis tone (default: 'neutral')model?: string- AI model to use. Defaults per provider:gpt-5.6-lunaat medium reasoning (OpenAI),claude-sonnet-4-5(Anthropic),gemini-3-flash-preview(Google). Baseten and OpenAI-compatible endpoints have no default — passmodelor set theBASETEN_MODEL/OPENAI_COMPATIBLE_MODELenvironment variable.languageCode?: string- Language code for transcript track selection (e.g., 'en', 'fr'). When omitted, prefers English if available.outputLanguageCode?: string- BCP 47 language code (e.g., 'en', 'fr', 'ja') for the generated title, description, and tags. When omitted or set to'auto', auto-detects from the selected transcript track's language. Falls back to unconstrained (LLM decides) if no language metadata is available.includeTranscript?: boolean- Include transcript in analysis (default: true)cleanTranscript?: boolean- Remove VTT timestamps and formatting from transcript (default: true)imageSubmissionMode?: 'url' | 'base64'- How to submit storyboard to AI providers (default: 'url')imageDownloadOptions?: object- Options for image download when using base64 modetimeout?: number- Request timeout in milliseconds (default: 10000)retries?: number- Maximum retry attempts (default: 3)retryDelay?: number- Base delay between retries in milliseconds (default: 1000)maxRetryDelay?: number- Maximum delay between retries in milliseconds (default: 10000)exponentialBackoff?: boolean- Whether to use exponential backoff (default: true)
promptOverrides?: object- Override specific sections of the prompt for custom use casestask?: string- Override the main task instructiontitle?: string- Override title generation guidancedescription?: string- Override description generation guidancekeywords?: string- Override keywords generation guidancequalityGuidelines?: string- Override quality guidelines
Returns:
interface SummaryAndTagsResult {
assetId: string;
title: string; // Short title
description: string; // Detailed description
tags: string[]; // Up to 10 relevant keywords
storyboardUrl?: string; // Video storyboard URL (undefined for audio-only assets)
usage?: TokenUsage; // Token usage from the AI provider
transcriptText?: string; // Raw transcript text (when includeTranscript is true)
}Analyzes a Mux asset for inappropriate content using OpenAI's Moderation API, Hive's Moderation API, or Google Cloud Vision SafeSearch.
By default it moderates everything that's available on the asset and reports what it skipped:
- Thumbnails — sampled storyboard frames (image moderation). Skipped for audio-only assets.
- Transcript — the caption track's text, in time windows (text moderation). Skipped when the asset has no ready caption track, the track has nothing to moderate, or
includeTranscriptisfalseon a video asset. Onlyopenaisupports text moderation;hiveandgoogle-vision-apiare image-only and report the transcript as skipped.
Parameters:
assetId(string) - Mux asset ID (video or audio-only)options(optional) - Configuration options
Options:
provider?: 'openai' | 'hive' | 'google-vision-api'- Moderation provider (default: 'openai')model?: string- OpenAI moderation model to use (default:omni-moderation-latest); ignored forhiveandgoogle-vision-apilanguageCode?: string- Language code of the caption track to moderate. When omitted, an English subtitles track is preferred and otherwise the first ready text track is used.includeTranscript?: boolean- Moderate the caption transcript alongside thumbnails (default: true). Whenfalse, video assets are moderated on thumbnails only andtranscriptModerationis skipped withskipReason: 'disabled'. Ignored for audio-only assets, where the transcript is the only surface that can be moderated.thresholds?: { sexual?: number; violence?: number }- Custom thresholds (default: {sexual: 0.7, violence: 0.8})thumbnailInterval?: number- Seconds between thumbnails for long videos (default: 10)thumbnailWidth?: number- Thumbnail width in pixels (default: 640)maxSamples?: number- Maximum number of thumbnails to sample. Acts as a cap: ifthumbnailIntervalproduces fewer samples than this limit the interval is respected; otherwise samples are evenly distributed with first and last frames pinned. (default: unlimited)maxConcurrent?: number- Maximum concurrent API requests (default: 5)imageSubmissionMode?: 'url' | 'base64'- How to submit images to AI providers (default: 'url')imageDownloadOptions?: object- Options for image download when using base64 modetimeout?: number- Request timeout in milliseconds (default: 10000)retries?: number- Maximum retry attempts (default: 3)retryDelay?: number- Base delay between retries in milliseconds (default: 1000)maxRetryDelay?: number- Maximum delay between retries in milliseconds (default: 10000)exponentialBackoff?: boolean- Whether to use exponential backoff (default: true)
Hive note: this workflow uses Hive for image moderation only. Transcript text is never sent to Hive; with provider: "hive" the transcript surface is reported as skipped (unsupported_provider), and an audio-only asset therefore throws because nothing can be moderated.
Google Vision note: SafeSearch returns the adult, violence, racy, spoof, and medical Likelihood enum values (UNKNOWN..VERY_LIKELY). @mux/ai consumes only adult (mapped to sexual) and violence, and converts the enum onto a 0..1 scale linearly: UNKNOWN=0, VERY_UNLIKELY=0.2, UNLIKELY=0.4, POSSIBLE=0.6, LIKELY=0.8, VERY_LIKELY=1.0. Because exceedsThreshold uses strict >, the default 0.8 threshold treats only VERY_LIKELY as exceeding — drop the threshold to e.g. 0.7 if you want LIKELY to flag. This mapping may change in future versions of @mux/ai.
Returns:
{
assetId: string;
// Which surfaces were moderated (derived from thumbnailModeration / transcriptModeration):
// 'thumbnails' = images only; 'transcript' = transcript text only; 'combined' = both.
mode: 'thumbnails' | 'transcript' | 'combined';
isAudioOnly: boolean;
thumbnailScores: Array<{ // Image (thumbnail) results; empty when thumbnailModeration.status === 'skipped'
url: string;
time?: number; // Time in seconds of the thumbnail within the video
sexual: number; // 0-1 score
violence: number; // 0-1 score
error: boolean;
errorMessage?: string;
}>;
transcriptScores: Array<{ // Time-windowed transcript results; empty when transcriptModeration.status === 'skipped'
startTime: number; // Seconds — start of the moderated time window. Ranges may overlap between consecutive entries by design.
endTime: number; // Seconds — end of the moderated time window
sexual: number; // 0-1 score
violence: number; // 0-1 score
error: boolean;
errorMessage?: string;
}>;
thumbnailModeration: { // Audit trail for the thumbnail surface
// 'completed' — ran and scored at least one thumbnail (others may carry a per-item `error`).
// 'skipped' — nothing to moderate (see `skipReason` / `skipMessage`).
// 'failed' — ran but every thumbnail errored (see `errorMessage`); contributes no scores.
status: 'completed' | 'skipped' | 'failed';
// Present only when status === 'skipped':
// 'no_video_track' — the asset has no video track, so there are no thumbnails.
// 'no_video_in_scope' — the requested `scope` lies entirely past the end of the video track.
skipReason?: 'no_video_track' | 'no_video_in_scope';
skipMessage?: string; // Present only when status === 'skipped'. Human-readable explanation.
errorMessage?: string; // Present only when status === 'failed'. Summarizes the per-item errors.
};
transcriptModeration: { // Audit trail for the transcript surface
// 'completed' — ran and scored at least one window (others may carry a per-item `error`).
// 'skipped' — nothing to moderate (see `skipReason` / `skipMessage`).
// 'failed' — ran but every window errored (see `errorMessage`); contributes no scores.
status: 'completed' | 'skipped' | 'failed';
// Present only when status === 'skipped':
// 'no_ready_text_track' — no ready caption/subtitle track (or none matching languageCode).
// 'no_cues' — a track exists but produced nothing to moderate: empty VTT body,
// no parseable cues, or no cue inside `scope`. Valid-but-empty, not an error.
// 'unsupported_provider' — the provider is image-only (hive / google-vision-api).
// 'disabled' — `includeTranscript: false` was passed for a video asset.
skipReason?: 'no_ready_text_track' | 'no_cues' | 'unsupported_provider' | 'disabled';
skipMessage?: string; // Present only when status === 'skipped'. Human-readable explanation.
errorMessage?: string; // Present only when status === 'failed'. Summarizes the per-item errors.
};
maxScores: { // Highest scores across thumbnails AND all transcript time windows
sexual: number;
violence: number;
};
coverage: {
// Counts describe thumbnail sampling only (transcript windows are excluded):
requestedSampleCount: number;
successfulSampleCount: number;
failedSampleCount: number;
sampleCoverage: number; // 0-1 fraction of requested samples that succeeded
// The booleans cover both surfaces:
isPartial: boolean; // true when any thumbnail or transcript window failed but the workflow still returned a result
isLowConfidence: boolean; // true when thumbnail coverage is thin or a surface that ran failed entirely
};
exceedsThreshold: boolean; // true if content should be flagged
thresholds: { // Threshold values used
sexual: number;
violence: number;
};
usage?: TokenUsage; // Workflow usage metadata
}Skipped surfaces are auditable, not silent. The result always carries thumbnailModeration and transcriptModeration, each describing whether that surface was completed or skipped — and, when skipped, a machine-readable skipReason plus a human-readable skipMessage. This lets callers tell "moderated cleanly with no findings" apart from "nothing was moderated" without inspecting an empty scores array. Skips are for expected conditions only (no caption track, an empty track, an image-only provider, includeTranscript: false); a caption track that exists but cannot be fetched is an error, not a skip. If both surfaces end up skipped, the call throws rather than returning exceedsThreshold: false for an asset nothing was checked on.
Failed surfaces are reported, not folded into completed. If a surface ran but every item errored (for example every transcript window was rejected by the provider), its status is failed with an errorMessage, and coverage.isLowConfidence is set because maxScores and exceedsThreshold then reflect only the other surface. If every item on every surface failed, the call throws.
Transcript moderation uses dynamic, overlapping time windows. Each transcriptScores entry is a moderated time window carrying startTime/endTime (in seconds) — directly analogous to how a flagged thumbnail carries its time — so consumers can locate flagged speech on the timeline rather than receiving a single verdict for the whole transcript. Window size scales with the asset's duration (windowSeconds = clamp(duration / 40, 20s, 120s)), and consecutive windows overlap (~15% of the window size, at least 5 seconds) so content that straddles a window boundary is still scored intact in at least one window. Windows are aligned to caption-cue boundaries (a single cue is never split across windows) so the reported timecodes stay accurate. Because windows overlap by design, consecutive transcriptScores entries' [startTime, endTime] ranges may overlap by roughly the overlap amount. Windows are sent to OpenAI as batched array requests (multiple window texts per /v1/moderations call via the array input, with results[] mapped back index-aligned); if a batch is rejected as too large it is split in half and retried down to a single window. When both thumbnails and transcript windows are moderated, mode is 'combined'; maxScores/exceedsThreshold aggregate the highest sexual/violence across thumbnails and all transcript windows.
Completeness and coverage. Before you rely on a result, understand exactly what was scored and how completely:
- All scored units are returned; scoring never stops at the first flag. Every sampled thumbnail appears in
thumbnailScoresand every transcript window intranscriptScores, whether or not it crosses a threshold.maxScoresandexceedsThresholdare a max-based rollup across all of them (exceedsThresholdistrueif any single unit'ssexual/violenceexceeds its threshold). To find which region triggered a flag, scan the arrays for entries above your threshold. - Thumbnail (visual) coverage is sampled, not exhaustive. Frames are sampled at
thumbnailInterval(default every 10s), capped bymaxSamples. Content between sampled frames is not inspected, so a result with no visual flag does not guarantee every frame is clean; lowerthumbnailIntervalor raisemaxSamplesfor denser coverage. Thecoveragecounts (requestedSampleCount,successfulSampleCount,failedSampleCount,sampleCoverage) describe thumbnail sampling only — transcript windows are excluded from the denominator.isPartialandisLowConfidencecover both surfaces:isPartialis true if any thumbnail or transcript window failed, andisLowConfidencesignals thin thumbnail sampling or a surface that failed entirely, without making thumbnail coverage exhaustive. - Transcript (text) coverage is exhaustive. Every caption cue falls in at least one time window and windows overlap, so all spoken content is moderated. Because windows overlap, the same flagged utterance near a boundary can appear in two adjacent
transcriptScoresentries with overlapping[startTime, endTime]ranges; merge/deduplicate overlapping flagged ranges if you want one finding per utterance.
Analyzes video frames to detect burned-in captions (hardcoded subtitles) that are permanently embedded in the video image.
Parameters:
assetId(string) - Mux video asset IDoptions(optional) - Configuration options
Options:
provider?: 'openai' | 'anthropic' | 'google' | 'baseten' | 'openai-compatible'- AI provider (default: 'openai')model?: string- AI model to use. Defaults per provider:gpt-5.6-lunaat medium reasoning (OpenAI),claude-sonnet-4-5(Anthropic),gemini-3-flash-preview(Google). Baseten and OpenAI-compatible endpoints have no default — passmodelor set theBASETEN_MODEL/OPENAI_COMPATIBLE_MODELenvironment variable.imageSubmissionMode?: 'url' | 'base64'- How to submit storyboard to AI providers (default: 'url')imageDownloadOptions?: object- Options for image download when using base64 modetimeout?: number- Request timeout in milliseconds (default: 10000)retries?: number- Maximum retry attempts (default: 3)retryDelay?: number- Base delay between retries in milliseconds (default: 1000)maxRetryDelay?: number- Maximum delay between retries in milliseconds (default: 10000)exponentialBackoff?: boolean- Whether to use exponential backoff (default: true)
promptOverrides?: object- Override specific sections of the detection prompttask?: string- Override the main analysis task instructionanalysisSteps?: string- Override the step-by-step analysis procedurepositiveIndicators?: string- Override criteria for classifying text as captionsnegativeIndicators?: string- Override criteria for ruling out captions
Returns:
{
assetId: string;
hasBurnedInCaptions: boolean; // Whether burned-in captions were detected
confidence: number; // Confidence score (0.0-1.0)
detectedLanguage: string | null; // Language of detected captions, or null
storyboardUrl: string; // URL to analyzed storyboard
usage?: TokenUsage; // Token usage from the AI provider
}Detection Logic:
- Analyzes video storyboard frames to identify text overlays
- Distinguishes between actual captions and marketing/end-card text
- Text appearing only in final 1-2 frames is classified as marketing copy
- Caption text must appear across multiple frames throughout the timeline
- Optimized prompts minimize false positives
Answer questions about asset content by analyzing storyboard frames and optional transcripts. For audio-only assets, this workflow analyzes transcript text only. By default, answers are "yes"/"no", but you can override the allowed responses.
Parameters:
assetId(string) - Mux asset ID (video or audio-only)questions(array) - Array of question objects- Each question object must have a
questionfield (string) - Each question may optionally include
answerOptions?: string[](defaults to["yes", "no"]) - Each question may optionally include
freeFormReply?: boolean(experimental — see below) - Example:
[{ question: "What is the production quality?", answerOptions: ["amateur", "semi-pro", "professional"] }]
- Each question object must have a
options(optional) - Configuration options
Options:
provider?: 'openai' | 'anthropic' | 'google' | 'baseten' | 'openai-compatible'- AI provider (default: 'openai')model?: string- AI model to use. Defaults per provider:gpt-5.6-lunaat medium reasoning (OpenAI),claude-sonnet-4-5(Anthropic),gemini-3-flash-preview(Google). Baseten and OpenAI-compatible endpoints have no default — passmodelor set theBASETEN_MODEL/OPENAI_COMPATIBLE_MODELenvironment variable.languageCode?: string- Language code for transcript track selection (e.g., 'en', 'fr'). When omitted, prefers English if available.includeTranscript?: boolean- Include transcript in analysis (default: true, required for audio-only assets)cleanTranscript?: boolean- Remove VTT timestamps and formatting from transcript (default: true)imageSubmissionMode?: 'url' | 'base64'- How to submit storyboard to AI providers (default: 'url')imageDownloadOptions?: object- Options for image download when using base64 modetimeout?: number- Request timeout in milliseconds (default: 10000)retries?: number- Maximum retry attempts (default: 3)retryDelay?: number- Base delay between retries in milliseconds (default: 1000)maxRetryDelay?: number- Maximum delay between retries in milliseconds (default: 10000)exponentialBackoff?: boolean- Whether to use exponential backoff (default: true)
storyboardWidth?: number- Storyboard resolution in pixels (default: 640)maxFreeFormAnswerLength?: number- Experimental. Maximum character length for free-form answers when a question setsfreeFormReply: true(default: 500). Keep low to bound the open-ended output channel.
Returns:
interface AskQuestionsResult {
assetId: string;
answers: Array<{
question: string; // The original question
answer: string | null; // Answer from allowed options (null when skipped)
confidence: number; // Confidence score (0.0-1.0)
reasoning: string; // AI's explanation based on observable evidence or why the question was skipped
skipped: boolean; // True when the question was not answerable from the asset content
}>;
storyboardUrl?: string; // URL to analyzed storyboard (undefined for audio-only assets)
usage?: TokenUsage; // Token usage from the AI provider
transcriptText?: string; // Raw transcript (when includeTranscript is true)
}Examples:
// Single question
const result = await askQuestions("asset-id", [
{ question: "Does this video contain cooking?" }
]);
console.log(result.answers[0].answer); // "yes" or "no" by default
console.log(result.answers[0].confidence); // 0.95
console.log(result.answers[0].reasoning); // "A chef prepares ingredients..."
// Multiple questions (efficient single API call)
const result = await askQuestions("asset-id", [
{ question: "Does this video contain people?" },
{ question: "Is this video in color?" },
{ question: "Does this video contain violence?" }
]);
// Without transcript (visual-only analysis)
const result = await askQuestions("asset-id", questions, {
includeTranscript: false
});
// Per-question answer options — mix yes/no with classification scales
const result = await askQuestions("asset-id", [
{ question: "Does this contain cooking?" }, // answer options default to yes/no
{ question: "What is the production quality?", answerOptions: ["amateur", "semi-pro", "professional"] },
{ question: "What is the primary content type?", answerOptions: ["tutorial", "entertainment", "news", "advertisement"] },
{ question: "What is the overall sentiment?", answerOptions: ["positive", "neutral", "negative"] },
]);Tips for Effective Questions:
- Be specific and focused on observable evidence
- Frame questions positively (prefer "Is X present?" over "Is X not present?")
- Avoid ambiguous or subjective questions
- Questions should have clear answers that map to your allowed options
- The AI prioritizes visual evidence when transcript and visuals conflict
⚠️ Experimental:freeFormReplyenables open-ended answers for a given question. This bypasses the enum schema and allows the model to reply with prose instead of yes/no oranswerOptions— use it only when your use case genuinely needs open-ended answers (e.g. describing a scene, extracting a quoted line) and treat the answer as untrusted model output. Mutually exclusive withanswerOptions(setting both throws). The length cap (maxFreeFormAnswerLength, default 500 chars) and the output-safety scrubber still apply. API shape may change.
// Experimental: free-form answers for a single question
const result = await askQuestions("asset-id", [
{ question: "Is this video about glasses?" }, // defaults to yes/no
{
question: "Describe the primary subject of this video in one sentence.",
freeFormReply: true,
},
], { maxFreeFormAnswerLength: 300 });
console.log(result.answers[1].answer); // e.g. "A pair of tortoiseshell glasses..."Generate AI-powered insights explaining viewer engagement patterns by analyzing hotspot data, heatmap statistics, visual frames, and transcripts.
Parameters:
assetId(string) - Mux asset IDoptions(optional) - Configuration options
Options:
-
provider?: 'openai' | 'anthropic' | 'google' | 'baseten' | 'openai-compatible'- AI provider (default: 'openai') -
model?: string- AI model to use. Defaults per provider:gpt-5.6-lunaat medium reasoning (OpenAI),claude-sonnet-4-5(Anthropic),gemini-3-flash-preview(Google). Baseten and OpenAI-compatible endpoints have no default — passmodelor set theBASETEN_MODEL/OPENAI_COMPATIBLE_MODELenvironment variable. -
hotspotLimit?: number- Number of engagement moments to analyze per direction (default: 5, range: 1-10). Note: actual moment count may be up to 2x this value since both peaks and valleys are fetched. -
timeframe?: string- Engagement data timeframe (default: '7:days')- Examples:
'60:minutes','24:hours','7:days','30:days'
- Examples:
-
skipShots?: boolean- Skip shots integration, use thumbnails instead (default: false). Recommended for latency-sensitive use cases.
Returns:
interface EngagementInsightsResult {
assetId: string;
momentInsights: Array<{
startMs: number; // Start time in milliseconds
endMs: number; // End time in milliseconds
timestamp: string; // Human-readable timestamp (e.g., "2:15")
engagementScore: number; // Normalized score (0.0-1.0)
insight: string; // Explanation of engagement pattern
}>;
overallInsight: {
summary: string; // Overall engagement summary
trends: string[]; // Key trends identified
};
usage?: { // Token usage statistics
inputTokens: number;
outputTokens: number;
totalTokens: number;
};
}Examples:
// Basic usage - informational insights
const result = await generateEngagementInsights("asset-id");
result.momentInsights.forEach(m => {
console.log(`${m.timestamp}: ${m.insight}`);
});
// Custom timeframe
const result = await generateEngagementInsights("asset-id", {
timeframe: "30:days",
hotspotLimit: 5,
});
console.log(result.overallInsight.summary);
console.log("Trends:", result.overallInsight.trends);
// Low-latency mode (skip shots polling)
const result = await generateEngagementInsights("asset-id", {
skipShots: true,
});Requirements:
- Newer or low-view videos may not have sufficient engagement data
- Works with both video and audio-only assets (audio-only skips visual analysis)
Use Cases:
- Content optimization based on viewer behavior
- Understanding what drives re-watching and engagement
- Identifying pacing issues and drop-off points
- A/B testing video variations
- Providing engagement feedback to content creators
Translates existing captions from one language to another and optionally adds them as a new track to the Mux asset. The source language is inferred from the track's metadata.
Parameters:
assetId(string) - Mux asset ID (video or audio-only)trackId(string) - ID of the source caption track to translatetoLanguageCode(string) - Target language code (e.g., 'es', 'fr', 'de')options- Configuration options
Options:
provider: 'openai' | 'anthropic' | 'google' | 'baseten' | 'openai-compatible'- AI provider (required)model?: string- Model to use (defaults to the provider's chat model if omitted)uploadToMux?: boolean- Whether to upload translated track to Mux (default: true)replaceExistingTracks?: 'fail' | 'replace_all' | 'replace_generated'- What to do when the asset already has a text track in the target language or with the target name (default:'fail', which rejects before any translation runs).'replace_all'deletes every such track first;'replace_generated'deletes only Mux-generated (ASR) tracks and rejects if anything else is in the way.trackName?: string- Name for the created Mux text track (default: " (Auto-translated)", e.g. "Spanish (Auto-translated)")trackPassthrough?: string-passthroughwritten on the created track, max 255 characters (default:{"mux_ai":{"workflow":"translate-captions"}})s3Endpoint?: string- S3-compatible storage endpoints3Region?: string- S3 region (default: 'auto')s3Bucket?: string- S3 bucket namestorageAdapter?: StorageAdapter- Optional adapter withputObjectandcreatePresignedGetUrlmethodss3SignedUrlExpirySeconds?: number- Expiry duration in seconds for S3 presigned GET URLs (default: 86400 / 24 hours)chunking?: object- Optional VTT-aware chunking controls for large caption translationsenabled?: boolean- Set tofalseto translate all cues in a single structured request (default:true)minimumAssetDurationSeconds?: number- Prefer a single request until the asset is at least this long (default:1800)targetChunkDurationSeconds?: number- Soft target for chunk duration once chunking starts (default:1800)maxConcurrentTranslations?: number- Max number of concurrent translation requests when chunking (default:4)maxCuesPerChunk?: number- Hard cap for cues included in a single AI translation chunk (default:80)maxCueTextTokensPerChunk?: number- Approximate cap for cue text tokens included in a single AI translation chunk (default:2000)
neverTranslate?: string[]- Terms (brand names, proper nouns) to preserve verbatim in the translated output; max 100 terms of 100 characters each,<and>not allowed. Compliance is verified and reported onresult.neverTranslateTermsPreserved, not guaranteed.
Returns:
interface TranslationResult {
assetId: string;
trackId: string; // Source track ID
sourceLanguageCode: string; // Inferred from track metadata
targetLanguageCode: string;
sourceLanguage: LanguageCodePair; // { iso639_1: string; iso639_3: string }
targetLanguage: LanguageCodePair; // { iso639_1: string; iso639_3: string }
originalVtt: string; // Original VTT content
translatedVtt: string; // Translated VTT content
uploadedTrackId?: string; // Mux track ID (if uploaded)
replacedTracks?: TextTrackSummary[]; // Existing tracks deleted before the upload ({ id, name, languageCode, status, textSource, passthrough })
presignedUrl?: string; // S3 presigned URL (default expiry: 24 hours)
usage?: TokenUsage; // Token usage from the AI provider
neverTranslateTermsPreserved?: boolean; // Present when neverTranslate terms were supplied; false if any term was not preserved verbatim
}Supported Languages:
All ISO 639-1 language codes are automatically supported using Intl.DisplayNames. Examples: Spanish (es), French (fr), German (de), Italian (it), Portuguese (pt), Polish (pl), Japanese (ja), Korean (ko), Chinese (zh), Russian (ru), Arabic (ar), Hindi (hi), Thai (th), Swahili (sw), and many more.
Chunking Behavior:
- Chunking is enabled by default for
translateCaptions - Shorter assets are translated in a single request until
minimumAssetDurationSecondsis reached - When chunking is active, requests stay aligned to VTT cues and the final VTT is rebuilt locally
- Chunk size is bounded by both cue count and approximate cue text token budget
Edits a caption track using LLM-powered profanity censorship, static find/replace, speaker-label replacement, or a combination of these operations. Optionally uploads the edited track to Mux.
Parameters:
assetId(string) - Mux asset ID (video or audio-only)trackId(string) - ID of the caption track to editoptions- Configuration options
Options:
provider?: 'openai' | 'anthropic' | 'google' | 'baseten' | 'openai-compatible'- AI provider (required whenautoCensorProfanityis set)model?: string- Model to use (defaults to the provider's chat model if omitted)autoCensorProfanity?: object- LLM-powered profanity censorship (optional)mode?: 'blank' | 'remove' | 'mask'- Replacement strategy (default: 'blank')'blank':shit→[____](bracketed underscores matching word length)'remove': word removed entirely'mask':shit→????(question marks matching word length)
alwaysCensor?: string[]- Words to always censor regardless of LLM outputneverCensor?: string[]- Words to never censor even if the LLM flags them (takes precedence overalwaysCensor)
replacements?: Array<{ find: string; replace: string; caseSensitive?: boolean }>- Static find/replace pairs (optional, no LLM needed). Each entry matches case-sensitively by default; setcaseSensitive: falseto match regardless of case.speakerReplacements?: Array<{ find: string; replace: string }>- Replaces bracketed speaker labels at the start of cues without changing matching words in spoken caption content.uploadToMux?: boolean- Whether to upload edited track to Mux (default: true)replaceExistingTracks?: 'replace' | 'fail'- What to do with the source track (default:'replace': the source is deleted and the edited track takes its place under the source's name; no other track is touched).'fail'deletes nothing and adds the edited track alongside the source, so it requires atrackNamedifferent from the source's. With either value,editCaptionsrejects before editing if a track other than the source already has the edited track's name, since Mux requires unique track names. Other tracks in the same language are never touched.trackName?: string- Name for the edited Mux text track (default: the source track's name)trackPassthrough?: string-passthroughwritten on the edited track, max 255 characters (default:{"mux_ai":{"workflow":"edit-captions"}})deleteOriginalTrack?: boolean- Deprecated, usereplaceExistingTracks. When set, the previous behaviour applies: create<source name> (<trackNameSuffix>), then delete the source iftrue. Cannot be combined withreplaceExistingTracksortrackName.trackNameSuffix?: string- Deprecated, usetrackName. Selects the previous naming scheme (default suffix 'edited', e.g. "Subtitles (edited)"). Cannot be combined withreplaceExistingTracksortrackName.s3Endpoint?: string- S3-compatible storage endpoints3Region?: string- S3 region (default: 'auto')s3Bucket?: string- S3 bucket namestorageAdapter?: StorageAdapter- Optional adapter withputObjectandcreatePresignedGetUrlmethodss3SignedUrlExpirySeconds?: number- Expiry duration in seconds for S3 presigned GET URLs (default: 86400 / 24 hours)
At least one of autoCensorProfanity, replacements, or speakerReplacements must be provided.
Returns:
interface ReplacementRecord {
cueStartTime: number; // Start time of the cue where the replacement occurred (seconds)
before: string; // Original word/phrase
after: string; // Replacement text
}
interface EditCaptionsResult {
assetId: string;
trackId: string;
originalVtt: string; // Original VTT content
editedVtt: string; // Edited VTT content
totalReplacementCount: number; // Total replacements across all operations
autoCensorProfanity?: { // Present when autoCensorProfanity was used
replacements: ReplacementRecord[]; // Each censored word with cue timing
};
replacements?: { // Present when replacements were used
replacements: ReplacementRecord[]; // Each static replacement with cue timing
};
speakerReplacements?: { // Present when speakerReplacements were used
replacements: ReplacementRecord[]; // Each speaker-label replacement with cue timing
};
uploadedTrackId?: string; // Mux track ID (if uploaded)
replacedTracks?: TextTrackSummary[]; // Existing tracks deleted before the upload, including the source when replaced in place
presignedUrl?: string; // S3 presigned URL (default expiry: 24 hours)
usage?: TokenUsage; // Token usage (only present if LLM was used)
}Generates AI-powered chapter markers by analyzing video or audio transcripts. Creates logical chapter breaks based on topic changes and content transitions.
Parameters:
assetId(string) - Mux asset ID (video or audio-only)options(optional) - Configuration options
Options:
languageCode?: string- Language code for captions (e.g., 'en', 'es', 'fr'). When omitted, prefers English if available.outputLanguageCode?: string- BCP 47 language code (e.g., 'en', 'fr', 'ja') for the generated chapter titles. When omitted or set to'auto', auto-detects from the selected transcript track's language. Falls back to unconstrained (LLM decides) if no language metadata is available.provider?: 'openai' | 'anthropic' | 'google' | 'baseten' | 'openai-compatible'- AI provider (default: 'openai')model?: string- AI model to use. Defaults per provider:gpt-5.6-lunaat medium reasoning (OpenAI),claude-sonnet-4-5(Anthropic),gemini-3-flash-preview(Google). Baseten and OpenAI-compatible endpoints have no default — passmodelor set theBASETEN_MODEL/OPENAI_COMPATIBLE_MODELenvironment variable.promptOverrides?: object- Override specific sections of the chaptering prompttask?: string- Override the main task instructionoutputFormat?: string- Override the expected output format descriptionchapterGuidelines?: string- Override chapter count and formatting guidelinestitleGuidelines?: string- Override chapter title style guidelines
minChaptersPerHour?: number- Minimum chapters to generate per hour of content (default: 3)maxChaptersPerHour?: number- Maximum chapters to generate per hour of content (default: 8)
Returns:
{
assetId: string;
languageCode?: string; // Resolved from input or track metadata
chapters: Array<{
startTime: number; // Chapter start time in seconds
title: string; // Descriptive chapter title
}>;
usage?: TokenUsage; // Token usage from the AI provider
}Requirements:
- Asset must have a ready caption/transcript track
- When
languageCodeis omitted, prefers an English track if available - Uses existing auto-generated or uploaded captions/transcripts
Example Output:
// Perfect format for Mux Player
player.addChapters([
{ startTime: 0, title: "Introduction and Setup" },
{ startTime: 45, title: "Main Content Discussion" },
{ startTime: 120, title: "Conclusion" }
]);Writes a set of source-grounded text artifacts from a video or audio asset. Extracts one editorial brief from the transcript (plus a scoped storyboard for video assets), then writes every variant × artifact combination from that brief in parallel.
Parameters:
assetId(string) - Mux asset ID (video or audio-only, must have a ready caption track)options- Configuration options
Options:
artifacts: GenerateTextArtifact[]- Deliverables to write for every variant (1-5, unique lowercase snake_case keys). Each artifact accepts optionalinstructions(up to 500 characters).{ key, channel?, maxLength?, format?, instructions? }channelis one ofgeneric(default),x,linkedin,facebook,instagram,tiktok,youtube. It guides the writing conventions and sets the default cap (300 words forgeneric).maxLengthis{ unit: "characters", value: 10-20000 }or{ unit: "words", value: 5-3000 }.xartifacts are always held to 280 characters, even whenmaxLengthis expressed in words.formatis"plain"(default, no Markdown syntax) or"markdown".
variants?: Array<{ key: string; instructions?: string }>- Named versions of the complete artifact set (1-5, unique keys). Defaults to[{ key: "default" }]. Omitinstructionsfor an independent take on the same brief.provider?: 'openai' | 'anthropic' | 'google' | 'baseten' | 'openai-compatible'- AI provider (default: 'openai')model?: string- AI model to use. Defaults per provider:gpt-5.6-lunaat medium reasoning (OpenAI),claude-sonnet-4-5(Anthropic),gemini-3-flash-preview(Google). Baseten and OpenAI-compatible endpoints have no default — passmodelor set theBASETEN_MODEL/OPENAI_COMPATIBLE_MODELenvironment variable.audience?: string- Intended reader, used as best-effort guidance (up to 160 characters)voice?: 'conversational' | 'editorial' | 'playful' | 'professional'- Best-effort writing voice for every artifactcallToAction?: 'none' | 'soft' | 'direct'- Whether and how the text should invite the reader to engage furtherbrandTerms?: string[]- Brand or domain terms to use exactly when the source supports them (1-10 terms, 40 characters each, 240 combined)useShots?: boolean- Attach up to 24 evenly sampled shot frames as extra visual evidence when the asset's shots are already generated. When they are not ready, the workflow uses the storyboard alone. Generate shots beforehand with the shots primitives if you need them (default: false, video assets only)languageCode?: string- BCP 47 language code for transcript track selection. When omitted, prefers English if available.outputLanguageCode?: string- BCP 47 language code for the generated text. When omitted or'auto', follows the selected transcript track's language when it is reliable.scope?: WorkflowScope- Asset-relative execution window. Bounds transcript cues, the storyboard, and shot selection.
Returns:
interface GenerateTextResult {
assetId: string;
variants: Array<{
key: string;
artifacts: Array<{
key: string;
content: string; // Empty when suppressed by the output-safety scrubber
}>;
}>;
storyboardUrl?: string; // Storyboard attached to the brief (undefined for audio-only assets)
usage?: TokenUsage; // Aggregate usage across the brief and every artifact
safety?: SafetyReport; // Output-side scrubbing report
}Errors:
validation_error- Invalid option bounds, malformed language tags, missing transcript, empty scope, oruseShotson an audio-only asset. Thrown before any model call.processing_error- An artifact was still empty after its generation retry (retryable), bounded paragraph repair failed to meet all length caps within two calls (retryable), the editorial brief was suppressed by the output-safety scrubber (retryable), or the model stopped before producing a complete response. Output-limit truncation is non-retryable; provider failures retain their retryability.content_policy_error- The provider blocked the transcript or an image under its content policy, or refused a paragraph repair (non-retryable).
Paragraph repair preserves typed errors' public classification and retryability, as well as attempt usage. It disables AI SDK transport retries and stops on provider failure; callers own transient retry/backoff at the job orchestration level and must respect the failure's retryability without expanding the two-call repair budget.
Creates dubbed audio from a Mux asset using ElevenLabs voice cloning and translation. The workflow uses the asset's default audio track and ElevenLabs' dubbing_v1 model. Source language is detected automatically unless fromLanguageCode is provided.
Downloaded audio keeps the format supplied by ElevenLabs, such as FLAC. The stored file's extension and Content-Type match that format. The result's dubbingId is an ElevenLabs project ID and can be used with the ElevenLabs project API.
ElevenLabs charges for the first target language when a project is created. The workflow does not automatically retry project creation. If a creation request times out or its response is lost, check your ElevenLabs account before starting another run to avoid creating and paying for a second project. Status checks and downloads retain workflow retries. Individual ElevenLabs API requests have a four-minute timeout; dubbingPollTimeoutSeconds controls the separate wait for dubbing to finish.
Parameters:
assetId(string) - Mux asset ID (video or audio-only; must contain audio). The workflow requests an audio static rendition if needed, or uses existing files for assets with the deprecatedmp4_supportoption.toLanguageCode(string) - Target language tag, such ases,ur,yue, orpt-BR. Known ISO 639-3 aliases, such asspaandurd, are also accepted. Region and script subtags are retained on Mux tracks; dubbing uses the primary language. See supported languages below.options(optional) - Configuration options
Options:
provider?: 'elevenlabs'- AI provider (default: 'elevenlabs')fromLanguageCode?: string- Source language tag, such asenoren-US. Known ISO 639-3 aliases, such aseng, are also accepted. Region and script subtags are dropped for ElevenLabs. Omit or use"auto"for automatic detection.numSpeakers?: number- Must be0for automatic speaker detection (default). Explicit speaker counts are not supported.uploadToMux?: boolean- Whether to upload dubbed track to Mux (default: true). Mux does not support adding audio tracks to clips, assets from a live stream (including finished recordings), or audio-only assets, so withtruethose are rejected with avalidation_errorbefore dubbing starts; passuploadToMux: false, uploadToS3: trueto dub them and use the file frompresignedUrlinstead.uploadCaptionsToMux?: boolean- Also attach the dub's translated transcript as a subtitles text track (default: false). Independent ofuploadToMux, which controls audio only; caption upload implies S3 staging even when audio upload is disabled.captionsTrackName?: string- Subtitle name override; defaults totrackName, then the generated audio track name.captionsReplaceExistingTracks?: 'fail' | 'replace_all' | 'replace_generated'- Subtitle conflict policy override; defaults toreplaceExistingTracks. Conflicts fail only subtitle attachment and are returned incaptionsOutcome; the dubbed audio and staged downloads remain available.replaceExistingTracks?: 'fail' | 'replace_all' | 'replace_generated'- What to do when the asset already has an audio track in the target language or with the target name (default:'fail', which rejects before dubbing starts).'replace_all'deletes them first. Audio tracks are never Mux-generated, so'replace_generated'only ever removes ASR captions.captionsReplaceExistingTrackscan override this policy for subtitles. The asset's primary audio track is never deleted under any policy.trackName?: string- Name for the created Mux tracks; audio and text tracks are separate name groups so both get the same name unlesscaptionsTrackNameoverrides the subtitle name (default:"<Language> (Auto-dubbed)")trackPassthrough?: string-passthroughwritten on the created tracks, max 255 characters (default:{"mux_ai":{"workflow":"translate-audio"}})s3Endpoint?: string- S3-compatible storage endpoints3Region?: string- S3 region (default: 'auto')s3Bucket?: string- S3 bucket namestorageAdapter?: StorageAdapter- Optional adapter withputObjectandcreatePresignedGetUrlmethodss3SignedUrlExpirySeconds?: number- Expiry duration in seconds for S3 presigned GET URLs (default: 86400 / 24 hours)dubbingPollTimeoutSeconds?: number- Max time to wait for ElevenLabs to finish dubbing before timing out (default: 7200 / 2 hours). Raise for long-form content or when jobs queue behind the concurrency limit.staticRenditionCleanup?: 'delete' | 'keep'- What to do with anaudio.m4astatic rendition the workflow created as dubbing input (default: 'delete'). A rendition that already existed on the asset is never deleted. When dubbing multiple languages concurrently on one asset, create the rendition before fanning out or pass 'keep' — the creating run's delete can otherwise race a concurrent run's source fetch.
Returns:
interface TranslateAudioResult {
assetId: string;
targetLanguageCode: string;
targetLanguage: LanguageCodePair; // { iso639_1: string; iso639_3: string }
dubbingId: string; // ElevenLabs project ID
uploadedTrackId?: string; // Mux audio track ID (if uploaded)
presignedUrl?: string; // S3 presigned audio URL (default expiry: 24 hours)
replacedTracks?: TextTrackSummary[]; // Existing audio/text tracks deleted before the uploads; `type` says which group
captionsTrackId?: string; // Mux text track ID for the dubbed captions (when uploadCaptionsToMux is true)
captionsOutcome?: DubbedCaptionsOutcome; // available, unavailable, created, or failed
captionsPresignedUrl?: string; // S3 presigned URL for the dub transcript VTT
createdStaticRenditionId?: string; // Static rendition ID this run created (undefined if one already existed)
staticRenditionCleanup: "deleted" | "delete_failed" | "kept" | "not_created";
usage?: TokenUsage; // Workflow usage metadata
}translateAudio reports transcript staging and attachment through captionsOutcome whenever staging is attempted. unavailable includes message and retryable; created includes trackId and replacedTracks; failed includes message, retryable, and replacedTracks, including deletions before failed creation. Subtitle failures preserve the completed dub and any staged download URLs. Outcome messages are safe for callers; provider and storage diagnostics are logged separately.
Requirements:
- Asset must have an
audio.m4astatic rendition (auto-requested if missing, and deleted again afterwards by default — seestaticRenditionCleanup) - Assets on the deprecated
mp4_supportoption are dubbed from their existing files, never modified:audio.m4aforaudio-onlyandaudio-only,capped-1080p,capped-1080p.mp4forcapped-1080p, andlow.mp4forstandard. A file over 1 GB is rejected before dubbing; move the asset to the Static Renditions API and add anaudio-onlyrendition instead. - An audio track to dub (video-only assets are rejected with a
validation_error) - ElevenLabs API key with access to Dubbing v1
- S3-compatible storage for Mux ingestion
Supported Languages:
See the Dubbing v1 language list for available targets. Language tags include es (Spanish), ur (Urdu), and yue (Cantonese). BCP-47 primary language subtags can have two or three letters. Known ISO 639-3 aliases are normalized automatically, for example urd becomes ur.
The workflow drops region and script subtags for ElevenLabs while retaining the requested tag on Mux tracks. For example, pt-BR requests Portuguese dubbing and produces a Mux track tagged pt-br. These qualifiers do not select a regional dialect in the dubbing_v1 model. ElevenLabs validates the primary target language when the project is created, after any required Mux rendition preparation.
targetLanguageCode contains the normalized requested tag, including any region or script subtags. The existing targetLanguage.iso639_1 result field also contains that tag, including three-letter values such as yue when no two-letter code exists. Track names are generated from the language name, such as "Spanish (Auto-dubbed)", unless trackName is provided.
Generate vector embeddings for transcript chunks from video or audio assets for semantic search.
Deprecated:
generateVideoEmbeddings is deprecated. Use generateEmbeddings instead.
Parameters:
assetId(string) - Mux asset ID (video or audio-only)options(optional) - Configuration options
Options:
provider?: 'openai' | 'google' | 'baseten' | 'openai-compatible'- Embedding provider (default: 'openai')model?: string- Embedding model to use. Defaults per provider:text-embedding-3-small(OpenAI),gemini-embedding-001(Google). Baseten and OpenAI-compatible endpoints have no default — passmodelor set theBASETEN_EMBEDDING_MODEL/OPENAI_COMPATIBLE_EMBEDDING_MODELenvironment variable.chunkingStrategy?: object- How to chunk the transcripttype: 'token' | 'vtt'- Chunking methodmaxTokens?: number- Maximum tokens per chunk (default: 500)overlap?: number- Token overlap between chunks (for type: 'token', default: 100)overlapCues?: number- VTT cue overlap between chunks (for type: 'vtt', default: 2)
languageCode?: string- Language code for transcript track selection. When omitted, prefers English if available.batchSize?: number- Maximum number of chunks to process concurrently (default: 5)
Returns:
{
assetId: string;
chunks: Array<{
chunkId: string;
embedding: number[]; // Vector embedding
metadata: {
startTime?: number; // Chunk start time in seconds
endTime?: number; // Chunk end time in seconds
tokenCount: number;
};
}>;
averagedEmbedding: number[]; // Single embedding for entire transcript
provider: string;
model: string;
metadata: {
totalChunks: number;
totalTokens: number;
chunkingStrategy: string;
embeddingDimensions: number;
generatedAt: string;
};
usage?: TokenUsage; // Workflow usage metadata
}Customize specific sections of the summarization prompt for different use cases like SEO, social media, or technical analysis. See the Prompt Customization guide for a full overview of the prompt builder pattern.
Tip: Before adding overrides, read through the default summarization prompt template in src/workflows/summarization.ts (the summarizationPromptBuilder config) so that you have clear context on what each section does and what you're changing.
import { getSummaryAndTags } from "@mux/ai/workflows";
// SEO-optimized metadata
const seoResult = await getSummaryAndTags(assetId, {
tone: "professional",
promptOverrides: {
task: "Generate SEO-optimized metadata that maximizes discoverability.",
title: "Create a search-optimized title (50-60 chars) with primary keyword front-loaded.",
keywords: "Focus on high search volume terms and long-tail keywords.",
},
});
// Social media optimized for engagement
const socialResult = await getSummaryAndTags(assetId, {
promptOverrides: {
title: "Create a scroll-stopping headline using emotional triggers or curiosity gaps.",
description: "Write shareable copy that creates FOMO and works without watching the video.",
keywords: "Generate hashtag-ready keywords for trending and niche community tags.",
},
});
// Technical/production analysis
const technicalResult = await getSummaryAndTags(assetId, {
tone: "professional",
promptOverrides: {
task: "Analyze cinematography, lighting, and production techniques.",
title: "Describe the production style or filmmaking technique.",
description: "Provide a technical breakdown of camera work, lighting, and editing.",
keywords: "Use industry-standard production terminology.",
},
});Available override sections:
| Section | Description |
|---|---|
task |
Main instruction for what to analyze |
title |
Guidance for generating the title |
description |
Guidance for generating the description |
keywords |
Guidance for generating keywords/tags |
qualityGuidelines |
General quality instructions |
Each override can be a simple string (replaces the section content) or a full PromptSection object for advanced control over XML tag names and attributes.
Returned by all workflows in the usage field:
interface TokenUsage {
inputTokens?: number; // Tokens in the input prompt
outputTokens?: number; // Tokens generated in the output
totalTokens?: number; // Total tokens consumed
reasoningTokens?: number; // Chain-of-thought reasoning tokens
cachedInputTokens?: number; // Input tokens served from cache
metadata?: {
assetDurationSeconds?: number;
thumbnailCount?: number;
};
}Returned by translateCaptions and translateAudio:
interface LanguageCodePair {
iso639_1: string; // Two-letter code (e.g., "en", "es") — use for Mux/browser players
iso639_3: string; // Three-letter code (e.g., "eng", "spa") — use for ElevenLabs
}