# Short Video Transcriber - TikTok, Reels & Shorts to Text, SRT (`sauliusautomatesit/short-video-transcriber`) Actor

Transcribe TikTok videos and YouTube Shorts to text, timestamped segments, SRT and VTT. Uses each platform's native captions when they exist and falls back to Whisper speech-to-text when they don't. No API key, no cookies.

- **URL**: https://apify.com/sauliusautomatesit/short-video-transcriber.md
- **Developed by:** [Saulius Saulenas](https://apify.com/sauliusautomatesit) (community)
- **Categories:** Videos, AI, Social media
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.70 / 1,000 transcript from native captions

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?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Short Video Transcriber — TikTok, Reels & Shorts to Text, SRT

**TikTok and YouTube Shorts to transcript, SRT and VTT. Native captions when they exist, Whisper when they don't.**

Paste short-form video URLs, get back the words. Every video comes back as plain text, timestamped
segments, a ready-to-use `.srt` subtitle file, a `.vtt` file, and the basic source metadata —
author, caption, hashtags, duration, language.

The Actor reads each platform's own caption track first and only runs Whisper speech-to-text on
videos that have none. That is why it is fast, and why videos with captions cost a fraction of
what a transcribe-everything tool charges.

- **No API key, no login, no cookies.**
- **90+ languages.** The transcript comes back in the video's own language by default.
- **Bulk-safe.** One dead link never breaks the rest of the run.
- **You only pay for delivered transcripts.** Failures, private videos and over-length videos are free.

***

### Supported platforms

| Platform | Video URLs | Native captions | Whisper fallback |
|---|---|---|---|
| **TikTok** | `tiktok.com/@user/video/123…`, `vm.tiktok.com/…` share links | ✅ creator captions and TikTok's own auto-captions | ✅ |
| **YouTube Shorts** | `youtube.com/shorts/ID`, `youtu.be/ID`, `youtube.com/watch?v=ID` | ✅ creator captions and YouTube auto-captions | ✅ |
| Instagram Reels | — | — | — |

Instagram Reels URLs are recognised and rejected with a clear message. Reels support ships as its
own Actor; this one does not pretend to handle it.

***

### What you get

One dataset row per video:

```json
{
  "url": "https://www.youtube.com/shorts/BDf5mOfSkkw",
  "platform": "youtube_shorts",
  "videoId": "BDf5mOfSkkw",
  "author": "UCUvUKGDBCqJzV4OwJkQWCHg",
  "authorName": "King Arthur Baking Company",
  "title": "The Only Sourdough Recipe You'll Ever Need",
  "hashtags": ["baking", "sourdough"],
  "durationSeconds": 15.0,
  "language": "en",
  "source": "native_captions",
  "text": "I'm going to show you how to make the only sourdough bread that you need to know…",
  "segments": [
    { "start": 0.0,  "end": 1.14, "text": "I'm going to show you how to make the" },
    { "start": 1.14, "end": 2.46, "text": "only sourdough bread that you need to" }
  ],
  "srt": "1\n00:00:00,000 --> 00:00:01,140\nI'm going to show you how to make the\n\n2\n…",
  "vtt": "WEBVTT\n\n00:00:00.000 --> 00:00:01.140\nI'm going to show you how to make the\n\n…",
  "wordCount": 62,
  "segmentCount": 9,
  "isAutoGenerated": true,
  "sourceDetail": "youtube_shorts_auto_captions",
  "viewCount": 184231,
  "publishedAt": "2026-02-14T10:03:11+00:00",
  "status": "ok",
  "error": null
}
```

`source` is always exactly one of:

- `native_captions` — the words came from the platform's own caption track.
- `whisper_asr` — the video had no usable captions, so the audio was transcribed with Whisper.

The `srt` and `vtt` fields are complete, valid subtitle files. Overlapping caption cues from
auto-generated tracks are trimmed, so subtitles do not double up on screen.

***

### How it works

For every URL:

1. **Identify the platform** and reject anything that is not a single short-form video, with a
   specific reason.
2. **Resolve the video** — title, author, hashtags, duration, view count.
3. **Check the duration** against your cap before doing anything expensive.
4. **Try native captions.** If the platform has a usable track, normalise it into text, timed
   segments, SRT and VTT. Charged as `caption_transcript`.
5. **Otherwise download the audio and run Whisper** (faster-whisper, CPU, int8). Charged as
   `asr_transcript`.

A video is charged for exactly one of those two events, after the row is written — never both,
never before.

***

### Pricing

| Event | Price | When it fires |
|---|---|---|
| `caption_transcript` | **$0.002** | A transcript was produced from the platform's native captions. |
| `asr_transcript` | **$0.040** | A transcript was produced by Whisper because there were no captions. |
| `apify-actor-start` | **$0.00005** | Apify's standard start event, charged once per gigabyte of the run's memory. This Actor runs at 2 GB, so **$0.0001 per run**. |

Those are the Free-plan prices. Apify's paid plans get its standard Store discount off every
event — Bronze 5 %, Silver 10 %, Gold and above 15 % — so `asr_transcript` costs $0.038, $0.036 or
$0.034 on those plans and `caption_transcript` costs $0.0019, $0.0018 or $0.0017.

Whisper costs about a second of CPU for every 1.5 seconds of audio, which is why it is priced
twenty times the caption rate. Most videos never touch it.

**Nothing else is charged.** In particular you are not charged for:

- videos that are deleted, private, region-locked or age-restricted;
- videos longer than your `maxDurationSeconds` cap;
- caption-less videos longer than `maxAsrDurationSeconds` (they return `ASR_DURATION_EXCEEDED`);
- videos where Whisper finds no speech at all (music-only clips);
- malformed URLs or unsupported platforms;
- any run that fails.

#### What a run actually costs

Caption availability is what moves the bill. On short-form video it varies a lot by niche —
talking-head, educational and news content is largely captioned; dance, music and meme content
often is not.

| 1,000 videos | Cost |
|---|---|
| All have captions | **$2.00** |
| 90 % captions / 10 % Whisper | **$5.80** |
| 70 % captions / 30 % Whisper | **$13.40** |
| None have captions | **$40.00** |

(Free-plan prices; a Gold plan pays 15 % less than each of these.)

Turn **"Transcribe with Whisper when there are no captions"** off to cap the price at $0.002 per
video: caption-less videos then return a `CAPTIONS_UNAVAILABLE` error, free of charge, and you can
decide what to do with them.

***

### Input

| Field | Type | Default | What it does |
|---|---|---|---|
| `videoUrls` | array | — | **Required.** TikTok video URLs and YouTube Shorts URLs. |
| `language` | string | *(blank)* | Two-letter code (`en`, `es`, `pt`…). Blank keeps the video's own language — usually what you want for a transcript. Setting it selects that language's caption track when the platform offers one (which may be a translation) and tells Whisper what to expect. |
| `allowWhisperFallback` | boolean | `true` | Off = captions only, and caption-less videos cost nothing. |
| `maxDurationSeconds` | integer | `600` | Videos longer than this are skipped, free. Max `1200`. |
| `maxAsrDurationSeconds` | integer | `180` | Length limit for the Whisper path only. Caption-less videos longer than this return `ASR_DURATION_EXCEEDED`, free. Captioned videos are unaffected. |
| `maxVideos` | integer | `1000` | Safety cap on the number of URLs processed. |
| `forceWhisper` | boolean | `false` | Transcribe with Whisper even when captions exist. Charged at the `asr_transcript` rate. |
| `whisperModel` | string | `base` | `tiny`, `base` or `small`. Only affects the Whisper fallback. |
| `beamSize` | integer | `1` | Whisper decoding beam width. |
| `concurrency` | integer | `5` | Videos resolved in parallel. Whisper always runs one at a time. |
| `includeFailedItems` | boolean | `true` | Keep a row (with an `error` code) for every failure, so inputs reconcile to outputs. |
| `useResidentialFallback` | boolean | `true` | Retries through a residential IP when a platform answers the normal proxy with a bot check. **YouTube needs this.** |
| `proxyConfiguration` | object | Apify Proxy | Recommended. Used for the first attempt on every request. |

#### Minimal input

```json
{
  "videoUrls": [
    "https://www.tiktok.com/@nasa/video/7189113838831488302",
    "https://www.youtube.com/shorts/BDf5mOfSkkw"
  ]
}
```

***

### Run it

#### In the Apify Console

Open the Actor, paste your URLs into **Video URLs**, one per line, and click **Start**. Results
appear in the dataset and can be exported as JSON, CSV, XLSX or Excel — or you can download the
`srt` column straight into a subtitle file.

#### API

```bash
curl -X POST "https://api.apify.com/v2/acts/YOUR_USERNAME~short-video-transcriber/run-sync-get-dataset-items?token=YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
        "videoUrls": [
          "https://www.tiktok.com/@nasa/video/7189113838831488302",
          "https://www.youtube.com/shorts/BDf5mOfSkkw"
        ],
        "maxDurationSeconds": 600
      }'
```

#### JavaScript

```js
import { ApifyClient } from 'apify-client';

const client = new ApifyClient({ token: 'YOUR_TOKEN' });

const run = await client.actor('YOUR_USERNAME/short-video-transcriber').call({
    videoUrls: [
        'https://www.tiktok.com/@nasa/video/7189113838831488302',
        'https://www.youtube.com/shorts/BDf5mOfSkkw',
    ],
});

const { items } = await client.dataset(run.defaultDatasetId).listItems();

for (const item of items) {
    if (item.status !== 'ok') {
        console.warn(`${item.inputUrl}: ${item.error}`);
        continue;
    }
    console.log(`${item.author} (${item.source}): ${item.text}`);
    // Save subtitles next to your video files:
    // await fs.writeFile(`${item.videoId}.srt`, item.srt);
}
```

#### Python

```python
from apify_client import ApifyClient

client = ApifyClient("YOUR_TOKEN")

run = client.actor("YOUR_USERNAME/short-video-transcriber").call(input={
    "videoUrls": [
        "https://www.tiktok.com/@nasa/video/7189113838831488302",
        "https://www.youtube.com/shorts/BDf5mOfSkkw",
    ],
    "allowWhisperFallback": True,
})

for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    if item["status"] != "ok":
        print(f"{item['inputUrl']}: {item['error']} — {item['errorMessage']}")
        continue
    print(item["author"], item["language"], item["wordCount"], "words")
    with open(f"{item['videoId']}.srt", "w", encoding="utf-8") as handle:
        handle.write(item["srt"])
```

#### n8n, Make and Zapier

The Actor works with the standard **Apify → Run Actor** node in n8n and the **Apify → Run an
Actor** module in Make. Point the node at this Actor, pass `videoUrls` as the input JSON, then read
the dataset items in the next step — `text` for the transcript, `srt` for a subtitle file, `error`
to branch on failures.

A common shape: a spreadsheet or webhook supplies URLs → this Actor returns transcripts → an LLM
node summarises, translates or tags them → the result is written back. Because failures come back
as rows with an `error` field rather than as a broken run, a filter on `status == "ok"` is all the
error handling most workflows need.

***

### Errors

Failed videos get a dataset row with `status: "error"`, an `error` code and a plain-English
`errorMessage`. They are never charged.

| Code | Meaning |
|---|---|
| `VIDEO_UNAVAILABLE` | Deleted, never existed, or region-locked. |
| `VIDEO_PRIVATE` | Private, friends-only or followers-only. |
| `DURATION_EXCEEDED` | Longer than `maxDurationSeconds`. |
| `ASR_DURATION_EXCEEDED` | No captions, and longer than `maxAsrDurationSeconds`. Raise that limit to transcribe it anyway. |
| `CAPTIONS_UNAVAILABLE` | No native captions and the Whisper fallback is turned off. |
| `NO_SPEECH_DETECTED` | Whisper found no speech — a music-only or silent video. |
| `UNSUPPORTED_PLATFORM` | Not a TikTok or YouTube Shorts URL. |
| `INVALID_URL` | Not a link to a single video (a profile or channel URL, for example). |
| `MEDIA_UNAVAILABLE` | The audio could not be downloaded for transcription. |
| `BLOCKED` / `RATE_LIMITED` | The platform refused the request. Try again, ideally with a proxy. |
| `MEMORY_LIMIT` | The video does not fit in the run's memory for the chosen model. Raise the memory or pick a smaller model. |

***

### Limits and good to know

- **Short-form only.** The default cap is 600 seconds and the maximum is 1200. For podcasts,
  webinars and long YouTube videos, use a general-purpose transcriber instead.
- **Memory.** Run with at least 2 GB. That covers the whole duration range with the `base` model;
  the Actor refuses an impossible combination up front instead of being killed mid-run.
- **Auto-generated captions are auto-generated.** When a transcript comes from a platform's own ASR
  track (`isAutoGenerated: true`), it carries that track's mistakes. For the highest accuracy on a
  specific video, turn captions off in your workflow and let Whisper do it — or use the `small` model.
- **Public videos only.** No login, no cookies, no private content.
- **Caption availability changes.** Platforms backfill captions hours after upload, so the same
  video can take the Whisper path today and the caption path tomorrow.
- **Leave the residential retry on for YouTube.** YouTube refuses datacenter IPs outright ("Sign
  in to confirm you're not a bot"). The Actor retries those requests through a residential IP
  automatically; with `useResidentialFallback` off, YouTube Shorts return `LOGIN_REQUIRED`.
  TikTok does not need it.
- **Whisper is length-limited; captions are not.** A caption-less video is transcribed up to
  `maxAsrDurationSeconds` (180 s by default — the length of the longest YouTube Short, and longer
  than the overwhelming majority of TikToks). Past that, Whisper's cost outgrows a per-video
  price, so the video returns `ASR_DURATION_EXCEEDED` and costs you nothing. Raise the limit if
  you want those anyway. Videos **with** captions run to the full `maxDurationSeconds` at the
  $0.002 rate, whatever their length.

### What this Actor does not do

It transcribes videos. It does not scrape comments, profiles, followers or hashtags, does not
monitor accounts, and does not summarise, translate or analyse sentiment — pipe the transcript into
an LLM step for that.

***

### Related searches

video to text · video transcript · transcribe video · TikTok transcript · TikTok to text ·
YouTube Shorts transcript · Shorts to text · caption extractor · subtitle downloader ·
SRT generator · VTT subtitles · speech to text · Whisper transcription

# Actor input Schema

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

TikTok video URLs and YouTube Shorts URLs to transcribe. One URL per line — paste a single link or thousands. Share links (vm.tiktok.com/…) and youtu.be links work too.

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

Two-letter language code (`en`, `es`, `pt`, `de`, `hi`…). Leave blank to keep the video's own language — the default, and what you want for a transcript. Setting it picks that language's caption track when the platform offers one (which may be a translation) and tells Whisper what to expect.

## `allowWhisperFallback` (type: `boolean`):

On (default): videos without native captions are transcribed with Whisper speech-to-text, charged at the higher `asr_transcript` rate. Off: those videos return a `CAPTIONS_UNAVAILABLE` error and cost you nothing — the cheapest way to run large batches.

## `forceWhisper` (type: `boolean`):

Off (default): native captions are used whenever the platform has them. On: every video is transcribed with Whisper and charged at the `asr_transcript` rate — worth it only when you have judged a platform's own captions to be too poor for your use.

## `maxDurationSeconds` (type: `integer`):

Videos longer than this are skipped with a `DURATION_EXCEEDED` error and are not charged. This is a short-form product; the default of 600 s (10 min) covers TikTok's longest uploads.

## `maxAsrDurationSeconds` (type: `integer`):

Applies only to videos with no native captions. Whisper costs roughly a second of CPU per 1.5 seconds of audio, so a per-video price only works up to a point; past this length a caption-less video returns `ASR_DURATION_EXCEEDED` and costs nothing. The default of 180 s is the length of the longest YouTube Short. Videos **with** captions are never subject to this — they run to the full `maxDurationSeconds`.

## `maxVideos` (type: `integer`):

Safety cap on how many URLs from the list are processed.

## `whisperModel` (type: `string`):

Accuracy vs speed for the Whisper fallback only — videos with native captions ignore this. `base` is the recommended balance.

## `beamSize` (type: `integer`):

Decoding beam width for the Whisper fallback. 1 is fastest; higher is marginally more accurate and slower.

## `concurrency` (type: `integer`):

How many videos to resolve at once. Whisper transcription is always serialised regardless of this setting, to keep the run inside its memory limit.

## `includeFailedItems` (type: `boolean`):

On (default): unavailable, private, too-long or unsupported videos get a dataset row carrying an `error` code, so you can reconcile inputs to outputs. Failed rows are never charged.

## `useResidentialFallback` (type: `boolean`):

On (default): when a platform answers the normal proxy with a bot check, the request is retried through Apify's residential proxy. YouTube needs this — without it, YouTube Shorts fail with `LOGIN_REQUIRED`. Residential traffic is only used for requests the cheap route already refused.

## `proxyConfiguration` (type: `object`):

Proxy used to reach TikTok and YouTube. Apify Proxy is recommended; the Actor automatically retries on a direct connection when a proxy IP is refused.

## Actor input object example

```json
{
  "videoUrls": [
    "https://www.youtube.com/shorts/BDf5mOfSkkw",
    "https://www.tiktok.com/@happydoggoniall/video/7673926170710199572"
  ],
  "allowWhisperFallback": true,
  "forceWhisper": false,
  "maxDurationSeconds": 600,
  "maxAsrDurationSeconds": 180,
  "maxVideos": 1000,
  "whisperModel": "base",
  "beamSize": 1,
  "concurrency": 5,
  "includeFailedItems": true,
  "useResidentialFallback": true,
  "proxyConfiguration": {
    "useApifyProxy": true
  }
}
```

# Actor output Schema

## `transcripts` (type: `string`):

Every transcribed video, including rows for the ones that failed.

# 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://www.youtube.com/shorts/BDf5mOfSkkw",
        "https://www.tiktok.com/@happydoggoniall/video/7673926170710199572"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("sauliusautomatesit/short-video-transcriber").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://www.youtube.com/shorts/BDf5mOfSkkw",
        "https://www.tiktok.com/@happydoggoniall/video/7673926170710199572",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("sauliusautomatesit/short-video-transcriber").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://www.youtube.com/shorts/BDf5mOfSkkw",
    "https://www.tiktok.com/@happydoggoniall/video/7673926170710199572"
  ]
}' |
apify call sauliusautomatesit/short-video-transcriber --silent --output-dataset

```

## MCP server setup

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

```

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/wqJtWiW7CHc1CPa0A/builds/Mxnltl0gZoDkVafuY/openapi.json
