YouTube Summarizer with Transcript Chunking and Whisper Fallback
Solution for the use case YouTube Video Summarizer.
- AI tools
- Python, youtube-transcript-api, faster-whisper, yt-dlp, OpenAI Python SDK, OpenAI API (gpt-6-luna), Ollama (optional, local)
- Language
- Python
- Published
- 2025-06-28
Approach
A Python command line tool that summarizes a YouTube video from its transcript and returns a short summary, key points and chapter timestamps.
- Transcript: captions via youtube-transcript-api; if a video has none, the audio is downloaded with yt-dlp and transcribed locally with faster-whisper.
- Chunking: each transcript line keeps its [mm:ss] timestamp, so the model can cite where a point is made.
- Summarization: map-reduce over chunks with the OpenAI Python SDK, either against OpenAI or a local model through Ollama's OpenAI-compatible API.
Implementation steps
- Extract the 11-character video id from watch, youtu.be, shorts, embed and live URLs
- Fetch captions in the preferred languages with youtube-transcript-api
- If there are no captions and the Whisper fallback is enabled, download the audio with yt-dlp and transcribe it with faster-whisper (CPU, int8, VAD filter)
- Prefix every transcript line with its [mm:ss] timestamp and group lines into chunks of about 12,000 characters
- Summarize each chunk into timestamped notes with a chat model (skipped when the video fits into one chunk)
- Merge the notes into a final summary, key points and chapter list, with the output language configurable
- Configure the model and endpoint via OPENAIMODEL and OPENAIBASE_URL to switch between OpenAI and Ollama
- Test offline with fake caption API, fake Whisper model and fake chat client
Code (Python)
"""Map-reduce summarization of long transcripts with an OpenAI-compatible chat model.
1. Chunk the timed transcript (each line keeps its [mm:ss] timestamp).
2. Map: summarize every chunk into notes with timestamps.
3. Reduce: merge the notes into one summary with key points and chapter timestamps.
Short transcripts that fit into one chunk skip the reduce step.
"""
import os
from .transcript import Segment
CHUNK_PROMPT = (
"You get part {part} of {total} of a video transcript. Every line starts with a [mm:ss] timestamp. "
"Write concise notes of the main points in this part, each note starting with the timestamp where "
"it is discussed. Use only information from the transcript."
)
FINAL_PROMPT = (
"Below are timestamped notes from a video titled \"{title}\". Write in {language}:\n"
"1. A summary of at most {words} words.\n"
"2. 5 to 7 key points as bullet points.\n"
"3. A list of chapters as \"mm:ss - topic\", using only timestamps that appear in the notes.\n"
"Do not add facts that are not in the notes."
)
def timestamp(seconds: float) -> str:
seconds = int(seconds)
hours, rest = divmod(seconds, 3600)
return f"{hours}:{rest // 60:02d}:{rest % 60:02d}" if hours else f"{rest // 60:02d}:{rest % 60:02d}"
def chunk_transcript(segments: list[Segment], max_chars: int = 12000) -> list[str]:
"""Group timestamped lines into chunks of at most max_chars (a single long line stays whole)."""
chunks, current, size = [], [], 0
for seg in segments:
line = f"[{timestamp(seg.start)}] {seg.text}"
if current and size + len(line) + 1 > max_chars:
chunks.append("\n".join(current))
current, size = [], 0
current.append(line)
size += len(line) + 1
if current:
chunks.append("\n".join(current))
return chunks
def make_client():
from openai import OpenAI
# OPENAI_BASE_URL is read by the SDK too; passing it explicitly makes the Ollama option obvious
return OpenAI(base_url=os.getenv("OPENAI_BASE_URL") or None)
class Summarizer:
def __init__(self, client=None, model: str | None = None, max_chars: int = 12000):
self.client = client or make_client()
self.model = model or os.getenv("OPENAI_MODEL", "gpt-6-luna")
self.max_chars = max_chars
def _chat(self, system: str, user: str) -> str:
response = self.client.chat.completions.create(
model=self.model,
messages=[{"role": "system", "content": system}, {"role": "user", "content": user}],
)
return response.choices[0].message.content.strip()
def summarize(self, segments: list[Segment], title: str = "untitled", language: str = "English",
words: int = 200) -> str:
if not segments:
raise ValueError("empty transcript")
chunks = chunk_transcript(segments, self.max_chars)
if len(chunks) == 1:
notes = chunks[0] # fits in one request: summarize the transcript directly
else:
notes = "\n\n".join(
self._chat(CHUNK_PROMPT.format(part=i, total=len(chunks)), chunk)
for i, chunk in enumerate(chunks, start=1)
)
final = FINAL_PROMPT.format(title=title, language=language, words=words)
return self._chat(final, notes)
Prompts
Below are timestamped notes from a video titled "{title}". Write in {language}:
1. A summary of at most {words} words.
2. 5 to 7 key points as bullet points.
3. A list of chapters as "mm:ss - topic", using only timestamps that appear in the notes.
Do not add facts that are not in the notes.
Performance
Expectations, not measurements:
- A video with captions needs one caption request plus one chat request per 12,000 characters of transcript (about 15 to 20 minutes of speech) and one final request.
- The Whisper fallback runs locally; on CPU with the small model expect roughly real time or slower.
- Summary quality depends on the model and on caption quality; there is no accuracy figure.
Lessons learned
The user passes a video URL and gets a plain-text summary, bullet-point key points and a chapter list such as 04:12 - Pricing, so they can jump to the parts they need. Caption languages and the summary language can be chosen. The tool says whether the transcript came from captions or from Whisper.