He estado construyendo kaiord-helper, una herramienta que automatiza la generación de entrenamientos en formato Garmin Connect JSON desde Train2Go. Es un pequeño pero potente experimento sobre cómo combinar parsing tradicional (regex) con LLM para manejar la complejidad del lenguaje natural.
Quería ayudar al repo de: github.com/pablo-albaladejo/kaiord
El Problema
Los entrenamientos en Train2Go se describen en prosa libre:
10' z1 + 4x(3' Z4 + 2' Z1) + 5' CD
Rodaje cómodo con cambios de ritmo: 2' fácil + 3x(30" rápido + 1'30" andando) + 3' CD
Necesitaba convertir esto a JSON compatible con la API de Garmin Connect, que espera una estructura rígida (pasos ejecutables, grupos de repetición, targets de zona/pace, etc.).
La Solución: Arquitectura Híbrida
La herramienta usa dos estrategias en paralelo:
- Parser Regex (por defecto) Para casos simples y bien formados:
// Reconoce: zonas (z1-z5), pace (5'40"), duración, repeticiones
const stepPattern = /(\d+)':"?\s*([a-z0-9\s]*)/gi;
Ventajas: Ultra rápido (<1ms), sin API keys, costo cero
Limitación: Falla con variaciones naturales ("treinta segundos", "recuperación activa")
- Claude Haiku (fallback) Cuando el regex falla, delegamos a Claude:
const parsed = await parseWorkout(text, { fallbackToLLM: true });
// Si regex falla → automáticamente intenta Claude
Claude entiende contexto, variaciones de lenguaje y hasta lógica ("más rápido que la vez anterior").
Ventajas: Robusto, flexible, natural
Desventaja: ~$0.0001/workout, latencia ~500ms
Stack Tecnológico
{
"runtime": "Node.js",
"language": "TypeScript",
"apis": ["Train2Go", "Garmin Connect", "Anthropic (Claude)"],
"tools": ["Cheerio (web scraping)", "dotenv (config)"],
"architecture": "Hexagonal (Parser → Builder → Clients)"
}
Características Implementadas
✅ Parsing flexible
Zonas FC (z1-z5), pace, duración, repeticiones explícitas (4x) e implícitas
Tipos de paso: warmup, cooldown, interval, recovery, rest
Recuperaciones con modificadores (andando, activa)
✅ Integración Train2Go
Obtiene entrenamientos de 7 días automáticamente
Extrae descripciones en español
Maneja sesiones con cookies
✅ Publicación en Garmin Connect
Genera y publica directamente: --publish
Validación de sesión antes de subir
ID de workout como respuesta
✅ Modo offline para testing
npx tsx src/index.ts --text "10' z1 + 4x(3' Z4 + 2' Z1) + 5' CD"
Lo que Aprendí
- La Trampa del Regex Puro Mantener patrones para todas las variaciones naturales es imposible. Un deportista escribe:
"5 minutos en zona 1"
"cinco minutos z1"
"5' z1"
"5:00 fácil"
Cada una necesita un patrón. Termina siendo un caos.
Lección: Conocer cuándo regex es insuficiente. Un LLM es más mantenible que 100 regex.
- APIs y Autenticación Real Aprendí sobre cookies de sesión, CSRF tokens, y cómo mantener credenciales seguras:
Train2Go requiere PHPSESSID + XSRF-TOKEN
Garmin Connect usa GARMIN_SESSIONID + connect-csrf-token
Las sesiones expiran → necesitas renovar desde el navegador
Lección: La autenticación en APIs reales es frágil. Documentar bien qué datos necesitas y por qué.
- Conversión entre Formatos El flujo es: texto → estructura intermedia → JSON Garmin
"10' z1 + 4x(3' Z4 + 2' Z1)"
↓
{ type: "single", intensity: "warmup", durationSeconds: 600, target: { zone: 1 } }
{ type: "repeat", iterations: 4, steps: [...] }
↓
[
{ type: "ExecutableStepDTO", stepType: { stepTypeKey: "warmup" }, ... },
{ type: "RepeatGroupDTO", numberOfIterations: 4, workoutSteps: [...] }
]
Esta abstracción intermedia es crucial para mantener el código agnóstico del formato de salida.
Lección: Cada capa debe tener responsabilidad única. El builder no debe parsear, el parser no debe conocer Garmin.
- Costos vs Complejidad Cada LLM call cuesta ~$0.0001, pero evita mantener regexes complejas. En 100 workouts: ~$0.01 de costo, pero 0 deuda técnica.
Arquitectura Actual (Simplified)
src/
├── index.ts ← CLI orchestrator
├── train2go/client.ts ← Obtiene entrenamientos
├── garmin/client.ts ← Publica en Garmin
├── parser/
│ ├── workout-parser.ts ← Regex + LLM fallback
│ ├── llm-fallback.ts ← Claude wrapper
│ ├── tokenizer.ts ← Tokenización
│ └── patterns.ts ← Regex patterns
├── builder/
│ └── garmin-builder.ts ← Estructura JSON Garmin
├── types.ts
└── utils.ts ← Conversiones (pace, duración)
La Próxima Mejora (En Backlog) --> REFACTORIZAR
Aunque la arquitectura actual funciona, hay un problema de mantenibilidad, extensibilidad y escalabilidad:
❌ Mantenibilidad (Hoy)
El parser híbrido (regex + LLM) es complejo:
El regex es frágil y necesita mantenimiento
Debugging es difícil: ¿falló regex o LLM?
Test coverage: necesitas casos para ambas ramas
🎯 Mantenibilidad (Objetivo)
Usar solo LLM, simplificar radicalmente:
Eliminar regex y todos sus patterns
Delegar 100% del parsing a Claude
Costo adicional negligible (~$0.0001 más por call)
Código 50% más pequeño, más fácil de mantener
❌ Extensibilidad (Hoy)
Solo soporta running. Otras modalidades requieren:
Nuevos targets (potencia para ciclismo, brazo para natación)
Nuevas intensidades y patrones
Regexes separados para cada deporte
🎯 Extensibilidad (Objetivo)
Arquitectura agnóstica de deporte:
Parser trabaja con cualquier descripción (training peak también las usa)
Builder adapta a cualquier formato (Zwift ZWO, TCX, etc.)
Deporte como parámetro, no hardcoding
❌ Escalabilidad (Hoy)
Solo Garmin Connect. Integrar Training Peaks requeriría:
Nuevo cliente de API
Nuevo builder (Training Peaks tiene schema diferente)
Duplicar lógica
🎯 Escalabilidad (Objetivo)
Arquitectura de adaptadores:
Core Parser + Builder reutilizable
Adaptadores por plataforma (Garmin, Training Peaks, Zwift)
CLI flexible que selecciona outputs
Roadmap
v1.0 (Actual) → Hybrid parser (regex+LLM), Garmin only, Running only
↓
v2.0 (Próximo) → Pure LLM parser, multi-sport, multi-output
↓
v3.0 (Futuro) → Training Peaks integration, web UI, cloud sync
Instalación y Uso
Setup
npm install
Offline test
npx tsx src/index.ts --text "10' z1 + 4x(3' Z4 + 2' Z1) + 5' CD"
Online mode (necesita TRAIN2GO_COOKIE)
npx tsx src/index.ts --date "2026-02-12" --fallback
Con publicación (necesita GARMIN_COOKIE + GARMIN_CSRF_TOKEN)
npx tsx src/index.ts --date "2026-02-12" --fallback --publish
Reflexión Final
Este proyecto me enseñó que la mejor solución no siempre es el patrón más "puro". Un regex perfecto no existe. Pero un LLM + una arquitectura clara sí.
A veces es más smart usar tecnología para eliminar complejidad, no añadirla.

Top comments (0)