Promesas Valencia (3): Extraer promesas con IA

Publicado el 19/06/2026

Aquí está el corazón —y la parte más difícil— del proyecto: convertir horas de transcripción automática de un pleno en una lista de compromisos estructurados, sin inventarse nada. Esta entrada cuenta el camino, con sus callejones sin salida, porque ahí está el aprendizaje.

La materia prima es ruido

Las transcripciones vienen de los subtítulos automáticos de YouTube (ASR). Eso significa: sin puntuación fiable, con errores en cifras y nombres propios, y en formato VTT lleno de marcas de tiempo y líneas repetidas (el «rolling caption»). Un pleno son un par de horas de eso. Cualquier solución tenía que convivir con ese ruido, no asumir un texto limpio.

Dos proveedores, una abstracción

Desde el principio se decidió no atarse a un proveedor de IA. La extracción habla con una abstracción (ClienteIA) que selecciona el backend por configuración:

  • Ollama autohospedado (modelos abiertos, sin coste por uso, datos en casa).
  • Anthropic / Claude (API de pago, mucho más capaz).

El extractor no sabe cuál está detrás: ambos exponen el mismo contrato de «devuélveme este JSON estructurado». Esa decisión de diseño resultó clave más adelante, cuando hubo que comparar uno con otro.

El primer muro: recall

Con Ollama y un modelo pequeño (qwen3:4b), un pleno del que Claude sacaba 30-40 promesas se quedaba en unas 10. No era un bug: un modelo de 4.000 millones de parámetros simplemente «no ve» tanto como uno de frontera. Esa brecha abrió toda una línea de trabajo para exprimir al máximo el modelo pequeño:

  • Troceo de la transcripción en bloques de ~1.000 palabras con solape. Los bloques pequeños combaten el efecto «lost in the middle» —el modelo pequeño se pierde en textos largos y densos, justo donde se concentran las ráfagas de subvenciones de los informes de alcaldía.
  • Subir de modelo: qwen3:4bqwen3:8b. Aquí entró una restricción muy real: el servidor de Ollama es un VPS sin GPU y con poca RAM. No se podía elegir el modelo más grande «porque sí»; había que evitar que el sistema se quedara sin memoria (OOM). El 8b fue el equilibrio entre calidad y caber en la máquina.
  • Reintentos por bloque, para que un fallo transitorio del modelo no haga perder todas las promesas de un fragmento entero.

El segundo muro: alucinaciones y la verificación de citas

Un modelo pequeño no solo ve menos: a veces se inventa compromisos o cifras. La defensa: exigir que cada promesa traiga una cita textual copiada de la transcripción, y verificarla contra el bloque antes de guardar. Si la cita no está, la promesa se descarta.

Pero la verificación literal era demasiado estricta: los modelos pequeños parafrasean al copiar («corrigen» la puntuación, reordenan palabras). Citas reales se estaban tirando. La solución fue una verificación en dos pasos:

  1. Coincidencia exacta de la cita (normalizada: sin tildes, ignorando puntuación, para tolerar el y los puntos de millar que el modelo añade).
  2. Si falla, un fuzzy por ventana deslizante: se acepta si una porción suficiente de las palabras de la cita aparece junta en el bloque.

Y como subproducto valiosísimo: la posición donde se localiza la cita da el minuto del vídeo. El modelo nunca decide la marca de tiempo; se deriva de dónde encaja la cita en el VTT con tiempos. Anclaje verificable y deep-link, sin fiarse del modelo para eso.

El tercer muro: duplicados

Con bloques pequeños y solapes aparecieron promesas repetidas y, peor, fragmentadas: el modelo desglosaba un mismo compromiso (p. ej. una ordenanza de venta de entradas) en varios ítems —«aprobar la ordenanza», «reserva del 10%», «cobro de entrada»— como si fueran tres cosas. La de-duplicación se atacó en varios frentes:

  • En el prompt: pedir explícitamente no fragmentar un compromiso en facetas.
  • Heurísticas sin IA: fusionar promesas del mismo momento del pleno con títulos parecidos, o con citas muy solapadas.
  • Semántica con embeddings: comparar el significado de título + descripción (coseno de vectores) para unir lo que es lo mismo aunque esté redactado distinto. Esto requiere un modelo de embeddings (nomic-embed-text), ligero y viable incluso en CPU.

Aquí una lección de proceso: el umbral de similitud empezó fijo en el código. En cuanto hubo que calibrarlo con casos reales, se hizo configurable por entorno y se añadió un log de los pares casi-duplicados con su coseno real. Calibrar a ciegas no funciona; hay que ver los números.

La conclusión incómoda y honesta

Tras todo ese esfuerzo, la realidad se impuso: Claude, leyendo el pleno entero de una vez, simplemente lo hace mucho mejor y con muchos menos duplicados de origen. El trabajo con el modelo pequeño no fue en vano —dejó un pipeline de verificación, timestamps y dedup que se reutiliza con cualquier proveedor—, pero abrió la pregunta que domina la siguiente entrada: si Claude es mejor, ¿cómo lo usamos dadas las condiciones reales (un VPS modesto, una suscripción en vez de API de pago)?