# Douyin 抖音视频转文字 Transcript Scraper - Speech to Text for AI (`lanternlab318/douyin-transcript`) Actor

Spoken words of any public Douyin video as plain text plus a timed transcript.

- **URL**: https://apify.com/lanternlab318/douyin-transcript.md
- **Developed by:** [Lantern Lab](https://apify.com/lanternlab318) (community)
- **Categories:** Social media, Videos, AI
- **Stats:** 9 total users, 8 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.99 / 1,000 transcripts

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

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

## Douyin 抖音视频转文字 Transcript Scraper

Turn any public Douyin (抖音) video into text: the **whole spoken transcript as one plain string** you can paste straight into a prompt, plus a **line-by-line transcript with a start and end second** for every line. Mandarin, **Cantonese (粤语)** and English come back labelled as what they are. Paste video links, `v.douyin.com` share links or bare IDs; no login, no cookies, no account of your own. **$1.99 per 1,000 transcripts plus $0.001 per 10 seconds of speech transcribed - about $4.99 per 1,000 for a typical 25-second talking video**, and nothing at all for a video with no speech in it. See [Pricing](#pricing).

Douyin itself captions almost nothing, so this Actor does the listening: an open-source speech model runs **inside the Actor container**. There is no speech API behind it, no API key to bring, and your audio and text never leave the run.

Input:

```json
{
  "videoUrls": [
    "https://www.douyin.com/video/7697968856207770723",
    "https://v.douyin.com/iJSMnqRm/"
  ],
  "includeSegments": true,
  "maxDurationSec": 600
}
```

Output - one row per video (`recordType: "transcript"`), plus one free row per input line and a `RUN_SUMMARY`. Sample from a real run; nickname partially masked:

```json
{
  "recordType": "transcript",
  "platform": "douyin",
  "dataSource": "guest_web",
  "videoId": "7697968856207770723",
  "awemeId": "7697968856207770723",
  "url": "https://www.douyin.com/video/7697968856207770723",
  "title": "小姨子，你嘅粤语真系要好好练下 厨房发生嘅小趣事，小姨子嘅粤语对白有啲搞笑。",
  "desc": "小姨子，你嘅粤语真系要好好练下 厨房发生嘅小趣事，小姨子嘅粤语对白有啲搞笑。\n#粤语搞笑 #粤语短剧 #姐夫小姨子 #广府粤语 #搞笑日常",
  "authorName": "追梦***",
  "publishedAt": "2026-10-18T11:30:44+00:00",
  "durationSec": 15.1,
  "language": "yue",
  "transcriptSource": "asr",
  "asrModel": "sensevoice-small",
  "text": "姐夫死得未吓，屙就快啦，我就快屙死啦，好快啦，你再忍一忍，咁，我依家去叫星家姐。姐得未，我就快啱死啦，你仲唔出嚟，我同姐夫先死，等阵留你自己一个死。开饭啦。",
  "wordCount": 65,
  "segments": [
    { "start": 0.19, "end": 6.8, "text": "姐夫死得未吓，屙就快啦，我就快屙死啦，好快啦，你再忍一忍，咁，我依家去叫星家姐。" },
    { "start": 7.75, "end": 13.46, "text": "姐得未，我就快啱死啦，你仲唔出嚟，我同姐夫先死，等阵留你自己一个死。" },
    { "start": 14.43, "end": 15.04, "text": "开饭啦。" }
  ],
  "hashtags": ["粤语搞笑", "粤语短剧", "广府粤语", "搞笑日常"],
  "likeCount": 338,
  "commentCount": 31,
  "input": "https://www.douyin.com/video/7697968856207770723",
  "videoStatus": "available",
  "accessMode": "direct",
  "collectionStatus": "complete",
  "partial": false,
  "partialReason": null,
  "elapsedMs": 23781,
  "collectedAt": "2026-10-24T10:26:54+00:00"
}
```

### What it does

- **The words, as plain text.** `text` is the whole transcript in one string, punctuated, ready to feed a language model with no post-processing.
- **The timeline.** `segments` gives every line with the second it starts and ends at, so you can quote a moment, cut a clip or line the transcript up with the video.
- **Cantonese stays Cantonese.** A 粤语 video comes back as `language: "yue"` in written Cantonese, not flattened into Mandarin. Mandarin is `zh`, English `en`, Japanese `ja`, Korean `ko`.
- **Douyin's own caption first.** When a video has a caption track, that text is used as it is - it is the platform's own and it costs no audio transfer. Almost no Douyin video has one, which is why the model is here.
- **The video it belongs to**: title, description, author's public nickname, publish time, length, hashtags, likes and comment count, so a transcript never arrives without knowing which post it came from.
- **Silence is said out loud.** A video with no speech - a music clip, a silent montage - comes back with `text: null`, `partialReason: transcript:no_speech_detected`, and **no charge**. It is never written as an empty transcript.

### What it does not do

- No keyword search: give it video links or IDs. Use the Douyin Search Scraper to find videos first.
- No comments (Douyin Comments Scraper), no account statistics (Douyin Profile Scraper).
- No private, age-restricted or login-only video. Public pages only, read signed out.
- No translation. The transcript is in the language that was spoken.
- No audio or video file is stored. The sound is decoded in memory, transcribed and dropped.

### Input

| Field | Type | Default | What it does |
| --- | --- | --- | --- |
| `videoUrls` | array | required | Video links, `/note/` links, `v.douyin.com` share links, `modal_id=` links or bare numeric IDs - one per line. |
| `maxItems` | integer | 1000 | Stop after this many videos; the rest are reported as not collected. |
| `includeSegments` | boolean | `true` | Include the timed line-by-line transcript. Off gives a smaller row and `segments: null`. |
| `allowAsr` | boolean | `true` | Transcribe the audio when Douyin has no caption track. Off means caption-only, and videos without one cost nothing. |
| `language` | string | empty | Leave empty and the model names the language itself, which is what you usually want. Set `zh`, `yue`, `en`, `ja` or `ko` and that code is reported instead of the model's answer; it does not change how the audio is read. |
| `maxDurationSec` | integer | 600 | Videos longer than this are skipped, flagged and not charged. |
| `maxAudioMb` | integer | 40 | Ceiling on one audio download. |
| `maxUpstreamRequests` | integer | derived | Hard ceiling on requests to Douyin. Left empty it is worked out from your input, so a run is never unbounded. |
| `proxyConfiguration` | object | off | Off by default: Douyin answers visitors without a proxy. |
| `residentialFallback` | boolean | `true` | Retry a refused video once through a residential proxy exiting in China. |

### Output

Every row carries `recordType`, `partial` and `collectedAt`. Three kinds:

- `transcript` - one per video asked for. The only charged kind, and only when it carries words.
- `input_status` - one free row per input line: `found`, `duplicate`, `invalid_url` or `not_collected`, so a line that produced nothing still says why.
- `RUN_SUMMARY` - the last row: input outcomes, transcript sources, languages, words produced, audio seconds transcribed and the CPU time spent on them, requests against the ceiling in force, why the run stopped, and `resumedAfterMigration`. Also saved as the `RUN_SUMMARY` key-value record.

`transcriptSource` is `platform_caption` when Douyin's own caption answered and `asr` when the model ran, and `null` when neither was tried (an image album, a video over `maxDurationSec`, `allowAsr: false`). A row where the model ran and heard no speech keeps `asr` and the detected `language`, but its `text` is `null`, `wordCount` is `0` and it is not charged - check `wordCount`, not `transcriptSource`, to tell whether you got words. `partialReason` uses a fixed vocabulary: `transcript:no_speech_detected`, `transcript:longer_than_maxDurationSec`, `transcript:asr_disabled_and_no_platform_caption`, `transcript:image_album_has_no_audio`, `transcript:audio_download_failed`, `transcript:audio_decode_failed`, `blocked:*`, `cap:*`, `detail:*`.

### Pricing

Pay per event: **$1.99 per 1,000 transcripts, plus $0.001 for each 10 seconds of speech the model transcribes, plus $0.005 for each run.** A video Douyin already captioned costs no audio events at all.

| Event | Price | Charged for |
| --- | --- | --- |
| `result` | $0.00199 ($1.99 per 1,000) | Each row that carries words. |
| `asr-10s` | $0.001 per started 10 seconds of speech | The stretches of a video the model actually found a voice in, added up and rounded up per video - silence and music-only passages are not charged. Not charged for a platform caption, and not charged when the model found no words. |
| `run-start` | $0.005, once per run | The signed-out browser session every run opens before it reads any post. |

What that works out to, per 1,000 videos of someone talking from start to finish (a video that is half quiet costs about half the audio part):

| Typical video | Price per 1,000 |
| --- | --- |
| 15 s | $3.49 |
| 25 s | $4.99 |
| 60 s | $7.99 |
| 3 min | $19.99 |
| any length, Douyin captioned it | $1.99 |

A row that only reports a failure - an input line with no video ID, a duplicate, a video Douyin would not describe, a video with no speech, one longer than `maxDurationSec`, one the run never reached - is written **free**. A run that reads nothing still pays the $0.005 session.

To cap a run, set **Maximum cost per run** in the run options. When the cap is reached the Actor stores no further rows, stops before the next video, marks what it cut short `partial` with `cap:maxTotalChargeUsd`, and records it in `RUN_SUMMARY`.

Apify may move a long run to another host, which starts the Actor again from the top. You are not charged twice for that: how far collection got - the input lines finished, the rows written and the videos already in the dataset - is saved in the run's own key-value store after every row (`RUN_PROGRESS`) and read back at start-up, so the videos already written are skipped in silence rather than collected and billed again, and `run-start` is charged once per run and not once per host. `RUN_SUMMARY.resumedAfterMigration` says whether it happened; its row and charge totals cover the whole run either way.

### Limits worth knowing before you buy

- **Douyin captions almost nothing.** Of sixteen videos measured on 2026-10-24, **none** carried a caption track, so in practice every transcript is the model's. `allowAsr: false` is therefore a caption-only mode that will usually return no text.
- **A speech model is not a stenographer.** Expect names, brand names and dialect words to come back approximated. `segments` keeps the timeline so you can check any line against the video.
- **A music-only or heavily mixed soundtrack may yield nothing**, or a handful of sung words. A row that comes back empty - including one where the model only looped a single short syllable over the music - says `no_speech_detected` and is free.
- **Image-album (图文) posts have no audio** and are reported as such.
- **Length costs money and time.** Three minutes of talking is twelve times the speech of fifteen seconds. `maxDurationSec` (default 600 s) is the brake.
- **No translation, no speaker labels, no on-screen text (OCR).**

### Resources

- Runs with **4 GB memory** by default; this is the minimum, because the speech model and the visitor-session browser both live in it. More memory gives more CPU and a faster run at about the same cost.
- A run opens a visitor session first (about 15-25 seconds), then spends roughly **1 second of CPU per 10 seconds of audio** on `sensevoice-small` (measured over 1,050 seconds of audio on 2026-10-24). The next video's download happens while the current one is being transcribed.

### Notes

- Models: **SenseVoice-Small** (Apache-2.0, FunAudioLLM) via sherpa-onnx and **Silero VAD** (MIT). Both ship inside the Actor image; nothing is downloaded at run time. There is one model and it is not selectable: a second engine (Whisper small via faster-whisper) was offered until 0.2.9 and removed because it was 9.6x slower on the same audio at no gain on Chinese.
- Counts (`likeCount`, `commentCount`) are a snapshot at `collectedAt`, as the exact numbers Douyin reports rather than the rounded `9.8万` the page shows. `null` means Douyin stated no count - never zero.
- Personal data is kept to the minimum: the author's public display name only. No account IDs, no handle, no bio, no follower counts.

# Actor input Schema

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

Public Douyin videos, one per line: a video link (https://www.douyin.com/video/7...), a share link (https://v.douyin.com/...), a link carrying modal_id=..., or the bare numeric video ID. Keyword search is not supported here - use the Douyin Search Scraper to find videos first.

## `maxItems` (type: `integer`):

Stop after this many videos. Input lines past the limit are reported as not collected rather than silently dropped.

## `includeSegments` (type: `boolean`):

Add segments\[{start, end, text}] - the transcript line by line with a start and end second for each line. Turn it off if you only want the plain text; the row is then smaller but says nothing about timing.

## `allowAsr` (type: `boolean`):

Most Douyin videos carry no caption track. With this on, the Actor downloads the lowest-bitrate rendition and recognizes the speech inside the Actor container with an open-source model - no speech API and no key. With it off, a video without a platform caption comes back with text: null and the reason, and costs nothing.

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

Leave empty and the model names the language itself, which is what you usually want on Douyin: Mandarin, Cantonese, English, Japanese and Korean come back labelled as what they are. Set a code (zh, yue, en, ja, ko) and that code is reported on every transcribed row instead of the model's own answer; it does not change how the audio is read.

## `maxDurationSec` (type: `integer`):

Videos longer than this are skipped: the row carries the video's metadata, text: null and partialReason transcript:longer_than_maxDurationSec, and is not charged. A video whose length Douyin does not state is treated as over the limit.

## `maxAudioMb` (type: `integer`):

Ceiling on one download. The lowest-bitrate rendition of a Douyin video is a few megabytes, so this only catches an outlier.

## `maxUpstreamRequests` (type: `integer`):

Hard ceiling on requests sent to Douyin in this run, retries included. Leave it empty and the Actor works one out from the number of videos you gave it, so a run is never unbounded; 0 sends no request at all. RUN_SUMMARY reports which ceiling was in force.

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

Proxy for all requests. Off by default: Douyin answers guests without a proxy.

## `residentialFallback` (type: `boolean`):

When Douyin refuses the request for a video (captcha, empty answer), retry that video once through an Apify residential proxy exiting in China. Residential traffic costs more than direct requests. A video Douyin does answer about - taken down, private, withheld - is never retried: no exit changes that answer.

## `maxFallbackVideos` (type: `integer`):

Upper limit per run on videos moved to the residential proxy. 0 turns the fallback off.

## `requestDelaySecs` (type: `number`):

Minimum pause between two requests from the same guest identity.

## `maxIdentityRotations` (type: `integer`):

How many times a refused request may be retried on a freshly minted guest identity before the video is reported as blocked.

## `msTokenWaitSecs` (type: `integer`):

How long a new guest page may take to receive its security token (msToken) before requests start. Longer waits make requests more likely to be answered.

## Actor input object example

```json
{
  "videoUrls": [
    "https://www.douyin.com/video/7467904379333528843",
    "https://www.douyin.com/video/7683038468016329571"
  ],
  "maxItems": 10,
  "includeSegments": true,
  "allowAsr": true,
  "language": "",
  "maxDurationSec": 600,
  "maxAudioMb": 40,
  "proxyConfiguration": {
    "useApifyProxy": false
  },
  "residentialFallback": true,
  "maxFallbackVideos": 3,
  "requestDelaySecs": 1,
  "maxIdentityRotations": 6,
  "msTokenWaitSecs": 20
}
```

# Actor output Schema

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

No description

## `inputs` (type: `string`):

No description

## `runSummary` (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://www.douyin.com/video/7467904379333528843",
        "https://www.douyin.com/video/7683038468016329571"
    ],
    "maxItems": 10,
    "proxyConfiguration": {
        "useApifyProxy": false
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("lanternlab318/douyin-transcript").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.douyin.com/video/7467904379333528843",
        "https://www.douyin.com/video/7683038468016329571",
    ],
    "maxItems": 10,
    "proxyConfiguration": { "useApifyProxy": False },
}

# Run the Actor and wait for it to finish
run = client.actor("lanternlab318/douyin-transcript").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.douyin.com/video/7467904379333528843",
    "https://www.douyin.com/video/7683038468016329571"
  ],
  "maxItems": 10,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}' |
apify call lanternlab318/douyin-transcript --silent --output-dataset

```

## MCP server setup

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

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/B0yfcXGGa1gYBUqiV/builds/ihaJFSmdIdbnVMFMv/openapi.json
