Volver al inicio

MediaBriefs

SaaS de inteligencia editorial para periodistas

Rol
AI Engineer & Full-Stack Developer
Estado
En producción

El problema

Un periodista con un programa diario necesita saber dos cosas cada mañana: de qué se está hablando y a quién puede llamar hoy. Ambas las resolvía leyendo medios a mano y cruzando de memoria contra su base de contactos. Y después de cada entrevista, transcribir para citar era otro trabajo manual — con el agravante de que saber quién dijo qué no es un extra en periodismo: es el producto.

Arquitectura

pipelinetext
6 feeds RSS peruanos (El Comercio, Gestión, BBC Mundo…)
   └─ NewsArticle ────────────────► ~200 artículos de las últimas 24 h
      └─ Claude Haiku 4.5 ────────► agrupa en 8–15 temas (clustering semántico)
         └─ CÓDIGO ───────────────► mentions, mediaCount, velocity, score, isNew
            └─ TopicDefinition + Snapshot ──► histórico temporal del tema
               └─ Claude Haiku 4.5 ────────► NER: personas, cargo, organización
                  └─ PersonProfile ────────► identidad canónica + alias
                     └─ match contra Guests ──► "este invitado tuyo encaja"
                        └─ Claude Haiku 4.5 ──► briefing · 3 preguntas · riesgo
El pipeline editorial completo. En paralelo corre el de transcripción.

Decisiones

01

Identidad canónica vs. observación

Un mismo político aparece como "Pedro Castillo", "pedro castillo" y "P. Castillo" en distintos medios. Si cada variante crea un registro nuevo, el cruce con la base de invitados no funciona nunca. El modelo aporta observaciones (PersonMention, con el nombre tal como apareció y su confianza de extracción); la base mantiene la identidad (PersonProfile, con una normalizedKey sin diacríticos y los alias acumulados). Esa separación permite auditar de dónde salió cada dato y corregir un perfil sin perder el historial.

02

Self-hostear el modelo que la API no ofrece

Whisper transcribe pero no separa hablantes. Monté pyannote 3.1 en un microservicio FastAPI propio, en su contenedor, para no arrastrar PyTorch, CUDA y HuggingFace a la imagen de Node ni atar el ciclo de despliegue del API a un stack de ML. El backend solo ve una llamada al microservicio que puede fallar sin consecuencias. El merge es el problema interesante: Whisper devuelve texto+tiempos, pyannote devuelve hablante+tiempos, y los cortes no coinciden — cada segmento recibe el hablante con mayor solapamiento temporal.

Whisper  ├──── "y eso es lo que el ministro…" ────┤
pyannote ├── SPEAKER_00 ──┤── SPEAKER_01 ──────────┤
                          ↑
             asignación por solapamiento dominante
03

Caché con TTL persistido, no en memoria

Los briefings se cachean 6 horas en MongoDB, no en un Map del proceso. Sobrevive a los reinicios del contenedor, se comparte entre instancias y —al ser multi-usuario— un briefing generado para un periodista sirve para todos los que abran ese tema. El costo por tema tiende a uno cada 6 h, no a uno por usuario por visita.

04

Salida del modelo que entra a una consulta

El match contra la base es por nombre exacto normalizado o alias registrado, con escapeRegex() sobre la entrada del modelo. Un nombre generado por un LLM que entra a una consulta de Mongo sin escapar es una inyección esperando a pasar.

El producto

La sala de redacción del periodista: agenda del día, recordatorios y el panel de perspectivas que cruza su actividad con lo que detectó el radar editorial.
Antes de agendar, el sistema valida los requisitos críticos y sugiere —no impone— priorizar un canal directo de recordatorio. La IA aconseja; el periodista decide.
El alta en dos pasos: primero los datos base, después la revisión final.
Cada programa lleva su mandato editorial. Ese texto es contexto para el modelo cuando genera briefings: el mismo tema se enfoca distinto en un magazine matutino que en una entrevista larga de análisis.
La base contra la que se cruzan las personas detectadas en las noticias. El match es por nombre normalizado o alias registrado — de ahí la separación entre identidad canónica y observación.
Cuatro vistas sobre la misma agenda: lista, calendario, línea de tiempo y kanban.
La entrada del pipeline de transcripción: se pega el enlace —YouTube, Vimeo, Twitter o directo— y sale la transcripción segmentada por hablante.
Configuración de la cuenta y del espacio de trabajo.

Cifras

7 600
líneas de TypeScript en el backendMediaBriefs §1
25
módulos de dominio NestJSMediaBriefs §1
61
endpoints en el contrato OpenAPIMediaBriefs §1
88
esquemas versionadosMediaBriefs §1
32
pantallas en el frontendMediaBriefs §1
~200
artículos procesados cada 2 hMediaBriefs §3

Lo que demuestra

HabilidadEvidencia
Salidas estructuradasTool-use forzado en las tres llamadas · enums en el esquema · índices en vez de texto libre
Reparto modelo/códigoEl LLM agrupa; el código calcula velocity, score y señales editoriales de forma auditable
Mitigación de alucinacionesReferencia por índice · identidad canónica vs. observación · escapeRegex sobre salida del modelo
Robustez en producciónDegradación en cascada con cuatro políticas · flag diarizationEnabled visible al usuario · reintentos de Bull
CostosHaiku en todo el pipeline · caché de briefings con TTL persistido y compartido entre usuarios
ArquitecturaMicroservicio para aislar el stack de ML · 25 módulos NestJS · contrato en repo propio

Deuda técnica reconocida

Un case study que solo lista aciertos no resiste una pregunta difícil.

  • La lógica de merge de diarización está duplicada en TypeScript y Python; el merge acabó ejecutándose en el backend, así que la versión Python es código muerto.
  • El clustering hace deleteMany + insertMany: hay una ventana breve con la colección vacía. Un bulkWrite con upserts por label sería más correcto.
  • Quedan endurecimientos de seguridad anotados en el código y pendientes de implementar.

Stack

  • NestJS
  • TypeScript
  • Node.js
  • MongoDB
  • Redis + Bull
  • Autenticación con sesión rotativa
  • OpenAPI 3
  • Next.js
  • React
  • Tailwind
  • Claude Haiku 4.5
  • Whisper
  • pyannote 3.1
  • FastAPI
  • PyTorch
  • Docker