← Back to blog

Build a YouTube Video Q&A Chatbot with Transcripts

Zied · 8/8/2026 · 9 min read

Build a YouTube Video Q&A Chatbot with Transcripts

Most people will not watch a 40-minute technical video to find one answer. Give them a chatbot that reads the transcript, retrieves the right segment, and cites the timestamp, and they get the answer in seconds.

This tutorial walks through the full pipeline: fetching a clean transcript JSON with one API call, chunking by timestamp for retrieval, embedding chunks into a vector store, and wiring everything into a grounded LLM prompt. By the end you will have runnable Python code and a clear picture of where things can go wrong.

Why Transcripts Beat Raw Video for Q&A

Video is opaque to language models. You cannot grep a video file. You cannot pass 40 minutes of audio into a standard LLM context window without transcription, and even if you could, the model has no way to tell a user "this was said at 23:14."

A transcript converts spoken content into text that you can chunk, embed, retrieve, and cite. The timestamp on each segment becomes the citation. When the model says "According to the video at 23:14, the recommended batch size is 32," the user can jump directly to that moment.

The retrieval-augmented generation (RAG) pattern is the right architecture here. Rather than stuffing the entire transcript into one giant prompt (which wastes tokens and dilutes focus), you embed transcript chunks into a vector store, retrieve the top-k most relevant chunks for each user question, and pass only those chunks to the LLM. Semantic chunking with 400-512 token windows achieves 88-92% recall depending on the splitting strategy, which is strong enough for production use.

Step 1: Fetch the Transcript as JSON

The YouTube Transcriber API returns a clean, timestamped JSON with one GET request. No browser automation, no scraping fragility, no quota gymnastics with the YouTube Data API v3.

curl "https://getyoutubetranscriber.com/api/v2/transcript?video_url=dQw4w9WgXcQ" \
  -H "Authorization: Bearer ytt_your_key_here"

The response shape:

{
  "video_id": "dQw4w9WgXcQ",
  "language": "en",
  "transcript": [
    { "text": "Welcome to the tutorial.", "start": 1200, "duration": 2100 },
    { "text": "Today we're covering batch inference.", "start": 3300, "duration": 3000 }
  ],
  "metadata": {
    "title": "Batch Inference Deep Dive",
    "author_name": "ML Explained",
    "thumbnail_url": "https://i.ytimg.com/vi/dQw4w9WgXcQ/hqdefault.jpg"
  }
}

start and duration are in milliseconds. Divide by 1000 to get seconds for display. The lang parameter lets you request a specific caption track when a video has multiple languages available.

Here is the same request in Python:

import requests

API_KEY = "ytt_your_key_here"
VIDEO_URL = "dQw4w9WgXcQ"

response = requests.get(
    "https://getyoutubetranscriber.com/api/v2/transcript",
    params={"video_url": VIDEO_URL},
    headers={"Authorization": f"Bearer {API_KEY}"},
)
response.raise_for_status()
data = response.json()
segments = data["transcript"]
title = data["metadata"]["title"]

And in Node.js:

const fetch = require("node-fetch");

const API_KEY = "ytt_your_key_here";
const VIDEO_URL = "dQw4w9WgXcQ";

const res = await fetch(
  `https://getyoutubetranscriber.com/api/v2/transcript?video_url=${VIDEO_URL}`,
  { headers: { Authorization: `Bearer ${API_KEY}` } }
);
const data = await res.json();
const { transcript, metadata } = data;

Step 2: Chunk Transcript Segments by Timestamp

Raw transcript segments are too fine-grained to embed individually. A typical segment is one sentence or less, maybe 10-20 tokens. Embedding each sentence separately means your retrieval returns fragments without enough context to answer a question.

Group consecutive segments into chunks of roughly 400-512 tokens, carrying the start time of the first segment in each chunk. This gives the model enough context per chunk while keeping chunks focused enough for precise retrieval.

def chunk_transcript(segments, max_tokens=450, overlap_tokens=50):
    """
    Group segments into chunks of ~max_tokens with timestamp-based overlap.
    Returns a list of dicts: {text, start_ms, end_ms}
    """
    chunks = []
    current_text = []
    current_tokens = 0
    current_start = None
    last_end = 0

    for seg in segments:
        # Rough token estimate: 1 token ≈ 4 chars
        seg_tokens = len(seg["text"]) // 4
        if current_tokens + seg_tokens > max_tokens and current_text:
            chunks.append({
                "text": " ".join(current_text),
                "start_ms": current_start,
                "end_ms": last_end,
            })
            # Keep last N words for overlap
            overlap_words = current_text[-5:]
            current_text = overlap_words
            current_tokens = sum(len(w) for w in overlap_words) // 4
            current_start = current_start  # keep original start for overlap window

        if current_start is None:
            current_start = seg["start"]
        current_text.append(seg["text"])
        current_tokens += seg_tokens
        last_end = seg["start"] + seg["duration"]

    if current_text:
        chunks.append({
            "text": " ".join(current_text),
            "start_ms": current_start,
            "end_ms": last_end,
        })

    return chunks

