Skip to content

Larr0n/SAPIT

Repository files navigation

S.A.P.I.T.

Sistema Automatizado de Procesamiento e Inteligencia de Tendencias

Monitor de relevancia pública para el Senador Nacional Agustín Coto (Tierra del Fuego · La Libertad Avanza). Extrae menciones de medios locales y nacionales, facebook y x, las clasifica con IA y las visualiza en un dashboard privado con acceso autenticado.


Índice

  1. ¿Qué hace?
  2. Arquitectura general
  3. Estructura de archivos
  4. Infraestructura necesaria
  5. Configuración inicial (paso a paso)
  6. Variables de entorno
  7. Pipeline de producción (GitHub Actions)
  8. Dashboard
  9. Base de datos (Supabase)
  10. Scrapers
  11. Agregar un scraper nuevo
  12. Solución de problemas frecuentes

1. ¿Qué hace?

SAPIT corre automáticamente todos los días a las 09:00 AM (hora Argentina) y ejecuta este proceso en cadena:

Scraping → Limpieza → Clasificación con IA → Base de datos → Dashboard

Scraping: recorre los buscadores de 8 medios periodísticos (6 locales de TDF + Infobae + La Nación) y las cuentas de redes sociales del senador en X y Facebook. Extrae todas las notas y posts que mencionan a Coto.

Limpieza: elimina duplicados, normaliza fechas, descarta registros sin texto o sin fecha válida, genera un identificador único (doc_id) por nota.

Clasificación con IA: Gemini analiza cada nota y asigna (a) un sentimiento — POS, NEG o NEU — y (b) un tópico corto descriptivo como Soberanía / Malvinas o Conflictos gremiales TDF.

Base de datos: los registros clasificados se guardan en Supabase usando upsert, por lo que correr el pipeline dos veces no genera duplicados.

Dashboard: una app de Streamlit con acceso por usuario y contraseña que visualiza tendencias, distribución por fuente, análisis de impacto por evento, y la tabla completa de registros con filtros.


2. Arquitectura general

┌─────────────────────────────────────────────────────┐
│                  GitHub Actions                      │
│  cron 12:00 UTC (09:00 ART) · ubuntu-22.04          │
│                                                      │
│  main.py                                             │
│  ├── scrapers (×8 medios + X + Facebook)            │
│  ├── limpieza.py        → deduplicación             │
│  ├── clasificador.py    → Gemini API                │
│  ├── conexion.py        → Supabase upsert           │
│  └── logger.py          → logs + Telegram           │
└────────────────────────┬────────────────────────────┘
                         │
                    Supabase (PostgreSQL)
                    ├── monitoreo_politico   ← datos
                    ├── eventos              ← eventos manuales
                    └── logs_ejecucion       ← historial pipeline
                         │
              ┌──────────┴──────────┐
              │   Streamlit Cloud   │
              │   dashboard.py      │
              │   (acceso privado)  │
              └─────────────────────┘

3. Estructura de archivos

sapit/
│
├── main.py                    # Orquestador del pipeline (producción)
├── config.yaml                # Configuración pública (senador, medios, eventos)
├── requirements.txt           # Dependencias Python
├── dashboard.py               # App Streamlit (visualización)
│
├── scrapers/
│   ├── scraper_actualidadtdf.py
│   ├── scraper_infofueguina.py
│   ├── scraper_minutofueguino.py
│   ├── scraper_provincia23.py
│   ├── scraper_tiempofueguino.py
│   ├── scraper_red23noticias.py
│   ├── scraper_infobae.py
│   ├── scraper_lanacion.py
│   ├── scraper_x.py
│   └── scraper_facebook.py
│
├──procesamiento/
│  ├── limpieza.py                # Normalización y deduplicación
│  ├── clasificador.py            # Clasificación con Gemini (google-genai)
│
│──db/
│  │── conexion.py                # Guardado en Supabase
│
│──utils/
│  │── logger.py                  # Logs en DB + alertas Telegram
│
│
└── .github/
    └── workflows/
        └── daily_sync.yml     # GitHub Actions workflow

