How to Build a RAG Pipeline with YouTube Transcripts
A working RAG pipeline over YouTube video transcripts: collect transcripts across a video list, chunk them, embed and store the vectors, retrieve the relevant chunks, and generate a cited answer - full code for every step, plus how to keep the index fresh.
00:09:00 · SEP 27, 2026
Resposta rápida
Colete as transcrições
Consiga uma chave no painel (100 créditos grátis, sem cartão) e busque a transcrição de cada vídeo pelo id. Os créditos são cobrados por requisição, não por minuto de vídeo, então uma palestra de 3 horas custa o mesmo 1 crédito que um clipe de 2 minutos - o que importa quando você está ingerindo todo o catálogo antigo de um canal:
async function fetchTranscript(videoId: string) {
const res = await fetch(
`https://getyoutubetranscript.com/api/v1/transcript?v=${videoId}`,
{ headers: { Authorization: `Bearer ${process.env.GETYOUTUBETRANSCRIPT_API_KEY}` } }
);
const json = await res.json();
if (!json.success) throw new Error(json.message);
return json.data as { video_id: string; title: string; transcript: string; word_count: number };
}
const videoIds = ['dQw4w9WgXcQ', 'jNQXAC9IVRw' /* ... */];
const videos = await Promise.all(videoIds.map(fetchTranscript));video_id e title vêm junto com o texto da transcrição - é a isso que a etapa 5 cita de volta na resposta.
Divida as transcrições em blocos
Divida cada transcrição em janelas sobrepostas antes de gerar o embedding - um embedding do vídeo inteiro é grosseiro demais para recuperar uma resposta específica, e nenhuma biblioteca é necessária para isso:
function chunkText(text: string, size = 1200, overlap = 200) {
const chunks: string[] = [];
let start = 0;
while (start < text.length) {
const end = Math.min(start + size, text.length);
chunks.push(text.slice(start, end));
start += size - overlap;
}
return chunks;
}
const records = videos.flatMap((video) =>
chunkText(video.transcript).map((text, chunkIndex) => ({
videoId: video.video_id,
title: video.title,
chunkIndex,
text,
}))
);Uma janela de 1.200 caracteres com 200 caracteres de sobreposição é um padrão razoável para transcrições faladas - reduza para clipes curtos e densos, aumente para conteúdo longo no estilo palestra, onde mais contexto ao redor ajuda.
Gere embeddings e armazene
Gere o embedding de cada bloco e mantenha o id e o título do vídeo junto com o vetor, já que é isso que transforma um bloco recuperado em uma citação mais tarde:
import OpenAI from 'openai';
const openai = new OpenAI();
async function embed(text: string) {
const res = await openai.embeddings.create({ model: 'text-embedding-3-small', input: text });
return res.data[0].embedding;
}
const index = await Promise.all(
records.map(async (r) => ({ ...r, embedding: await embed(r.text) }))
);Isso mantém os vetores em memória, por clareza. Passando de algumas centenas de blocos, troque o array por pgvector, Pinecone ou outro vector store - a etapa de recuperação abaixo só precisa de uma função que retorne os top-k vetores mais próximos, não exatamente esse formato.
Recupere os blocos relevantes
No momento da consulta, gere o embedding da pergunta com o mesmo modelo e classifique os blocos armazenados por similaridade de cosseno:
function cosineSimilarity(a: number[], b: number[]) {
let dot = 0, normA = 0, normB = 0;
for (let i = 0; i < a.length; i++) {
dot += a[i] * b[i];
normA += a[i] ** 2;
normB += b[i] ** 2;
}
return dot / (Math.sqrt(normA) * Math.sqrt(normB));
}
async function retrieve(query: string, k = 5) {
const queryEmbedding = await embed(query);
return index
.map((r) => ({ ...r, score: cosineSimilarity(r.embedding, queryEmbedding) }))
.sort((a, b) => b.score - a.score)
.slice(0, k);
}k=5 é um ponto de partida - poucos blocos e o modelo não tem contexto suficiente para responder por completo; muitos, e blocos irrelevantes começam a diluir a resposta.
Gere uma resposta fundamentada
Passe os blocos recuperados ao modelo como contexto numerado e instrua-o a responder somente com base no que está ali e a citar suas fontes - é isso que mantém a resposta rastreável até um vídeo real, em vez do conhecimento geral do próprio modelo:
async function answer(query: string) {
const matches = await retrieve(query);
const context = matches
.map((m, i) => `[${i + 1}] From "${m.title}":\n${m.text}`)
.join('\n\n');
const completion = await openai.chat.completions.create({
model: 'gpt-4.1-mini',
messages: [
{
role: 'system',
content:
"Answer only using the numbered transcript excerpts below. Cite sources as [1], [2], etc. " +
"If the excerpts don't contain the answer, say so.",
},
{ role: 'user', content: `${context}\n\nQuestion: ${query}` },
],
});
return completion.choices[0].message.content;
}Citar pelo título do vídeo, em vez de inventar um timestamp, corresponde ao que a API de transcrição realmente retorna - não deixe o modelo fabricar um timestamp que nunca recebeu.
Mantenha o índice atualizado
Para um canal que continua publicando, verifique novos uploads periodicamente com o endpoint gratuito /channel/latest (0 créditos), pule os ids de vídeo que você já indexou e busque, divida em blocos e gere embeddings apenas dos novos. Assim, o custo de ingestão fica proporcional ao conteúdo novo, em vez de reindexar tudo a cada execução.
Por que transcrições funcionam bem para RAG
Vídeo é um dos formatos mais incômodos para construir recuperação em cima - você não consegue dar grep em um vídeo. Uma transcrição transforma isso de volta em texto, que seu stack de embedding e recuperação já sabe lidar, sem que você precise rodar seu próprio processo de speech-to-text. E como uma API que cobra por requisição não mede pelo tamanho do vídeo, ingerir um backlog de conteúdo longo (podcasts, palestras, streams de várias horas) não fica sistematicamente mais caro do que ingerir clipes curtos - exatamente o tipo de acervo que um pipeline de RAG costuma ser construído para pesquisar.
A referência completa de endpoints, autenticação e limites de taxa está na documentação da API. Consiga uma chave com 100 créditos grátis no painel para testar isso de ponta a ponta.
Perguntas frequentes sobre pipeline de RAG
Preciso de um banco de dados vetorial de verdade para seguir isso?
Não - o array em memória usado neste guia funciona bem até algumas centenas de blocos e é a forma mais rápida de verificar o pipeline de ponta a ponta. Migre para pgvector ou um vector store gerenciado quando estiver indexando mais que isso, ou quando precisar que o índice persista entre reinicializações.
Qual modelo de embedding eu devo usar?
text-embedding-3-small é um padrão razoável - barato e bom o suficiente para a maioria dos casos de recuperação de transcrição. Mude para text-embedding-3-large somente se estiver vendo problemas de qualidade na recuperação que ele plausivelmente resolveria; ele custa mais por chamada.
O que acontece se um vídeo não tiver transcrição nenhuma?
O endpoint de transcrição retorna um código de erro claro (TRANSCRIPT_DISABLED ou TRANSCRIPT_NOT_FOUND) em vez de uma string vazia - verifique success: false e pule esse vídeo, em vez de gerar o embedding de uma string vazia como se fosse conteúdo real.
Quanto custa indexar, digamos, 200 vídeos?
200 créditos para as buscas de transcrição, não importa se são clipes de 2 minutos ou vídeos de 2 horas - os 100 créditos grátis do cadastro cobrem, sozinhos, aproximadamente metade de um backlog de 200 vídeos. O custo de embedding é separado e cobrado pelo seu provedor de embeddings.
Posso citar um timestamp exato em vez de apenas o título do vídeo?
Não com este endpoint - a API pública de transcrição retorna texto puro, não segmentos com timestamp, então os exemplos aqui citam pelo título e id do vídeo. Se a citação no nível de timestamp importa para o seu caso de uso, mantenha os blocos menores para que a citação no nível do vídeo fique o mais precisa possível.
Relacionados
- YouTube API Quota Exceeded: Causes and Fixes
- YouTube Transcript API Rate Limit: What It Is and How to Handle 429s
- YouTube Transcript MCP Server: Setup Guide for Claude and Other AI Tools
- YouTube Transcripts in n8n: HTTP Request Workflow Guide
- Migrating from Supadata: A Field Guide
- How to Get YouTube Transcripts in Python
- Migrating from TranscriptAPI.com: A Field Guide
- Build an AI YouTube Video Summarizer with LangChain and Next.js
- The Best YouTube Transcript APIs in 2026
- GetYouTubeTranscript vs TranscriptAPI
- GetYouTubeTranscript vs youtubetotranscript.com
- GetYouTubeTranscript vs youtube-transcript.io
- GetYouTubeTranscript vs NoteGPT