# Video OCR - Extract On-Screen Text from Video (Captions) (`kantolabs/video-ocr`) Actor

Video OCR extracts text from video files - burned-in subtitles, captions, ad hook text, titles, lower-thirds, slide text - as timed segments with frame images, a transcript and an SRT/VTT track. Chains with Video Frame Extractor. $2 per 1,000 frames with text; frames without text are free.

- **URL**: https://apify.com/kantolabs/video-ocr.md
- **Developed by:** [Kanto Labs](https://apify.com/kantolabs) (community)
- **Categories:** Videos, AI, Automation
- **Security:** Limited permissions
- **Stats:** 3 total users, 2 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.60 / 1,000 frames

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

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

### What does Video OCR do?

**Video OCR** reads the on-screen text of a video file so you can **extract text from video** without watching it: **burned-in subtitles**, **captions**, ad **hook text**, titles, **lower-thirds** (names and job titles), slide and screen-recording text, prices and product names. You give it direct video links (or the frames of a [Video Frame Extractor](https://apify.com/kantolabs/video-frame-extractor) run) and get back every distinct piece of text **once**, with the time it appears and disappears, where it sits on the screen, a frame image, an LLM-ready transcript and an **SRT/VTT on-screen text track**.

A caption shown for 4 seconds is one row, not four: the same text in the same place over consecutive frames is merged into one **text segment**, even when OCR misreads a letter in one frame or the caption builds up word by word.

### Why use this video OCR API?

- **Video-native, not image OCR.** One run decodes the video, samples a frame every second (your choice), skips frames that did not change, reads the text and merges it over time. Image OCR actors make you do all of that yourself.
- **You only pay for frames that contain text.** Frames with no text, blank frames, unchanged frames, failed and refused videos are free. A video without any on-screen text costs nothing.
- **Timestamps you can trust**: `startSeconds` / `endSeconds` per segment, accurate to the sampling interval (1 s by default).
- **Ready for AI and automation**: `fullText` (one line per segment with its time), `hookText` (text shown in the first 3 seconds), SRT and VTT files, clean JSON for n8n, Make, Zapier or your own code.
- **No API keys, no data sent to third parties**: open-source PP-OCR models (Apache-2.0) run inside the Actor.
- **Chains with Video Frame Extractor**: pick a finished Video Frame Extractor dataset and the text of those frames is read with their original timestamps.

### Use cases

- **Burned-in subtitles to text / SRT**: recover subtitles that only exist inside the picture (hardsubs), e.g. to translate or index them.
- **Ad and UGC teardown**: pull the hook text, claims, prices and call-to-action overlays of ad creatives and short-form videos you have the files for.
- **Lower-thirds and names in news, interviews and webinars**: who appears when, with their title.
- **Slides and screen recordings**: searchable text of every screen, with time ranges.
- **Product text in videos**: brand names, packaging text, prices, phone numbers and URLs shown on screen.
- **Video search and RAG**: put the on-screen text next to the transcript so an LLM sees what the viewer reads.

### How to extract text from a video

1. Paste one or more **direct video file links** into **Video URLs** (MP4, MOV, WEBM, MKV, AVI, M4V, MPEG-TS; your own storage, S3/R2/GCS, an Apify key-value store, Dropbox or Google Drive "anyone with the link" share links).
2. Choose how often a frame is read (**every 1 s** is right for subtitles and captions; 0.5 s for fast word-by-word captions; 2-5 s for slides and long screen recordings).
3. Pick the **text language** (script) and, optionally, a **text area** (bottom = subtitles and lower-thirds only).
4. Click **Start**. Text segments appear in the **On-screen text** view; the **Videos** view has the transcript and SRT/VTT links.

Pages of video platforms (YouTube, TikTok, Instagram, Facebook, X, Vimeo, Twitch...) are refused politely, free of charge: their terms forbid downloading. Use files you are allowed to process.

### Input example

```json
{
  "videoUrls": ["https://example.com/ads/summer-sale.mp4"],
  "sampleIntervalSeconds": 1,
  "ocrLanguage": "en",
  "textRegion": "full"
}
```

Chain from Video Frame Extractor: leave **Video URLs** empty and pick its run's dataset in **Video Frame Extractor dataset**, or paste its frame links into **Frame image URLs**.

### Output example

One row per text segment:

```json
{
  "type": "text",
  "fileName": "sample-ocr-captions.mp4",
  "segmentIndex": 5,
  "text": "There are 3 types of
spectra that we can use",
  "startSeconds": 10,
  "endSeconds": 13,
  "durationSeconds": 3,
  "timecode": "00:00:10.000",
  "endTimecode": "00:00:13.000",
  "timestampSeconds": 10,
  "frameUrl": "https://api.apify.com/v2/key-value-stores/.../records/ca66bc8e-sample-ocr-captions-0007-000010000ms.jpg?signature=...",
  "confidence": 0.9906,
  "box": { "x": 0.1444, "y": 0.6695, "width": 0.7097, "height": 0.0812 },
  "position": "bottom",
  "lineCount": 2,
  "framesSeen": 2,
  "isPersistent": false
}
```

`fullText` of the same video (transcript style, one line per segment):

```text
[0:01] If you do a quick search for spectroscopy data,
[0:04] there are numerous ways that the data can appear.
[0:07] Those variations are based on the source of the cosmic light.
[0:10] There are 3 types of spectra that we can use
[0:13] Continuous Spectrum
```

Plus one summary row per video (`type: "video"`) with `status` (`ok`, `no-text`, `failed`, `skipped`), `segmentCount`, `framesWithText` (the frames charged), `hookText`, `fullText`, `srtUrl`, `vttUrl`, and `frames[]` with `index`, `timestampSeconds`, `timecode`, `url` and `text` of every frame that had text - the same field names as Video Frame Extractor, so the two join on `timestampSeconds`.

`box` is in fractions of the frame (0-1). `isPersistent` marks text that stays on screen for most of the video (watermarks, channel logos, "Artist's concept" labels) so you can filter it out.

### How much does it cost to extract text from a video?

Pay per event: **$0.002 per frame with text** ($2 per 1,000 frames), plus Apify's tiny actor-start fee. Store discounts: Silver $0.0018, Gold $0.0016 per frame.

**Free:** frames where no text was found, blank frames, frames identical to the previous one, failed or refused videos. A video with no on-screen text at all costs $0.

Worked examples, measured on real videos (1 frame per second unless noted):

| Video | Frames read | Frames with text | Cost |
|---|---|---|---|
| 30-second vertical explainer, captions most of the time (the built-in sample) | 29 | 24 | $0.048 |
| 37-second promo with burned-in English subtitles | 32 | 32 | $0.064 |
| 71-second vertical promo: big title text + name lower-thirds | 70 | 44 | $0.088 |
| 33-second TV ad, text only on a magazine cover and the end card | 30 | 7 | $0.014 |
| 60-second interview, no on-screen text | 60 | 0 | **$0.00** |
| 3.6-minute screen recording, one frame every 2 s (unchanged screens skipped) | 26 | 26 | $0.052 |
| 18.7-minute subtitled documentary (600 frames spread over the video) | 387 | 382 | $0.76 |

**Max frames per video** (default 600) caps the cost of one video: longer videos are sampled evenly instead. **Max cost per run** is respected exactly: reading stops when the budget is reached, the video is marked `truncated`, and later videos get a free `skipped` row.

### Languages

`en` reads English and also Chinese and Japanese. Other options: Latin with accents (French, German, Spanish, Portuguese, Italian, Czech, Polish...), Cyrillic (Russian, Ukrainian, Bulgarian...), Korean, Arabic, Devanagari (Hindi, Marathi...), Greek, Thai.

### Tips for best results

- **Subtitles only**: use **Text area = bottom** - signs, logos and UI text elsewhere in the picture are ignored and the run is faster. For small or thin subtitles also try **Minimum confidence 0.6**.
- **Word-by-word TikTok captions**: set **Read a frame every** to 0.5 s; the growing caption is merged into one segment with its full sentence.
- **Slides and screen recordings**: 2-5 s is enough and much cheaper; unchanged screens are skipped for free anyway.
- **Stylised fonts** can produce letter errors (for example 0 vs O). Check `confidence` and `framesSeen`; a text seen in many frames is reliable.

### Limits

- Text accuracy is that of a fast CPU OCR model: clean captions, titles and lower-thirds read very well; tiny UI text, handwriting, heavily stylised or moving 3D text can be misread or missed.
- Timing is accurate to the sampling interval (a caption shown for less than one interval can be missed).
- Text that moves (scrolling credits, tickers, tiles sliding across the screen) or sits in a busy moving scene (street signs) produces several short segments instead of one.
- Videos up to 120 minutes / 2 GB by default (raise the limits up to 600 minutes / 4 GB).

### Related Kanto Labs actors

- [Video Frame Extractor](https://apify.com/kantolabs/video-frame-extractor) - keyframes, scenes, thumbnails and evenly spaced frames from any video file. Chain its dataset into Video OCR.
- [Video to Slides](https://apify.com/kantolabs/video-to-slides) - unique slides of a lecture or webinar as images + PDF with slide text.
- [Video to LLM Context](https://apify.com/kantolabs/video-to-llm-context) - keyframes per scene + transcript, packaged for an LLM.
- [Video Scene Detection & Shot List](https://apify.com/kantolabs/video-scene-detection) - every cut with timecodes, a shot list and CSV/EDL exports, to pair on-screen text with each shot.
- [Video Ad Creative Breakdown](https://apify.com/kantolabs/video-creative-breakdown) - on-screen text plus speech, shots, pacing and CTA in one breakdown per ad or short video.
- [AI Image Caption & Alt Text Generator](https://apify.com/kantolabs/image-caption-generator) - captions, alt text and tags for images and video frames, to describe what is shown next to the text that is read.

### FAQ

#### Is this the same as transcribing a video?

No. Transcription turns **speech** into text. Video OCR reads the **text shown on screen**. Use both for a complete picture: the transcriber for what is said, Video OCR for what is written.

#### Can I use YouTube or TikTok links?

No. Those platforms forbid downloading in their terms. Use the original files (your own uploads, ad library exports you are allowed to use, your storage).

#### Does it work from n8n, Make or Zapier?

Yes. Run the Actor (or a saved task) through the Apify integration and read the dataset; the `fullText` and `srtUrl` fields are ready to send to an LLM, a sheet or a subtitle tool.

#### What happens when a video has no text?

It gets a `no-text` summary row and is not charged.

#### Something went wrong?

Every problem (404, a web page instead of a file, too large, unreadable file) gets a clear `error` in its row and is free. If a result looks wrong, open an Issue with the video link and we will look at it.

# Actor input Schema

## `videoUrls` (type: `array`):

Direct links to video files (MP4, MOV, WEBM, MKV, AVI, M4V, MPEG-TS...), one per line: your own storage, S3/R2/GCS, CDNs, an Apify key-value store, Dropbox or Google Drive share links ('anyone with the link'). Pages of video platforms (YouTube, TikTok, Instagram, Vimeo...) are refused. Leave the sample in place only if you want it read.

## `sampleIntervalSeconds` (type: `number`):

How often a frame is read. 1 s catches every subtitle and caption; 0.5 s suits fast word-by-word TikTok captions; 2-5 s is cheaper for slides, screen recordings and long videos. Frames identical to the previous one are skipped for free.

## `ocrLanguage` (type: `string`):

Script of the on-screen text. `en` also reads Chinese and Japanese. Use `latin` for French, German, Spanish, Portuguese, Czech, Polish and other accented Latin languages, `cyrillic` for Russian, Ukrainian, Bulgarian.

## `textRegion` (type: `string`):

Where to look for text. `bottom` = lower 45 % of the frame (subtitles, captions, lower-thirds) and skips signs and logos elsewhere; `top` = upper 45 % (titles, news tickers); `custom` uses "Custom text area". A smaller area is also faster.

## `cropBox` (type: `object`):

Used when "Text area" is `custom`. In percent of the frame: {"x": 0, "y": 70, "width": 100, "height": 30} reads only the bottom 30 %.

## `minConfidence` (type: `number`):

Text lines read with a lower OCR confidence (0.5-0.99) are dropped. Lower it (0.6) for small or stylised text, raise it (0.85) to drop noise from busy backgrounds.

## `minTextLength` (type: `integer`):

Ignore text blocks shorter than this (letters and digits only), e.g. stray single letters on a T-shirt.

## `datasetId` (type: `string`):

Chain from Kanto Labs Video Frame Extractor (or any dataset with image links in `frames[].url`, `url` or `imageUrl`): pick the dataset of a finished run and the text of those frames is read and merged, keeping the original timestamps.

## `frameUrls` (type: `array`):

Image links (JPG, PNG, WEBP) to read instead of, or as well as, videos - for example the frame links of a Video Frame Extractor run. Their timestamps are taken from Video Frame Extractor file names; other images are read in the given order.

## `maxFramesPerVideo` (type: `integer`):

Upper limit of frames read per video. When a video would need more (long video, short interval), frames are spread evenly over the whole video instead. Caps the cost per video at this many frames x the frame price.

## `maxVideoMinutes` (type: `integer`):

Longer videos are refused (free).

## `maxVideoSizeMb` (type: `integer`):

Larger files are refused (free).

## `downloadTimeoutSecs` (type: `integer`):

Give up on a video that does not download within this time (free).

## Actor input object example

```json
{
  "videoUrls": [
    "https://api.apify.com/v2/key-value-stores/aXcXwfChlioPWAUJv/records/sample-ocr-captions.mp4?signature=ImU3YcEhw0ddoYvT7QD5"
  ],
  "sampleIntervalSeconds": 1,
  "ocrLanguage": "en",
  "textRegion": "full",
  "minConfidence": 0.7,
  "minTextLength": 2,
  "maxFramesPerVideo": 600,
  "maxVideoMinutes": 120,
  "maxVideoSizeMb": 2000,
  "downloadTimeoutSecs": 600
}
```

# Actor output Schema

## `results` (type: `string`):

No description

## `files` (type: `string`):

No description

# 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 = {
    "videoUrls": [
        "https://api.apify.com/v2/key-value-stores/aXcXwfChlioPWAUJv/records/sample-ocr-captions.mp4?signature=ImU3YcEhw0ddoYvT7QD5"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("kantolabs/video-ocr").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 = { "videoUrls": ["https://api.apify.com/v2/key-value-stores/aXcXwfChlioPWAUJv/records/sample-ocr-captions.mp4?signature=ImU3YcEhw0ddoYvT7QD5"] }

# Run the Actor and wait for it to finish
run = client.actor("kantolabs/video-ocr").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 '{
  "videoUrls": [
    "https://api.apify.com/v2/key-value-stores/aXcXwfChlioPWAUJv/records/sample-ocr-captions.mp4?signature=ImU3YcEhw0ddoYvT7QD5"
  ]
}' |
apify call kantolabs/video-ocr --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,kantolabs/video-ocr"
        }
    }
}
```

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/JM3eH0vA55NN9r4hF/builds/ajDCxBkfuDYCDOalG/openapi.json
