View in English
Construyendo un Coach de Carrera con IA en Tiempo Real: La Inmersión Técnica
Feb 25, 2026
Updated: Jun 25, 2026

Construyendo un Coach de Carrera con IA en Tiempo Real: La Inmersión Técnica

Introducción

Durante el Gemini Field Test participé en la construcción de algo que hasta hace poco parecía ciencia ficción: un coach de carrera impulsado por inteligencia artificial capaz de analizar telemetría en tiempo real y dar instrucciones al piloto mientras conduce a más de 200 km/h en el circuito de Thunderhill Raceway, en Sacramento, CA.

Vikram Tiwari ya contó la narrativa del día de carrera en "The Race for Real Time", y Matt Thompson junto con Ajeet Mirwani detallaron la arquitectura split brain de alto nivel en "Beyond the Chatbot: A Blueprint for Trustable AI". Este artículo es el tercero de la serie, y es el que va directo al código.

Aquí vamos a desarmar pieza por pieza cómo se construyó este sistema. Desde el servidor SSE que transmite telemetría a 10Hz, pasando por la detección de vueltas con Haversine y el círculo de fricción para análisis de conducción, hasta la integración con Gemini Nano corriendo directamente en el navegador. Todo está organizado en un codelab de 7 pasos progresivos que cualquier desarrollador puede seguir.

Arquitectura General

El sistema sigue una arquitectura desacoplada con cuatro capas principales:

  • Servidor de telemetría: Express.js sirviendo datos CSV vía Server Sent Events a 10Hz.
  • Frontend: React 19 + TypeScript consumiendo el stream vía EventSource.
  • Visualización 3D: Three.js a través de React Three Fiber para renderizar el circuito y el auto.
  • IA multi nivel: una pipeline de coaching que va desde lógica determinista, pasando por Gemini Nano (on device vía Chrome Prompt API), hasta Gemini Flash y Pro para generación de audio.

La clave del diseño es lo que los artículos compañeros llaman la "arquitectura split brain": el código determinista maneja las decisiones críticas de seguridad (no necesitas un LLM para decir "¡FRENA!"), mientras que Gemini Nano enriquece los mensajes con contexto natural. Cada nivel tiene su rol y su latencia.

Pasos 1-2: El Servidor de Telemetría y el Parseo de Datos

Todo comienza con datos. El archivo scripts/telemetry-server.js es un servidor Express minimalista que lee un CSV de telemetría real capturada en Thunderhill y lo transmite como un stream SSE.

app.get("/events", (req, res) => {
  res.writeHead(200, {
    "Content-Type": "text/event-stream",
    "Cache-Control": "no-cache",
    Connection: "keep-alive",
  });

  let index = 0;
  const interval = setInterval(() => {
    if (index >= frames.length) {
      index = 0; // Loop the data
    }
    const frame = frames[index];
    res.write(`data: ${JSON.stringify(frame)}\n\n`);
    index++;
  }, 100); // 10Hz

  req.on("close", () => {
    clearInterval(interval);
  });
});

Los headers SSE son críticos: text/event-stream le indica al navegador que mantenga la conexión abierta, no-cache evita que un proxy interrumpa el stream, y keep-alive mantiene la conexión TCP activa. El intervalo de 100ms nos da una frecuencia de actualización de 10Hz, suficiente para telemetría en tiempo real sin saturar el navegador.

Parseo de Coordenadas DMS

Los datos GPS del CSV vienen en formato DMS (grados, minutos, segundos), no en decimales. El parser utiliza una regex para extraer los componentes y convertirlos:

function parseDMS(value: string): number {
  const match = value.match(/(\d+)°([\d.]+)\s*([NSEW])/);
  if (!match) return parseFloat(value) || 0;
  const degrees = parseInt(match[1], 10);
  const minutes = parseFloat(match[2]);
  const direction = match[3];
  let decimal = degrees + minutes / 60;
  if (direction === "S" || direction === "W") decimal = -decimal;
  return decimal;
}

El TelemetryFrame resultante tiene más de 23 campos: posición GPS, velocidad, RPM, posición de acelerador y freno, fuerzas G laterales y longitudinales, temperatura de aceite y refrigerante, presión de aceite, voltaje de batería, gradiente del terreno, y más. PapaParse se encarga del parsing del CSV en el frontend.

Paso 3: Consumo en Tiempo Real con useRealtimeTelemetry

El hook useRealtimeTelemetry es el corazón del sistema de datos en el frontend. Conecta al stream SSE usando EventSource e implementa un patrón de reconexión con backoff exponencial:

const delay = Math.min(1000 * Math.pow(2, failedAttemptsRef.current), 30000);

