Douyin (抖音) doesn't publish subtitles in its web data, so if you want the words spoken in a video you have to run speech recognition yourself. Most open-source Douyin "video to text" projects chain together a video downloader, ffmpeg and a paid speech API (DashScope, SiliconFlow and so on), and they tend to break whenever Douyin changes its cookies.
This tutorial shows two ways to get timestamped Chinese transcripts with punctuation without any of that: a no-code run and a few lines of Python. Then a do-it-yourself section on running the speech model on your own machine, with the accuracy comparison I measured between Whisper and SenseVoice on real Douyin narration.
Disclosure: I built the Apify actor used in options 1 and 2 (Douyin Transcript Scraper). The DIY section works without it.
What you get per video
One row per video. Real values from a run on 26 Oct 2026, an 11-minute movie-explainer video (电影解说) by 心影说:
| Field | Example |
|---|---|
url, videoId
|
https://www.douyin.com/video/7696934197226786058 |
authorNickname |
心影说 |
description |
法律迟迟不肯给出公道,八旬老兵该如何讨回尊严? #伏罗希洛夫射手 #电影解说 |
createdAt |
2026-10-17T09:00:00Z |
durationSeconds |
664.7 |
likeCount, commentCount, shareCount, collectCount
|
163, 2, 10, 21
|
language |
zh |
characterCount |
4031 |
hook |
the opening sentence (segments starting in the first 3 seconds) |
transcript |
timed segments [{"start": 0.07, "end": 17.94, "text": "老人架起狙击步枪,…"}]
|
musicTitle, audioSource
|
@心影说创作的原声, original_sound_mp3
|
Option 1: No code (Apify Console)
- Open the actor page and click Try for free. Apify's free plan includes monthly platform credit; the actor itself is pay-per-event (see the Store page for the current price), and videos without speech aren't charged.
- Paste Douyin videos (
douyin.com/video/…links,v.douyin.com/…share links or numeric IDs), or add creators by profile link (douyin.com/user/MS4wLjAB…). - Pick the Transcript format: timed segments, plain text or both.
- Click Start and export the results as CSV, JSON or Excel.
For creators, the actor transcribes their newest videos up to Max videos per creator. Douyin shows logged-out visitors roughly the 40 newest videos of an account, so for older videos paste their links directly.
Option 2: Python
pip install apify-client, then take your API token from Apify Console → Settings → Integrations.
from apify_client import ApifyClient
client = ApifyClient("<YOUR_APIFY_TOKEN>")
run = client.actor("axlymxp/douyin-transcript-scraper").call(run_input={
"videoUrls": ["https://www.douyin.com/video/7696934197226786058"],
"profileUrls": [
"https://www.douyin.com/user/MS4wLjABAAAAOzRPcYBhm0xO0mXI5UGjHc2udHifSV-6xuI5mpo_UNKFN6qLVzfWb4QMT8BTOk2n",
],
"maxVideosPerProfile": 5,
"outputFormat": "both", # segments in `transcript`, joined text in `transcriptText`
})
for item in client.dataset(run["defaultDatasetId"]).iterate_items():
if item["error"]: # no_speech, not_available, too_long, ...
print("skip", item["videoId"], item["error"])
continue
print(item["authorNickname"], item["likeCount"], item["characterCount"], "chars")
print(" ", item["transcriptText"][:80])
A natural next step is to feed transcriptText to an LLM for an English summary or translation.
Example output
One real row (trimmed to two segments; coverUrl removed):
{
"videoId": "7696934197226786058",
"url": "https://www.douyin.com/video/7696934197226786058",
"authorNickname": "心影说",
"description": "法律迟迟不肯给出公道,八旬老兵该如何讨回尊严? #伏罗希洛夫射手 #电影解说",
"createdAt": "2026-10-17T09:00:00Z",
"durationSeconds": 664.7,
"likeCount": 163,
"language": "zh",
"audioSource": "original_sound_mp3",
"characterCount": 4031,
"transcript": [
{"start": 0.07, "end": 17.94, "text": "老人架起狙击步枪,枪口牢牢锁定了对面的三个败类,但一枪爆头的死法显然只会便宜了这几个人渣。他随即调整枪口的角度,对准了坐在正中间的那人。那小子正用双腿夹着香槟酒瓶。就在他拔出瓶塞的瞬间,老人猛的扣下了扳机。"},
{"start": 19.3, "end": 41.44, "text": "玻璃碎片混着鲜血,四下飞溅,不但将现场伪装成了意外爆炸,还顺带废掉了这家伙的作案工具。随后,老人迅速把狙击枪锁进柜子,套上外套,捡起地上的弹壳,神色自若的走下楼,悄无声息的混进了人群之中。…"}
],
"error": null
}
DIY: transcribe Mandarin audio yourself (no API key)
If you already have the audio (your own videos, or files you downloaded), you can run a strong Mandarin speech model on a normal CPU.
Whisper or SenseVoice?
I ran the same 60-second clip of that video's narration through three models on a single cloud vCPU:
| Model | Time for 60 s | What came out |
|---|---|---|
Whisper base
|
11.5 s | Garbled homophones: 扳机 (trigger) became 半斤, 狙击步枪 (sniper rifle) became 嫁起居及不强; traditional and simplified characters mixed |
Whisper small
|
37 s | Much better, but in traditional characters, and on another clip it inserted a Korean phrase that nobody said |
| SenseVoice-Small | 9.3 s | Simplified Chinese with punctuation; 扳机, 狙击步枪, 神色自若, 悄无声息 all correct |
SenseVoice (from Alibaba's FunAudioLLM team) also covers Cantonese, English, Japanese and Korean. The easiest way to run it on CPU is sherpa-onnx: pip install sherpa-onnx av numpy, then download sherpa-onnx-sense-voice-zh-en-ja-ko-yue-2024-07-17.tar.bz2 and silero_vad.onnx from the sherpa-onnx asr-models release.
import av, numpy as np, sherpa_onnx
SV = "sherpa-onnx-sense-voice-zh-en-ja-ko-yue-2024-07-17"
def load_audio(path, rate=16000):
"""Any audio/video file -> 16 kHz mono float32 (PyAV, no ffmpeg binary needed)."""
chunks = []
with av.open(path) as c:
rs = av.AudioResampler(format="s16", layout="mono", rate=rate)
for frame in c.decode(audio=0):
chunks += [f.to_ndarray().reshape(-1) for f in rs.resample(frame)]
chunks += [f.to_ndarray().reshape(-1) for f in rs.resample(None)]
return np.concatenate(chunks).astype(np.float32) / 32768.0
recognizer = sherpa_onnx.OfflineRecognizer.from_sense_voice(
model=f"{SV}/model.int8.onnx", tokens=f"{SV}/tokens.txt",
num_threads=1, use_itn=True, language="auto") # use_itn adds punctuation
vad_cfg = sherpa_onnx.VadModelConfig()
vad_cfg.silero_vad.model = "silero_vad.onnx"
vad_cfg.silero_vad.max_speech_duration = 20 # target length; it cuts at the next pause, so some run longer
vad_cfg.sample_rate = 16000
vad = sherpa_onnx.VoiceActivityDetector(vad_cfg, buffer_size_in_seconds=60)
samples = load_audio("my_video.mp3")
window = vad_cfg.silero_vad.window_size
segments = []
for i in range(0, len(samples) + window, window):
if i < len(samples):
vad.accept_waveform(samples[i:i + window])
else:
vad.flush()
while not vad.empty():
segments.append((vad.front.start / 16000, np.array(vad.front.samples, dtype=np.float32)))
vad.pop()
for start, seg in segments: # one segment per decode call (see below)
stream = recognizer.create_stream()
stream.accept_waveform(16000, seg)
recognizer.decode_stream(stream)
print(f"[{start:7.2f}] {stream.result.text}")
Two things that cost me time:
- Cut the audio into short segments. On a single 60-second chunk, SenseVoice dropped words in one of three clips. Splitting with voice-activity detection into shorter pieces (most of mine ended up 15-25 seconds) fixed that, and gives you timestamps for free.
-
Don't batch segments of different lengths. sherpa-onnx can decode a list of streams at once (
decode_streams), but the batch is padded to its longest segment. On one vCPU that made transcription about 2.5x slower than decoding one segment at a time: roughly 19 s of CPU per audio minute instead of 7.7 s.
Getting the audio out of Douyin is the other half of the job: the video and its download links come from Douyin's signed web API, and the files are big (an 11-minute explainer is 150-210 MB as video). When a video uses the creator's original sound, its audio-only MP3 is much smaller (17-29 MB for 11-17 minutes), which is what the actor downloads.
FAQ
Do I need a Douyin account? No. Only public videos are read, logged out.
Does Douyin provide subtitles? Not in its public web data; every transcript is generated from the audio.
How accurate is it? Clear Mandarin narration comes out very well. Background music, dialects, heavy slang or several people talking at once lower accuracy, and song lyrics are transcribed like speech.
Traditional characters? SenseVoice outputs simplified Chinese; convert with a library such as OpenCC if you need traditional.
Is it legal? These are public videos read without logging in. How you use the transcripts is on you: respect Douyin's terms, copyright and privacy law.
Questions or a video that won't work? Open an issue on the actor page and I'll take a look.
Top comments (0)