Volver al blogIntegraciones

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

Respuesta rápida

Obtén la transcripción de cada video mediante una API alojada, divide el texto en fragmentos, incrusta los fragmentos y guarda los vectores - luego, en el momento de la consulta, incrusta la pregunta, recupera los fragmentos más cercanos y pásaselos a un LLM como contexto verificado con citas al video de origen.
01

Recopila las transcripciones

Consigue una clave desde el panel (100 créditos gratis, sin tarjeta) y obtén la transcripción de cada video por su id. Los créditos se cobran por solicitud, no por minuto de video, así que una conferencia de 3 horas cuesta el mismo 1 crédito que un clip de 2 minutos - algo que importa en cuanto empiezas a indexar todo el catálogo de un 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 y title llegan junto con el texto de la transcripción - eso es lo que el paso 5 usa para citar la respuesta.

02

Divide las transcripciones en fragmentos

Divide cada transcripción en ventanas superpuestas antes de incrustarla - incrustar un video entero es demasiado impreciso para recuperar una respuesta concreta, y no hace falta ninguna librería para esto:

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,
  }))
);

Una ventana de 1.200 caracteres con 200 de superposición es un valor por defecto razonable para transcripciones habladas - redúcela para clips cortos y densos, auméntala para contenido tipo conferencia donde más contexto alrededor ayuda.

03

Incrusta y almacena

Incrusta cada fragmento y guarda el id y el título del video junto al vector, ya que eso es lo que convierte un fragmento recuperado en una cita más adelante:

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) }))
);

Esto guarda los vectores en memoria por claridad. Pasadas unas pocas centenas de fragmentos, cambia el array por pgvector, Pinecone u otro almacén de vectores - el paso de recuperación de abajo solo necesita una función que devuelva los k vectores más cercanos, no exactamente esta forma.

04

Recupera los fragmentos relevantes

En el momento de la consulta, incrusta la pregunta con el mismo modelo y ordena los fragmentos guardados por similitud coseno:

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 es un punto de partida - con muy pocos fragmentos el modelo no tiene suficiente contexto para responder del todo, y con demasiados los fragmentos irrelevantes empiezan a diluir la respuesta.

05

Genera una respuesta verificada

Pasa los fragmentos recuperados al modelo como contexto numerado e indícale que responda solo con lo que hay ahí y que cite sus fuentes - esto es lo que mantiene la respuesta trazable hasta un video real en lugar del conocimiento general del propio 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 por título del video, en lugar de inventar una marca de tiempo, coincide con lo que la API de transcripción realmente devuelve - no dejes que el modelo invente una marca de tiempo que nunca se le dio.

Mantén el índice actualizado

Para un canal que sigue publicando, revisa si hay subidas nuevas con regularidad usando el endpoint gratuito /channel/latest (0 créditos), omite los ids de video que ya tienes indexados, y obtén, divide e incrusta solo los nuevos. Así el costo de ingesta se mantiene proporcional al contenido nuevo, en lugar de reindexar todo en cada ejecución.

Por qué las transcripciones funcionan bien para RAG

El video es uno de los formatos más incómodos para construir recuperación sobre él - no puedes hacer grep de un video. Una transcripción lo convierte de nuevo en texto, algo que tu stack de embeddings y recuperación ya sabe manejar, sin que tengas que correr tu propio proceso de reconocimiento de voz. Y como una API que cobra por solicitud no mide por duración del video, ingerir un archivo de contenido largo (podcasts, conferencias, streams de varias horas) no se vuelve sistemáticamente más caro que ingerir clips cortos - que es exactamente el tipo de archivo para el que suele construirse un pipeline de RAG.

La referencia completa de endpoints, autenticación y límites de velocidad está en los documentos de la API. Consigue una clave con 100 créditos gratis desde el panel para probarlo de principio a fin.

Preguntas frecuentes sobre el pipeline de RAG

Q01

¿Necesito una base de datos vectorial de verdad para seguir esta guía?

No - el array en memoria de esta guía funciona bien hasta unos pocos cientos de fragmentos y es la forma más rápida de verificar el pipeline de principio a fin. Pasa a pgvector o a un almacén de vectores gestionado cuando indexes más que eso, o cuando necesites que el índice persista entre reinicios.

Q02

¿Qué modelo de embeddings debería usar?

text-embedding-3-small es un valor por defecto razonable - barato y suficientemente bueno para la mayoría de casos de recuperación sobre transcripciones. Cambia a text-embedding-3-large solo si ves problemas de calidad en la recuperación que ese modelo pueda solucionar de forma plausible; cuesta más por llamada.

Q03

¿Qué pasa si un video no tiene transcripción?

El endpoint de transcripción devuelve un código de error claro (TRANSCRIPT_DISABLED o TRANSCRIPT_NOT_FOUND) en lugar de una cadena vacía - comprueba success: false y omite ese video en vez de incrustar una cadena vacía como si fuera contenido real.

Q04

¿Cuánto cuesta indexar, digamos, 200 videos?

200 créditos por las transcripciones, sin importar si son clips de 2 minutos o videos de 2 horas - los 100 créditos gratis de registro cubren por sí solos aproximadamente la mitad de un backlog de 200 videos. El costo de embeddings es aparte y lo factura tu proveedor de embeddings.

Q05

¿Puedo citar una marca de tiempo exacta en lugar de solo el título del video?

No con este endpoint - la API pública de transcripción devuelve texto plano, no segmentos con marca de tiempo, así que los ejemplos de esta guía citan por título e id del video. Si citar a nivel de marca de tiempo es importante para tu caso de uso, ten esto en cuenta al elegir el tamaño de fragmento: fragmentos más pequeños hacen que la cita a nivel de video sea lo más precisa posible.

Relacionado