4. Infraestructura necesaria

Antes de empezar necesitás tener cuentas en estos servicios. Todos tienen plan gratuito suficiente para este proyecto:

Servicio Para qué Plan
GitHub Repositorio + Actions (cron gratuito) Free
Supabase Base de datos PostgreSQL Free (500MB)
Google AI Studio API de Gemini para clasificación Free (con límites)
Streamlit Cloud Hosting del dashboard Free
Telegram Alertas del pipeline Free

5. Configuración inicial (paso a paso)

5.1 Clonar el repositorio

git clone https://github.com/tu-usuario/sapit.git
cd sapit

5.2 Crear las tablas en Supabase

En Supabase Dashboard → SQL Editor, ejecutar este script:

-- Tabla principal de menciones
CREATE TABLE monitoreo_politico (
    doc_id       TEXT PRIMARY KEY,
    fuente       TEXT,
    plataforma   TEXT,
    url          TEXT,
    texto        TEXT,
    fecha        DATE,
    platform_id  TEXT,
    sentimiento  TEXT CHECK (sentimiento IN ('POS','NEG','NEU')),
    topico_corto TEXT,
    creado_en    TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

-- Tabla de eventos manuales
CREATE TABLE eventos (
    id        SERIAL PRIMARY KEY,
    fecha     DATE NOT NULL,
    titulo    TEXT NOT NULL,
    detalle   TEXT,
    tipo      TEXT NOT NULL DEFAULT 'legislativo'
              CHECK (tipo IN ('personal','legislativo','mediatico')),
    creado_en TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

-- Tabla de logs del pipeline
CREATE TABLE logs_ejecucion (
    id                    SERIAL PRIMARY KEY,
    modulo                TEXT,
    estado                TEXT,
    mensaje_error         TEXT,
    registros_procesados  INT DEFAULT 0,
    duracion_segundos     INT DEFAULT 0,
    creado_en             TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

-- Índices de performance
CREATE INDEX idx_monitoreo_fecha    ON monitoreo_politico(fecha);
CREATE INDEX idx_monitoreo_fuente   ON monitoreo_politico(fuente);
CREATE INDEX idx_monitoreo_sent     ON monitoreo_politico(sentimiento);
CREATE INDEX idx_eventos_fecha      ON eventos(fecha);
CREATE INDEX idx_logs_creado        ON logs_ejecucion(creado_en);

5.3 Obtener las credenciales de Supabase

Supabase Dashboard → Project Settings → API

  • Project URL: https://XXXX.supabase.co
  • service_role key: en la sección "Project API keys" → hacer clic en "Reveal" al lado de service_role

⚠️ Usar siempre la service_role key, no la anon key. La anon key respeta las políticas RLS y puede devolver 0 filas si la tabla no es pública.

5.4 Obtener la API key de Gemini

  1. Ir a aistudio.google.com
  2. Crear una API key nueva
  3. Guardarla — solo se muestra una vez

Opcional: verificar qué modelos están disponibles para tu cuenta:

GEMINI_API_KEY=AIza... python check_gemini_models.py

Esto imprime la lista exacta para usar en MODELOS_FALLBACK dentro de clasificador.py.

5.5 Crear el bot de Telegram

  1. Abrir Telegram y buscar @BotFather
  2. Enviar /newbot, seguir las instrucciones, copiar el token
  3. Enviar cualquier mensaje al bot nuevo
  4. Obtener tu chat_id visitando: https://api.telegram.org/bot<TOKEN>/getUpdates
    • Buscar "chat":{"id": XXXXXXXXX} en la respuesta

5.6 Cookies para X (Twitter) y Facebook (opcional)

Los scrapers de redes sociales requieren una sesión autenticada. Para exportar las cookies:

  1. Instalar la extensión Cookie-Editor en Chrome/Firefox
  2. Iniciar sesión en X o Facebook en el navegador
  3. Abrir Cookie-Editor → "Export" → "Export as JSON"
  4. Guardar el contenido JSON como valor del Secret correspondiente

Si no configurás las cookies, los scrapers de RRSS se omiten silenciosamente sin detener el pipeline.

5.7 Configurar Secrets en GitHub

GitHub → tu repositorio → Settings → Secrets and variables → Actions → New repository secret

Secret Valor
SUPABASE_URL https://XXXX.supabase.co
SUPABASE_KEY service_role key
GEMINI_API_KEY AIza...
TELEGRAM_TOKEN token del bot
TELEGRAM_CHAT_ID tu chat ID numérico
X_COOKIES_JSON JSON de cookies de X (opcional)
COOKIE_FB JSON de cookies de Facebook (opcional)

5.8 Configurar Secrets en Streamlit Cloud

share.streamlit.io → tu app → ⋮ → Settings → Secrets

SUPABASE_URL = "https://XXXX.supabase.co"
SUPABASE_KEY = "eyJ..."   # service_role key

[cookie]
name = "monitor_coto"
key  = "una_clave_secreta_larga"   # cualquier string largo y aleatorio
expiry_days = 1

[credentials.usernames.ADMIN]
name     = "ADMIN"
password = "$2b$12$..."   # hash bcrypt de la contraseña

[credentials.usernames.OTRO_USUARIO]
name     = "Nombre"
password = "$2b$12$..."

Cómo generar el hash bcrypt de una contraseña:

import bcrypt
print(bcrypt.hashpw("tu_contraseña".encode(), bcrypt.gensalt(12)).decode())

6. Variables de entorno

Para uso local, crear un archivo .env en la raíz del proyecto:

# Base de datos
SUPABASE_URL=https://XXXX.supabase.co
SUPABASE_KEY=eyJ...

# IA
GEMINI_API_KEY=AIza...

# Alertas (opcional en local)
TELEGRAM_TOKEN=
TELEGRAM_CHAT_ID=

# Redes sociales (opcional)
X_COOKIES_JSON=
COOKIE_FB=

7. Pipeline de producción (GitHub Actions)

El archivo .github/workflows/daily_sync.yml define el pipeline automático.

Horario: todos los días a las 09:00 AM hora Argentina (12:00 UTC).

Disparo manual: GitHub → tu repositorio → Actions → "SAPIT · Daily ETL Pipeline" → "Run workflow".

Secuencia de pasos:

1. Checkout del repositorio
2. Notificación Telegram: inicio
3. Instalar Python 3.11 + dependencias
4. Instalar Playwright + Chromium (para scrapers que lo necesitan)
5. Ejecutar main.py (pipeline completo)
6. Leer pipeline_report.json y enviar reporte detallado por Telegram

Reporte de Telegram que llega al finalizar:

SAPIT — OK
Fecha: 14/04/2026 09:14 | Duracion: 187s

SCRAPERS (23 registros crudos):
  [OK] infobae: 4
  [OK] lanacion: 2
  [OK] actualidadtdf: 8
  [--] tiempofueguino: 0
  ...

LIMPIEZA: 18 nuevos (5 descartados)

CLASIFICACION: 18 OK / 0 fallidos
POS: 5  NEG: 3  NEU: 10

TOP TOPICOS:
  - Soberania / Malvinas (4)
  - Sesion ordinaria (3)

GUARDADOS EN DB: 18

Logs: https://github.com/...

8. Dashboard

El dashboard corre en Streamlit Cloud y requiere usuario y contraseña.

URL: configurada al deployar en share.streamlit.io (apunta al archivo dashboard.py).

Tabs disponibles

↗ Tendencia — serie temporal de menciones desglosada por sentimiento (POS/NEG/NEU), con eventos anotados como líneas verticales. Incluye rangeslider y botones de navegación rápida (7d / 14d / 1m / Todo).

◫ Distribución — menciones por fuente (stacked bar), share of voice local vs. nacional (donut), distribución de sentimiento (donut), y ranking de los 10 tópicos más frecuentes.

◈ Eventos — análisis de impacto de cada evento: baseline (promedio 7 días previos), menciones el día del evento, eco en los 3 días siguientes, y uplift porcentual. Cada evento muestra su detalle expandido.

≡ Evidencia — tabla completa de registros con filtros por sentimiento, fuente y tópico. Exportación a CSV del filtro activo.

Filtros globales

La barra superior aplica a todos los tabs simultáneamente:

  • Desde / Hasta: rango de fechas
  • Fuente: filtrar por medio específico o "Todas"
  • Sentimiento: POS / NEG / NEU / Todos

Gestión de eventos

El panel lateral incluye un formulario para agregar eventos directamente a la base de datos (sin tocar código). Los eventos nuevos aparecen en los gráficos en menos de 5 minutos (TTL del cache).

Los eventos se pueden eliminar con el botón 🗑️ al lado de cada uno en el Tab de Eventos.

Actualización de datos

Los datos se cachean 1 hora. Para forzar actualización inmediata: botón "🔄 Actualizar datos" en el sidebar.


9. Base de datos (Supabase)

Tabla monitoreo_politico

Columna Tipo Descripción
doc_id TEXT PK Hash MD5 de fuente+URL (12 chars)
fuente TEXT Nombre del medio (ej: infobae)
plataforma TEXT noticias o red_social
url TEXT URL original de la nota
texto TEXT Texto completo (titular + volanta + cuerpo)
fecha DATE Fecha de publicación
platform_id TEXT ID original en la plataforma (tweet_id, etc.)
sentimiento TEXT POS, NEG o NEU
topico_corto TEXT Etiqueta temática generada por Gemini
creado_en TIMESTAMPTZ Timestamp de inserción

Tabla eventos

Columna Tipo Descripción
id SERIAL PK ID autoincremental
fecha DATE Fecha del evento
titulo TEXT Título corto
detalle TEXT Descripción completa
tipo TEXT personal, legislativo o mediatico
creado_en TIMESTAMPTZ Timestamp de inserción

Tabla logs_ejecucion

Columna Tipo Descripción
id SERIAL PK ID autoincremental
modulo TEXT Nombre del módulo (scraper_infobae, etc.)
estado TEXT EXITO o ERROR
mensaje_error TEXT Detalle del error si aplica
registros_procesados INT Cantidad de registros en esa ejecución
duracion_segundos INT Duración del paso
creado_en TIMESTAMPTZ Timestamp

10. Scrapers

Medios locales (Tierra del Fuego)

Scraper URL Método Paginación
actualidadtdf actualidadtdf.com.ar requests + BS4 WordPress ?s=coto&page=N
infofueguina infofueguina.com requests + BS4 /noticias/buscar/?buscar=coto&p=N
minutofueguino minutofueguino.com.ar requests + BS4 /buscador/?q=coto&p=N (base 0)
provincia23 provincia23.com.ar requests + BS4 WordPress ?s=coto&page=N
tiempofueguino tiempofueguino.com Playwright (scroll infinito) Scroll automático
red23noticias red23noticias.com.ar requests + BS4 Sin paginación (página única)

Medios nacionales

Scraper URL Método Paginación
infobae infobae.com/tag/agustin-coto requests + BS4 Sin paginación (tag page)
lanacion lanacion.com.ar/buscador Playwright Botón "Siguiente"

Redes sociales

Scraper Plataforma Requiere Ventana
scraper_x X (Twitter) Cookies JSON Últimas 24hs
scraper_facebook Facebook Cookies JSON Últimas 24hs

Comportamiento con fechas

Todos los scrapers de noticias aceptan fecha_desde y fecha_fin. Cuando la paginación llega a notas más antiguas que fecha_desde, se detiene automáticamente. Para rangos mayores a 30 días, el max_paginas se eleva automáticamente a 20.

Los scrapers de redes sociales no soportan filtro de fechas históricas — siempre capturan la ventana de las últimas 24hs en base a la sesión activa.


11. Agregar un scraper nuevo

Para agregar un nuevo medio, el scraper debe seguir esta interfaz:

from datetime import date

def scraper_nuevo_medio(
    fecha_desde: date | None = None,
    fecha_fin:   date | None = None,
) -> list[dict]:
    """
    Devuelve una lista de diccionarios con este esquema exacto.
    Todos los campos son obligatorios salvo volanta y seccion.
    """
    return [
        {
            "url":               "https://...",
            "fuente":            "nombre_del_medio",   # snake_case, sin espacios
            "plataforma":        "noticias",            # o "red_social"
            "titular":           "Título de la nota",
            "volanta":           None,                  # o string
            "cuerpo":            "Texto completo...",
            "seccion":           "politica",
            "fecha_publicacion": "2026-04-14",          # YYYY-MM-DD
        }
    ]

Luego agregarlo en main.py:

# En main.py — sección tareas
from scraper_nuevo_medio import scraper_nuevo_medio
tareas.append(("nuevo_medio", scraper_nuevo_medio))

Y agregarlo a medios_locales en config.yaml si es un medio de TDF:

medios_locales:
  - "nuevo_medio"

12. Solución de problemas frecuentes

El pipeline corre pero no llegan datos nuevos

Verificar en Supabase → Table Editor → monitoreo_politico que los registros se estén insertando. Si la tabla está vacía, revisar los logs en logs_ejecucion o en GitHub Actions → la ejecución más reciente.

Error 429 en Gemini

La API gratuita tiene límites de requests por minuto. El clasificador tiene fallback automático a modelos más livianos. Si persiste, reducir el batch_size a 5 en clasificador.py. Para verificar qué modelos están disponibles:

GEMINI_API_KEY=AIza... python check_gemini_models.py

El dashboard no carga datos

  1. Verificar que SUPABASE_URL y SUPABASE_KEY están en los Secrets de Streamlit Cloud (no confundir con los Secrets de GitHub — son diferentes).
  2. Asegurarse de que la key sea la service_role, no la anon.
  3. Hacer Reboot de la app desde Streamlit Cloud después de cambiar Secrets.

El scraper de X o Facebook no funciona

Las cookies expiran. Hay que regenerarlas:

  1. Cerrar sesión y volver a iniciar sesión en el navegador
  2. Exportar las cookies con Cookie-Editor
  3. Actualizar el Secret correspondiente en GitHub
  4. Las cookies de X duran aproximadamente 30 días; las de Facebook, entre 7 y 90 días según la configuración de la cuenta.

st.secrets has no key "X"

El formato TOML de Streamlit es estricto. Las claves sueltas deben ir antes de cualquier sección [...]. Ver el ejemplo en la sección 5.8.

Playwright no encuentra Chromium en GitHub Actions

Asegurarse de que el workflow incluya:

- run: playwright install chromium
- run: playwright install-deps chromium

Ambos pasos son necesarios — el segundo instala las dependencias del sistema operativo.


Dependencias principales

google-genai          # SDK de Gemini (clasificación IA)
supabase              # cliente de base de datos
streamlit             # dashboard web
streamlit-authenticator  # autenticación por usuario/contraseña
playwright            # scraping de páginas con JS (TiempoFueguino, LaNación)
beautifulsoup4        # parsing HTML
requests              # HTTP
pandas                # manipulación de datos
plotly                # gráficos interactivos
python-dotenv         # carga de .env en local
bcrypt                # hashing de contraseñas

SAPIT · Sistema Automatizado de Procesamiento e Inteligencia de Tendencias

About

S.A.P.I.T. (Sistema Automatizado de Procesamiento e Inteligencia de Tendencias): Análisis de sentimiento y detección de tópicos sobre un Senador por Tierra del Fuego, en el período de un mes antes de asumir a posteriori, actualizandose diariamente.

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages