I wanted to ask Andrej Karpathy's YouTube channel questions and get back the exact moment in the video where he answers them. Not a summary. A link that starts the video at the right second.
His three most recent videos together are almost 10 hours long. This post shows how I turned them into a searchable index in about 60 lines of Python: 15,285 caption segments, 598 one-minute chunks, and a search that answers "what is flash attention" with &t=7241s.
Full disclosure: I built the transcript scraper used here and sell it on the Apify Store. Everything below is a real run from 20 October 2026, with the real output.
Why YouTube transcripts are awkward to get in bulk
For one video, you can open the transcript panel and copy it. For a whole channel, the usual paths are:
- The YouTube Data API. It lists videos, but it only downloads captions for videos you own.
- An open-source transcript library to fetch captions in your own code.
- A hosted scraper that takes a channel, playlist or list of videos and returns the transcripts as JSON.
I use option 3, YouTube Transcript Scraper on Apify, because it returns transcripts ready for indexing. You need no YouTube account, no Google API key and no cookies.
What you need
- A free Apify account. The free plan includes $5 of credit each month.
- Your Apify API token (Console → Settings → API & Integrations).
- Python 3 with
requests. Nothing else: the search below is plain Python, so you can run it before you pick a vector database.
Step 1: Get every transcript from the channel
The input takes a channel handle. maxVideos limits how many of its newest videos you get:
{
"channelUrls": ["@AndrejKarpathy"],
"maxVideos": 3,
"transcriptFormat": "both"
}
transcriptFormat: "both" returns the timestamped segments and the joined plain text. For RAG you want the segments, because they carry the start time of each line. You can also pass videoUrls, playlistUrls, a dateFrom and dateTo window, and a language code. If a video has no captions in that language, YouTube translates them, and wasTranslated tells you so.
Here is one real row from my run, with the long fields cut:
{
"videoId": "l8pRSuU81PU",
"url": "https://www.youtube.com/watch?v=l8pRSuU81PU",
"title": "Let's reproduce GPT-2 (124M)",
"channelName": "Andrej Karpathy",
"publishedAt": "2024-06-09T16:31:35-07:00",
"durationSeconds": 14486,
"viewCount": 1212773,
"language": "en",
"availableLanguages": ["en"],
"isAutoGenerated": true,
"wasTranslated": false,
"status": "success",
"transcript": [
{ "start": 0.16, "duration": 4.52, "end": 4.68, "text": "hi everyone so today we are going to be" },
{ "start": 2.36, "duration": 3.64, "end": 6, "text": "continuing our Zero to Hero series and" }
],
"transcriptText": "hi everyone so today we are going to be continuing our Zero to Hero series and ..."
}
That one video has 6,049 segments and 223,000 characters of text. Each row also includes the description (with the chapter list, when the creator wrote one), likeCount, commentCount, keywords and the thumbnail.
Notice isAutoGenerated: true. All three videos use YouTube's automatic captions, so the text has no punctuation and has some misheard words ("chpd" for ChatGPT). Embeddings handle that well. A keyword search like the one below is more sensitive to it.
Step 2: Chunk by time, not by tokens
Most RAG tutorials split text every N tokens. For video, split by time instead. Each chunk then keeps the start time of its first segment, and that start time becomes the citation:
def chunk(video, window=60):
chunks, words, start = [], [], None
for seg in video["transcript"]:
if start is None:
start = seg["start"]
words.append(seg["text"])
if seg["end"] - start >= window:
chunks.append((video, int(start), " ".join(words)))
words, start = [], None
if words:
chunks.append((video, int(start), " ".join(words)))
return chunks
60 seconds is about 150 to 200 spoken words, which is a good size for both embeddings and an LLM context window. A link to the chunk is video["url"] + f"&t={start}s".
Step 3: Search the chunks
Here is the full script. It runs the scraper, chunks every video, and ranks the chunks with BM25, the classic keyword ranking that Lucene and Elasticsearch use by default:
import math, os, re, sys, requests
from collections import Counter
ACTOR = "harshmaur~youtube-transcript-scraper"
TOKEN = os.environ["APIFY_TOKEN"]
videos = requests.post(
f"https://api.apify.com/v2/acts/{ACTOR}/run-sync-get-dataset-items",
headers={"Authorization": f"Bearer {TOKEN}"},
json={"channelUrls": ["@AndrejKarpathy"], "maxVideos": 3, "transcriptFormat": "both"},
timeout=300,
).json()
videos = [v for v in videos if v["status"] == "success"]
def chunk(video, window=60):
chunks, words, start = [], [], None
for seg in video["transcript"]:
if start is None:
start = seg["start"]
words.append(seg["text"])
if seg["end"] - start >= window:
chunks.append((video, int(start), " ".join(words)))
words, start = [], None
if words:
chunks.append((video, int(start), " ".join(words)))
return chunks
chunks = [c for v in videos for c in chunk(v)]
print(f"{len(videos)} videos, {sum(len(v['transcript']) for v in videos)} segments, {len(chunks)} chunks")
tokenize = lambda s: re.findall(r"[a-z0-9]+", s.lower())
docs = [Counter(tokenize(text)) for _, _, text in chunks]
avg_len = sum(sum(d.values()) for d in docs) / len(docs)
df = Counter(term for d in docs for term in d)
def bm25(query, doc, k1=1.5, b=0.75):
length = sum(doc.values())
score = 0.0
for term in tokenize(query):
if term not in doc:
continue
idf = math.log(1 + (len(docs) - df[term] + 0.5) / (df[term] + 0.5))
tf = doc[term]
score += idf * tf * (k1 + 1) / (tf + k1 * (1 - b + b * length / avg_len))
return score
def ask(question, k=3):
ranked = sorted(range(len(chunks)), key=lambda i: bm25(question, docs[i]), reverse=True)
print(f"\nQ: {question}")
for i in ranked[:k]:
video, start, text = chunks[i]
print(f"- {video['title']} @ {start // 3600}:{start // 60 % 60:02d}:{start % 60:02d} {video['url']}&t={start}s")
print(f" \"{text[:160]}...\"")
for q in sys.argv[1:]:
ask(q)
Run it with your questions as arguments:
APIFY_TOKEN=... python3 ask_channel.py "when should I use a thinking model" "why are LLMs bad at spelling" "how does gradient accumulation work" "what is flash attention"
The real output
3 videos, 15285 segments, 598 chunks
Q: when should I use a thinking model
- Deep Dive into LLMs like ChatGPT @ 2:39:55 https://www.youtube.com/watch?v=7xTGNNLPyMI&t=9595s
"on many math uh and kind of code problems that have verifiable Solutions so there's like an answer three Etc now these thinking models you can access in for exa..."
- How I use LLMs @ 0:30:24 https://www.youtube.com/watch?v=EWvNQjAaOHw&t=1824s
"you now when you go to Gro for example when I start a new conversation with grock um when you put the question here like hello you should put something importan..."
- Deep Dive into LLMs like ChatGPT @ 3:27:03 https://www.youtube.com/watch?v=7xTGNNLPyMI&t=12423s
"labeling instructions at open a so that's what we're getting back now I do think that the uh things change a little bit when you actually go and reach for one o..."
Q: why are LLMs bad at spelling
- Deep Dive into LLMs like ChatGPT @ 1:47:56 https://www.youtube.com/watch?v=7xTGNNLPyMI&t=6476s
"was Data labeler and I was creating one of these one of these would be uh a really terrible answer for the assistant and the other would be okay and so I'd like..."
- Deep Dive into LLMs like ChatGPT @ 2:03:32 https://www.youtube.com/watch?v=7xTGNNLPyMI&t=7412s
"third character and the actual truth is u2s uqs uh which looks correct to me so um again an example of spelling related tasks not working very well a very famou..."
- Deep Dive into LLMs like ChatGPT @ 2:00:37 https://www.youtube.com/watch?v=7xTGNNLPyMI&t=7237s
"the model to copy paste those token IDs and um kind of unpack them into Dots here and so it creates this string and then it calls python routine. count and then..."
Q: how does gradient accumulation work
- Let's reproduce GPT-2 (124M) @ 2:45:50 https://www.youtube.com/watch?v=l8pRSuU81PU&t=9950s
"processed per step is B * T * gradient accumulation so long story short here we have the optimization it looks uh reasonable right we're starting at a good spot..."
- Let's reproduce GPT-2 (124M) @ 2:43:54 https://www.youtube.com/watch?v=l8pRSuU81PU&t=9834s
"well uh so where's the loss after that cross entropy and by default the reduction uh here in Cross entropy is also I don't know why they don't show it but it's ..."
- Let's reproduce GPT-2 (124M) @ 2:40:59 https://www.youtube.com/watch?v=l8pRSuU81PU&t=9659s
"loss objective and this is the average the one over four because there are four independent examples here and then we have the four examples and their mean squa..."
Q: what is flash attention
- Let's reproduce GPT-2 (124M) @ 2:00:41 https://www.youtube.com/watch?v=l8pRSuU81PU&t=7241s
"attention will come here and we will take out these four lines and Flash attention implements these four lines really really quickly and how does it do that wel..."
- Let's reproduce GPT-2 (124M) @ 2:04:40 https://www.youtube.com/watch?v=l8pRSuU81PU&t=7480s
"single fused kernel flash attention and that's what we are about to use so great example I think of being aware of um memory hierarchy the fact that flops don't..."
- Let's reproduce GPT-2 (124M) @ 2:02:40 https://www.youtube.com/watch?v=l8pRSuU81PU&t=7360s
"we're getting a t BYT Matrix of attention which is a Million numbers even for a single head at a single batch index at like so so basically this is a ton of mem..."
The run took 35 seconds and returned all three transcripts.
What worked and what did not:
- Specific technical terms work very well. All three "flash attention" hits are inside the chapter "flash attention, 96ms" (2:00:18 in the GPT-2 video). The three "gradient accumulation" hits are all inside the "gradient accumulation" chapter, which starts at 2:34:09.
- The spelling question hit the right place. The chapter "tokenization revisited: models struggle with spelling" starts at 2:01:11. Two of the three hits overlap it: the chunk at 2:00:37 runs into it, and the one at 2:03:32 is in the middle of it.
-
Even the loosely worded question found real answers. The top hit, 2:39:55 in "Deep Dive", is where he says to use thinking models for prompts that need advanced reasoning, and GPT-4o for simpler questions. The second hit is the last minute of the chapter "Thinking models and when to use them" (0:22:54 in "How I use LLMs"). BM25 still only matches words, so a question phrased differently from what he said can miss. That is the point where you swap BM25 for embeddings: embed each chunk's text once, embed the question, and rank by cosine similarity. The chunks and the
&t=links stay the same.
Once you have the top chunks, give their text to an LLM with the instruction "answer only from these excerpts and cite the links", and you have a channel Q&A bot with clickable sources.
Tips for a real index
-
Use the chapter list. When the
descriptioncontains lines like00:22:54 Thinking models and when to use them, parse them and store the chapter title with each chunk. It is a strong extra signal for search. - Overlap the chunks. Start a new chunk every 30 seconds with a 60-second window, so an answer that crosses a border is not split in two.
-
Keep a stable key. Use
videoId+startas the chunk ID, so you can run the scraper again on a schedule and add only new videos.dateFromlimits each run to recent uploads. -
Check
isAutoGenerated. If a creator uploaded their own captions, the text has punctuation and correct names, and keyword search works better.
Use it from an AI agent
If you work in Claude, Cursor or another MCP client, you can give the agent the scraper as a tool and let it fetch transcripts itself:
{
"mcpServers": {
"youtube-transcripts": {
"command": "npx",
"args": ["-y", "mcp-remote", "https://mcp.apify.com/?tools=harshmaur/youtube-transcript-scraper"]
}
}
}
The first time your client starts the server, a browser window opens to connect your Apify account.
What this costs
$5 per 1,000 transcripts on the free plan, and less on paid plans. You pay only for transcripts that come back. A private, deleted or caption-less video returns a row with status: "failed", and that row is free. There is no start fee.
My three-video run cost 1.4 cents. Indexing a channel with 200 videos costs about $1, once. For more than about 20 videos, do not use the run-and-wait endpoint, which waits at most 5 minutes. Start the run with POST /v2/acts/{ACTOR}/runs, wait for it to finish, then read /v2/datasets/{defaultDatasetId}/items. After that, a weekly run with dateFrom costs a few cents.
FAQ
Can I download all transcripts from a YouTube channel at once?
Yes. Put the channel handle in channelUrls and set maxVideos to the number of videos you want. Playlists work the same way with playlistUrls.
Do I need a YouTube API key?
No. You need only an Apify API token, which comes free with an Apify account.
What happens when a video has no transcript?
You get a row with status: "failed" and no charge. The row has only videoId, url, errorCode and error, with no transcript fields, so filter on status == "success" as the script does.
Can I get transcripts in another language?
Set language to a code like es or de. If the video has captions in that language, you get them. If not, you get a machine translation and wasTranslated is true. If no translation is available, you get the original captions, and language shows which language you got.
Do I get SRT or VTT files?
Yes. Set transcriptFormat to srt or vtt, and each row has an srt or vtt field that holds the subtitle file as text. If you already have an SRT or VTT file from somewhere else and only want the words, this free SRT and VTT to text converter removes the timestamps and the repeated lines that YouTube's automatic captions often have.
Try it
YouTube Transcript Scraper on Apify. Paste a channel handle into the input form, press Start, and copy the script above to search what comes back.
If you build a channel Q&A bot with it, tell me in the comments what you indexed and which retrieval worked best for you.
The Apify links in this post include my referral code.
Top comments (0)