Search Documentation
Search across all documentation pages
Subtitles

Subtitles

Transcodely supports flexible subtitle processing as part of your transcoding outputs. You can pass through existing subtitle tracks, convert between formats, burn subtitles directly into the video, extract embedded subtitles as sidecar files, or generate captions automatically from the audio track.

Subtitle tracks are configured in the subtitle_tracks array of an Output Specification. Each output can include up to 8 subtitle tracks.


The SubtitleTrack object

AttributeTypeRequiredDescription
operationenumYesSubtitle operation. One of: passthrough, convert, burn_in, extract, generate. See Operations.
source_stream_indexintegerOne ofIndex of the subtitle stream in the input file (0—31). Mutually exclusive with source_url. Not used by generate (captions come from the audio).
source_urlstringOne ofURL to an external subtitle file. Supported schemes: gs://, s3://, https://. Mutually exclusive with source_stream_index. Max 2048 characters. Not used by generate.
input_formatenumNoInput subtitle format. One of: srt, webvtt, ttml, ass. Auto-detected when omitted.
output_formatenumNoOutput subtitle format. One of: srt, webvtt, ttml, ass. Required for convert. Optional for extract, where it specifies the sidecar file format. For generate, one of webvtt or srt (defaults to webvtt).
languagestringNoISO 639-2 three-letter language code (e.g., eng, spa, fra, deu). For generate, may also be auto (or omitted) to auto-detect the spoken language.
labelstringNoHuman-readable label (e.g., "English", "Spanish (Latin America)"). Max 64 characters.
is_defaultbooleanNoMark as the default subtitle track for the player.
hearing_impairedbooleanNoFlag for hearing-impaired / closed caption tracks (SDH).
forcedbooleanNoFlag for forced subtitles (e.g., foreign-language dialogue in an otherwise native-language film).
burn_in_styleobjectNoStyling for burn-in subtitles. Only valid when operation is burn_in. See Burn-in styling.
generate_chaptersbooleanNoWhen true on a track with operation generate (AI captions), also produce auto-generated chapter markers from the caption transcript. Opt-in and free. See Auto-chapters.
{
  "operation": "convert",
  "source_url": "gs://my-bucket/subs/english.srt",
  "input_format": "srt",
  "output_format": "webvtt",
  "language": "eng",
  "label": "English",
  "is_default": true
}

Operations

Each subtitle track requires exactly one operation that determines how Transcodely processes it.

OperationDescriptionSourceOutput
passthroughCopy the subtitle track to the output without modification.source_stream_index or source_urlPreserves original format.
convertConvert between subtitle formats.source_stream_index or source_urloutput_format required.
burn_inRender subtitles directly into the video frames. Produces a hard-subbed video — subtitles cannot be turned off by the viewer.source_stream_index or source_urlNo sidecar file produced. burn_in_style optional.
extractExtract an embedded subtitle stream as a standalone sidecar file.source_stream_index onlyoutput_format specifies the sidecar file format.
generateGenerate captions from the audio track using automatic speech recognition. Produces a sidecar caption file and, for streaming outputs, a subtitle rendition flagged as auto-generated.Audio track (no source_stream_index / source_url)output_format optional — webvtt (default) or srt.

Note: The extract operation only accepts source_stream_index since it extracts subtitles already embedded in the input file. Use convert with source_url for external subtitle files.


Generating captions (AI)

The generate operation transcribes the video’s audio track into a caption file. It is the one operation that needs no subtitle source — the captions come from the spoken dialogue itself. Add it to any output that has audio:

{
  "operation": "generate"
}

That is the whole minimal configuration. Everything else is optional:

  • language — omit it (or set "auto") to auto-detect the spoken language. Pass an explicit ISO 639-2 code (e.g. "eng", "deu") to force transcription in that language. auto is valid only for generate; the other operations reject it.
  • output_formatwebvtt (default) or srt. Rich formats (ttml, ass) are not offered for generated captions.
  • label, is_default — carried through to the manifest like any other track.
{
  "operation": "generate",
  "language": "auto",
  "output_format": "webvtt",
  "is_default": true
}

A few rules keep the behavior predictable:

  • One generate track per job. The audio is transcribed once, so a second generate track is rejected at job creation.
  • Never burned in. burn_in_style is invalid on a generate track. Generate the caption sidecar, then add a separate burn_in track if you also want hard-subs.
  • Composes with everything else. A generate track sits alongside convert/passthrough tracks of other languages, and works with HLS, DASH, and dual-format packaging.