Each chunk now carries start_ms and end_ms. When you retrieve it later, you can format the timestamp as HH:MM:SS and link directly to that point in the video.

def ms_to_timestamp(ms):
    s = ms // 1000
    return f"{s // 3600:02d}:{(s % 3600) // 60:02d}:{s % 60:02d}"

Step 3: Embed Chunks and Store for Retrieval

With chunks in hand, embed each one using OpenAI's text-embedding-3-small (or any model you prefer) and store the vectors alongside the chunk metadata in a vector database. For prototyping, chromadb works locally with no infrastructure.

import chromadb
from openai import OpenAI

openai_client = OpenAI(api_key="sk-...")
chroma_client = chromadb.Client()
collection = chroma_client.create_collection(name="video_qa")

chunks = chunk_transcript(segments)

for i, chunk in enumerate(chunks):
    embedding = openai_client.embeddings.create(
        input=chunk["text"],
        model="text-embedding-3-small",
    ).data[0].embedding

    collection.add(
        ids=[f"chunk_{i}"],
        embeddings=[embedding],
        documents=[chunk["text"]],
        metadatas=[{
            "start_ms": chunk["start_ms"],
            "end_ms": chunk["end_ms"],
            "video_id": VIDEO_URL,
            "title": title,
        }],
    )

print(f"Indexed {len(chunks)} chunks from '{title}'")

For production, swap ChromaDB for Pinecone, Weaviate, or pgvector depending on your stack. The embedding and retrieval logic stays the same.

Step 4: Wire Chunks into a Grounded LLM Prompt

When a user asks a question, embed the query, retrieve the top-k chunks, and build a prompt that forces the model to ground its answer in the retrieved text.

def answer_question(question, collection, openai_client, k=4):
    # Embed the question
    q_embedding = openai_client.embeddings.create(
        input=question,
        model="text-embedding-3-small",
    ).data[0].embedding

    # Retrieve top-k chunks
    results = collection.query(query_embeddings=[q_embedding], n_results=k)
    docs = results["documents"][0]
    metas = results["metadatas"][0]

    # Format context with timestamps
    context_blocks = []
    for doc, meta in zip(docs, metas):
        ts = ms_to_timestamp(meta["start_ms"])
        context_blocks.append(f"[{ts}] {doc}")
    context = "\n\n".join(context_blocks)

    # Build grounded prompt
    system_prompt = (
        "You are a helpful assistant answering questions about a YouTube video. "
        "Answer only using the transcript excerpts below. "
        "When you reference information, include the timestamp in brackets, e.g. [00:23:14]. "
        "If the excerpts do not contain the answer, say so."
    )
    user_prompt = f"Transcript excerpts:\n\n{context}\n\nQuestion: {question}"

    response = openai_client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[
            {"role": "system", "content": system_prompt},
            {"role": "user", "content": user_prompt},
        ],
    )
    return response.choices[0].message.content


# Example
answer = answer_question("What batch size does the speaker recommend?", collection, openai_client)
print(answer)
# → "The speaker recommends a batch size of 32 for most training runs [00:23:14],
#    though they note that larger batches may require learning rate scaling [00:31:02]."

The grounded prompt structure does three things: it constrains the model to the retrieved text (reducing hallucination), it instructs the model to cite timestamps, and it gives the model an out when the information is not in the transcript.

For a more complete production guide on feeding transcript data to LLMs, see Feed YouTube Transcripts to GPT and Claude.

Gotcha: Videos with No Transcript or Auto-Captions Only

Not every video has a transcript. Some creators disable captions. Others have only auto-generated captions, which can be sparse or missing entirely for music-heavy or non-speech content.

Your application needs to handle this before it tries to chunk and embed an empty array.

response = requests.get(
    "https://getyoutubetranscriber.com/api/v2/transcript",
    params={"video_url": VIDEO_URL},
    headers={"Authorization": f"Bearer {API_KEY}"},
)

if response.status_code == 404:
    # No transcript available for this video
    raise ValueError(f"No transcript found for video {VIDEO_URL}.")

