Back to blog

yt-dlp subtitles to plain text: download YouTube captions and clean them up

The yt-dlp commands that download only YouTube captions, why the auto-caption VTT file has three times too many words, and a short script that turns it into a clean transcript.

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.

Guide

The command that downloads only the captions

bash
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-download writes no video and no audio. Only the caption file is fetched.
  • --write-auto-subs asks for YouTube's automatic captions. --write-subs asks for captions the uploader supplied. Pass both to get whichever exists. Uploaded captions are usually better, so check first.
  • --sub-langs en picks 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 get VIDEO_ID.en.json3.

To see what a video has before you download anything:

bash
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.

Guide

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.

Guide

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 downloaded11,458
Same captions as json3, one event per caption3,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.

Guide

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:

python
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.

Guide

The same thing from Python

You do not have to shell out. The options map to dictionary keys:

python
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.

Guide

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, not en.*, 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.
Troubleshooting

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.