← Back to blog

YouTube Transcript API: Build a Video Summarizer with GPT

David Boulen · 6/22/2026 · 11 min read

YouTube Transcript API: Build a Video Summarizer with GPT

YouTube Transcript API: Build a Video Summarizer with GPT

Developer building a YouTube transcript API pipeline with Python and GPT code on screen

TL;DR: Fetch a clean YouTube transcript via API → chunk it to fit GPT's context window → prompt with a JSON schema → cache results by video_id. The full working pipeline is in Python below, with Node.js and Go alternatives at the end.

YouTube hosts over 500 hours of video uploaded every minute (Global Media Insight, 2026). That's a staggering amount of content that most people will never watch in full — but still need the signal from. A YouTube transcript API combined with GPT changes that: give it a video URL, get back a structured summary in milliseconds.

This post walks you through building exactly that. You'll fetch a clean transcript, chunk it to fit within GPT's token limits, write prompts that produce structured output, and cache results so you're not re-fetching and re-summarizing the same video twice. By the end you'll have a working /summarize endpoint you can drop into any app.

Architecture: Fetch, Chunk, Summarize, Return

The pipeline has four stages:

  1. Fetch — retrieve the raw transcript via a YouTube transcript API
  2. Chunk — split into segments that fit within GPT's token budget
  3. Summarize — call OpenAI's chat completions API with a structured prompt
  4. Cache — store the result keyed by video ID

Each stage is independent, which makes the whole thing easy to test, swap out, or scale. The transcript fetch is the only part with external dependencies you don't control — YouTube blocks scrapers aggressively, which is why it's worth using a managed API rather than a DIY scraper. More on that below.

The skeleton

Here's what the full flow looks like before filling in each function:

async def summarize_video(video_id: str) -> dict:
    # 1. Check cache first
    cached = cache.get(video_id)
    if cached:
        return cached

    # 2. Fetch transcript
    transcript = await fetch_transcript(video_id)

    # 3. Chunk if needed
    chunks = chunk_transcript(transcript, max_tokens=100_000)

    # 4. Summarize
    summary = await summarize_chunks(chunks)

    # 5. Store and return
    cache.set(video_id, summary, ttl=86400)
    return summary

Pulling Clean Transcripts to Avoid Garbage-In Summaries

The quality of your summary depends entirely on the quality of the transcript. YouTube auto-captions are notoriously messy: run-on sentences, missing punctuation, broken speaker changes, HTML entities like & sprinkled throughout. Feed that into GPT and you get summaries that miss context, hallucinate structure, or produce incoherent bullet points.

Why raw captions fail

Auto-generated captions frequently output something like:

um so today we're gonna be talking about uh you know the whole concept of like
machine learning and and how it relates to data pipelines & um yeah

GPT will attempt to summarize that faithfully — filler words, broken encoding, and all.

Fetching clean JSON transcripts

The cleanest path is a YouTube transcript API that handles extraction and normalization for you. YouTube Transcriber returns transcripts as clean JSON — no HTML, no raw timestamps cluttering the text, with proper sentence boundaries where the captions have them. A single GET request:

curl -X GET "https://api.getyoutubetranscriber.com/transcript?video_id=dQw4w9WgXcQ&lang=en" \
  -H "Authorization: Bearer YOUR_TOKEN"

Returns structured JSON you can work with immediately:

{
  "video_id": "dQw4w9WgXcQ",
  "language": "en",
  "transcript": [
    { "start": 0.0, "duration": 4.2, "text": "We're no strangers to love" },
    { "start": 4.2, "duration": 3.8, "text": "You know the rules and so do I" }
  ]
}

Assembling the transcript string

To assemble this into a single string for GPT, join the text fields:

import httpx

async def fetch_transcript(video_id: str) -> str:
    async with httpx.AsyncClient() as client:
        resp = await client.get(
            "https://api.getyoutubetranscriber.com/transcript",
            params={"video_id": video_id, "lang": "en"},
            headers={"Authorization": f"Bearer {TRANSCRIPT_API_TOKEN}"},
        )
        resp.raise_for_status()
        data = resp.json()

    segments = data.get("transcript", [])
    if not segments:
        raise ValueError(f"No transcript available for video {video_id}")

    return " ".join(seg["text"] for seg in segments)