Esto garantiza que si el servidor se cae, el cliente reintenta con intervalos crecientes (1s, 2s, 4s, 8s...) hasta un máximo de 30 segundos, evitando bombardear un servidor que podría estar recuperándose.

El patrón más interesante es la actualización de doble frecuencia. No todos los componentes necesitan los datos a la misma velocidad:

const HISTORY_UPDATE_INTERVAL_MS = 200; // 5Hz para el mapa

// Frecuencia completa para los gauges:
setCurrentFrame(frame);

// Throttled para el historial:
if (now - lastHistoryUpdate.current > HISTORY_UPDATE_INTERVAL_MS) {
  setData(prev => [...prev, frame]);
  lastHistoryUpdate.current = now;
}

Los indicadores de velocidad, RPM y fuerzas G se actualizan a los 10Hz completos del servidor; necesitan esa inmediatez. Pero el rastro del mapa (que acumula puntos en un array creciente) se limita a 5Hz. Sin esta separación, el array de historial crecería al doble de velocidad y el re render del mapa consumiría recursos innecesarios. El hook también soporta el protocolo GPSD para conexión con hardware GPS real.

Paso 4: Detección de Vueltas y la Vuelta Ideal

La detección de vueltas requiere saber cuándo el auto cruza la línea de meta. Sin un sensor físico, usamos GPS y la fórmula de Haversine para calcular distancias entre dos puntos sobre la esfera terrestre:

function calcDistance(
  f1: {latitude: number; longitude: number},
  f2: {latitude: number; longitude: number}
) {
  const R = 6371e3;
  const dLat = ((f2.latitude - f1.latitude) * Math.PI) / 180;
  const dLon = ((f2.longitude - f1.longitude) * Math.PI) / 180;
  const a = Math.sin(dLat/2) * Math.sin(dLat/2) +
    Math.cos((f1.latitude * Math.PI)/180) *
    Math.cos((f2.latitude * Math.PI)/180) *
    Math.sin(dLon/2) * Math.sin(dLon/2);
  return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
}

Un cruce de meta se detecta cuando el auto está a menos de 20 metros del punto de inicio y han pasado al menos 30 segundos desde el último cruce (para evitar falsos positivos al inicio de la vuelta). La búsqueda heurística del punto de inicio evalúa posiciones candidatas cada 5 segundos durante los primeros 3 minutos de datos.

La Vuelta Ideal

La función resampleLap() toma una vuelta cruda (puntos irregulares en tiempo) y la interpola linealmente a intervalos regulares de 5 metros, creando una representación uniforme de la trayectoria. Después, calculateIdealLap() implementa un algoritmo de micro sectores: divide el circuito en sectores de 50 metros y, para cada sector, selecciona el fragmento más rápido de entre todas las vueltas completadas. El resultado es una vuelta "frankenstein" que representa lo mejor que el piloto logró en cada sección del circuito, no necesariamente de la misma vuelta.

Paso 5: El Motor de Análisis de Conducción (useDrivingAnalysis)

El hook useDrivingAnalysis analiza una ventana deslizante de 60 frames (6 segundos a 10Hz) para generar insights sobre el estilo de conducción.

Detección de Fases

Cada punto se clasifica en una fase del circuito basándose en las fuerzas G laterales y el delta de velocidad:

  • Entry (entrada a curva): |latG| > 0.3 con velocidad decreciente.
  • Apex (punto de máximo giro): |latG| > 0.3 con velocidad estable.
  • Exit (salida de curva): |latG| > 0.3 con velocidad creciente.
  • Straight (recta): |latG| <= 0.3.

El Círculo de Fricción

Este es posiblemente el cálculo más revelador de todo el sistema. El círculo de fricción combina las fuerzas G laterales y longitudinales para determinar cuánto del agarre disponible del neumático está siendo utilizado:

const comboG = Math.sqrt(latG * latG + longG * longG);
const MAX_TIRE_G = 1.3;
const tireUsagePct = (comboG / MAX_TIRE_G) * 100;

if (tireUsagePct < 60) status = "COLD";
else if (tireUsagePct < 85) status = "UNDER_DRIVING";
else if (tireUsagePct < 105) status = "AT_LIMIT";
else status = "OVER_DRIVING";

El valor MAX_TIRE_G = 1.3 representa el límite teórico de agarre de los neumáticos. Un piloto experto opera consistentemente entre 85% y 105%. Por debajo de 60%, los neumáticos no están generando suficiente temperatura. Por encima de 105%, el auto está deslizando, lo cual es más lento y peligroso. El motor también detecta flags de seguridad como COASTING_DETECTED (acelerador suelto sin frenar) y PANIC_BRAKE_IN_TURN (frenada brusca en curva).

Paso 6: Visualización 3D con Three.js

