HomeBlogTranscribePlansAPI Keys

RAG sur une chaîne YouTube : du RSS aux réponses sourcées

Pipeline partant du flux RSS d'une chaîne YouTube, passant par les transcriptions et les embeddings, jusqu'aux réponses sourcées

Construire un RAG sur une chaîne YouTube, c'est répondre à « qu'a dit ce créateur au sujet de X, et dans quelle vidéo ? » — sur tout ce qu'il a publié. Le pipeline tient en quatre étapes : découvrir les vidéos via RSS, récupérer les transcriptions, découper et vectoriser, répondre avec attribution. Voici chaque étape en Python exécutable, y compris ce que la plupart des articles passent sous silence : la gestion du quota et ce à quoi ressemble honnêtement la citation sans horodatage.

Étape 1 : les IDs de vidéos depuis le flux RSS de la chaîne

YouTube fournit toujours un vrai flux RSS par chaîne, sans clé 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")]

Mise en garde : le flux ne renvoie que les ~15 mises en ligne les plus récentes. Pour une chaîne active, exécutez ce code régulièrement et accumulez les IDs dans un fichier ou une table ; le catalogue au-delà exige la YouTube Data API ou une liste manuelle établie une fois. Pour maintenir un index à jour, en revanche, le RSS est parfait — c'est aussi la colonne vertébrale du pipeline chaîne vers newsletter.

Étape 2 : récupérer les transcriptions en respectant le quota

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))]

Faites le calcul sur votre formule avant de boucler sur une chaîne : le niveau gratuit offre 5 vidéos par mois, ce qui couvre une preuve de concept et rien d'autre. La formule basique (5,99 $) en donne 50, la pro (9,99 $) en donne 500 — la pro est le plancher réaliste pour indexer une chaîne plus ses mises en ligne courantes.

Étape 3 : découper et vectoriser

N'importe quelle base vectorielle convient ; Chroma garde l'exemple court. Les sous-titres automatiques manquent souvent de ponctuation, donc je découpe par nombre de mots plutôt que par phrases :

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"]}],
        )

Étape 4 : répondre avec citations — version honnête

L'API renvoie du texte brut sans horodatage, donc je vais être franc : vous ne pouvez pas citer « à 23:41 » à partir de ces données. Ce que vous pouvez faire — et c'est généralement suffisant — c'est l'attribution par vidéo au niveau du chunk : chaque extrait récupéré connaît son videoId et son title, donc les réponses citent la vidéo d'origine, avec un lien fonctionnel.

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: ..."
)

Envoyez prompt au LLM de votre choix — un modèle local fonctionne très bien ici, même configuration que le pipeline de résumé Ollama.

Notes d'assemblage

Pour les adeptes des frameworks : tout ceci se transpose tel quel sur LlamaIndex ou LangChain si vous préférez ne pas coder le découpage à la main. Dans tous les cas, la couche transcription est identique, et c'est la seule partie avec un compteur — récupérez une clé sur youtube2text.org/app/keys et commencez avec le niveau gratuit pour valider le pipeline avant de le pointer vers 300 vidéos.