Honesty markers

Generated captions are always labelled as machine-produced — Transcodely makes no accuracy guarantee:

  • The SubtitleResult carries auto_generated: true.
  • In HLS manifests the track is tagged CHARACTERISTICS="public.accessibility.transcribes-spoken-dialog" (the DASH equivalent is best-effort and may be HLS-only initially).

Pricing

AI captions are billed as an additive fee of €0.05 per minute of source video — metered on the input duration, charged once per job regardless of how many renditions you produce. It is added on top of the encode cost and the processing fee as a fees line item on the job (see Job.fees), and is additive to the processing fee: the two are separate services and neither absorbs the other. The fee is only confirmed if a caption artifact is actually produced — no artifact, no charge.


The SubtitleResult object

For every caption artifact it produces, the worker reports a SubtitleResult on the completed job’s subtitle_results array (mirroring thumbnail_results). These are read-only on job responses.

AttributeTypeDescription
output_idstringID (out_...) of the output the track was configured on.
operationenumThe operation that produced the artifact. One of generate, extract, convert.
formatenumCaption file format: webvtt, srt, ttml, or ass.
languagestringDetected (for generate with auto-detect) or configured ISO 639-2 language code.
labelstringHuman-readable label.
auto_generatedbooleantrue when produced by generate (automatic speech recognition).
storage_keystringCaption file key in the output destination.
urlstringFully-qualified URL to the caption file (same resolution rules as ThumbnailResult.url).
signed_urlstring | nullReady-to-fetch HTTPS URL for the caption file, signed and time-limited. Managed-storage destinations only, on Get and Watch but never List, and it expires. See Signed asset URLs.
transcript_storage_keystringgenerate only: key of the segment-level transcript JSON.
transcript_urlstringgenerate only: URL of the transcript JSON.
transcript_signed_urlstring | nullgenerate only: signed, time-limited HTTPS URL for the transcript JSON. Same rules and lifetime as signed_url.

Retro-captioning a hosted video

You do not have to re-encode a video to caption it. Point a captions-only job at an already-hosted video with input_video_id and a single generate track — no video variants, no output origin (the managed storage is inferred):

{
  "input_video_id": "vid_a1b2c3d4e5f6g7",
  "outputs": [
    {
      "subtitle_tracks": [
        { "operation": "generate" }
      ]
    }
  ]
}

The caption artifact is stored under the video’s managed storage and surfaced on the job’s subtitle_results. In the dashboard this is the Generate captions button on the video detail page. If the video’s original source was removed by a lifecycle rule, Transcodely transparently falls back to the highest-bitrate rendition as the transcription input.

Fixing a generated caption

Because captions are machine-produced, you may want to correct them. There is no special edit endpoint — use the tools that already exist:

  1. Download the caption file from SubtitleResult.url.
  2. Edit it in any subtitle editor.
  3. Re-attach it as a convert (or passthrough) track from source_url on your next job, replacing the generated one.

Formats

Transcodely supports four subtitle formats for input and output.

FormatAPI ValueExtensionDescription
SRTsrt.srtSubRip Text. The most common subtitle format. Plain text with timestamps. Widely supported by all players.
WebVTTwebvtt.vttWeb Video Text Tracks. Standard for HTML5 <video> and HLS/DASH streaming. Supports styling and positioning.
TTMLttml.ttmlTimed Text Markup Language. XML-based format used in broadcast, DASH, and IMSC1 (Netflix, Disney+).
ASSass.assAdvanced SubStation Alpha. Rich formatting with fonts, colors, and animations. Common in anime and fansubs.

Format compatibility

Not all format conversions are lossless. Converting from a rich format to a simpler one may discard styling information:

FromToNotes
srtwebvttLossless. SRT is a subset of WebVTT.
webvttsrtPositioning and styling cues are stripped.
asssrt / webvttRich formatting (fonts, colors, animations) is stripped.
ttmlsrt / webvttXML styling and layout is stripped.
srt / webvttttmlText content is preserved; no styling added.

Container compatibility

For passthrough and convert operations, the subtitle format muxed into the output must be compatible with the output container. For passthrough the relevant format is input_format; for convert it is output_format. Submitting an incompatible combination is rejected at job creation with a parameter_incompatible error.

ContainerAllowed subtitle formats
hlswebvtt
dashwebvtt, ttml
adaptivewebvtt
mp4srt, ttml
movsrt, ttml
webmsrt, webvtt
mkvsrt, ass

