La máquina detrás del torneo

Cómo funciona ⚙️

Durante 8 semanas, la tabla de posiciones se actualizó sola: subías la foto de tu vuelta al chat de WhatsApp y aparecías en la página. Esta es la explicación completa de cómo — con ejemplos reales — para que cualquiera pueda armar algo parecido.

En una frase: fotos de WhatsApp → un job las bombea a un servidor casero → un modelo de visión (Claude) las lee → lo dudoso se aparta para revisión humana → lo confirmado se escribe en un JSON → un script genera HTML estático → git push → Cloudflare Pages lo publica. Sin base de datos, sin backend, sin apps.

El flujo completo

📱Chat de WhatsAppcada quien sube la foto de su pantalla de resultados
↓ launchd en la laptop, cada 30 min
📦El bombeo (pump)fotos nuevas + metadata → rsync → servidor casero (wa-dump/ + manifest.jsonl)
↓ launchd en el servidor, cada 30 min
👁️Lectura con visiónClaude API lee cada pantalla: tipo, tiempos, personajes, confianza
🪪Identidadremitente (JID) → persona; el personaje en pantalla NO identifica a nadie
🚦La compuerta (gate)lo claro se auto-acepta; lo dudoso se "parquea" para un humano
📄leaderboard.json → HTML estáticoun generador Python arma la página completa
↓ git push
☁️Cloudflare Pagesredeploy automático en ~30 segundos, gratis

Paso a paso, con ejemplos

  1. La fuente: un chat normal No hay app ni formulario. La familia sube al grupo de WhatsApp la foto de su pantalla de time trial (contra el fantasma) — la misma foto que subirían para presumir. Ese es todo el "input" del sistema: el punto era que participar no costara nada.
  2. El bombeo: de la laptop al servidor WhatsApp guarda las fotos del chat en la máquina donde está abierto. Un job programado (launchd, el cron de macOS) extrae las fotos nuevas del grupo con su metadata (quién la mandó, cuándo) y las empuja por rsync al servidor casero. Cada foto queda registrada en un manifiesto de una línea por imagen:
    {"source_id": "38170", "sender_jid": "[email protected]",
     "ts": "2026-06-14T09:12:44-06:00", "path": "wa-dump/38170.jpg"}

    La semana se calcula después a partir de ts (semanas que arrancan en domingo) — así una foto que llega tarde cae en la semana correcta.

  3. La lectura: un modelo de visión con reglas aprendidas Cada foto nueva pasa por la API de Claude con un prompt que sabe leer pantallas de MK8. Devuelve JSON estructurado:
    {"screen_type": "ghost_time",
     "username": "Decr", "time": "2:05.601",
     "track": null, "confidence": "high"}
    Lo interesante no es el modelo — es lo que hubo que enseñarle con fotos reales. Las reglas se descubrieron corrigiendo lecturas una por una las primeras semanas:
    • R1: la fila dorada es la vuelta del jugador — y NO siempre está arriba. Dorada hasta abajo = no le ganaste a tu fantasma. (El error más común antes de esta regla.)
    • R2: el nombre en pantalla suele ser el personaje, no la persona. La identidad viene del remitente, nunca de la foto.
    • R3: la gente reenvía pantallas de otros (Pato mandaba las suyas desde el teléfono de Fernando).
    • R4: la pista casi nunca sale en la foto — se declara por semana, no se lee.
    • R5: la consola está en español; el prompt lo asume.

    Cada foto que el modelo leyó mal se convirtió en un caso de prueba (un "eval") con la respuesta correcta. El lector se corre contra ese banco antes de tocar el prompt: si una mejora rompe un caso viejo, se nota al instante. Precisión medida sobre el histórico real: ~92% de lecturas correctas sin intervención.

  4. ¿Quién es quién? Un mapa de remitentes Los personajes se repiten (hubo tres Toads en un mismo grupo del torneo); el remitente no. Un archivo plano mapea el identificador de WhatsApp de cada quien a su nombre en la tabla:
    {"[email protected]": "Decr",
     "[email protected]": "Jorge", ...}
    Cuando escribe alguien nuevo, su foto se aparta y un comando (map-sender) lo da de alta y re-procesa sus fotos pendientes.
  5. La compuerta: auto-aceptar lo claro, apartar lo dudoso La decisión de diseño más importante. El pipeline corre solo cada 30 minutos en modo auto: las lecturas con confianza alta se publican sin preguntar; las dudosas (borrosas, recortadas, remitente desconocido, campos faltantes) se van a una cola de "parqueados" con su razón, y ahí esperan a un humano:
    # ver la cola de dudosos
    ./pipeline/run-pipeline.sh parked
    # corregir a mano un tiempo que la visión no pudo leer
    ./pipeline/run-pipeline.sh enter --id 38235 --time 2:18.062
    # o quitar de la tabla algo que se aceptó mal
    ./pipeline/run-pipeline.sh remove --ids 37024
    La filosofía es publicar y corregir, no pedir permiso para cada foto: la tabla siempre está viva y los errores (pocos) se arreglan con un comando. Todas las decisiones quedan en un ledger append-only donde la última palabra gana — así un item apartado puede re-entrar cuando su causa raíz se arregla, y el job automático nunca pisa una corrección humana (un lock evita que se interleaven).
  6. Publicar: JSON → HTML estático → git push La "base de datos" es un archivo: leaderboard.json (la tabla), roster.json (los inscritos) y weeks.json (qué pista es cada semana):
    [{"week": 8, "track": "Mount Wario"},
     {"week": 7, "track": "Moo Moo Meadows"}, ...]
    Un generador en Python convierte esos tres archivos en un index.html completo — CSS incluido, cero JavaScript de framework — y lo empuja a un repo de GitHub conectado a Cloudflare Pages. El push ES el deploy: ~30 segundos después está en línea. El generador es determinista (mismos datos → mismo HTML), así que solo se commitea cuando algo cambió de verdad.
  7. Salud: una página de status diaria Otro job de launchd genera cada mañana a las 8:00 una página de salud del pipeline (/status.html): cuántas fotos llegaron, cuántas se leyeron, qué hay parqueado. Un vistazo de 10 segundos con el café dice si la máquina sigue viva — sin abrir una terminal.
  8. El día del torneo: el mismo truco, operado en vivo El 25 de julio no hubo tiempo para pipelines: las páginas de grupos y bracket y de resultados se operaron en vivo desde un teléfono, con un agente (Claude Code en modo remoto) del otro lado: le mandabas la foto del proyector con las posiciones, leía los puntos de las barras de colores (las filas resaltadas son los humanos), editaba el bloque de datos dentro del HTML, hacía push y ~30 segundos después el bracket estaba actualizado para todos. Catorce publicaciones en una tarde — incluyendo un cambio de formato a media competencia — sin abrir la laptop.