This gives you clean prose that GPT can parse without fighting formatting noise. For more on the API's JSON format and language options, see How to Get a YouTube Transcript as JSON in 5 Minutes.

Python Code for Chunking Long Transcripts Within Token Limits

GPT-4o has a 128,000-token context window — large enough for most videos. A typical 10-minute video produces around 1,500–2,500 tokens of transcript; a 60-minute video might hit 15,000–25,000 tokens. According to OpenAI's API documentation, GPT-4o supports up to 128K input tokens with a maximum output of 16,384 tokens. You're generally fine without chunking unless you're processing long interviews, lectures, or conference talks.

Token counting with tiktoken

That said, a robust implementation should handle the edge cases. The approach below uses tiktoken — OpenAI's tokenizer — to count tokens accurately, then splits at sentence boundaries to keep chunks coherent:

import tiktoken

def chunk_transcript(text: str, max_tokens: int = 100_000) -> list[str]:
    """Split transcript into chunks that fit within the token budget."""
    enc = tiktoken.encoding_for_model("gpt-4o")
    tokens = enc.encode(text)

    if len(tokens) <= max_tokens:
        return [text]  # No chunking needed

    # Split into sentences first
    sentences = text.replace(". ", ".|").replace("? ", "?|").split("|")

    chunks = []
    current_chunk = []
    current_count = 0

    for sentence in sentences:
        sentence_tokens = len(enc.encode(sentence))
        if current_count + sentence_tokens > max_tokens:
            if current_chunk:
                chunks.append(" ".join(current_chunk))
            current_chunk = [sentence]
            current_count = sentence_tokens
        else:
            current_chunk.append(sentence)
            current_count += sentence_tokens

    if current_chunk:
        chunks.append(" ".join(current_chunk))

    return chunks

Set max_tokens conservatively — leaving room for your system prompt and the model's output. With GPT-4o's 128K window, 100,000 tokens for the transcript gives you comfortable headroom.

Handling multi-chunk videos

Gotcha: If a video has multiple chunks, summarize each chunk separately, then do a final "summary of summaries" pass. This prevents information from later chunks being silently dropped.

async def summarize_chunks(chunks: list[str]) -> dict:
    if len(chunks) == 1:
        return await call_gpt_summarize(chunks[0])

    # Multi-chunk: summarize each, then consolidate
    partial_summaries = []
    for chunk in chunks:
        result = await call_gpt_summarize(chunk, is_partial=True)
        partial_summaries.append(result["summary"])

    consolidated_text = "\n\n---\n\n".join(partial_summaries)
    return await call_gpt_summarize(consolidated_text, is_consolidation=True)

Prompt Patterns for Accurate, Structured Summaries

Open-ended prompts like "summarize this video" produce inconsistent output — sometimes bullets, sometimes paragraphs, sometimes a mix. For an API response you're serving to other applications, you want deterministic structure.

Using JSON mode with a schema

The most reliable approach is to specify the exact output schema in the system prompt and use response_format={"type": "json_object"}:

import openai

SYSTEM_PROMPT = """You are a video content analyst. Summarize YouTube video transcripts into structured JSON.

Return ONLY valid JSON in this exact schema:
{
  "title_suggestion": "A concise title (max 10 words)",
  "one_line_summary": "Single sentence capturing the core topic",
  "key_points": ["Point 1", "Point 2", "Point 3", "Point 4", "Point 5"],
  "detailed_summary": "2-3 paragraph narrative summary",
  "topics": ["topic1", "topic2", "topic3"],
  "target_audience": "Who this content is for",
  "sentiment": "informative|educational|entertainment|promotional|news"
}

Rules:
- key_points must have 3-7 items, each under 20 words
- detailed_summary must be 100-250 words
- Do not add commentary outside the JSON"""

