> ## Documentation Index
> Fetch the complete documentation index at: https://docs.camb.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Subtitles

> Generate cue-ready subtitles in one or more languages using the Python or TypeScript SDK

## Overview

Turn spoken media into cue-ready subtitles in the source language and any number of target languages, in a single job. The service transcribes the source, translates into each target language you request, and, when you ask for it, segments every language into cues sized for on-screen display. The pipeline is asynchronous: submit a job with a publicly accessible or signed media URL, poll until it completes, then retrieve the result for each language.

### Prerequisites

<Steps>
  <Step title="Create an account">
    Sign up at [CAMB.AI Studio](https://studio.camb.ai) if you haven't already.
  </Step>

  <Step title="Get your API key">
    Go to **Settings → API Keys** in Studio and copy your key. See [Authentication](/getting-started/authentication) for details.
  </Step>

  <Step title="Install the SDK">
    <CodeGroup>
      ```bash Python theme={null}
      pip install camb-sdk
      ```

      ```bash TypeScript theme={null}
      npm install @camb-ai/sdk
      ```
    </CodeGroup>

    Skip this step if you're using the [direct API](/tutorials/direct-api).
  </Step>

  <Step title="Set your API key to use in your code">
    ```bash theme={null}
    export CAMB_API_KEY="your_api_key_here"
    ```
  </Step>
</Steps>

***

## Code

<CodeGroup>
  ```python Python theme={null}
  import os
  import time
  from camb.client import CambAI
  from camb.types.language_enums import Languages

  client = CambAI(api_key=os.getenv("CAMB_API_KEY"))

  def create_subtitles():
      # Step 1: Submit the subtitle job
      response = client.subtitles.create_subtitle(
          source_language=Languages.EN_US,  # English (US)
          target_languages=[Languages.ES_ES],  # Spanish (Spain)
          media_url="https://example.com/meeting.mp3",
          transcription_mode="fast",
      )

      task_id = response.task_id
      print(f"Subtitle task created: {task_id}")

      # Step 2: Poll until complete
      while True:
          status = client.subtitles.get_subtitle_task_status(task_id=task_id)
          print(f"Status: {status.status}")

          if status.status == "SUCCESS":
              # Step 3: Retrieve the result for one target language
              result = client.subtitles.get_subtitle_result_for_language(
                  run_id=status.run_id,
                  language=Languages.ES_ES,
              )
              transcript = result["transcript"] if isinstance(result, dict) else result.transcript
              for segment in transcript[:3]:
                  print(f"[{segment['start']:.2f}-{segment['end']:.2f}] {segment['speaker']}: {segment['text']}")
              break
          elif status.status == "ERROR":
              print(f"Subtitle task failed: {status.exception_reason}")
              break

          time.sleep(5)

  create_subtitles()
  ```

  ```javascript TypeScript theme={null}
  import { CambClient, CambApi } from '@camb-ai/sdk';

  const client = new CambClient({
      apiKey: process.env.CAMB_API_KEY
  });

  async function createSubtitles() {
      // Step 1: Submit the subtitle job
      const response = await client.subtitles.createSubtitle({
          source_language: CambApi.Languages.EN_US, // English (US)
          target_languages: [CambApi.Languages.ES_ES], // Spanish (Spain)
          media_url: 'https://example.com/meeting.mp3',
          transcription_mode: 'fast'
      });

      const taskId = response.task_id;
      console.log(`Subtitle task created: ${taskId}`);

      // Step 2: Poll until complete
      while (true) {
          const status = await client.subtitles.getSubtitleTaskStatus({
              task_id: taskId
          });

          console.log(`Status: ${status.status}`);

          if (status.status === 'SUCCESS') {
              // Step 3: Retrieve the result for one target language
              const result = await client.subtitles.getSubtitleResultForLanguage({
                  run_id: status.run_id,
                  language: CambApi.Languages.ES_ES
              });

              for (const segment of result.transcript.slice(0, 3)) {
                  console.log(
                      `[${segment.start.toFixed(2)}-${segment.end.toFixed(2)}] ${segment.speaker}: ${segment.text}`
                  );
              }
              break;
          } else if (status.status === 'ERROR') {
              console.error(`Subtitle task failed: ${status.exception_reason}`);
              break;
          }

          await new Promise(resolve => setTimeout(resolve, 5000));
      }
  }

  createSubtitles();
  ```
</CodeGroup>

<Note>
  In the Python SDK, subtitle results come back as a plain dict (`result["transcript"]`), not a typed model like transcription's `get_transcription_result`. The `isinstance` check above handles that shape safely.
</Note>

### Generating multiple languages

List every locale you need in `target_languages` to translate and segment all of them from the same media URL:

<CodeGroup>
  ```python Python theme={null}
  response = client.subtitles.create_subtitle(
      source_language=Languages.EN_US,
      target_languages=[Languages.ES_ES, Languages.FR_FR],
      media_url="https://example.com/meeting.mp3",
  )
  ```

  ```javascript TypeScript theme={null}
  const response = await client.subtitles.createSubtitle({
      source_language: CambApi.Languages.EN_US,
      target_languages: [CambApi.Languages.ES_ES, CambApi.Languages.FR_FR],
      media_url: 'https://example.com/meeting.mp3'
  });
  ```
</CodeGroup>

Include the source language itself in `target_languages` if you also want same-language, subtitle-ready cues for the original speech.

To fetch every language on the run in one call instead of one at a time, drop the `language` argument:

<CodeGroup>
  ```python Python theme={null}
  result = client.subtitles.get_subtitle_result(run_id=status.run_id)
  ```

  ```javascript TypeScript theme={null}
  const result = await client.subtitles.getSubtitleResult({ run_id: status.run_id });
  ```
</CodeGroup>

With a single target language, this returns the same shape as `get_subtitle_result_for_language`. With more than one, it returns a result keyed by language.

***

## Subtitle Formatting

By default, target-language dialogue is translated but left unsegmented, the same speech-boundary segments you'd get from Translation, not subtitle-length cues. Pass `formatting_options` to segment every language into cues sized for on-screen display, and export directly as `srt` or `vtt`:

<CodeGroup>
  ```python Python theme={null}
  from camb.types.subtitle_formatting_options import SubtitleFormattingOptions

  formatting_options = SubtitleFormattingOptions(
      max_segment_duration_in_seconds=6,
      max_characters_in_segment=42,
  )

  response = client.subtitles.create_subtitle(
      source_language=Languages.EN_US,
      target_languages=[Languages.ES_ES],
      media_url="https://example.com/meeting.mp3",
      formatting_options=formatting_options,
  )
  ```

  ```javascript TypeScript theme={null}
  const response = await client.subtitles.createSubtitle({
      source_language: CambApi.Languages.EN_US,
      target_languages: [CambApi.Languages.ES_ES],
      media_url: 'https://example.com/meeting.mp3',
      formatting_options: {
          max_segment_duration_in_seconds: 7, // Default: 7 seconds; caps each subtitle segment length.
          min_segment_duration_in_seconds: 1, // Default: 1 second; avoids very short subtitle flashes.
          max_characters_in_segment: 42, // Default: 42 characters; keeps subtitle lines readable.
      }
  });
  ```
</CodeGroup>

* **`max_segment_duration_in_seconds`** (default `7.0`): the longest a single cue may stay on screen.
* **`min_segment_duration_in_seconds`** (default `1.0`): the shortest a single cue may stay on screen.
* **`max_characters_in_segment`** (default `42`): the maximum number of characters allowed in one cue.

Once the task succeeds, request the export directly by passing `format_type` (and optionally `data_type`) to the result call. The response then carries a single `transcript` string instead of a segment array:

<CodeGroup>
  ```python Python theme={null}
  result = client.subtitles.get_subtitle_result_for_language(
      run_id=status.run_id,
      language=Languages.ES_ES,
      format_type="vtt",
      data_type="raw_data",
  )
  print(result["transcript"] if isinstance(result, dict) else result.transcript)
  ```

  ```javascript TypeScript theme={null}
  const result = await client.subtitles.getSubtitleResultForLanguage({
      run_id: status.run_id,
      language: CambApi.Languages.ES_ES,
      format_type: CambApi.TranscriptFileFormat.Vtt,
      data_type: CambApi.TranscriptDataType.RawData
  });
  console.log(result.transcript);
  ```
</CodeGroup>

<Info>
  Need a plain transcript in a single language instead of translated, multi-language subtitles? Use the [Transcription](/tutorials/transcription-with-sdk) tutorial. It shares the same `formatting_options` field for subtitle-ready cues.
</Info>

***

## Parameters

### Required

| Parameter          | Type                | Description                                                                                                                                                                                       |
| ------------------ | ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `source_language`  | `Languages` enum    | Language spoken in the source media (e.g. `Languages.EN_US`). The raw API also accepts locale-tag strings like `"en-us"`; numeric IDs still work but are deprecated. See [Languages](#languages). |
| `target_languages` | list of `Languages` | One or more output languages. Pass a single-item list when you only need one language (for example `[Languages.ES_ES]`).                                                                          |
| `media_url`        | string              | Publicly accessible or signed URL to the audio/video file. Local file upload is not supported for subtitles — host the file first.                                                                |

### Optional

| Parameter             | Type                                       | Description                                                                                                                                           |
| --------------------- | ------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
| `project_name`        | string                                     | Label for the job in your dashboard                                                                                                                   |
| `project_description` | string                                     | Additional notes for the job                                                                                                                          |
| `folder_id`           | integer                                    | Place the run inside a specific folder                                                                                                                |
| `transcription_mode`  | `fast` or `slow`                           | Choose a faster transcript or a more thorough pass for the source media; defaults to `fast`                                                           |
| `formatting_options`  | `SubtitleFormattingOptions` object or dict | Segments every language into subtitle-ready cues instead of leaving translated dialogue unsegmented. See [Subtitle Formatting](#subtitle-formatting). |

### Result shape

`get_subtitle_result_for_language` and `get_subtitle_result` return a `transcript` array for each language, with the same segment shape as transcription:

| Field     | Type   | Description                                    |
| --------- | ------ | ---------------------------------------------- |
| `start`   | float  | Segment start time in seconds                  |
| `end`     | float  | Segment end time in seconds                    |
| `text`    | string | Transcribed or translated text for the segment |
| `speaker` | string | Speaker label (e.g. `Speaker 1`)               |

When a run has more than one language and you call `get_subtitle_result` without a `language`, the result is keyed by language instead of being a single `transcript` array. When you pass `format_type` and/or `data_type`, every returned language carries a single `transcript` string (the rendered `srt`/`vtt`/`txt` content or a presigned URL to it) instead of a segment array.

***

## Languages

The Python and TypeScript SDKs expect the `Languages` enum for both `source_language` and `target_languages`:

| Language         | Enum              | Locale (raw API) |
| ---------------- | ----------------- | ---------------- |
| English (US)     | `Languages.EN_US` | `en-us`          |
| Spanish (Spain)  | `Languages.ES_ES` | `es-es`          |
| French (France)  | `Languages.FR_FR` | `fr-fr`          |
| German (Germany) | `Languages.DE_DE` | `de-de`          |
| Mandarin Chinese | `Languages.ZH_CN` | `zh-cn`          |
| Japanese (Japan) | `Languages.JA_JP` | `ja-jp`          |
| Arabic           | `Languages.AR_SA` | `ar-sa`          |

If you're calling the API directly (not via the SDK), pass the locale tag. Numeric language IDs still work but are deprecated.

<Card icon="languages" href="/api-reference/endpoint/get-target-languages" arrow={true} cta="Browse all languages">
  For the full list of supported source and target languages, see the Source Languages and Target Languages reference.
</Card>

***

## Tips

* **Supported formats:** `.mp3`, `.wav`, `.aac`, `.flac`, `.mp4`, `.mov`, and `.mxf` (MXF is enterprise-only). For best quality use a lossless format like WAV or FLAC.
* **`media_url` is required:** host the recording and pass a publicly accessible or signed URL. Local file upload is not supported on the subtitle endpoint.
* **`target_languages` is required and non-empty:** always pass it as a list, even for a single language.
* **Polling timeout:** for long media, cap your polling loop (e.g. 60 attempts x 5s = 5 minutes) and handle the timeout gracefully.
* **Pick the right source language:** specifying the correct source language significantly improves both transcription and translation accuracy.

***

## Next Steps

<CardGroup cols={2}>
  <Card title="Create Subtitle API" icon="book" href="/api-reference/endpoint/create-subtitle">
    Full API reference for the subtitle endpoint.
  </Card>

  <Card title="Poll Subtitle Result" icon="clock" href="/api-reference/endpoint/poll-subtitle-result">
    Status polling endpoint reference.
  </Card>

  <Card title="Get Subtitle Run Result" icon="captions" href="/api-reference/endpoint/get-subtitle-run-result">
    Retrieve subtitle segments and timing data for every language on a run.
  </Card>

  <Card title="Transcription" icon="captions" href="/tutorials/transcription-with-sdk">
    Transcribe a single language, with the same subtitle-ready formatting options.
  </Card>
</CardGroup>