La visualización 3D en TrackMap3D.tsx transforma datos GPS en una escena tridimensional interactiva usando React Three Fiber.

Proyección de Coordenadas

Las coordenadas GPS se proyectan a metros planos usando la aproximación local:

  • latScale = 111000 (metros por grado de latitud, constante).
  • lonScale = 111000 * cos(centerLat) (metros por grado de longitud, que varía con la latitud).

Los puntos se almacenan en Float32Array para enviarlos directamente a la GPU sin conversiones intermedias. El modelo del auto incluye flechas auxiliares (arrow helpers) que visualizan el vector de fuerzas G en tiempo real.

Cámaras y Dinámica del Auto

El sistema ofrece modos de cámara chase (persecución) y bumper (primera persona), ambos suavizados con interpolación lineal (lerp). La orientación del auto se calcula así:

  • Yaw (giro horizontal): atan2 entre puntos consecutivos.
  • Pitch (cabeceo): pendiente del terreno + gForceLong * 0.08.
  • Roll (balanceo): gForceLat * 0.15.

Los factores 0.08 y 0.15 son valores empíricos que producen una animación visualmente convincente sin exagerar los movimientos.

Paso 7: Coaching con IA usando Gemini Nano

Aquí es donde todo cobra vida. Gemini Nano corre directamente en el navegador a través de la Chrome Prompt API (window.LanguageModel), eliminando la latencia de red para las respuestas de coaching.

Nota: verifica el estado de la API antes de publicar o copiar este código. La Chrome Prompt API es una superficie experimental (origin trial) y ha cambiado entre versiones de Chrome: el global window.LanguageModel, la forma de las opciones de create() y outputLanguage han variado. El snippet a continuación refleja la API tal como estaba a principios de 2026; fija la versión de Chrome que uses y revisa la documentación oficial, porque puede romperse en versiones posteriores.

const session = await window.LanguageModel!.create({
  outputLanguage: 'en',
  initialPrompts: [{
    role: "system",
    content: `You are a Race Spotter.
Check the "flags" in the input JSON. Priority is Top to Bottom.
PRIORITY 1: SAFETY
- If safety_status is "UNSTABLE" -> "Smooth it out! Reset."
PRIORITY 2: CRITICAL ERRORS
- If error_type is "LATE_BRAKE_T9" -> "BRAKE! Crest approaching!"
PRIORITY 3: PACE
- If opportunity is "UNDER_DRIVING_T5" -> "Trust the compression. Full throttle."`
  }]
});

La Matriz de Prioridades

La lógica de coaching sigue una jerarquía estricta: SEGURIDAD > ERRORES CRÍTICOS > RITMO. Esto es fundamental. No importa si el piloto podría ir más rápido en la curva 5 si en este momento está perdiendo control en la curva 9. El payload que se envía a Nano tiene esta estructura:

{
  "context": {
    "location": "T5_ENTRY",
    "speed": 145,
    "tire_status": "AT_LIMIT",
    "grip_pct": 92
  },
  "flags": {
    "safety_status": "STABLE",
    "error_type": null,
    "opportunity": "UNDER_DRIVING_T5"
  },
  "delta": -0.3
}

Triggers Deterministas

El sistema no consulta a Nano en cada frame. Utiliza triggers deterministas con un cooldown de 8 segundos entre mensajes:

  • Desviación de ritmo: velocidad >10 km/h por debajo de la vuelta ideal durante más de 3 segundos.
  • Cambio de elevación: gradiente del terreno >3% (relevante en Thunderhill, que tiene colinas significativas).
  • Nueva mejor vuelta: celebración inmediata.
  • Cambio de ubicación: nueva sección del circuito.

Solo cuando un trigger se activa, el código empaqueta el contexto y consulta a Nano para enriquecer el mensaje con lenguaje natural.

Coaching Predictivo

El módulo usePredictiveCoaching implementa una funcionalidad que lleva el coaching más allá de lo reactivo: predice errores antes de que ocurran.

El sistema analiza vueltas anteriores para detectar "zonas de error", lugares donde el piloto perdió más de 15 km/h respecto a la vuelta ideal. Luego, durante la vuelta actual, calcula un punto de anticipación:

const speed = Math.max(currentFrame.speed, 50);
const lookaheadSeconds = 8;
const lookaheadMeters = (speed / 3.6) * lookaheadSeconds;
const targetDist = currentDist + lookaheadMeters;
const upcomingMistake = mistakeZones.find(z =>
  z.startDist >= targetDist - 30 && z.startDist <= targetDist + 30
);

A 150 km/h, el lookahead es de aproximadamente 333 metros: 8 segundos antes de llegar a una zona problemática, el piloto recibe una advertencia. La velocidad mínima se fija en 50 km/h para evitar lookaheads demasiado cortos en zonas lentas. Las zonas de error se mapean a puntos del circuito con una tolerancia de 150 metros.