async def call_gpt_summarize(
    transcript: str,
    is_partial: bool = False,
    is_consolidation: bool = False,
) -> dict:
    if is_partial:
        user_msg = f"Summarize this transcript segment:\n\n{transcript}"
    elif is_consolidation:
        user_msg = f"Consolidate these partial summaries into one unified summary:\n\n{transcript}"
    else:
        user_msg = f"Summarize this full video transcript:\n\n{transcript}"

    client = openai.AsyncOpenAI()
    response = await client.chat.completions.create(
        model="gpt-4o",
        messages=[
            {"role": "system", "content": SYSTEM_PROMPT},
            {"role": "user", "content": user_msg},
        ],
        response_format={"type": "json_object"},
        temperature=0.3,  # Lower temperature = more consistent structure
    )

    import json
    return json.loads(response.choices[0].message.content)

Temperature and validation

Temperature at 0.3 is the right call here. You want accuracy and consistency, not creativity. Higher temperatures cause the model to deviate from the schema.

Best practice: Always validate the returned JSON against your schema before serving it. GPT occasionally omits optional fields or reorders arrays. A Pydantic model catches this:

from pydantic import BaseModel

class VideoSummary(BaseModel):
    title_suggestion: str
    one_line_summary: str
    key_points: list[str]
    detailed_summary: str
    topics: list[str]
    target_audience: str
    sentiment: str

# In your summarize function:
return VideoSummary(**raw_json).model_dump()

Caching Results to Save Credits and API Calls

Transcribing and summarizing the same video repeatedly is pure waste. A caching layer keyed by video_id eliminates duplicate calls at both the transcript fetch and GPT stages.

Redis caching implementation for YouTube transcript and GPT summarization data pipeline

Redis cache implementation

For production use, Redis is the standard choice. For development or lower-scale deployments, a simple file-based cache works fine. Here's a Redis implementation with a sensible TTL:

import json
import redis.asyncio as redis

class SummaryCache:
    def __init__(self, redis_url: str = "redis://localhost:6379"):
        self.client = redis.from_url(redis_url)
        self.transcript_ttl = 7 * 86400   # 7 days — transcripts rarely change
        self.summary_ttl = 24 * 3600      # 24 hours — summaries can be regenerated

    async def get_transcript(self, video_id: str) -> str | None:
        data = await self.client.get(f"transcript:{video_id}")
        return data.decode() if data else None

    async def set_transcript(self, video_id: str, transcript: str) -> None:
        await self.client.setex(
            f"transcript:{video_id}", self.transcript_ttl, transcript
        )

    async def get_summary(self, video_id: str) -> dict | None:
        data = await self.client.get(f"summary:{video_id}")
        return json.loads(data) if data else None

    async def set_summary(self, video_id: str, summary: dict) -> None:
        await self.client.setex(
            f"summary:{video_id}", self.summary_ttl, json.dumps(summary)
        )

Wire it into the main pipeline:

cache = SummaryCache()

async def summarize_video(video_id: str) -> dict:
    # Check summary cache first
    cached_summary = await cache.get_summary(video_id)
    if cached_summary:
        return {**cached_summary, "cached": True}

    # Check transcript cache before fetching
    transcript = await cache.get_transcript(video_id)
    if not transcript:
        transcript = await fetch_transcript(video_id)
        await cache.set_transcript(video_id, transcript)

    chunks = chunk_transcript(transcript)
    summary = await summarize_chunks(chunks)
    await cache.set_summary(video_id, summary)

    return {**summary, "cached": False}

Cost math

GPT-4o costs roughly $2.50 per 1M input tokens. A 10-minute video transcript (≈2,000 tokens) costs a fraction of a cent to summarize. Cache even 50% of repeat requests and the savings compound fast at scale.

Gotcha: YouTube sometimes updates auto-captions after upload, especially for content with poor initial caption quality. Consider invalidating transcript cache after 7 days, or expose a force_refresh=True parameter in your API for cases where freshness matters.

Putting It Together: A FastAPI Endpoint

Here's the full /summarize endpoint wiring all four stages together:

from fastapi import FastAPI, HTTPException, Query
from pydantic import BaseModel

app = FastAPI()
cache = SummaryCache()

class SummaryResponse(BaseModel):
    video_id: str
    title_suggestion: str
    one_line_summary: str
    key_points: list[str]
    detailed_summary: str
    topics: list[str]
    target_audience: str
    sentiment: str
    cached: bool

