Sebastian Gomez
Construyendo un Coach de Carrera con IA en Tiempo Rea
Construyendo un Coach de Carrera con IA en Tiempo Real: La Inmersion TecnicaDurante el Gemini Field Test, participé en la construcción algo que hasta hace poco parecia ciencia ficcion: un coach de carrera impulsado por inteligencia artificial capaz de analizar telemetria en tiempo real y dar instrucciones al piloto mientras conduce a mas de 200 km/h en el circuito de Thunderhill Raceway en Sacramento CA.
Vikram Tiwari ya conto la narrativa del dia 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 articulo es el tercero de la serie, y es el que va directo al codigo.
Aqui vamos a desarmar pieza por pieza como se construyo este sistema. Desde el servidor SSE que transmite telemetria a 10Hz, pasando por la deteccion de vueltas con Haversine y el circulo de friccion para analisis de conduccion, hasta la integracion con Gemini Nano corriendo directamente en el navegador. Todo esta organizado en un codelab de 7 pasos progresivos que cualquier desarrollador puede seguir.
Arquitectura GeneralEl sistema sigue una arquitectura desacoplada con cuatro capas principales:
- Servidor de telemetria: Express.js sirviendo datos CSV via Server-Sent Events a 10Hz
- Frontend: React 19 + TypeScript consumiendo el stream via
EventSource - Visualizacion 3D: Three.js a traves de React Three Fiber para renderizar el circuito y el auto
- IA multi-nivel: Una pipeline de coaching que va desde logica determinista, pasando por Gemini Nano (on-device via Chrome Prompt API), hasta Gemini Flash y Pro para TTS
La clave del diseno es lo que los articulos companeros llaman la "arquitectura split-brain": el codigo determinista maneja las decisiones criticas 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 Telemetria y el Parseo de DatosTodo comienza con datos. El archivo scripts/telemetry-server.js es un servidor Express minimalista que lee un CSV de telemetria 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 criticos: text/event-stream le indica al navegador que mantenga la conexion abierta, no-cache evita que un proxy interrumpa el stream, y keep-alive mantiene la conexion TCP activa. El intervalo de 100ms nos da una frecuencia de actualizacion de 10Hz, suficiente para telemetria 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 mas de 23 campos: posicion GPS, velocidad, RPM, posicion de acelerador y freno, fuerzas G laterales y longitudinales, temperatura de aceite y refrigerante, presion de aceite, voltaje de bateria, gradiente del terreno, y mas. PapaParse se encarga del parsing del CSV en el frontend.
El hook useRealtimeTelemetry es el corazon del sistema de datos en el frontend. Conecta al stream SSE usando EventSource e implementa un patron de reconexion 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 maximo de 30 segundos, evitando bombardear un servidor que podria estar recuperandose.
El patron mas interesante es la actualizacion 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 separacion, el array de historial creceria al doble de velocidad y el re-render del mapa consumiria recursos innecesarios. El hook tambien soporta el protocolo GPSD para conexion con hardware GPS real.
Paso 4: Deteccion de Vueltas y la Vuelta IdealLa deteccion de vueltas requiere saber cuando el auto cruza la linea de meta. Sin un sensor fisico, usamos GPS y la formula 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 esta a menos de 20 metros del punto de inicio y han pasado al menos 30 segundos desde el ultimo cruce (para evitar falsos positivos al inicio de la vuelta). La busqueda heuristica del punto de inicio evalua posiciones candidatas cada 5 segundos durante los primeros 3 minutos de datos.
La Vuelta Ideal
La funcion resampleLap() toma una vuelta cruda (puntos irregulares en tiempo) y la interpola linealmente a intervalos regulares de 5 metros, creando una representacion uniforme de la trayectoria. Despues, calculateIdealLap() implementa un algoritmo de micro-sectores: divide el circuito en sectores de 50 metros y, para cada sector, selecciona el fragmento mas rapido de entre todas las vueltas completadas. El resultado es una vuelta "frankenstein" que representa lo mejor que el piloto logro en cada seccion del circuito, no necesariamente de la misma vuelta.
El hook useDrivingAnalysis analiza una ventana deslizante de 60 frames (6 segundos a 10Hz) para generar insights sobre el estilo de conduccion.
Deteccion de Fases
Cada punto se clasifica en una fase del circuito basandose en las fuerzas G laterales y el delta de velocidad:
- Entry (entrada a curva):
|latG| > 0.3con velocidad decreciente - Apex (punto de maximo giro):
|latG| > 0.3con velocidad estable - Exit (salida de curva):
|latG| > 0.3con velocidad creciente - Straight (recta):
|latG| <= 0.3
El Circulo de Friccion
Este es posiblemente el calculo mas revelador de todo el sistema. El circulo de friccion combina las fuerzas G laterales y longitudinales para determinar cuanto del agarre disponible del neumatico esta 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 limite teorico de agarre de los neumaticos. Un piloto experto opera consistentemente entre 85% y 105%. Por debajo de 60%, los neumaticos no estan generando suficiente temperatura. Por encima de 105%, el auto esta deslizando, lo cual es mas lento y peligroso. El motor tambien detecta flags de seguridad como COASTING_DETECTED (acelerador suelto sin frenar) y PANIC_BRAKE_IN_TURN (frenada brusca en curva).
La visualizacion 3D en TrackMap3D.tsx transforma datos GPS en una escena tridimensional interactiva usando React Three Fiber.
Proyeccion de Coordenadas
Las coordenadas GPS se proyectan a metros planos usando la aproximacion local:
latScale = 111000(metros por grado de latitud, constante)lonScale = 111000 * cos(centerLat)(metros por grado de longitud, que varia 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.
Camaras y Dinamica del Auto
El sistema ofrece modos de camara chase (persecucion) y bumper (primera persona), ambos suavizados con interpolacion lineal (lerp). La orientacion del auto se calcula asi:
- Yaw (giro horizontal):
atan2entre puntos consecutivos - Pitch (cabeceo): pendiente del terreno +
gForceLong * 0.08 - Roll (balanceo):
gForceLat * 0.15
Los factores 0.08 y 0.15 son valores empiricos que producen una animacion visualmente convincente sin exagerar los movimientos.
Paso 7: Coaching con IA usando Gemini NanoAqui es donde todo cobra vida. Gemini Nano corre directamente en el navegador a traves de la Chrome Prompt API (window.LanguageModel), eliminando la latencia de red para las respuestas de coaching.
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 logica de coaching sigue una jerarquia estricta: SEGURIDAD > ERRORES CRITICOS > RITMO. Esto es fundamental. No importa si el piloto podria ir mas rapido en la curva 5 si en este momento esta perdiendo control en la curva 9. El payload que se envia 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:
- Desviacion de ritmo: velocidad >10 km/h por debajo de la vuelta ideal durante mas de 3 segundos
- Cambio de elevacion: gradiente del terreno >3% (relevante en Thunderhill, que tiene colinas significativas)
- Nueva mejor vuelta: celebracion inmediata
- Cambio de ubicacion: nueva seccion del circuito
Solo cuando un trigger se activa, el codigo empaqueta el contexto y consulta a Nano para enriquecer el mensaje con lenguaje natural.
Coaching PredictivoEl modulo usePredictiveCoaching implementa una funcionalidad que lleva el coaching mas alla de lo reactivo: predice errores antes de que ocurran.
El sistema analiza vueltas anteriores para detectar "zonas de error", lugares donde el piloto perdio mas de 15 km/h respecto a la vuelta ideal. Luego, durante la vuelta actual, calcula un punto de anticipacion:
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 problematica, el piloto recibe una advertencia. La velocidad minima 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-SpeechEl 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 via WebSocket
- Gemini Pro (generateContentStream): generacion por streaming
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 depthwriteString(36, "data");
view.setUint32(40, dataLength, true);
return new Uint8Array(buffer);
}
Modulacion 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 rapido y fuerte. Un "buen ritmo, mantene asi" llega mas relajado. Un isFetchingRef actua como lock de serializacion para evitar que multiples mensajes se superpongan.
El proyecto esta organizado en 7 ramas incrementales (step-01 a step-07), cada una construyendo sobre la anterior: https://github.com/seagomezar/real-time-coach-codelab
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 telemetria en tiempo real y mapa 2D, sin necesidad de IA ni 3D todavia.
https://github.com/seagomezar/real-time-coach-codelab
ConclusionConstruir un coach de carrera con IA en tiempo real nos dejo varias lecciones claras:
- Lo determinista primero, la IA despues: Las decisiones criticas de seguridad nunca dependen de un modelo de lenguaje. La IA enriquece, no decide.
- La frecuencia importa: No todo necesita actualizarse a 10Hz. El patron 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 stitcheando los mejores fragmentos de cada vuelta es simple y efectiva.
Para la narrativa completa del dia de carrera, lean el articulo de Vikram Tiwari "The Race for Real-Time". Para entender la filosofia de la arquitectura split-brain y por que separar logica determinista de IA generativa, el articulo de Matt Thompson y Ajeet Mirwani "Beyond the Chatbot: A Blueprint for Trustable AI" es lectura obligada.
Este codelab esta disponible como repositorio publico con los 7 pasos progresivos. Clonen el repo, hagan checkout de step-01, y construyan su propio coach de carrera.
Sebastian Gomez
Creador de contenido principalmente acerca de tecnología.