Surtidito — Recetario de retrieval (cómo se extrae la información)
Fecha: 2026-09-13 · Contexto: mapa reproducible del pipeline de retrieval de
~/nef/surtido para exportarlo a otro proyecto. De dónde sale cada dato, cómo
se normaliza y guarda, cómo se enriquece y cómo se exporta. Refs archivo:línea.
Idea fuerza: el retrieval es local-first Python (fetch → normalizar →
guardar JSON/DB → enriquecer con LLM/NLP → render → exportar), con una capa
delgada de lectura (asgi.py, FastAPI en Vercel) y un Worker aparte para
trainability de YouTube. El artefacto público real es reporte/feed-todo.rss.xml.
Las 6 etapas
0. Registro de fuentes (config estática)
1. Fetch (cada fuente tiene su fetcher)
2. Normalizar + guardar (JSON local + Supabase)
3. Enriquecer (traducir, extraer, triage, NLP, sentimiento)
4. Consolidar / exportar (feed-todo.json + feed-todo.rss.xml)
5. Servir (asgi.py: /, /feed.rss.xml, /leer*)
Etapa 0 — Registro de fuentes (config, no código)
| Recurso | Archivo | Contenido |
|---|---|---|
| Canales YouTube + metadata editorial | channels.json | channel_id, nombre, categoria, tipo_medio, sesgo, credibilidad_10, mbfc_url |
| Listas de fuentes X / RSS / Telegram | feed.py:45 FUENTES, :84 CUENTAS_X, :421 TECH_RSS, :471 HERMANAS_RSS, :504 HERMANAS_X, :524 TECH_X, :536 TELEGRAM | hard-coded |
| Perfiles/sitios de búsqueda web | referencias.py:45-60 | PERFILES, SITIOS, PAYWALL |
| Términos diseño/ML | archivo.py:32-48; terminos_dsml.txt (26 KB) | filtro + catálogo generado por scripts/terminos_desde_second_brain.py |
| Trending | feeds.json | región MX, max 20 |
| Fuentes propuestas por usuarios | Supabase tabla fuentes (supabase.sql:27) | estado pendiente/aprobada/rechazada |
Etapa 1 — Fetch: fuente → función
| Fuente | Fetcher | Notas |
|---|---|---|
| X/Twitter timeline | feed.obtener_timeline():259 → _rsshub_url():253 | RSSHub local :1200; env RSSHUB_URL, TWITTER_AUTH_TOKEN |
| YouTube videos | feed._fetch_yt():315 / _fetch_yt_rss():350; ingest.rss_entries():120 | Atom oficial; Invidious de respaldo |
| YouTube transcripción | ingest.fetch_transcript():171; leer._transcripto_youtube():161 | youtube-transcript-api, es>en |
| YouTube audio→STT | ingest.fallback_whisper():237 | yt-dlp -x + faster-whisper |
| Comentarios YT | sentimiento.collect_yt():565 | yt-dlp --write-comments |
| RSS tech EN+CN | feed._fetch_tech_rss():807 | arXiv, HF, HN, 36kr, QbitAI, Ars, Verge… |
| RSS hermanas + paywall | feed._fetch_hermanas_rss():757 | Pie de Página, Mongabay, NYT, WaPo… |
| X tech / hermanas | feed._fetch_tech_x():727 / _fetch_hermanas_x():789 | vía RSSHub |
| Telegram | feed._fetch_tech_tg():546 | RSSHub :1200/telegram/channel/<c> |
| Links destino de posts X (“silo”) | feed.generar_silo():636 | URLs externas, términos DS/ML |
| Prensa nocturna (RSS) | scripts/prensa_noche.py:54 | Página 12, Contralínea, Río Doce… |
| NYT/WaPo con cookies | prensa_sesion._trae():161 | env NYT_COOKIE/WAPO_COOKIE |
| NYT/WaPo con navegador | prensa_selenium._trae():117 | Brave + Selenium, perfil data/prensa-perfil |
| Búsqueda web | referencias._buscar():82 | ddgs y/o SerpAPI |
| Wayback / archive.today | referencias._wayback():107 / _archive_today():128 | snapshot de links muertos |
| Papers CN | papers_cn.buscar():129 | OpenAlex, institutions.country_code:CN |
| Reddit / Bluesky / Mastodon / Talkwalker / NewsAPI / Google News | sentimiento.collect_*():185-638; pulso.py | corpus social |
Etapa 2 — Normalizar y guardar
Normalizadores: feed._atom_entries():198 (RSS/Atom→dict), feed._normalizar_todo():906,
lecturas._unifica():93 (dedupe por URL vía _norm_url():43),
feed._fusionar_con_existente():398 (acumula histórico, dedup por URL).
Storage local (reporte/, versionado)
| Archivo | Productor | Contenido |
|---|---|---|
feed.json / feed.rss.xml | feed.generar():1022 | corte fresco, 3 d / 5 por medio / 40 tope |
feed-tech.json/.rss.xml | feed.generar_tech():835 | tech (añade en, categoria) |
feed-todo.json / feed-todo.rss.xml | feed.generar_todo():927 | todo lo extraído + ediciones, full history, sin cuotas |
x-silos.json | feed.generar_silo():636 | post_partida, url_destino, dominio, medio, terminos[] |
lecturas.json/.html | lecturas.generar():123 | timeline unificado por secciones |
archivo-diseno-ml.json/.html | archivo.acumular():117 | titulo,url,medio,grupos,terminos,paywall,texto_local |
referencias.json/.html | referencias.buscar():246 | perfiles,queries,buscador,wayback{},archive_today{} |
papers-cn.json | papers_cn.main():186 | openalex_id,titulo,abstract,citas,instituciones |
analisis.json / nube.html | nlp.analizar():267 | nube, temas, emergentes |
sentimiento.json | sentimiento.puntuar():657 | balance por fuente/tema |
pulso.json | pulso._documento():190 | pulso social |
YYYY-MM-DD.json/.html/.md | proponer.armar_propuesta():412 / reporte.py | ediciones |
leer_cache/{hash}.json | leer._guardar_cache() | artículo traducido cacheado |
leer_cache/{hash}.lectura.json | leer._guardar_lectura() | extracción cruda cacheada |
Storage de trabajo (NO exportar)
| Path | Contenido |
|---|---|
candidatas/YYYY-MM-DD.json (+ transcripciones.json) | candidatas YouTube con triage LLM (ingest.main():352) |
candidatas/prensa-YYYY-MM-DD.json | notas prensa nocturna |
data/corpus/*.jsonl | corpus social puntuado (sentimiento.guardar()) |
data/prensa/*.json | texto completo de prensa, local-only/gitignored |
data/prensa-perfil/ | perfil Brave con cookies de sesión |
Supabase (Postgres/PostgREST, supabase.py; schema supabase.sql)
suscriptores(:8) — newsletter.fuentes(:27) — fuentes propuestas (RLS: público leeaprobada).salidas(:60) — clicks de salida, sin IP/UA (service_role).artefactos(:77) — blobs jsonb (feed,analisis,nube,leer:{hash}); lectura pública.- Helpers:
disponible():22,insertar():37,listar():48,actualizar():63,upsert_artefacto():77,leer_artefacto():98. - Usado por
leer.py:52/70(cache de traducción) yproponer.py:467.
Otros
Cloudflare D1 worker/schema.sql (canales/videos/estado, leído por trainability.py:33),
Tinybird (src/tinybird/), Cloudinary (imagen.py:143), Matomo (analitica.py).
Etapa 3 — Enriquecer
| Función | Qué hace |
|---|---|
traducciones.traducir/deepl(), leer._traducir():410 | ES↔EN (DeepL o LLM) |
leer._fetch():94 / _extraer():123 | fetch + trafilatura del artículo |
leer._resumen_extractivo():316 | resumen sin LLM |
ingest.triage():263, triage_rapido.main():85 | triage de candidatas por LLM (Ollama/DeepSeek) |
nlp.analizar():267 | nube de palabras, temas, emergentes |
sentimiento.puntuar():657 | sentimiento (robertuito local) |
proponer.generar_terna():371 / _llm_texto():231 | propuesta de edición (terna) |
Etapa 4 — Consolidar / exportar
- RSS (export principal) —
feed._render_rss():1258construye RSS 2.0 desde cualquier doc. Corte curado con_items_alt_rss():1212(limite=40, por_medio=5, max_dias=3;Nonedesactiva límites = full history). URL absoluta en_link_rss():1234; categoría inferida en_categoria_rss():1244. Salidas:reporte/feed.rss.xml(corte) yreporte/feed-todo.rss.xml(todo). - JSON —
feed-todo.json,feed.json,feed-tech.json,lecturas.json,referencias.json,archivo-diseno-ml.json,papers-cn.json,analisis.json,sentimiento.json,pulso.json,x-silos.json,candidatas/*.json, ediciones. - HTML —
feed.html,hn-*.html(hn.py),lecturas/archivo/referencias.html,reporte/YYYY-MM-DD.html(reporte.render_html():62). - Markdown —
reporte.render_md():276;GET /leer/descargar→leer.render_md():591conContent-Disposition: attachment. - Email (Resend) —
mailing.py(cmd_send():826,cmd_broadcast():886,enviar_propuesta():785). (En limpieza 2026-09-12 se sacó del sitio; el motor queda como infra compartida de/leer.) - Push externo —
scripts/feed_rss.sh:22sube el consolidado aRSS_PUSH_URLconContent-Type: application/rss+xml.
Etapa 5 — Servir (asgi.py, FastAPI)
| Ruta | Línea | Qué hace |
|---|---|---|
/ | :398 | consola ámbar, feed acordeón |
/feed.rss.xml | :633 | raw; prefiere feed-todo.rss.xml (_feed_rss_path():210) |
/leer | :255 | versión traducida (IRC) |
/leer/lectura | :325 | JSON de lectura rápida (leer.lectura_json():659) |
/leer/descargar | :286 | descarga .md |
/salto | :244 | redirección anónima a origen |
/health | :695 | salud |
reporte/** se incluye en el bundle de la función (vercel.json, includeFiles).
Receta rápida (comandos en orden)
cd ~/nef/surtido
# 1) corpus local (lecturas + archivo diseño/ML + referencias web)
./scripts/local_news.sh
# 2) prensa nocturna (NYT/WaPo + medios MX/AR)
.venv/bin/python scripts/prensa_noche.py
# 3) social / señales (opcional)
.venv/bin/python sentimiento.py
.venv/bin/python pulso.py
# 4) consolidar y exportar RSS (feed fresco + feed-todo + warm cache lectura)
./scripts/feed_rss.sh --limite 40
# con RSS_PUSH_URL definido, además empuja el XML a R2/Pages
# 5) servidor local de lectura
.venv/bin/python -m uvicorn asgi:app --reloadSchedulers declarados: vercel.json crons /api/proponer (03:00 UTC) y
/api/cron (08:00 UTC) — esos endpoints no están en este checkout.
worker/index.js:161 (Cloudflare) hace trending cada 15 min.
Gotchas (importantes al exportar)
- Prensa completa es local-only (
data/, gitignored) y nunca se exporta; las transcripciones no se citan, solo se triage (ingest.py:38). feed-todo.rss.xmles el output público real — se carga a la función de Vercel víaincludeFiles: reporte/**.artefactos(Supabase) es la caché cloud de/leer; el espejo local esreporte/leer_cache/(gitignored).- Límites del corte (
_items_alt_rss) existen para que un flood de una sola fuente no desplace a las demás; el consolidado los desactiva. VERCEL_OIDC_TOKENhay que quitarlo antes de correrhn.py/feed en local (scripts/actualizar_hn.sh:46-51).- X es el eslabón frágil (RSSHub +
TWITTER_AUTH_TOKEN); el fallback al/feed/del sitio ya está enobtener_timeline. El corpus RSS es lo estable. - Sin fechas van al fondo en el consolidado (
_ordenar+sin_fecha);_fecha_parse/_ordenarnormalizan a UTC (mezcla naive/aware cross-fuentes).
Variables de entorno (solo nombres)
- Core:
SUPABASE_URL,SUPABASE_SERVICE_ROLE_KEY/SUPABASE_ANON_KEY,LLM_BACKEND(ollama|gateway|deepseek),OLLAMA_MODEL,OLLAMA_URL,DEEPSEEK_API_KEY,AI_GATEWAY_*,WEB_URL,RSS_URL,RSS_PUSH_URL. - Fetch local:
RSSHUB_URL,TWITTER_AUTH_TOKEN,NEWSAPI_KEY,BSKY_IDENTIFIER,BSKY_APP_PASSWORD,TALKWALKER_RSS,YOUTUBE_API_KEY,YT_DATA_API_KEY,YT_WORKER_URL,SERPAPI_KEY,SERPAPI_ENGINE,SERPAPI_BUDGET,OPENALEX_MAILTO. - Prensa:
NYT_COOKIE,WAPO_COOKIE,PRENSA_MAX_DIA,PRENSA_ESPERA_MIN/MAX. - Extra:
LINGVA_URL,DEEPL_API_KEY,WHISPER_MODEL,TRIAGE_CHARS,GBRIDGE_URL,IMAGEN_PROVEEDOR,RAPIDAPI_KEY,CLOUDINARY_URL,RESEND_API_KEY/RESEND_FROM/EDITOR_EMAIL,MATOMO_*,TINYBIRD_*.
Para re-evaluar
- Si X deja de responder → quitar X del cuerpo, dejar sólo RSS.
- Si el blog consume el RSS: ajustar
limite/por_medio/max_diasen_items_alt_rss. - Al portar a otro proyecto: llevarse
feed.py(fetch+consolidación+RSS) como núcleo;asgi.pysólo si se quiere la capa de lectura.