TL;DR: yt-dlp --skip-download --write-auto-subs --sub-langs en --sub-format "json3/vtt" URL downloads only the captions, with no video or audio. The default VTT file is not a transcript: automatic captions repeat every line, and in our test the raw file held about three times the real word count. Ask for json3 and join the lines, or dedupe the VTT. On a server, the same command hits the bot check, which is covered at the end.
The command that downloads only the captions
yt-dlp --skip-download --write-auto-subs --sub-langs en --sub-format "json3/vtt" -o "%(id)s" "https://www.youtube.com/watch?v=VIDEO_ID"What each part does:
--skip-downloadwrites no video and no audio. Only the caption file is fetched.--write-auto-subsasks for YouTube's automatic captions.--write-subsasks for captions the uploader supplied. Pass both to get whichever exists. Uploaded captions are usually better, so check first.--sub-langs enpicks the language.--sub-format "json3/vtt"is a preference list. yt-dlp takes the first format that exists for that video.-o "%(id)s"names the file after the video ID, so you getVIDEO_ID.en.json3.
To see what a video has before you download anything:
yt-dlp --list-subs "URL"It prints two tables, "Available automatic captions" and "Available subtitles", with the formats offered for each language. If both tables are empty, the video has no captions and yt-dlp has nothing to download.
Why the fallback format matters
We ran this with yt-dlp 2026.10.26. On one test video, asking for plain --sub-format json3 printed No subtitle format found matching "json3" for language en, using vtt. On another, json3 was listed and downloaded fine. The formats on offer vary by video, so a preference list such as json3/vtt saves you from a failed run and a script that has to handle both.
Why the VTT file is not a transcript
Open an automatic caption file in VTT format and you will see the problem. YouTube's automatic captions are rolling: each cue shows the previous line again and adds the next one, and the second line carries inline timing tags like <00:00:31.279><c> now</c>.
We downloaded the automatic English captions of one public conference talk and counted:
| Words | |
|---|---|
| Body text of the VTT file, as downloaded | 11,458 |
| Same captions as json3, one event per caption | 3,832 |
The VTT file had 3,297 inline timing tags and about three times the real word count. Feed that to a summarizer or an embedding model and you pay for the same sentence three times and get duplicate chunks.
Clean text from json3
The json3 file is JSON with an events list. Each event has segs, a list of text pieces. Joining them gives one clean line per caption:
import json, sys
def json3_to_text(path):
events = json.load(open(path, encoding="utf-8"))["events"]
lines = []
for e in events:
segs = e.get("segs")
if not segs:
continue
line = "".join(s.get("utf8", "") for s in segs).replace("\n", " ").strip()
if line:
lines.append(line)
return " ".join(lines)
print(json3_to_text(sys.argv[1]))Run it as python3 clean.py VIDEO_ID.en.json3. On the talk above it printed [Music] [Applause] so I want to start by um offering you a free no Tech life hack...: sound tags such as [Music] are left in, so strip them with a regex if you do not want them. If you also need timestamps, each event carries tStartMs and dDurationMs in milliseconds.
If a video only offers VTT, remove the <...> tags and skip any line you already saw in the previous cue. That works, but json3 saves you the work when it exists.
The same thing from Python
You do not have to shell out. The options map to dictionary keys:
from yt_dlp import YoutubeDL
opts = {
"skip_download": True,
"writeautomaticsub": True,
"subtitleslangs": ["en"],
"subtitlesformat": "json3/vtt",
"outtmpl": "%(id)s",
"quiet": True,
}
with YoutubeDL(opts) as ydl:
ydl.download(["https://www.youtube.com/watch?v=VIDEO_ID"])This wrote VIDEO_ID.en.json3 in the working directory in our test. Add "writesubtitles": True to prefer uploaded captions.
A 429 you can cause yourself
This one cost us a failed run. We tried --sub-langs "en.*", which looks like a sensible way to get English plus regional variants. With --write-auto-subs it also matches every auto-translated track named en-xx. --list-subs showed 54 tracks starting with en on that video: en and 53 translations such as en-sq and en-ar. yt-dlp downloaded en and then en-sq, and the next request returned HTTP Error 429: Too Many Requests.
Two rules follow:
- Use
--sub-langs en, noten.*, unless you really want the translated tracks. - When you loop over many videos, add
--sleep-subtitles 5(seconds before each subtitle download) or--sleep-requests 1.
When it works on your laptop and fails on a server
Everything above uses the same YouTube requests that the video download does. From a cloud server, you will often get Sign in to confirm you're not a bot instead of a file. The fixes, in order, are in our guide to the yt-dlp bot check error. It includes block rates we measured across 800 residential proxy exits.
If text is all you want, there is a shorter path. TranscriptFetch takes a video URL or ID and returns the transcript as JSON, already deduplicated, with timestamps, and it handles the blocking on its side. The first 50 transcripts each month are free and failed requests are never charged. For a single video, the free YouTube transcript generator works in the browser without an account.
FAQ
Add --skip-download and one of --write-subs (uploaded captions) or --write-auto-subs (YouTube's automatic captions), then pick a language with --sub-langs, for example yt-dlp --skip-download --write-auto-subs --sub-langs en URL. No video or audio file is downloaded.
YouTube's automatic captions are rolling captions: each cue repeats the previous line and adds the next one, with inline timing tags. In a test on one conference talk, the raw VTT body held 11,458 words for a transcript of 3,832. The json3 format has one event per caption and no repeats.
A common cause is a wildcard like --sub-langs "en.*" together with --write-auto-subs. It matches every auto-translated track as well (54 tracks on the video we tested), and each one is a separate request. Ask for en only, and add --sleep-subtitles when you loop over many videos.
No. If --list-subs shows no subtitles and no automatic captions, there is nothing to download. You would need speech to text on the audio instead.
