TikTok Transcript in Python: Captions First, Whisper as Fallback
A TikTok transcript is useful for more than accessibility. Marketers skim competitor ads as text instead of scrubbing through them, researchers search what creators actually said, and editors turn a talking-head clip into a caption file. Most of the search results for "tiktok transcript" are web tools that do this behind a text box. This post builds the same thing in about 60 lines of Python and measures how well the two ways of getting the text agree.
The design: many TikTok videos already carry an auto-generated caption track, and fetching it is fast and free. When it is missing, download the audio and run speech recognition. The questions worth measuring are how often the captions exist, and how close Whisper's output is to them when you do need it.
The two sources of text
| Source | How you get it | Speed | When it fails |
|---|---|---|---|
| Caption track | yt-dlp --write-subs, no video download |
A few seconds | The video has no caption track |
| Speech recognition | Download the video's audio, run Whisper | Roughly 4–11 s of compute per video on a laptop CPU, plus model load | Music-only clips, slang and names |
yt-dlp does the fetching in both cases. TikTok serves captions as WebVTT, the same cue-and-timestamp structure as SRT with a header line and a period before the milliseconds, so turning them into plain text is a matter of dropping the timing lines.
How often do captions exist?
I sampled the eight most recent videos from four public accounts (@tiktokcreators, @nba, @espn and @tiktokforbusiness) and asked yt-dlp which subtitle tracks each one offered. I only listed the tracks; I didn't download or keep anyone's transcript for this count.
| Account | Videos with an English caption track |
|---|---|
| @tiktokcreators | 6 / 8 |
| @nba | 7 / 8 |
| @espn | 7 / 8 |
| @tiktokforbusiness | 5 / 8 |
| Total | 25 / 32 (78%) |
So the caption path covers most videos, but roughly one in five has nothing to fetch. Those tend to be music-led clips or videos where the speech is minimal. A tool that relies on captions alone will fail on them silently by returning an empty string, which is why the fallback matters.
How close is Whisper to the captions?
To measure the fallback, I took six recent videos from TikTok's official account, all of which do have captions, and transcribed their audio with whisper-base.en on the CPU of an M1 Pro laptop. Then I compared Whisper's text with the caption track using word error rate (WER): the number of word substitutions, insertions and deletions needed to turn one into the other, divided by the length of the reference. Here the reference is the caption track, which is itself machine-generated, so treat this as agreement between two systems rather than accuracy against a human transcript.
| Video length | Caption words | Whisper words | Whisper compute time | WER vs captions |
|---|---|---|---|---|
| 65 s | 224 | 220 | 7.4 s | 8.5% |
| 69 s | 160 | 161 | 5.2 s | 1.3% |
| 50 s | 129 | 127 | 4.4 s | 4.7% |
| 106 s | 339 | 336 | 10.9 s | 5.9% |
| 108 s | 330 | 324 | 10.7 s | 13.9% |
| 75 s | 272 | 273 | 9.3 s | 7.4% |
Five of the six land between 1% and 9%, and the word counts are within six words every time. Whisper ran between about 8 and 13 times faster than real time on these clips.
A single WER number hides what the differences actually are, so I lined the two texts up word by word for the two worst videos:
- In the 13.9% video, a food-and-travel clip, most differences are harmless: the captions wrote "gonna" where Whisper wrote "going to", and the captions kept a stutter ("are are") that Whisper dropped. The meaningful errors are names. A café called "Mori Matcha" became "Morimacha", and "matcha" alone came out as "macha" once and "mantra" once.
- In the 8.5% video, about K-pop on the For You feed, the band name "ATEEZ" became "80s", and "For You page" was heard as "food pages". The captions got both right.
That pattern repeats across the sample. On ordinary speech the two sources agree closely. The places they disagree are proper nouns and platform slang, and there the caption track usually wins, likely because it has access to context Whisper doesn't, such as the text in the video's description. For marketing research that matters, because the product name is often exactly the word you care about.
Two ways the fallback goes wrong
My first batch run looked fine until I checked one row: a 6-second video had produced a 599-word transcript. Two separate problems were hiding in the fallback path.
The first was the audio source. TikTok exposes an "audio only" format, and yt-dlp's bestaudio picks it. On that video, the audio-only file was 172 seconds long, because it was the entire background track, a lo-fi song, not the 6 seconds of the clip. Whisper then transcribed nearly three minutes of music. Taking the audio from the video file itself, with -f "best[vcodec!=none][acodec!=none]/best", fixed it: the extracted audio matched the clip's length, and the transcript dropped to 3 words.
The second problem was real. Three other uncaptioned videos were event recaps with music and no speech. Whisper on music loops a phrase: "keep it running" or "see you guys in the next video" repeated hundreds of times, giving 367 to 1,141 words for clips of 60 to 103 seconds. These are the same "thank you"-style hallucinations I saw on silence in the SRT test, only longer. The script now rejects a Whisper result when it runs faster than about 4.5 words per second, which no human speaks for long, or when any four-word phrase repeats five or more times, and it reports "no speech" instead.
With both fixes, I ran the script over the 16 most recent videos from @tiktokcreators and @tiktokforbusiness:
| Outcome | Videos | Median time per video |
|---|---|---|
| Caption track found | 11 | 4.1 s |
| Whisper fallback, real speech | 2 | about 9 s |
| Whisper fallback, flagged as no speech | 3 | about 30 s |
That's 16 transcripts or explicit "no speech" results in 163 seconds, with the model loaded once for the whole batch. Before the fixes, the same 16 videos produced four transcripts that were pure invention.
The script
Here is the version I'd use. It tries the caption track first, falls back to Whisper when there isn't one, rejects music-only hallucinations, and labels which source the text came from so you know how much to trust the names.
import re
import subprocess
import sys
import tempfile
from collections import Counter
from pathlib import Path
WHISPER_MODEL = "openai/whisper-base.en"
def run(args):
return subprocess.run(args, capture_output=True, text=True)
def captions(url, workdir):
"""Try the video's own captions first. Returns plain text or None."""
run(["yt-dlp", "-q", "--no-warnings", "--skip-download", "--write-subs",
"--sub-langs", "eng.*,en.*", "--sub-format", "vtt",
"-o", f"{workdir}/video.%(ext)s", url])
vtts = sorted(Path(workdir).glob("video*.vtt"))
if not vtts:
return None
lines = []
for line in vtts[0].read_text(encoding="utf-8").splitlines():
line = line.strip()
if not line or line == "WEBVTT" or "-->" in line or line.isdigit():
continue
lines.append(re.sub(r"<[^>]+>", "", line))
return " ".join(lines)
def whisper(url, workdir):
"""Fallback: download the audio as 16 kHz mono and transcribe it."""
from transformers import pipeline
# Take the audio from the video file itself. On some TikToks the separate
# "audio only" format is the full background song, not the clip's audio.
run(["yt-dlp", "-q", "--no-warnings", "-f", "best[vcodec!=none][acodec!=none]/best", "-x",
"--audio-format", "wav", "--postprocessor-args", "ffmpeg:-ac 1 -ar 16000",
"-o", f"{workdir}/audio.%(ext)s", url])
asr = pipeline("automatic-speech-recognition", model=WHISPER_MODEL)
return asr(f"{workdir}/audio.wav", return_timestamps=True)["text"].strip()
def looks_hallucinated(text, seconds):
"""Whisper on music-only audio tends to loop one phrase. Flag that."""
words = text.lower().split()
if not words:
return False
if len(words) / max(seconds, 1) > 4.5: # fast speech is about 3 words per second
return True
grams = Counter(" ".join(words[i:i + 4]) for i in range(len(words) - 3))
return bool(grams) and grams.most_common(1)[0][1] >= 5
def transcript(url, force_whisper=False):
with tempfile.TemporaryDirectory() as workdir:
if not force_whisper:
text = captions(url, workdir)
if text:
return "captions", text
text = whisper(url, workdir)
seconds = float(run(["ffprobe", "-v", "error", "-show_entries", "format=duration",
"-of", "csv=p=0", f"{workdir}/audio.wav"]).stdout or 0)
if looks_hallucinated(text, seconds):
return "no speech", ""
return "whisper", text
if __name__ == "__main__":
source, text = transcript(sys.argv[1], force_whisper="--whisper" in sys.argv)
print(f"[{source}] {len(text.split())} words")
print(text)
On one of the 69-second videos, the whole command took 4.2 seconds through the caption path and 19.1 seconds with --whisper. Most of that second number is loading the model and downloading the video; the transcription itself was 5.2 seconds. If you're processing a batch, create the pipeline once outside the loop and the per-video cost drops to the compute time in the table.
A few details in the code are deliberate:
--skip-downloadwith--write-subsfetches only the caption file, which is a couple of kilobytes, instead of the video.--sub-langs "eng.*,en.*"matches TikTok'seng-UStrack names as well as plainen.- The audio comes from the video file, not TikTok's separate audio-only format, and is converted to 16 kHz mono during download, which is the input Whisper expects.
- I pass
return_timestamps=Trueand don't setchunk_length_s. Clips under two minutes don't need chunking, and in my earlier test on making SRT files with Whisper, chunked mode invented whole sentences during silence.
Choosing a Whisper model
whisper-base.en was a reasonable default here: fast on a CPU and close to the captions on clear speech. If your videos are mostly music with occasional speech, or the names matter, a larger checkpoint is worth the extra seconds. If the speech isn't English, switch to a multilingual checkpoint such as openai/whisper-base and drop the .en suffix. The .en checkpoints raise an error if you pass them a language, so drop that argument when you switch to one.
What to watch for
The transcript is the starting point, not the deliverable. If you're studying competitor ads, the reusable part is the structure: what the hook is, how many seconds pass before the product appears, and the order of the claims. Copying the actual lines is a different matter, both legally and because audiences notice.
Respect the platform, too. Fetch public videos at a reasonable rate, don't redistribute other people's full transcripts, and check TikTok's terms for your use. I kept this post to counts and a few short word pairs for that reason.
If you'd rather not run yt-dlp and a model yourself, UGCfy AI's free tiktok transcript tool uses the same captions-first approach. It falls back to speech recognition when captions are missing, and it reports the detected language. The free tier allows three transcripts a day without an account, which suits occasional lookups better than this script does.
What I took away
Captions first is the right default: they existed for about four out of five videos in my sample, arrive in seconds, and handled names better than Whisper did. Whisper is a good fallback on real speech, agreeing with the captions on 91–99% of words in five of six videos. Its mistakes cluster on exactly the words a marketer would search for, so check the names. And on clips with no speech at all, it will invent some unless you check for that, which is the cheapest safeguard in the whole script.

