HomeBlogTranscribePlansAPI Keys

RAG sobre un canal de YouTube: del RSS a respuestas con citas

Pipeline desde el feed RSS de un canal de YouTube pasando por transcripciones y embeddings hasta respuestas con citas

Construir RAG sobre un canal de YouTube significa responder "¿qué ha dicho este creador sobre X, y en qué video?" — a través de todo lo que ha publicado. El pipeline son cuatro pasos: descubrir videos vía RSS, obtener transcripciones, trocear y generar embeddings, responder con atribución. Aquí está cada paso en Python ejecutable, incluidas las partes que la mayoría de los artículos omiten: el manejo de cuotas y cómo es honestamente la citación sin marcas de tiempo.

Paso 1: IDs de video desde el feed RSS del canal

YouTube todavía ofrece un feed RSS de verdad por canal, sin clave de API:

import requests
import xml.etree.ElementTree as ET

CHANNEL_ID = "UCxxxxxxxxxxxxxxxxxxxxxx"
NS = "{http://www.youtube.com/xml/schemas/2015}"

feed = requests.get(
    f"https://www.youtube.com/feeds/videos.xml?channel_id={CHANNEL_ID}",
    timeout=30,
).text
video_ids = [el.text for el in ET.fromstring(feed).iter(f"{NS}videoId")]

Advertencia: el feed solo devuelve las ~15 subidas más recientes. Para un canal activo, ejecuta esto de forma programada y acumula los IDs en un archivo o tabla; el catálogo antiguo más allá de eso requiere la YouTube Data API o una lista manual de una sola vez. Sin embargo, para mantener un índice actualizado, RSS es perfecto — también es la columna vertebral del pipeline de canal a newsletter.

Paso 2: obtén las transcripciones, respeta la cuota

import time

API_KEY = "yt_your_key_here"

def fetch(video_id: str) -> dict | None:
    resp = requests.get(
        "https://youtube2text.org/api/transcribe",
        params={"url": f"https://www.youtube.com/watch?v={video_id}"},
        headers={"x-api-key": API_KEY},
        timeout=90,
    )
    if resp.status_code == 404:
        return None  # TRANSCRIPT_UNAVAILABLE — no captions, skip
    if resp.status_code == 429:
        time.sleep(resp.json()["error"].get("retryAfterSeconds", 30))
        return fetch(video_id)
    resp.raise_for_status()
    return resp.json()["result"]

transcripts = [t for vid in video_ids if (t := fetch(vid))]

Haz las cuentas de tu plan antes de iterar sobre un canal: el nivel gratuito son 5 videos al mes, que cubre una prueba de concepto y nada más. El básico ($5.99) compra 50, el pro ($9.99) compra 500 — el pro es el suelo realista para indexar un canal más sus subidas continuas.

Paso 3: trocear y generar embeddings

Cualquier base vectorial sirve; Chroma mantiene el ejemplo corto. Los subtítulos automáticos suelen carecer de puntuación, así que troceo por número de palabras en lugar de por frases:

import chromadb

def chunk(text: str, size: int = 300, overlap: int = 50):
    words = text.split()
    for i in range(0, len(words), size - overlap):
        yield " ".join(words[i:i + size])

client = chromadb.PersistentClient(path="./channel_index")
col = client.get_or_create_collection("channel")

for t in transcripts:
    for i, piece in enumerate(chunk(t["content"])):
        col.add(
            ids=[f'{t["videoId"]}-{i}'],
            documents=[piece],
            metadatas=[{"videoId": t["videoId"], "title": t["title"]}],
        )

Paso 4: responder con citas — la versión honesta

La API devuelve texto plano sin marcas de tiempo, así que seré directo contigo: no puedes citar "en el 23:41" con estos datos. Lo que sí puedes hacer — y lo que normalmente basta — es atribución de video a nivel de fragmento: cada fragmento recuperado conoce su videoId y su title, así que las respuestas citan qué video lo dijo, con un enlace funcional.

hits = col.query(query_texts=["what's their take on electric trucks?"], n_results=5)

context = "\n\n".join(
    f'[{m["title"]}] (https://www.youtube.com/watch?v={m["videoId"]})\n{doc}'
    for doc, m in zip(hits["documents"][0], hits["metadatas"][0])
)
prompt = (
    "Answer using only the excerpts below. After each claim, "
    f"cite the video title in brackets.\n\n{context}\n\nQuestion: ..."
)

Pasa prompt al LLM que prefieras — un modelo local funciona perfectamente aquí, la misma configuración que el pipeline de resumen con Ollama.

Notas de ensamblaje

Para la gente de frameworks: esto se mapea uno a uno sobre LlamaIndex o LangChain si prefieres no hacer el troceado a mano. En cualquier caso la capa de transcripción es idéntica, y es la única parte con contador — consigue una clave en youtube2text.org/app/keys y empieza con el nivel gratuito para validar el pipeline antes de apuntarlo a 300 videos.