Add subtitles to videos
🤖/video/subtitle adds subtitles and closed captions to videos.

This Robot supports SRT, VTT, ASS, and the timestamped JSON output of /speech/transcribe with "format": "json". Pass the video as "video" and the transcript as "subtitles" in the use parameter. JSON words are grouped into captions and converted to ASS for burned word highlighting, or plain SRT cues for external subtitle streams. External JSON cues preserve casing and punctuation. Provider segments do not control caption breaks. SRT and VTT timing is preserved.
Without a preset or custom ffmpeg options, the legacy "flash" preset produces FLV output. With this default, burned subtitles use MP3 audio; external subtitles copy the input audio. Set "preset": "empty" to keep the input container, such as MP4 for an MP4 input.
When burning subtitles into HLG or PQ HDR video, the video is tone-mapped to SDR BT.709 before captions are drawn. Caption colors retain their intended RGB values, and the output is tagged as SDR. External subtitles preserve the source video’s HDR colors. SDR inputs keep their existing processing. If ffmpeg.pre:filter:v supplies its own tone mapping, that filter controls color conversion.
Usage example
Transcribe an uploaded video into timestamped words, then burn in uppercase social captions with the spoken word highlighted. Captions normally contain up to three words, breaking at sentence endings and pauses longer than 0.4 seconds. Social phrasing can use one extra word to keep a short phrase together or avoid a sentence-final orphan:
{
"steps": {
"captioned": {
"ffmpeg_stack": "v7",
"preset": "empty",
"robot": "/video/subtitle",
"style": "social",
"subtitles_type": "burned",
"use": {
"steps": [
{
"as": "video",
"name": ":original"
},
{
"as": "subtitles",
"name": "transcript"
}
]
}
},
"transcript": {
"format": "json",
"provider": "aws",
"robot": "/speech/transcribe",
"source_language": "en-US",
"use": ":original"
}
}
}Parameters
interpolateboolean | Record<string, boolean>Controls whether Assembly Variables are interpolated for individual instruction fields.
By default, most Robot instruction fields interpolate Assembly Variables. Set this to
falseto treat every instruction field as literal text, or set an individual field path tofalseto treat only that field as literal text. For Robot-specific fields that are literal by default, set this totrueor set that field path totrueto opt back into interpolation.Use field names such as
path, or dotted paths such asffmpeg.vffor nested objects.output_metaRecord<string, boolean> | boolean | Array<string>Allows you to specify a set of metadata that is more expensive on CPU power to calculate, and thus is disabled by default to keep your Assemblies processing fast.
For images, you can add
"has_transparency": truein this object to extract if the image contains transparent parts and"dominant_colors": trueto extract an array of hexadecimal color codes from the image.For images, you can also add
"blurhash": trueto extract a BlurHash string — a compact representation of a placeholder for the image, useful for showing a blurred preview while the full image loads.For images,
"thumbhash": trueinstead extracts a base64-encoded ThumbHash intometa.thumbhash, together withmeta.has_alpha(whether an alpha channel exists, even when fully opaque). It describes EXIF-oriented pixels and uses the first frame of animated images. Extraction is best-effort: images above 40 megapixels, unsupported formats, or a failed/bounded decode produce no placeholder. Successful extraction adds a metadata charge equivalent to 20% of that file's bytes. No ThumbHash surcharge applies when disabled or when no hash is produced.Set this option on the Step producing the image, such as
/upload/handlefor uploaded originals or/image/resizefor processed outputs. Setting it only on/transloadit/storedoes not request extraction: storage preserves the producer's metadata. Transloadit Storage persists a generated hash with its immutable version and returns it in stored results and native asset reads. Direct S3 uploads do not generate placeholders. A placeholder contains image information, so protect it with the same access controls as the full image.For videos, you can add the
"colorspace": trueparameter to extract the colorspace of the output video.For videos, you can also add
"interlaced": trueto detect whether the video is interlaced. This combines the cheap ffprobefield_orderflag with a boundedidetsampling pass over the first frames of the source, exposinginterlaced,field_order, and a diagnosticinterlace_detectionobject underfile.meta. This is computationally expensive and billed accordingly.For audio, you can add
"mean_volume": trueto get a single value representing the mean average volume of the audio file.You can also set this to
falseto skip metadata extraction and speed up transcoding.user_metaRecord<string, any>(default:{})Adds custom JSON metadata to each emitted file without changing its contents. Nested objects and arrays are supported.
Inheritance depends on the Robot. Values merge with existing
user_metaon the output file; the current Step replaces matching top-level keys. Assign required keys explicitly when a Robot creates fresh outputs.In processing Steps,
${file.*}refers to the first input and${result.*}to the emitted file. Values are evaluated per output after the Robot runs, before subsequent metadata extraction and temporary storage. On:original, values are evaluated per upload before metadata extraction.Downstream Steps read
${file.user_meta.key}. See Custom metadata for a complete example and inheritance rules.resultboolean(default:false)Whether the results of this Step should be present in the Assembly Status JSON
queuebatchSetting the queue to 'batch', manually downgrades the priority of jobs for this step to avoid consuming Priority job slots for jobs that don't need zero queue waiting times
force_acceptboolean(default:false)Force a Robot to accept a file type it would have ignored.
By default, Robots ignore files they are not familiar with. 🤖/video/encode, for example, will happily ignore input images.
With the
force_acceptparameter set totrue, you can force Robots to accept all files thrown at them. This will typically lead to errors and should only be used for debugging or combatting edge cases.ignore_errorsboolean | Array<meta | execute>(default:[])Ignore errors during specific phases of processing.
Setting this to
["meta"]will cause the Robot to ignore errors during metadata extraction.Setting this to
["execute"]will cause the Robot to ignore errors during the main execution phase.Setting this to
trueis equivalent to["meta", "execute"]and will ignore errors in both phases.usestring | Array<string> | Array<object> | objectSpecifies which Step(s) to use as input.
- You can pick any names for Steps except
":original"(reserved for user uploads handled by Transloadit) - You can provide several Steps as input with arrays:
{ "use": [ ":original", "encoded", "resized" ] } - You can also tag input Steps with
asto pass semantic intent to robots:{ "use": [ { "name": ":original", "as": "image" }, { "name": ":original", "as": "mask" } ] }
TipThat's likely all you need to know about
use, but you can view Advanced use cases.- You can pick any names for Steps except
ffmpegobjectA parameter object to be passed to FFmpeg. If a preset is used, the options specified are merged on top of the ones from the preset. For available options, see the FFmpeg documentation. Options specified here take precedence over the preset options.
ffmpeg_stackv6 | v7 | v8 | string(default:"v6.0.0")Selects the FFmpeg stack version to use for encoding. We currently recommend using "v7". The exact versions "v6.0.0", "v7.0.0", and "v8.0.0" are legacy values that remain accepted for backward compatibility. Deprecated "v5.x" values are also accepted.
widthstring | number | nullWidth of the new video, in pixels.
If the value is not specified and the
presetparameter is available, thepreset's supplied width will be implemented.heightstring | number | nullHeight of the new video, in pixels.
If the value is not specified and the
presetparameter is available, thepreset's supplied height will be implemented.presetandroid | android-high | android-low | android_high | android_low | dash-1080p-video | dash-1080p_video |Converts a video according to pre-configured settings.
You can use the value
'empty'here if you specify your own FFmpeg parameters using the Robot or do not want Transloadit to set any encoding settings.subtitles_typeburned | external | burn(default:"external")Determines if subtitles are added as a separate stream to the video (value
"external") that then can be switched on and off in your video player, or if they should be burned directly into the video (value"burned"or"burn") so that they become part of the video stream.stylesocialApplies a ready-made appearance for burned subtitles: heavy Poppins text, a thick outline, and a soft black shadow. JSON captions render in uppercase in the lower third. By default, JSON font sizing uses the shorter side of the video and fits each caption within the safe width, preferring one line and using at most two. Words are never split. Each caption grows from 85% to full size over 120 milliseconds, then its words stay in place while the highlight changes color. The transcript file is preserved. Existing styling parameters override the preset; an explicit
font_sizedisables automatic fitting. ASS files retain their own styles. With timestamped JSON from/speech/transcribe, the active word is highlighted until the next word or caption starts; other words usefont_color. Trailing commas and periods are omitted from social JSON captions; question marks and exclamation marks are preserved. Word highlighting and grouping require JSON input.highlight_colorstring | string(default:"FFD60A")The accent color of the active word in JSON subtitles. Only applies to burned JSON subtitles. With
"style": "social", the previous word stays highlighted through short gaps until the next word or caption starts. Without the social style, gaps usefont_colorfor the entire caption.Use
"rgb","rrggbb", or"aarrggbb"format, optionally prefixed with#. Three-digit colors are expanded to six digits. Colors without alpha are fully opaque. For eight-digit colors, the first two digits specify transparency ("00"is fully opaque,"ff"is fully transparent).max_words_per_captionstring | number(default:3)The target maximum number of timestamped JSON words shown in a caption. Captions break earlier at sentence-ending punctuation or pauses longer than 0.4 seconds. With
"style": "social", a trailing English function word such as “by,” “to,” or “the” moves to the next caption when possible. Short phrases such as “line of code” can stay together, and a sentence-final single word can join the previous caption. These adjustments allow at most one extra word per caption, without crossing sentence endings or long pauses. Single-word captions are preserved when the limit is 1. A caption stays visible until the next caption starts when the pause is at most 0.4 seconds, and disappears at its last word’s end during longer pauses. Does not change SRT, VTT, or ASS cues.border_stylebox | outline | shadow(default:"outline")Specifies the style of the subtitle. Use the
border_colorparameter to specify the color of the border.border_colorstring | string(default:"000000")The color for the subtitle border. The default is opaque black. Only applies to burned subtitles.
Use
"rgb","rrggbb", or"aarrggbb"format, optionally prefixed with#. Three-digit colors are expanded to six digits. Colors without alpha are fully opaque. For eight-digit colors, the first two digits specify transparency ("00"is fully opaque,"ff"is fully transparent).fontstringThe font family to use. Also includes boldness and style of the font. The default is Arial. With
"style": "social", the default is Poppins, using its ExtraBold weight for JSON captions. An explicit font overrides the style.Here is a list of all supported fonts.
font_colorstring | string(default:"FFFFFF")The color of the subtitle text. Only applies to burned subtitles.
Use
"rgb","rrggbb", or"aarrggbb"format, optionally prefixed with#. Three-digit colors are expanded to six digits. Colors without alpha are fully opaque. For eight-digit colors, the first two digits specify transparency ("00"is fully opaque,"ff"is fully transparent).font_sizestring | numberSpecifies the size of the text in the 288-pixel subtitle coordinate space, scaled with the video height. The default is 16. With
"style": "social", the default is 54. For social JSON captions, an omitted value scales this default with the shorter side of the video, increases it by 18% for portrait video, and shrinks captions as needed to fit at most two lines. Poppins capital letters are about 7.5% of the shorter side before fitting, or about 9% for portrait video. An explicit value keeps height-relative sizing and disables automatic fitting.boldbooleanSpecifies whether the subtitle text should be bold. Only applies to burned subtitles. The default is
false. With"style": "social", the default istrue; an explicitfalseoverrides the style.italicboolean(default:false)Specifies whether the subtitle text should be italic. Only applies to burned subtitles.
outline_widthstring | number | nullSpecifies the width of the text outline in the 288-pixel subtitle coordinate space, scaled with the video height. Numeric strings such as
"4"are also accepted. Only applies to burned subtitles with"outline"or"box"border styles. If omitted ornull, the default width is used (2 with"style": "social", proportional to the final fitted font size for social JSON captions).positionbottom | bottom-left | bottom-right | center | left | right | top |(default:"bottom")Specifies the position of the subtitles. With
"style": "social", the default bottom position places JSON captions in the lower third, with extra bottom clearance on portrait video. An explicit value overrides the style.languagestring | nullSpecifies the language of the subtitle stream using a three-letter language code (e.g.
"eng","deu","spa"). Only used if the subtitles are external. When adding multiple subtitle streams, set a different language for each stream so that video players can offer language selection.namestring | nullSpecifies a human-readable name for the subtitle track (e.g.
"English","Deutsch","Español"). Only used if the subtitles are external. This name is displayed in video player subtitle menus alongside the language.keep_subtitlesboolean(default:false)Specifies if existing subtitles in the input file should be kept or be replaced by the new subtitle. Only used if the subtitles are external. Set this to
truewhen chaining multiple/video/subtitleSteps to add several subtitle streams (e.g. different languages) to the same video.
Demos
Related blog posts
- Easily burn subtitles into videos with our new Robot
- Re-Loadit: simplify uploads with /upload/handle Robot
- New feature: auto-transcribe videos with subtitles
- Build a Reddit video subtitling bot with Transloadit
- Creating engaging audio visualizations with Transloadit
- Styling subtitles with Transloadit: 3 creative ways
- What’s new in video: transitions, chapters, CMAF, and more
- Social-style captions with the spoken word highlighted