Назад в блогИнтеграции

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

Краткий ответ

Получите транскрипт каждого видео через размещённый API, разбейте текст на части, преобразуйте части в эмбеддинги и сохраните векторы - затем во время запроса преобразуйте вопрос в эмбеддинг, найдите ближайшие части и передайте их LLM как обоснованный контекст с указанием источника видео.
01

Соберите транскрипты

Получите ключ в панели управления (100 бесплатных кредитов, без карты) и получите транскрипт каждого видео по id. Кредиты списываются за запрос, а не за минуту видео, так что 3-часовая лекция стоит тот же 1 кредит, что и 2-минутный ролик - это важно, когда вы загружаете весь архив канала:

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 и title возвращаются вместе с текстом транскрипта - именно на них ссылается ответ в шаге 5.

02

Разбейте транскрипты на части

Разбейте каждый транскрипт на перекрывающиеся окна перед преобразованием в эмбеддинги - эмбеддинг всего видео целиком слишком грубый, чтобы найти по нему конкретный ответ, и для этого не нужна никакая библиотека:

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

Окно в 1200 символов с перекрытием в 200 символов - разумное значение по умолчанию для транскриптов устной речи - уменьшите его для коротких насыщенных роликов, увеличьте для длинного лекционного контента, где помогает больше окружающего контекста.

03

Преобразуйте в эмбеддинги и сохраните

Преобразуйте каждую часть в эмбеддинг и храните id и название видео вместе с вектором, поскольку именно это превращает найденную часть в ссылку на источник позже:

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

Здесь векторы хранятся в памяти для наглядности. При превышении нескольких сотен частей замените массив на pgvector, Pinecone или другое векторное хранилище - шагу поиска ниже нужна только функция, возвращающая top-k ближайших векторов, а не именно эта структура.

04

Найдите релевантные части

Во время запроса преобразуйте вопрос в эмбеддинг той же моделью и ранжируйте сохранённые части по косинусному сходству:

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 - хорошая отправная точка: слишком мало частей - модели не хватает контекста для полного ответа, слишком много - нерелевантные части начинают размывать ответ.

05

Сгенерируйте обоснованный ответ

Передайте найденные части модели как пронумерованный контекст и укажите ей отвечать только на основе этого материала и указывать источники - именно это делает ответ прослеживаемым до реального видео, а не общих знаний самой модели:

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

Указание источника по названию видео, а не выдуманная временная метка, соответствует тому, что реально возвращает API транскриптов - не позволяйте модели придумывать временную метку, которую ей никогда не передавали.

Поддерживайте индекс актуальным

Для канала, который продолжает публиковать видео, проверяйте новые загрузки по расписанию через бесплатный эндпоинт /channel/latest (0 кредитов), пропускайте уже проиндексированные id видео и получайте, разбивайте на части и преобразуйте в эмбеддинги только новые. Тогда стоимость загрузки остаётся пропорциональной новому контенту, а не полной переиндексации при каждом запуске.

Почему транскрипты хорошо подходят для RAG

Видео - один из самых неудобных форматов для построения поиска по контенту: видео нельзя прогрепать. Транскрипт превращает его обратно в текст, с которым уже умеет работать ваш существующий стек эмбеддингов и поиска, без запуска собственного распознавания речи. А поскольку API с оплатой за запрос, а не за длительность видео, не учитывает длину видео при тарификации, загрузка архива длинного контента (подкасты, лекции, многочасовые трансляции) не становится систематически дороже, чем загрузка коротких роликов - а это именно тот архив, для поиска по которому обычно и строится RAG.

Полный справочник по эндпоинтам, аутентификации и лимитам запросов есть в документации API. Получите ключ с 100 бесплатными кредитами в панели управления, чтобы попробовать всё от начала до конца.

Частые вопросы о RAG-пайплайне

Q01

Нужна ли мне настоящая векторная база данных для этого руководства?

Нет - массив в памяти из этого руководства прекрасно работает вплоть до нескольких сотен частей и является самым быстрым способом проверить пайплайн от начала до конца. Переходите на pgvector или управляемое векторное хранилище, когда индексируете больше этого объёма или когда индексу нужно сохраняться между перезапусками.

Q02

Какую модель эмбеддингов использовать?

text-embedding-3-small - разумный выбор по умолчанию: дёшево и достаточно хорошо для большинства задач поиска по транскриптам. Переходите на text-embedding-3-large, только если видите проблемы с качеством поиска, которые она правдоподобно решит; она стоит дороже за вызов.

Q03

Что произойдёт, если у видео вообще нет транскрипта?

Эндпоинт транскрипта возвращает понятный код ошибки (TRANSCRIPT_DISABLED или TRANSCRIPT_NOT_FOUND) вместо пустой строки - проверяйте success: false и пропускайте это видео, а не преобразуйте пустую строку в эмбеддинг, как будто это реальный контент.

Q04

Сколько стоит проиндексировать, скажем, 200 видео?

200 кредитов за получение транскриптов, независимо от того, это 2-минутные ролики или 2-часовые видео - 100 бесплатных кредитов при регистрации сами по себе покрывают примерно половину архива из 200 видео. Стоимость эмбеддингов отдельная и оплачивается вашим провайдером эмбеддингов.

Q05

Могу ли я указать точную временную метку вместо просто названия видео?

Не с этим эндпоинтом - публичный API транскриптов возвращает обычный текст, а не размеченные по времени сегменты, поэтому в примерах здесь источник указывается по названию видео и id. Если для вашей задачи важна точность на уровне временной метки, делайте части меньше, чтобы ссылка на уровне видео оставалась максимально точной.

Похожие материалы