Sistema de Text to Speech

El coaching solo sirve si el piloto puede escucharlo. El sistema de TTS en useTTS.ts soporta 4 proveedores:

  • Browser SpeechSynthesis: API nativa, sin latencia de red.
  • Google Cloud TTS: alta calidad, requiere API key.
  • Gemini Flash (Live WebSocket): streaming de audio vía WebSocket.
  • Gemini Pro (generateContentStream): generación de audio por streaming. Conviene aclarar que aquí no se usa un modelo dedicado de TTS, sino generación de audio como salida del modelo.

Para los proveedores que devuelven audio PCM crudo (sin headers), el sistema construye un header WAV de 44 bytes:

function createWavHeader(dataLength: number, options: WavConversionOptions) {
  const { numChannels, sampleRate, bitsPerSample } = options;
  const buffer = new ArrayBuffer(44);
  const view = new DataView(buffer);
  writeString(0, "RIFF");
  view.setUint32(4, 36 + dataLength, true);
  writeString(8, "WAVE");
  writeString(12, "fmt ");
  // ... PCM format, sample rate, bit depth
  writeString(36, "data");
  view.setUint32(40, dataLength, true);
  return new Uint8Array(buffer);
}

Modulación por Urgencia

No todos los mensajes tienen la misma prioridad, y la voz lo refleja:

const urgencySettings = {
  URGENT: { rate: 1.5, pitch: 1.1, volume: 1.0 },
  CALM:   { rate: 0.9, pitch: 0.9, volume: 0.8 },
  STANDARD: { rate: 1.1, pitch: 1.0, volume: 0.9 }
};

Un "¡FRENA!" se reproduce rápido y fuerte. Un "¡buen ritmo, mantené así!" llega más relajado. Un isFetchingRef actúa como lock de serialización para evitar que múltiples mensajes se superpongan.

Estructura del Codelab

El proyecto está organizado en 7 ramas incrementales (step-01 a step-07), cada una construyendo sobre la anterior: https://github.com/seagomezar/real-time-coach-codelab.

Nota: verifica que el repositorio público y las ramas `step-01` a `step-07` existan y sean accesibles antes de seguir el codelab, ya que todas las instrucciones dependen de poder clonarlo.

  • Paso 1, `step-01`: proyecto base, configuración de React + Vite.
  • Paso 2, `step-02`: servidor SSE de telemetría y parsing de datos.
  • Paso 3, `step-03`: hook useRealtimeTelemetry con backoff y doble frecuencia.
  • Paso 4, `step-04`: detección de vueltas, Haversine, vuelta ideal.
  • Paso 5, `step-05`: motor de análisis: círculo de fricción, fases, flags.
  • Paso 6, `step-06`: visualización 3D con Three.js / React Three Fiber.
  • Paso 7, `step-07`: coaching IA con Gemini Nano, predicción y TTS.

Cada paso tiene un conjunto autocontenido de cambios. Un desarrollador puede hacer git checkout step-03, ejecutar npm install && npm run dev, y tener un sistema funcional con telemetría en tiempo real y mapa 2D, sin necesidad de IA ni 3D todavía.

Conclusión

Construir un coach de carrera con IA en tiempo real nos dejó varias lecciones claras:

  • Lo determinista primero, la IA después: las decisiones críticas de seguridad nunca dependen de un modelo de lenguaje. La IA enriquece, no decide.
  • La frecuencia importa: no todo necesita actualizarse a 10Hz. El patrón de doble frecuencia es aplicable a cualquier sistema que combine datos en tiempo real con visualizaciones costosas.
  • On device AI cambia las reglas: Gemini Nano corriendo en el navegador elimina la latencia de red para las respuestas de coaching. En un auto a 200 km/h, cada milisegundo cuenta.
  • Los micro sectores son poderosos: la idea de construir una vuelta ideal uniendo los mejores fragmentos de cada vuelta es simple y efectiva.

Para la narrativa completa del día de carrera, lean el artículo de Vikram Tiwari "The Race for Real Time". Para entender la filosofía de la arquitectura split brain y por qué separar lógica determinista de IA generativa, el artículo de Matt Thompson y Ajeet Mirwani "Beyond the Chatbot: A Blueprint for Trustable AI" es lectura obligada.

Este codelab está disponible como repositorio público con los 7 pasos progresivos. Clonen el repo, hagan checkout de step-01, y construyan su propio coach de carrera.

Sebastian Gomez

Sebastian Gomez

Creador de contenido principalmente acerca de tecnología.

Leave a Reply

0 Comments

Advertisements

Related Posts

Categorias