HomeBlogTranscribePlansAPI Keys

YouTube 채널로 RAG 구축하기: RSS에서 출처 있는 답변까지

YouTube 채널 RSS 피드에서 스크립트와 임베딩을 거쳐 출처 있는 답변까지 이어지는 파이프라인

YouTube 채널로 RAG를 구축한다는 것은 "이 크리에이터가 X에 대해 뭐라고 했고, 어느 영상에서 했나?"라는 질문에 그가 게시한 모든 콘텐츠에 걸쳐 답한다는 뜻입니다. 파이프라인은 네 단계입니다. RSS로 영상을 발견하고, 스크립트를 가져오고, 청크로 나눠 임베딩하고, 출처를 달아 답변합니다. 각 단계를 실행 가능한 Python으로 소개하며, 대부분의 글이 건너뛰는 부분 — 할당량 처리와 타임스탬프 없이 인용이 솔직히 어떤 모습인지 — 도 다룹니다.

1단계: 채널 RSS 피드에서 영상 ID 얻기

YouTube는 여전히 채널마다 진짜 RSS 피드를 제공하며, 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")]

주의: 피드는 최근 업로드 약 15개만 반환합니다. 활발한 채널이라면 이 코드를 스케줄에 걸어 두고 ID를 파일이나 테이블에 누적하세요. 그 너머의 과거 카탈로그는 YouTube Data API나 일회성 수동 목록이 필요합니다. 하지만 인덱스를 최신으로 유지하는 데는 RSS가 완벽합니다. 채널-뉴스레터 파이프라인의 근간이기도 합니다.

2단계: 스크립트 가져오기, 할당량 존중하기

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

채널 전체를 루프로 돌리기 전에 요금제 계산부터 하세요. 무료 요금제는 월 5개 영상으로, 개념 증명 정도만 커버하고 그 이상은 아닙니다. 기본($5.99)은 50개, 프로($9.99)는 500개입니다. 채널 하나를 인덱싱하고 이후 업로드까지 따라가려면 프로가 현실적인 하한선입니다.

3단계: 청크 분할과 임베딩

어떤 벡터 스토어든 됩니다. 예제를 짧게 하려고 Chroma를 씁니다. 자동 자막에는 문장 부호가 없는 경우가 많아서, 저는 문장 대신 단어 수 기준으로 청크를 나눕니다:

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

4단계: 인용을 곁들인 답변 — 솔직한 버전

이 API는 타임스탬프 없는 일반 텍스트를 반환하므로, 솔직하게 말하겠습니다. 이 데이터로는 "23:41 지점에서"라고 인용할 수 없습니다. 대신 가능한 것 — 그리고 대개는 충분한 것 — 은 청크 수준의 영상 귀속입니다. 검색된 모든 청크는 자신의 videoIdtitle을 알고 있으므로, 답변은 어느 영상에서 나온 말인지 동작하는 링크와 함께 인용할 수 있습니다.

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

prompt를 원하는 LLM에 넣으면 됩니다. 로컬 모델도 잘 동작하며, Ollama 요약 파이프와 같은 구성입니다.

조립 노트

프레임워크파를 위해: 청크 분할을 직접 만들고 싶지 않다면 이 구성은 LlamaIndexLangChain에 일대일로 대응됩니다. 어느 쪽이든 스크립트 레이어는 동일하고, 미터기가 달린 부분도 그것뿐입니다. youtube2text.org/app/keys에서 키를 받고, 300개 영상에 들이대기 전에 무료 요금제로 파이프라인부터 검증하세요.