Voltar ao blogIntegrações

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

Busque a transcrição de cada vídeo por meio de uma API hospedada, divida o texto em blocos, gere embeddings desses blocos e armazene os vetores - depois, no momento da consulta, gere o embedding da pergunta, recupere os blocos mais próximos e entregue-os a um LLM como contexto fundamentado, com citações de volta ao vídeo de origem.
01

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.

02

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.

03

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.

04

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.

05

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

Q01

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.

Q02

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.

Q03

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.

Q04

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.

Q05

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