Turna
Fotografías el cuadrante de turnos que te dan en el trabajo y aparece en tu calendario, repartido día a día, en menos de diez segundos.
- Precisión
- 313/313 días
- Falsos seguros
- 0
- Casos de prueba
- 15 (8 sintéticos, 7 reales)
- Decisiones cerradas
- 15
- Plataforma
- Android
- Stack
- Kotlin, Jetpack Compose, Python, Visión por IA
Mucha gente con turnos rotativos recibe su mes de trabajo en un papel: una tabla con nombres en las filas, días en las columnas y códigos de una letra en las celdas. Después lo copian a mano al calendario del móvil, o no lo copian y viven mirando la foto.
Turna elimina ese paso. Fotografías el papel y el mes queda repartido en el calendario del sistema, no en un calendario propio de la app, para que se sincronice con lo que ya usas.
De dónde sale
Nació en una sesión de ideación de diez apps móviles con potencial, filtradas por cuatro criterios: problema real, MVP en menos de ocho semanas, demostrable en un vídeo vertical y monetización clara. De las diez se eligió esta por la relación entre el dolor del usuario y la dificultad de construirla.
La razón de fondo es que el problema ya existe como objeto fotografiable. El papel del cuadrante está ahí, en la taquilla o en el grupo de WhatsApp. Eso elimina el mayor riesgo de casi todas las demás ideas: convencer a alguien de que cambie un hábito.
Para quién es
El usuario primario es enfermería y auxiliares de hospital con turnos rotativos en España. Secundarios: hostelería, seguridad privada, tripulación de vuelo y gran superficie.
El perfil manda sobre todo lo demás: no es técnico, no quiere otra app y solo va a dar una oportunidad al producto. Si el primer cuadrante sale mal, no hay segundo intento. Toda la semana 0 (más abajo) existe por esta frase.
El modelo es freemium: un cuadrante gratis, después suscripción (3,99 €/mes o
24,99 €/año) o pago único de 39,99 € de por vida. El bucle de crecimiento
orgánico es compartir el calendario con la pareja o los compañeros por un
enlace .ics.
Decisiones de producto
Hay quince decisiones registradas con su porqué. Las que más condicionan lo que se construye:
| # | Decisión |
|---|---|
| D-001 | Sin registro para la primera extracción. El valor se demuestra antes de pedir cuenta. |
| D-002 | Se escribe en el calendario nativo del sistema, no en uno propio de la app. |
| D-004 | Pantalla de revisión obligatoria. Nunca se puede saltar. |
| D-005 | Compartir por enlace de calendario (.ics/webcal://), no por invitación a la app. |
| D-007 | Solo imagen como entrada en el MVP. Nada de PDF ni Excel. |
| D-008 | Sin intercambios de turnos entre compañeros: evita convertirse en un marketplace de dos lados. |
| D-010 | Plataforma única y nativa. Decidido en la semana 1: Android. |
| D-012 | Escala ×2 de imagen y consenso de dos lecturas, fijos en producción, no configurables. |
| D-013 | Sentinela "CIERRE" para horas de fin abiertas («hasta que cierre el negocio»). |
| D-014 | Cierre de la semana 0 con 313/313 y 0 falsos seguros, aceptando un hueco de validación (ver abajo). |
Semana 0: comprobar que el modelo puede leer un cuadrante
Antes de escribir una línea de la app se construyó un banco de pruebas. Sin eso, «va mejor» es una impresión; con eso es un número.
El banco de pruebas
Tres piezas:
- Fixtures: imágenes de cuadrantes, sintéticas (generadas) y reales (conseguidas en grupos de Facebook y por búsqueda directa).
- Expected: la verdad de referencia de cada fixture, anotada a mano, en JSON, día a día.
- Scripts:
run_extraction.pyllama al modelo con la imagen y guarda el resultado;run_eval.pycompara contra la verdad y genera un informe.
Las quince fixtures
Ocho sintéticas, generadas con HTML/CSS y capturadas como imagen, cubren los casos que había que forzar: turno base, noches con saliente, vacaciones parciales, grupos rotativos sin nombre, horas numéricas, formato calendario con vocabulario propio (UCI), una captura de WhatsApp con ruido de chat, y una fila ambigua con dos personas de nombre parecido donde el modelo debe rendirse en vez de adivinar.
Siete reales, y cada una trajo un problema distinto:
| Fixture | Origen | Lo que enseña |
|---|---|---|
| r1 | Cafetería española | Sin leyenda impresa; el código D es ambiguo (Domingo o Descanso). |
| r2 | Hostelería española, semanal | Turno partido: dos bloques de trabajo con hueco. |
| r3 | Restaurante español, semanal | Hora de cierre abierta (CIERRE). |
| r4 | Hostelería española | Tres semanas en una hoja, turnos partidos y CIERRE. |
| r5 | Hospital de Perú | Formato quincenal, turno M/T combinado sin partir. |
| r6 | Hospital de Colombia | Código combinado turno+tarea (C2, N1…); D (Descanso) y L (Libre) como códigos distintos. |
| r7 | Hospital, país sin confirmar | Turno D de 12 h frente a M de 6 h, con verificación cruzada de horas impresas. |
El hueco aceptado
No hay ningún cuadrante confirmado de un hospital español, que es justamente el usuario primario, pese a buscarlo activamente en grupos de Facebook durante cuatro días sin respuesta. Se cerró la semana 0 igualmente (D-014), con una mitigación explícita: el primer cuadrante real de hospital español que llegue se incorpora al banco antes de volver a tocar el prompt.
Iteración de modelo y prompt
| Corrida | Modelo | Prompt | Escala y consenso | Precisión | Falsos seguros |
|---|---|---|---|---|---|
| gpt-4o | gpt-4o | v1 | — | 30,7 % | 148 |
| gpt-4o-v2 | gpt-4o | v2 | — | 35,8 % | 138 |
| gpt-5.1-final | gpt-5.1 | v3 | ×2 + consenso 2 | 68,8 % | 48 |
| gpt-5.4-final | gpt-5.4 | v3 | ×2 + consenso 2 | 100 % | 0 |
| gpt-5.5-v2/v3 | gpt-5.5 | v2/v3 | — | 96,3 % | 7-8 |
| gpt-5.5-final | gpt-5.5 | v3 | ×2 + consenso 2 | 100 % | 0 |
| v6-completa (15 fixtures) | gpt-5.5 | v6 | ×2 + consenso 2 | 95,2 % → 98,7 %* | 15 → 4* |
| v7-completa (15 fixtures) | gpt-5.5 | v7 | ×2 + consenso 2 | 100 % | 0 |
* Tras corregir un bug del evaluador: usaba un glosario global fijo en vez de leer la leyenda de cada documento.
Seis hallazgos
- El modelo importa mucho más que el prompt. Pasar de gpt-4o a gpt-5.5 fue un salto de más de sesenta puntos. Iterar el prompt movía como mucho cinco.
- La resolución de la imagen es la palanca más infravalorada. Ampliar ×2 antes de enviar resolvió los últimos falsos seguros: en tablas densas el modelo perdía la alineación de columna, no la lectura del carácter.
- El consenso de dos lecturas es la red de seguridad real. El modelo nunca ha demostrado saber decir «no estoy seguro» por sí solo: devuelve confianza 1.0 incluso en los días que falla. Si las dos lecturas no coinciden, el día se marca y se pregunta.
- Un día tiene dos bloques solo si el documento imprime dos horarios
separados. Nunca por analogía con el nombre del código. Esta regla
salió de una regresión real del prompt v6: enseñarle que
M/Teran dos franjas hizo que generalizaraMN(guardia de 24 h) como dos bloques y le abriera un hueco falso. - La leyenda del documento manda siempre sobre un glosario global. La
misma letra (
D,C,M) significa cosas distintas en centros distintos; un glosario fijo de español de España rompía la evaluación de las fixtures latinoamericanas. - Ningún cambio de prompt entra sin correr las quince fixtures y confirmar que ninguna baja respecto a la versión anterior. Es una regla no negociable, no una buena intención.
El esquema cambió por el camino
La estructura de salida de la extracción evolucionó varias veces durante la semana 0, siempre empujada por una fixture concreta:
- De
inicio/finplanos a un array desegmentospor día, para soportar turnos partidos (r2, r4). - Sentinela
"CIERRE"para horas de fin no fijas (r3). - El color de celda no es fiable como señal. Se confirmó en r5: era decorativo, no codificaba el turno.
- Código y tarea (r6) se fusionan en un solo campo
codigo, con la tarea como metadato opcional. La justificación no es «eso es de otro mercado», sino que la tarea no cambia cuándo trabaja la persona.
La competencia, probada de primera mano
Existe un competidor directo, Shift2Cal, de un estudio italiano generalista: foto, IA, calendario. Tracción modesta en Android (unas mil descargas, 4,3 estrellas) y casi nula en iOS. Probada: lenta, interpreta mal el cuadrante y la pantalla de resultado no se entiende.
El aprendizaje no fue «se puede hacer mejor», sino algo más incómodo: el mercado no premia «más IA», premia que se vea claro y no tarde nada. Una app rival sin ninguna IA, Shifter, genera cariño solo por tener un calendario simple y con colores. La pantalla de revisión de Turna tiene que competir en simplicidad, no solo en precisión.
Semana 1: la app y el backend
Decisiones de arranque
- Android nativo, Kotlin y Jetpack Compose (D-010, D-015).
- Nombre de paquete
com.turna.app, corregido desdecom.cuadrante.appal cerrarse la marca. «Turna» es la marca; «cuadrante» sigue siendo el sustantivo del dominio (la clase de datos, el documento que se fotografía), y no se renombró indiscriminadamente. - Un solo repositorio:
app/para el cliente Android,core/para el núcleo de extracción compartido yservices/extraccion/para el backend HTTP. minSdk26, que dajava.timenativo. Cero permisos declarados hasta que hagan falta; los del calendario llegan en la semana 4.
Tres pantallas
El primer commit es un esqueleto que navega, sin lógica real todavía:
- Captura, con el escáner de documentos de ML Kit (detección de bordes y corrección de perspectiva) y un botón para importar desde la galería.
- Revisión, una rejilla del mes. Los datos de ejemplo incluyen a propósito un día dudoso y un turno partido, para que el diseño los tenga en cuenta desde el principio.
- Confirmación, un mensaje simple de éxito.
Verificado en Android Studio con Clean and Assemble Project with Tests:
BUILD SUCCESSFUL, compilación limpia desde cero.
El backend reutiliza literalmente el código validado
De tres arquitecturas consideradas se eligió extraer el núcleo de
evals/run_extraction.py a un paquete compartido core/, para que el banco
de pruebas y el servicio de producción importen el mismo código. Nunca
copias que puedan divergir.
A core/ se movieron el esquema de extracción, el prompt v7, la preparación
de imagen (escala ×2), la llamada al modelo, el consenso de dos lecturas, el
modelo semántico del día y el cálculo de coste.
La verificación del traslado no fue visual:
preparar_imagen: bytes idénticos, cero diferencias.- Prompt y esquema: hashes idénticos.
- Las quince fixtures corridas contra el servicio HTTP real, no contra resultados guardados: 313/313, 0 falsos seguros.
Y aun así aparecieron tres bugs reales, todos corregidos antes de dar el resultado por bueno:
- Una variable local
turnostapaba el módulo importado dentro deconsensuar(). _clave_semanticano comparaba el código de turno, solo tipo y franja. En las cuatro fixtures sin horas impresas, dos lecturas con códigos distintos (Mfrente aT) pasaban como «de acuerdo» porque caían en la misma franja «desconocida». Corregido: el código solo cuenta cuando no hay segmentos; si los hay, el horario es la verdad.- Referencias sueltas a
ESQUEMA_EXTRACCIONy al cálculo de coste que no se habían movido con el resto.
El resultado es un endpoint POST /extraer (imagen, usuario y sector → JSON
de turnos con el detalle del consenso día a día) y un GET de estado.
Escala ×2 y consenso son fijos (D-012). Sin autenticación ni despliegue
todavía: solo local.
Lo que sigue, en orden
- Conectar app y backend con datos reales, sustituyendo
CuadranteDeMentirapor turnos de verdad en la pantalla de revisión, incluidos los días dudosos. - Diseño final de la pantalla de revisión, compitiendo explícitamente en simplicidad contra Shift2Cal y Shifter.
- Escritura real en el calendario del sistema, con permisos.
- Enlace de compartir
.icsy muro de pago. - Incorporar al banco el primer cuadrante real de hospital español en cuanto aparezca.
Lo que deliberadamente no se toca
La versión iOS queda aplazada sin fecha. No se aborda hasta tener usuarios de
pago reales en Android y una señal concreta de fricción, como parejas con
iPhone que no puedan recibir el calendario compartido. La lógica de
extracción en core/ es independiente de plataforma y se reutilizaría sin
cambios; lo que habría que rehacer es toda la capa nativa (SwiftUI, VisionKit,
EventKit, WidgetKit).
Notas de este proyecto
- Iteré el prompt durante días y lo que subió la precisión fue cambiar de modelo
Pasé de 30,7 % a 100 % de días correctos leyendo cuadrantes de turnos. Casi nada de esa mejora vino del prompt, y la métrica que importaba no era la precisión.