El stack completo

PiezaQué esPor qué esta
WhatsAppLa fuente de datosDonde la familia ya estaba. Participar = mandar una foto.
launchdEl cron de macOSDos jobs de 30 min (laptop y servidor) + el status diario. Cero infraestructura nueva.
rsyncTransporte laptop → servidorIncremental, reintentable, aburrido en el buen sentido.
Python + pytestPipeline, resolver, generadorComponentes chicos con contratos JSON entre sí; ~150 tests en verde antes de cada cambio.
Claude API (visión)El lector de pantallasLee JSON estructurado de una foto de un proyector, en español, con reglas raras (R1–R5).
Banco de evalsFotos reales + respuesta correctaCada error se vuelve examen. El prompt solo mejora si pasa todo el banco.
JSON planoLa "base de datos"Legible, diffeable, respaldable con cp. A esta escala, una DB solo estorba.
git + GitHubHistorial y transporte de deployCada publicación es un commit: auditable y reversible.
Cloudflare PagesHostingEstático, gratis, dominio propio, redeploy automático en cada push.
HTML/CSS a manoTodas las páginasSin frameworks ni build. La página del bracket es un HTML con un bloque de datos editable adentro.

Si quieres armar el tuyo

Nada de esto es específico de Mario Kart. La receta general, en orden:

  1. Empieza por el chat que ya existe No pidas a nadie instalar nada ni llenar formularios. Tu fuente de datos es el grupo donde tu gente ya comparte fotos.
  2. Haz que las fotos caigan en una carpeta con manifiesto Como sea que las saques (export, script, a mano al principio), el contrato es simple: una carpeta de imágenes + un JSONL con remitente y fecha por imagen. Todo lo demás se cuelga de eso.
  3. Ponle un lector de visión con salida JSON Prompt que pide campos concretos + nivel de confianza. Y la parte que nadie te cuenta: corrige sus errores a mano las primeras semanas y convierte cada error en un caso de prueba. Las 5 reglas que harán tu sistema confiable solo viven en TUS fotos.
  4. Separa identidad de contenido Quién manda la foto es un dato del chat; qué dice la foto es un dato del modelo. Un mapita remitente→persona te salva de todos los "en la pantalla dice Toad".
  5. Construye la compuerta antes que el dashboard Auto-acepta con confianza alta, aparta lo demás en una cola con razón, y dale al humano 3 comandos: ver, corregir, quitar. Publicar-y-corregir le gana a aprobar-cada-cosa desde el día uno.
  6. Genera HTML estático desde JSON y despliega con git push Un script que lee tus JSON y escupe una página completa. GitHub + Cloudflare Pages (o Netlify, o GitHub Pages) hacen el resto gratis. Si tu página necesita datos en vivo, pon los datos como bloque editable dentro del HTML — editar + push es tu "backend".
  7. Programa dos crons y una página de status Uno que bombea, uno que procesa, y una página diaria que te diga que siguen vivos. Si el status está verde, no abres la terminal.

Escala real de este proyecto: un grupo de ~40 personas, ~150 fotos en 8 semanas, un servidor casero, y un costo de API de centavos por semana. Todo el código son componentes chicos de Python con contratos JSON entre sí — ninguna pieza sabe de las demás más que su archivo de entrada y de salida.

Los cinco principios, si solo te llevas algo: estático > servidor · JSON plano > base de datos · publica-y-corrige > aprueba-cada-cosa · los errores del modelo son tus mejores tests · deja al humano exactamente lo dudoso, ni más ni menos.