Promesas Valencia (4): La cola, Claude en local y decidir según las condiciones
Publicado el 20/06/2026
La entrada anterior terminó con una conclusión: Claude extrae mejor, pero hay que usarlo dentro de las condiciones reales del proyecto. Esta entrada va sobre eso —la infraestructura que hace el trabajo en segundo plano y las decisiones de «qué herramienta y por qué ahora»—, porque ahí es donde más se aprende a diseñar con la cabeza fría.
Primer giro: Whisper fuera, VTT dentro
El proyecto arrancó transcribiendo el audio con Whisper como servicio aparte.
Sobre el papel, elegante. En la práctica, una pieza pesada que mantener. Se
retiró en favor de algo más simple: aportar la transcripción como VTT, ya sea
subido a mano desde el backoffice o bajado con yt-dlp en local. Menos partes
móviles, menos cosas que pueden fallar en el servidor. Primera lección:
la solución más simple que cubre el caso real gana a la más sofisticada.
Procesar sin bloquear: la cola
Extraer las promesas de un pleno tarda —especialmente con Ollama en CPU—. No puede ocurrir mientras un administrador espera mirando una pantalla colgada. Así que el trabajo va a una cola y se procesa en segundo plano, mientras la tabla de trabajos en la base de datos da visibilidad del progreso en el backoffice.
La primera versión fue una cola artesanal: un worker que sondeaba la base de
datos. Funcionaba, pero era código propio para un problema ya resuelto. Cuando
quedó claro que el dolor real era perder el progreso en cada redespliegue (un
trabajo de decenas de bloques volvía a empezar de cero), se migró a Redis con
apalis, la librería estándar de background jobs del ecosistema. Las
decisiones de diseño concretas:
- Concurrencia 1: se procesa un pleno a la vez. No es una limitación caprichosa: en un VPS sin GPU, dos extracciones simultáneas competirían por la memoria y provocarían un OOM. Aquí, restringir es proteger.
- Checkpoint por bloque: el avance se guarda continuamente. Si el proceso muere o se redespliega, el trabajo se reanuda por donde iba, sin duplicar (al reanudar se repuebla la de-duplicación desde la base de datos).
- Re-entrega de huérfanos:
apalisreencola un trabajo si el worker muere. El valor por defecto se ajustó de 1 hora a 120 segundos tras leer el código de la librería y entender que el latido del worker va cada 30 s en una tarea aparte. Resultado: tras un redespliegue, el cálculo reanuda en ~2 minutos en vez de quedarse parado hasta una hora. Lección: antes de configurar a ojo, mira cómo funciona por dentro. - Cancelación cooperativa: como
apalisno mata la tarea en vuelo, la cancelación se hace por señal entre bloques. El bloque en curso termina y luego para limpiamente, conservando lo extraído.
Segundo giro: Claude en local, con la suscripción
Si Claude es el mejor extractor, ¿cómo usarlo sin disparar el coste? Aquí entró una distinción que mucha gente confunde:
- La suscripción de Claude (la de pago mensual) sirve para las apps y para Claude Code, pero no da clave de API.
- La API de Anthropic se paga aparte, por consumo de tokens.
La jugada: usar Claude Code en modo headless (claude -p) en local, que
aprovecha la suscripción sin clave de API, para generar el JSON de promesas a
partir del VTT, y luego subirlo a la app. Así, el VTT entero cabe en una sola
llamada (sin trocear → casi sin duplicados) y el coste marginal es cero.
Esto vivió primero como un script de bash… que dio problemas. La lección llegó en forma de tres bugs encadenados, todos instructivos:
Credit balance is too low: el subprocesoclaudeheredaba la variableANTHROPIC_API_KEYdel.envy se iba a la API de pago (sin saldo) en lugar de a la suscripción. Solución: quitar esa variable del entorno del subproceso. Sutil y muy fácil de pasar por alto.Prompt is too long: el VTT crudo de un pleno son2 millones de caracteres (más de 1 millón de tokens). No cabía. Solución: enviar a Claude el texto plano (sin marcas de tiempo ni líneas repetidas), que ya sabíamos derivar; pasó de 2 MB a224 KB. Y los timestamps no se pierden, porque se derivan en el servidor al cruzar la cita con el VTT con tiempos.- Errores silenciosos: el script fallaba sin decir por qué (login mal
detectado, un 502 confundido con «endpoint no desplegado»). La decisión final
fue reescribir el script como un subcomando del propio binario Rust
(
extraer-local): manejo de errores de verdad, detección del login fallido, y un mensaje útil en cada punto.
Diseñar el flujo para no malgastar trabajo
La versión final de extraer-local ordena los pasos con una lógica clara: lo
caro va lo más tarde posible y lo que puede fallar barato, primero.
- ¿Hay ya un JSON cacheado de este pleno? Si sí, no se vuelve a llamar a Claude (la extracción son minutos).
- Validar el login —antes de gastar la extracción—. Si las credenciales fallan, se aborta ahí.
- Generar con Claude solo si no había caché, y guardarlo.
- Subir al pleno.
Si la subida falla (un 502 del proxy, una sesión caducada), el JSON ya está en disco: al reintentar, no se reextrae. Pequeñas decisiones de orden que ahorran tiempo y dinero.
La idea que atraviesa todo
No hay una «mejor herramienta» en abstracto. Hay una mejor herramienta para estas condiciones: un VPS modesto sin GPU, una suscripción en vez de API, un proyecto que tiene que sobrevivir a redespliegues. Ollama tiene sentido para autohospedar sin coste; Claude, para calidad máxima vía suscripción; la cola en Redis, porque el dolor era perder progreso; la concurrencia 1, porque la RAM manda. Aprender a leer esas condiciones —y a no enamorarse de la solución brillante que no encaja— ha sido, quizá, el aprendizaje más transferible de todo el proyecto.