Extraer transcripciones de un canal de YouTube (yt-dlp)

Canal objetivo: TheDataPubChannel (53 streams). Ubicación: ~/Downloads/datapub

Pipeline (reutilizable)

  1. Listar: yt-dlp --flat-playlist --print "%(id)s|%(title)s|%(duration_string)s|%(upload_date)s" "<url>/streams" > _raw/_list.tsv
    • Ojo: flat-playlist NO trae upload_date. Hay que obtenerla por-video: cut -d'|' -f1 _raw/_list.tsv | xargs -I{} -P6 yt-dlp --skip-download --print "%(id)s|%(upload_date)s" "https://www.youtube.com/watch?v={}"
  2. Subtítulos auto: yt-dlp --skip-download --write-auto-subs --sub-langs LANG --sub-format vtt -o "_raw/%(id)s.%(ext)s" URL
    • Template %(id)s.%(ext)s auto-agrega el idioma → ID.es.vtt / ID.en.vtt
  3. Convertir VTT → txt plano: python3 _raw/_convert.py (quita timestamps, tags, música, deduplica)
  4. Script de descarga masiva: _raw/_download_subs.sh (idempotente, salta lo existente)

Gotcha crítico: rate-limit 429

YouTube throttlea el endpoint de timedtext. Reglas:

  • Las traducciones auto (en sobre video en es) se throttlean MUCHO más que el original ASR (es).
  • Reintentos agresivos CALIENTAN el límite y termina afectando hasta el original. Solución: cooldown de 10-15 min SIN requests, luego ritmo suave (1 sub cada ~30s, backoff 90s en 429).
  • Correr en background: nohup ./_raw/_download_subs.sh es en > _raw/_download.log 2>&1 &

Calidad del resultado

  • es (ASR original): fiel pero con ruido de relleno (“ah ah ah”, letras sueltas).
  • en (auto-traducido): más limpio porque YouTube limpia al traducir, pero deriva de la transcripción ruidosa.
  • Para base de conocimiento consultable: transcripts/es/*.txt + index.csv

403 Forbidden en descarga de audio (video data) — FIX

Los streams de googlevideo dan 403 aunque las captions (timedtext) funcionen. Causa: bot-detection de YouTube. Receta que FUNCIONA (yt-dlp 2026.07):

yt-dlp -f 140 \
  --cookies-from-browser firefox \
  --extractor-args "youtube:player_client=web_embedded,default" \
  --js-runtimes node \
  --remote-components ejs:github \
  -o "audio/%(id)s.%(ext)s" URL
  • Cookies de Chrome NO sirven en macOS (v10 cookies: “no key found”, app-bound encryption).
  • --remote-components ejs:github baja el solver del JS challenge (“n challenge”).
  • web_embedded,default destapa los formatos audio-only (140 m4a, 251 opus).

faster-whisper (global)

  • Instalado global en python 3.12.2: ~/.asdf/installs/python/3.12.2/bin/python3.12 (NO en 3.14: PyAV no compila ahí).
  • Modelo large-v3 ya descargado en ~/.cache/huggingface/hub/[CREDENCIAL]
  • Benchmark M1 Pro (10c, 16GB), large-v3 int8, idioma es:
    • cpu_threads=8 → 2.29x cpu_threads=4 → 2.92x (4 gana por contención)
    • beam=5 → 3.18x beam=1 → 4.36x
    • 71.1 h de audio total → beam5≈22h, beam1≈16h de cómputo

Export a second-brain (nodo.ws)

Script reusable: ~/Downloads/datapub/_raw/_to_secondbrain.py

  • Copia transcripciones → data/cursos/datapub-<charla>/<charla>.srt (texto limpio, sin timestamps; el srtToText de import-cursos lo maneja).
  • Genera _stubs/projects/datapub-<charla>.md (project status: shipped, uses/grants solo a nodos EXISTENTES — la convención de import-cursos.mjs no pone nodos nuevos en uses para no dejar edges colgantes).
  • Genera _stubs/cursos-nodos/<slug>.md para conceptos faltantes (rust, pyo3, bm25, topic-modeling, NER, MDP, CCA, portfolio-opt, kelly, causal-inference, explainable-ai).
  • Escribe reports/cursos-analisis.md. Adopción: revisar stubs → node scripts/adoptar-cursos.mjs → npm run build. Contrato clave: la integridad referencial (lib/graph.mjs) exige que todo uses/grants.node/edge.to apunte a slug existente; falla el build si no. Projects y nodes viven en dirs separados y no comparten namespace, pero se prefijan datapub- por claridad.