This check does not apply to burn_in (subtitles are rendered into the video stream) or extract (subtitles are written to a standalone sidecar file), and is skipped when the relevant format field is left unset.


Source options

Every subtitle track requires a source — either an embedded stream from the input file or an external subtitle file. The two modes are mutually exclusive.

Embedded stream

Use source_stream_index to reference a subtitle stream embedded in the input file by its zero-based index. Transcodely does not currently surface embedded subtitle streams through the API, so determine the stream index out-of-band (for example, by running ffprobe on the source file) before submitting the job.

{
  "operation": "passthrough",
  "source_stream_index": 0,
  "language": "eng",
  "label": "English"
}

External file

Use source_url to reference a standalone subtitle file by URL. Supported schemes: gs://, s3://, and https://. The subtitle file is downloaded and processed alongside the video.

{
  "operation": "convert",
  "source_url": "gs://my-bucket/subs/spanish.srt",
  "input_format": "srt",
  "output_format": "webvtt",
  "language": "spa",
  "label": "Spanish"
}

Burn-in styling

The burn_in_style object controls the appearance of burned-in subtitles. All fields are optional with sensible defaults.

AttributeTypeDefaultDescription
font_familystring"Arial"Font family name. Max 64 characters.
font_sizeinteger24Font size in pixels (8—120).
font_colorstring"#FFFFFF"Text color as hex (e.g., "#FFFFFF" for white).
outline_colorstring"#000000"Outline color as hex (e.g., "#000000" for black).
outline_widthinteger2Outline thickness in pixels (0—10). Set to 0 for no outline.
margin_bottominteger30Bottom margin in pixels (0—200). Distance from the bottom of the video frame.
{
  "burn_in_style": {
    "font_family": "Helvetica",
    "font_size": 28,
    "font_color": "#FFFF00",
    "outline_color": "#000000",
    "outline_width": 3,
    "margin_bottom": 40
  }
}

Tip: For maximum readability across different video content, use a white font with a black outline (outline_width of 2—3). Increase font_size for 4K outputs where the default 24px may appear small.


Auto-chapters

Auto-chapters turns your video’s spoken content into a set of named chapter markers — a title and a start/end time for each segment — so viewers can jump between sections of a longer video.

Auto-chapters is an add-on to AI captions. It is opt-in and only runs when you request generated captions (subtitle operation generate) and set generate_chapters to true on that track. It never runs on its own, and never on jobs where you don’t enable it. There is no additional charge — auto-chapters is bundled with AI captions for free.

Rolling out: auto-chapters requires the subtitle operation generate (AI-generated captions), which is being rolled out. Until generate is available on your account, the generate_chapters flag has no effect and the chapter_results array stays empty.

How it works

  1. You submit a subtitle track with operation generate and generate_chapters set to true.
  2. Transcodely generates the caption transcript on its own infrastructure in the EU (self-hosted speech-to-text — the audio is not sent to any third party).
  3. The resulting transcript text — never the audio or video itself — is processed by Anthropic to produce chapter titles and timestamps.
  4. Transcodely writes the chapters as a WebVTT chapters file and reports them on the job.

Subprocessor note: enabling auto-chapters sends caption transcript text to Anthropic, a third-party subprocessor. No other feature sends your content off Transcodely’s own infrastructure. See the Privacy Policy for the full list of subprocessors and the data each one receives.

Enabling auto-chapters

Set generate_chapters on a track whose operation is generate:

{
  "operation": "generate",
  "language": "eng",
  "label": "English",
  "generate_chapters": true
}

The ChapterResult object

Each generate track that produces chapters is reported as a transcodely.v1.ChapterResult in the job’s chapter_results array. Times are in seconds as JSON numbers (fractional seconds allowed), and all field names are snake_case.

AttributeTypeDescription
output_idstringThe out_ output the generate track was configured on.
languagestringISO 639-2 three-letter language code, matching the caption track’s language.
storage_keystringStorage key of the WebVTT chapters file in the output destination.
urlstringResolved URL for the chapters file (same resolution rules as the caption artifact URL).
chaptersarrayThe chapter markers, ordered by start time. Each entry has start_seconds, end_seconds, and title.
{
  "output_id": "out_a1b2c3d4e5f6g7",
  "language": "eng",
  "storage_key": "chapters/english.vtt",
  "url": "https://cdn.example.com/chapters/english.vtt",
  "chapters": [
    { "start_seconds": 0, "end_seconds": 42.5, "title": "Introduction" },
    { "start_seconds": 42.5, "end_seconds": 128, "title": "Getting Started" },
    { "start_seconds": 128, "end_seconds": 305.2, "title": "Advanced Usage" }
  ]
}