@app.get("/summarize", response_model=SummaryResponse)
async def summarize(
    video_id: str = Query(..., description="YouTube video ID"),
    lang: str = Query("en", description="Transcript language code"),
):
    try:
        result = await summarize_video(video_id)
        return SummaryResponse(video_id=video_id, **result)
    except ValueError as e:
        raise HTTPException(status_code=404, detail=str(e))
    except Exception as e:
        raise HTTPException(status_code=500, detail="Summarization failed")

Call it:

curl "http://localhost:8000/summarize?video_id=dQw4w9WgXcQ"

Response:

{
  "video_id": "dQw4w9WgXcQ",
  "title_suggestion": "Never Gonna Give You Up — Rick Astley",
  "one_line_summary": "A pop declaration of unconditional romantic commitment.",
  "key_points": [
    "Promises to never abandon the relationship",
    "Expresses complete emotional dedication",
    "Contrasts insincere people with the narrator's loyalty"
  ],
  "detailed_summary": "The song presents a first-person narrator...",
  "topics": ["pop music", "romance", "loyalty"],
  "target_audience": "General pop music listeners",
  "sentiment": "entertainment",
  "cached": false
}

JavaScript and Go Alternatives

If Python isn't your stack, the same approach works across languages.

Node.js

const axios = require('axios');

async function fetchTranscript(videoId, lang = 'en') {
  const { data } = await axios.get(
    'https://api.getyoutubetranscriber.com/transcript',
    {
      params: { video_id: videoId, lang },
      headers: { Authorization: `Bearer ${process.env.TRANSCRIPT_API_TOKEN}` },
    }
  );
  return data.transcript.map(seg => seg.text).join(' ');
}

Go

func fetchTranscript(videoID, lang string) (string, error) {
    req, _ := http.NewRequest("GET",
        "https://api.getyoutubetranscriber.com/transcript", nil)
    q := req.URL.Query()
    q.Add("video_id", videoID)
    q.Add("lang", lang)
    req.URL.RawQuery = q.Encode()
    req.Header.Set("Authorization", "Bearer "+os.Getenv("TRANSCRIPT_API_TOKEN"))

    resp, err := http.DefaultClient.Do(req)
    if err != nil {
        return "", err
    }
    defer resp.Body.Close()

    var result struct {
        Transcript []struct{ Text string `json:"text"` } `json:"transcript"`
    }
    json.NewDecoder(resp.Body).Decode(&result)

    var parts []string
    for _, seg := range result.Transcript {
        parts = append(parts, seg.Text)
    }
    return strings.Join(parts, " "), nil
}

The structure is identical across all three: fetch, assemble, chunk, summarize.

Common Issues and How to Fix Them

YouTube blocking transcript requests at scale

One problem you'll hit before long: YouTube blocks direct transcript requests when you exceed rate limits or hit IP-based restrictions. The youtube-transcript-api library runs into this frequently in production — the open-source library works great for low volume but starts failing at scale. Using a managed API that handles proxy rotation and retries on your behalf removes this operational burden entirely from your code.

Other edge cases

  • Videos without captions: Not all YouTube videos have transcripts. Always handle the empty-transcript case explicitly rather than passing a blank string to GPT.
  • Non-English transcripts: Pass lang=es (or the appropriate BCP-47 code) to get the native-language transcript. YouTube Transcriber supports 125+ languages.
  • Very short videos: Videos under 60 seconds often have sparse or missing captions. Consider a minimum-length check before attempting summarization.
  • GPT refusals: Certain video content (news, political commentary) may trigger content policy responses. Catch these and return a graceful error to your users.

Try It With Free Credits

YouTube Transcriber starts at 100 free credits — no credit card required. Each successful transcript call costs one credit, so you can run the full pipeline above against 100 real videos before spending anything.

Sign up at getyoutubetranscriber.com and you'll have your first summary running in under five minutes. The docs cover all available endpoints, language codes, and response schemas.


Sources: YouTube uploads statistics — Global Media Insight (2026); GPT-4o token limits — OpenAI API documentation; Video summarization with LLMs — NVIDIA AI Blueprint