# Script to Subtitle Aligner (SRT/VTT + Misread Check) (`gaocun/script-subtitle-aligner`) Actor

Give an audio or video URL and its script. Get SRT/VTT subtitles timed to each spoken word, a report of skipped and misread lines, and an optional MP4 with burned-in karaoke subtitles. 18 languages, including Chinese, Japanese and Korean. Batch many files. Without a script, it transcribes.

- **URL**: https://apify.com/gaocun/script-subtitle-aligner.md
- **Developed by:** [gaocun](https://apify.com/gaocun) (community)
- **Categories:** Videos, AI
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $20.00 / 1,000 media minutes

This Actor is paid per event and usage. You are charged both the fixed price for specific events and for Apify platform usage.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Script to Subtitle Aligner

### What does Script to Subtitle Aligner do?

**Script to Subtitle Aligner** turns an audio or video file and its script into **SRT and VTT subtitles** that start and stop with the real speech. It also gives a **report of the script lines that were skipped or misread**. It can also make an **MP4 with the subtitles burned in**, with a karaoke highlight on each word. If you have no script, the Actor transcribes the media to subtitles.

To try it, click **Start**. The prefilled example uses a short speech clip and its script, and it finishes in less than a minute.

- **Subtitles timed to each spoken word.** The Actor uses the word times of the speech recognizer, not an even split of the text.
- **Skipped and misread line report.** You see which lines nobody said and which lines have words that do not match the speech.
- **Your text, not the recognizer's text.** The subtitles show your script exactly, with your punctuation and your line breaks.
- **Video with burned-in subtitles.** Get an MP4 for TikTok, Reels, or Shorts. In karaoke mode, each word turns yellow when it is said.
- **Batch.** Give many files in one run, each with its own script and language. A file that fails does not stop the batch.
- **Share links work.** You can use Dropbox and Google Drive share links.
- **18 languages,** including Chinese (Simplified and Traditional), Japanese, Korean, English, Spanish, French, and German.
- **No external AI API.** Whisper runs inside the Actor. Your media does not go to a third-party AI service.
- **All Apify platform features:** API access, schedules, webhooks, and integrations with Make, Zapier, and other tools.

#### Who uses Script to Subtitle Aligner?

- Creators who subtitle AI-generated video, voice-overs, and text-to-speech output.
- Dubbing and localization teams who check that a voice actor read every line correctly.
- Teams that time an existing transcript to an audiobook, a podcast, a course, or a training video.

### How to use Script to Subtitle Aligner

1. Put a link to your audio or video file in **Media URL**. Use a direct file link, or a Dropbox or Google Drive share link.
2. Paste your script in **Script**. Put each subtitle on its own line.
3. Select the **Spoken language**. Auto detection also works, but it can fail when the media starts with music.
4. Optional: in **Video with subtitles**, select **Plain subtitles** or **Karaoke**.
5. Click **Start**.
6. When the run ends, download `subtitles.srt`, `subtitles.vtt`, or `video.mp4` from the **Output** tab. Read `missingLines` and `misreadLines` to see which lines to check.

For many files, use **Batch** in place of **Media URL** and **Script**. See [Batch](#batch).

### How much does it cost to make subtitles?

The Actor uses the **pay per event + usage** model:

- **$0.02 per started minute of media** (event `media-minute`). A 10-minute video costs $0.20.
- **Platform usage** for the run. The default run uses 4096 MB of memory. In our test, an 11-second clip used 0.02 compute units. Transcription runs about 3 times faster than real time, so a 10-minute file uses about 0.25 compute units. The price of a compute unit depends on your Apify plan.

A failed run (bad URL, no speech, no audio track) is not charged per minute. In a batch, a file that fails is not charged.

**Video with subtitles** has no extra event price, but it uses more platform usage, because the Actor encodes the video again. Plan for an encode time of up to the length of the video for 1080p at the default memory. For videos longer than 20 minutes, set the memory to 8192 MB or more.

### Input

| Field | Required | Description |
| --- | --- | --- |
| `mediaUrl` | Yes, if no `batch` | Direct http(s) link to an audio or video file (mp4, mov, mkv, webm, mp3, wav, m4a, and other formats), or a Dropbox or Google Drive share link. Maximum length is 60 minutes. Maximum size is 2 GB. |
| `script` | No | The spoken text, in order, one subtitle per line. Leave empty to transcribe only. |
| `language` | No | The spoken language. Default is auto detection. For Chinese, select Simplified or Traditional to set the characters of the transcript. |
| `videoWithSubtitles` | No | `off` (default), `plain`, or `karaoke`. Makes `video.mp4` with the subtitles burned in. The media must have a picture. |
| `batch` | No | A list of files. Each item has `mediaUrl`, and optional `script` and `language`. When you use `batch`, the Actor ignores `mediaUrl` and `script`. |

```json
{
  "mediaUrl": "https://raw.githubusercontent.com/openai/whisper/main/tests/jfk.flac",
  "script": "And so, my fellow Americans,\nask not what your country can do for you,\nask what you can do for your country.",
  "language": "en"
}
```

### Output

The Actor writes two subtitle files in the key-value store, `subtitles.srt` and `subtitles.vtt`. With `videoWithSubtitles`, it also writes `video.mp4`. It also writes one dataset item and the same data in the `OUTPUT` record. This is the output for the input above:

```json
{
  "mediaUrl": "https://raw.githubusercontent.com/openai/whisper/main/tests/jfk.flac",
  "mode": "align",
  "timing": "measured",
  "tokenErrorRate": 0.0,
  "missingLines": [],
  "misreadLines": [],
  "language": "en",
  "durationSeconds": 11.0,
  "billedMinutes": 1,
  "srtUrl": "https://api.apify.com/v2/key-value-stores/.../records/subtitles.srt",
  "vttUrl": "https://api.apify.com/v2/key-value-stores/.../records/subtitles.vtt",
  "transcript": "And so my fellow Americans, ask not what your country can do for you. Ask what you can do for your country.",
  "cues": [
    { "index": 0, "startMs": 0, "endMs": 2120, "text": "And so, my fellow Americans,", "matchedTokens": 5, "expectedTokens": 5 },
    { "index": 1, "startMs": 3260, "endMs": 7440, "text": "ask not what your country can do for you,", "matchedTokens": 9, "expectedTokens": 9 },
    { "index": 2, "startMs": 8020, "endMs": 10340, "text": "ask what you can do for your country.", "matchedTokens": 8, "expectedTokens": 8 }
  ]
}
```

| Field | Meaning |
| --- | --- |
| `mode` | `align` when you gave a script. `transcribe` when you did not. |
| `timing` | `measured`: each line edge comes from the speech. `estimated`: the script and the speech differ too much for exact edges, so the Actor splits the measured speech window by line length. `asr`: transcription only. |
| `tokenErrorRate` | Edit distance divided by the number of script tokens. This is the character error rate for Chinese, Japanese, and Korean, and the word error rate for other languages. Punctuation is not counted. |
| `missingLines` | Script lines that nobody said. These lines get no subtitle. A skipped line does not change the timing of the other lines. |
| `misreadLines` | Script lines that were said, but with one or more tokens that do not match the speech. Each item gives `matchedTokens` and `expectedTokens`. |
| `cues[].matchedTokens` | Tokens of the line that the speech matched exactly. A value lower than `expectedTokens` shows a misread or a skipped word. |
| `videoUrl` | The link to the MP4 with burned-in subtitles. Only when `videoWithSubtitles` is not `off`. |

#### Batch

Give the files in `batch`. Each file can have its own script and language. If an item has no language, the Actor uses `language`.

```json
{
  "videoWithSubtitles": "karaoke",
  "batch": [
    { "mediaUrl": "https://example.com/episode-1.mp4", "script": "First line
Second line", "language": "en" },
    { "mediaUrl": "https://example.com/episode-1-es.mp4", "script": "Primera línea
Segunda línea", "language": "es" }
  ]
}
```

The Actor processes the files one by one. It writes one dataset item for each file, with `item` set to the position of the file in `batch` (1 is the first file). The files of item 1 are `subtitles-1.srt`, `subtitles-1.vtt`, and `video-1.mp4`. If a file fails, its item has an `error` field, and the Actor continues with the next file. A batch run does not write the `OUTPUT` record.

### How does the alignment work?

1. The Actor downloads the file and decodes the audio.
2. Whisper (`small` model, on the Actor's CPU) transcribes the speech and gives a time for each word.
3. The Actor aligns the transcript to your script as one token stream with edit distance. Chinese, Japanese, and Korean use one token per character. Other languages use one token per word.
4. Each line gets the time of its first and last matched token.
5. For the video, each word of the line gets the time of its matched word. A word that the speech did not match gets a time between its neighbours. FFmpeg draws the subtitles with libass and encodes H.264 video and AAC audio.

The segment cuts of the speech recognizer do not change the result. One recognized segment can hold two lines, and one line can be split over two segments.

### Other Actors from the same author

| Actor | What it does |
| --- | --- |
| [AI Video Defect Scanner](https://apify.com/gaocun/ai-video-defect-scanner) | Checks a batch of videos for black frames, frozen frames, silent or missing audio, wrong resolution, and corrupted data. |

### FAQ

#### How accurate are the subtitle times?

In our test with 40 Chinese text-to-speech lines, the median error was 0.17 s at the start of a line and 0.06 s at the end. The largest error was 0.45 s. Results for real voices with music or noise can be less accurate.

#### Why is a correct line in `misreadLines`?

The speech recognizer can write a word that sounds the same as your word. In Chinese, this happens most with characters that have the same sound, for example 在 and 再. Listen to the line before you ask for a new recording.

#### What happens when the script and the speech are very different?

If `tokenErrorRate` is higher than 0.5, `timing` is `estimated`. The Actor then splits the measured speech window by line length. Check that the script belongs to the file and that the language is correct.

#### What are the limits?

- The media must have a speech track. A file with music only fails with "No speech was found".
- The maximum length is 60 minutes and the maximum size is 2 GB.
- For many files, use `batch`. The run time limit applies to the whole batch.
- **Video with subtitles** needs a picture. An audio file fails with a clear message and is not charged.
- In the video, the Actor breaks a long line into lines of about equal width. This also works for Chinese and Japanese, which have no spaces. A short line is easier to read: use about 16 characters or fewer for each line of a portrait video.

#### Can I use the Actor from my own code?

Yes. Use the [Apify API](https://docs.apify.com/api/v2) or the Apify client for JavaScript or Python. The **API** tab of this Actor shows ready-to-use code.

#### Where can I report a problem?

Open an issue in the **Issues** tab of this Actor. Include the run ID, so we can find the run log.

# Actor input Schema

## `mediaUrl` (type: `string`):

Direct http(s) link to an audio or video file (mp4, mov, mkv, webm, mp3, wav, m4a, ...). Dropbox and Google Drive share links also work. Other links must return the file itself, not a web page. Maximum length is 60 minutes.

## `script` (type: `string`):

The text that is spoken, in order. Put each subtitle on its own line. Leave empty to only transcribe the media.

## `language` (type: `string`):

Set the language when you know it. Auto detection can fail when the media starts with music or silence.

## `videoWithSubtitles` (type: `string`):

Also make an MP4 with the subtitles burned into the picture. Karaoke turns each word yellow when it is said. The media must have a picture.

## `batch` (type: `array`):

Optional. Process many files in one run. Each item has mediaUrl, and optional script and language. When you use Batch, the Actor ignores Media URL and Script. Example: \[{"mediaUrl": "https://example.com/ep1.mp4", "script": "Line one\nLine two", "language": "en"}]

## Actor input object example

```json
{
  "mediaUrl": "https://raw.githubusercontent.com/openai/whisper/main/tests/jfk.flac",
  "script": "And so, my fellow Americans,\nask not what your country can do for you,\nask what you can do for your country.",
  "language": "auto",
  "videoWithSubtitles": "off"
}
```

# Actor output Schema

## `report` (type: `string`):

One item for each media file, with the timing mode, the error rate, the skipped and misread lines, the file links, and all subtitle cues.

## `srt` (type: `string`):

The subtitles in SubRip format. For a batch, use srtUrl of each item in the report.

## `vtt` (type: `string`):

The subtitles in WebVTT format. For a batch, use vttUrl of each item in the report.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "mediaUrl": "https://raw.githubusercontent.com/openai/whisper/main/tests/jfk.flac",
    "script": `And so, my fellow Americans,
ask not what your country can do for you,
ask what you can do for your country.`
};

// Run the Actor and wait for it to finish
const run = await client.actor("gaocun/script-subtitle-aligner").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "mediaUrl": "https://raw.githubusercontent.com/openai/whisper/main/tests/jfk.flac",
    "script": """And so, my fellow Americans,
ask not what your country can do for you,
ask what you can do for your country.""",
}

# Run the Actor and wait for it to finish
run = client.actor("gaocun/script-subtitle-aligner").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "mediaUrl": "https://raw.githubusercontent.com/openai/whisper/main/tests/jfk.flac",
  "script": "And so, my fellow Americans,\\nask not what your country can do for you,\\nask what you can do for your country."
}' |
apify call gaocun/script-subtitle-aligner --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,gaocun/script-subtitle-aligner"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/rOeF9wSkergISxSuW/builds/nNLhnAirlrzLTZNNS/openapi.json