The equivalent WebVTT chapters file:

WEBVTT

00:00:00.000 --> 00:00:42.500
Introduction

00:00:42.500 --> 00:02:08.000
Getting Started

00:02:08.000 --> 00:05:05.200
Advanced Usage

Examples

Passthrough embedded subtitle

Pass through an embedded subtitle track from the input file into an HLS output without modification.

{
  "type": "hls",
  "video": [
    {"codec": "h264", "resolution": "1080p", "quality": "standard"}
  ],
  "subtitle_tracks": [
    {
      "operation": "passthrough",
      "source_stream_index": 0,
      "language": "eng",
      "label": "English",
      "is_default": true
    }
  ]
}

Convert SRT to WebVTT

Convert an external SRT file to WebVTT format for HLS streaming compatibility.

{
  "type": "hls",
  "video": [
    {"codec": "h264", "resolution": "1080p", "quality": "standard"}
  ],
  "subtitle_tracks": [
    {
      "operation": "convert",
      "source_url": "gs://my-bucket/subs/english.srt",
      "input_format": "srt",
      "output_format": "webvtt",
      "language": "eng",
      "label": "English",
      "is_default": true
    }
  ]
}

Burn-in with custom styling

Burn subtitles directly into the video frames with custom font, color, and positioning. The resulting video will have permanently visible subtitles.

{
  "type": "mp4",
  "video": [
    {"codec": "h264", "resolution": "1080p", "quality": "standard"}
  ],
  "subtitle_tracks": [
    {
      "operation": "burn_in",
      "source_url": "https://cdn.example.com/subs/spanish.srt",
      "language": "spa",
      "burn_in_style": {
        "font_family": "Helvetica",
        "font_size": 28,
        "font_color": "#FFFF00",
        "outline_color": "#000000",
        "outline_width": 3,
        "margin_bottom": 40
      }
    }
  ]
}

Multi-language streaming with SDH

An adaptive output carrying four subtitle tracks: English (default) and Spanish converted from SRT to WebVTT, an English SDH track flagged hearing_impaired, and a forced French track passed through from an embedded stream.

{
  "type": "adaptive",
  "video": [
    {"codec": "h264", "resolution": "1080p", "quality": "standard"},
    {"codec": "h264", "resolution": "720p", "quality": "standard"}
  ],
  "subtitle_tracks": [
    {
      "operation": "convert",
      "source_url": "gs://my-bucket/subs/en.srt",
      "output_format": "webvtt",
      "language": "eng",
      "label": "English",
      "is_default": true
    },
    {
      "operation": "convert",
      "source_url": "gs://my-bucket/subs/en-sdh.srt",
      "output_format": "webvtt",
      "language": "eng",
      "label": "English (SDH)",
      "hearing_impaired": true
    },
    {
      "operation": "convert",
      "source_url": "gs://my-bucket/subs/es.srt",
      "output_format": "webvtt",
      "language": "spa",
      "label": "Spanish"
    },
    {
      "operation": "passthrough",
      "source_stream_index": 2,
      "language": "fra",
      "label": "French (Forced)",
      "forced": true
    }
  ]
}

Validation rules

Transcodely validates subtitle track configuration at job creation time. The following constraints are enforced:

RuleDescription
Maximum 8 tracksEach output supports up to 8 subtitle_tracks entries.
Mutually exclusive sourcesource_stream_index and source_url cannot both be set on the same track. The passthrough and extract operations additionally require that exactly one is set.
extract requires stream indexThe extract operation only accepts source_stream_index, not source_url.
convert requires output formatThe convert operation requires output_format to be set.
Container compatibilityFor passthrough and convert, the subtitle format must be compatible with the output container. See Container compatibility.
burn_in_style requires burn-inThe burn_in_style object is only valid when operation is burn_in. Generated captions are never burned in.
Single generate trackAt most one generate track per job — the audio is transcribed once.
generate takes no sourceA generate track must not set source_stream_index or source_url; captions come from the audio.
generate output formatFor generate, output_format (if set) must be webvtt or srt.
Language formatlanguage must be a valid ISO 639-2 three-letter code matching ^[a-z]{3}$, or auto for the generate operation.
Stream index rangesource_stream_index must be between 0 and 31 (inclusive).
URL schemessource_url must use gs://, s3://, or https:// scheme. Max 2048 characters.
Label lengthlabel must not exceed 64 characters.