response.raise_for_status()
data = response.json()

if not data.get("transcript"):
    raise ValueError("Transcript returned empty. The video may have captions disabled.")

segments = data["transcript"]

A few things to know:

  • The lang parameter lets you fall back to a secondary language if your preferred language track is absent. Try lang=en first, then check whether the returned language field matches what you requested.
  • Auto-captions on YouTube are generated by Google's ASR models. Quality varies by speaker accent, audio quality, and subject matter. For technical content with jargon, auto-captions often contain errors that will degrade retrieval accuracy.

If your application targets a wide range of user-submitted videos rather than a curated library, expect a meaningful share of requests to hit this case. Build your fallback early rather than as an afterthought.

Putting It Together: A Minimal End-to-End Script

import requests
import chromadb
from openai import OpenAI

API_KEY = "ytt_your_key_here"
OPENAI_KEY = "sk-..."
VIDEO_URL = "YOUR_VIDEO_ID"

def ms_to_timestamp(ms):
    s = ms // 1000
    return f"{s // 3600:02d}:{(s % 3600) // 60:02d}:{s % 60:02d}"

def fetch_transcript(video_url, api_key):
    r = requests.get(
        "https://getyoutubetranscriber.com/api/v2/transcript",
        params={"video_url": video_url},
        headers={"Authorization": f"Bearer {api_key}"},
    )
    if r.status_code == 404:
        raise ValueError("No transcript available.")
    r.raise_for_status()
    data = r.json()
    if not data.get("transcript"):
        raise ValueError("Empty transcript.")
    return data["transcript"], data["metadata"]["title"]

def chunk_transcript(segments, max_tokens=450):
    chunks, current, start = [], [], None
    for seg in segments:
        if start is None:
            start = seg["start"]
        current.append(seg["text"])
        if sum(len(t) for t in current) // 4 >= max_tokens:
            chunks.append({"text": " ".join(current), "start_ms": start})
            current, start = [], None
    if current:
        chunks.append({"text": " ".join(current), "start_ms": start})
    return chunks

def build_index(chunks, video_id, title, openai_client):
    client = chromadb.Client()
    col = client.create_collection("video_qa")
    for i, chunk in enumerate(chunks):
        emb = openai_client.embeddings.create(
            input=chunk["text"], model="text-embedding-3-small"
        ).data[0].embedding
        col.add(
            ids=[f"c{i}"],
            embeddings=[emb],
            documents=[chunk["text"]],
            metadatas=[{"start_ms": chunk["start_ms"], "video_id": video_id, "title": title}],
        )
    return col

def ask(question, col, openai_client):
    qe = openai_client.embeddings.create(
        input=question, model="text-embedding-3-small"
    ).data[0].embedding
    res = col.query(query_embeddings=[qe], n_results=4)
    ctx = "\n\n".join(
        f"[{ms_to_timestamp(m['start_ms'])}] {d}"
        for d, m in zip(res["documents"][0], res["metadatas"][0])
    )
    resp = openai_client.chat.completions.create(
        model="gpt-4o-mini",
        messages=[
            {"role": "system", "content": "Answer using only the transcript excerpts. Cite timestamps."},
            {"role": "user", "content": f"Excerpts:\n\n{ctx}\n\nQuestion: {question}"},
        ],
    )
    return resp.choices[0].message.content

# Run it
openai_client = OpenAI(api_key=OPENAI_KEY)
segments, title = fetch_transcript(VIDEO_URL, API_KEY)
chunks = chunk_transcript(segments)
col = build_index(chunks, VIDEO_URL, title, openai_client)

while True:
    q = input("Ask a question (or 'quit'): ")
    if q.lower() == "quit":
        break
    print(answer_question(q, col, openai_client))

This script runs locally with no persistent infrastructure. Swap ChromaDB for your production vector store and wrap the REPL in a web endpoint to ship a real product.

What to Build Next

This pattern extends naturally. You can index entire playlists instead of single videos, run the same chunking and embedding pipeline across hundreds of videos, and let users ask questions across a whole channel's content library.

The chunking approach here also applies directly to fine-tuning datasets and RAG corpora for AI research. If you are building at that scale, the OpenAI embeddings documentation covers model selection, dimensions, and cost tradeoffs worth reading before you commit to an embedding model.

Get Started with 100 Free Credits

The YouTube Transcriber API gives you 100 free credits with no credit card required. That is enough to index several long videos and build a working prototype today. Sign up at getyoutubetranscriber.com, grab your Bearer token, and run the fetch command at the top of this guide against a video you actually care about. The chatbot logic above is copy-paste ready.