ML sin Cloud: Del Navegador al Escritorio con TensorFlow.js, ONNX y una API OpenAI Local
Un pipeline de machine learning completo que no toca ningún servidor externo: TensorFlow.js entrena en el navegador, Transformers.js ejecuta modelos ONNX en Electron, la regresión se entrena con gradient descent puro en JavaScript, y todo se expone como una API compatible con OpenAI en localhost:8765.
cadences.app · ML Trainer Hub · Audio Hub
1 La premisa: ML que no sale de tu máquina
La mayoría de plataformas SaaS envían tus datos a una API cloud para cualquier tarea de ML — embeddings, clasificación, predicción. El problema es triple: latencia (cada request cruza internet), coste (pagas por token/embedding) y privacidad (tus datos salen de tu máquina).
En Cadences construimos la alternativa: un pipeline ML completo que opera en dos capas — navegador y escritorio — sin tocar ningún servidor. El navegador entrena y ejecuta modelos con TensorFlow.js. Una app Electron actúa como servidor ML local con Transformers.js. Los modelos se sincronizan entre ambas capas. Y todo se expone con una API OpenAI-compatible.
🧠 Dos capas, cero cloud
2 Arquitectura: browser + desktop, sync bidireccional
El sistema tiene dos capas que operan independientemente y se sincronizan:
Capa Browser — cadences.app (React)
TensorFlow.js entrena modelos con los datos del proyecto. Los modelos se persisten en IndexedDB via model.save('indexeddb://...'). Federated Learning con differential privacy. AutoML analiza datos y recomienda modelos.
Capa Desktop — ML Trainer Hub (Electron)
Electron 33 con servidor Fastify 5 en port 8765. Base de datos SQLite via better-sqlite3 (WAL mode). Inferencia con Transformers.js. Training real de regresión. API /v1/ OpenAI-compatible.
Sync Layer — Push & Pull
pushToMLTrainer(): exporta modelo del browser → crea job en el servidor local. pullFromMLTrainer(): descarga modelo del server → importa a IndexedDB del browser. Health check cada 30s con exponential backoff.
3 TensorFlow.js in-browser: entrenamiento sin backend
La capa browser carga TensorFlow.js desde CDN y ejecuta training directamente en la pestaña del usuario. Los datos nunca salen del navegador — se cargan desde IndexedDB (donde el hook de persistencia los almacena localmente), se procesan en memoria, y el modelo resultante se guarda de vuelta en IndexedDB con model.save().
trainingService.js
Servicio de training que construye modelos tf.sequential() con capas dense. Compilación con adam optimizer. Callbacks de progreso por época. Métricas: loss, accuracy, MSE, MAE.
indexedDBService.js
Persistencia de modelos TF.js en IndexedDB. Save/load/delete/list. Cada modelo almacena topología (layers), pesos (Float32Array) y metadata (métricas, fecha, tipo).
MLProvider (Context)
514 líneas. React Context que orquesta todo el ML del browser: estado de training, modelos cargados, conexión con ML Trainer local, federated learning state, AutoML recommendations.
Frontend components
MLTrainerStatus (conexión, auto-check 30s), MLTrainerModelsPanel (listar, buscar, filtrar, pull/push), MLTrainerTutorial (onboarding 4 pasos). Hooks: useMLTrainerConnection, useMLTrainerModels.
4 Electron como servidor ML local
ML Trainer Hub es una app Electron 33 que arranca un servidor Fastify 5 en el sistema del usuario. Electron es solo el shell — el motor real es un servidor HTTP que funciona idéntico si lo arrancas desde terminal con node src/server.js. Electron añade: system tray, UI nativa, config persistida con electron-store, y auto-start.
| Capa Electron | Archivo | Responsabilidad |
|---|---|---|
| Main process | main.js (459 líneas) | BrowserWindow, tray, IPC handlers, lifecycle |
| Preload | preload.js | contextBridge — 12 invoke + 4 on channels con whitelisting |
| HTTP server | server.js | Fastify + CORS + static UI + routes |
| Database | db/index.js | SQLite via better-sqlite3 · WAL mode |
| Training engine | training/ (3 archivos) | manager + data-loader + regression |
| Inference | inference/server.js (708 líneas) | Transformers.js pipelines + regresión custom |
| API routes | api/routes.js (753 líneas) | 30+ endpoints REST + /v1/ OpenAI-compatible |
IPC con whitelisting: El preload expone window.electron.invoke(channel, data) y window.electron.on(channel, callback) con lista blanca de channels. contextIsolation: true, nodeIntegration: false — security best practices sin excepciones.
5 Transformers.js: modelos HuggingFace en Node.js via ONNX
@xenova/transformers es el port JavaScript de la librería transformers de Hugging Face. Ejecuta modelos convertidos a ONNX directamente en Node.js sin Python ni GPU. La primera carga descarga el modelo al cache local — las siguientes cargas son instantáneas.
| Tipo | Pipeline | Modelo | Uso |
|---|---|---|---|
| Embeddings | feature-extraction | Xenova/all-MiniLM-L6-v2 | Vectores 384-dim para similarity search |
| Clasificación | text-classification | Xenova/distilbert-sst-2 | Sentiment analysis, categorización |
| NER | token-classification | Xenova/bert-base-NER | Extracción de entidades (PER, ORG, LOC) |
| Text generation | text-generation | Xenova/gpt2 | Completions y chat (modelo ligero) |
| Regresión | — | JS puro (ver sección 6) | Predicción numérica desde datos tabulares |
6 Regresión lineal: gradient descent puro en JavaScript
La regresión es el único tipo de modelo con entrenamiento real — no es un wrapper sobre un modelo pre-entrenado. Implementamos mini-batch gradient descent desde cero en JavaScript, con todas las técnicas que harías en scikit-learn o PyTorch, menos la GPU.
📐 Xavier Initialization
Pesos iniciales escalados por la raíz cuadrada de la dimensionalidad de entrada. Evita el problema de vanishing gradients.
🔧 L2 Regularization
Ridge regression — penaliza pesos grandes para evitar overfitting. Lambda = 0.001 por defecto.
🛑 Early Stopping
Patience de 10 épocas. Si la loss no mejora en 10 épocas consecutivas, restaura los mejores pesos y para.
🔀 5-Fold Cross-Validation
Divide datos en 5 folds, entrena en 4 y valida en 1. Reporta media ± desviación. Implementado manualmente con shuffled index arrays.
📊 Z-Score Normalization
Todas las features se normalizan a media 0 y desviación 1 antes del training. Los stats se guardan con el modelo para inferencia.
Métricas de evaluación
7 Data pipeline: de un proyecto Cadences a un dataset de training
El data loader (data-loader.js) carga datos directamente desde la API de Cadences y los transforma en matrices numéricas listas para training:
1. Fetch desde Cadences API
Descarga filas del proyecto con GET /api/data-rows?projectId=.... Soporta múltiples proyectos como fuente. Paginación automática.
2. Análisis automático de tipos
Detecta columnas numéricas, texto, fecha, booleanas, selects. Calcula cardinalidad. Identifica candidatas a target y a features.
3. Feature engineering
Z-score normalization. One-hot encoding para categóricos. Manejo de valores faltantes (media, mediana, cero o drop). Expansión polinomial con interacciones para regresión no lineal. Custom JS transforms: new Function('row', 'index', script).
4. Split train/test
80/20 por defecto. Shuffle previo para evitar selection bias. 5-fold cross-validation en training set.
8 API OpenAI-compatible en localhost
El servidor expone endpoints /v1/ que siguen el formato exacto de la API de OpenAI. Cualquier cliente configurado para OpenAI funciona apuntando a http://localhost:8765 — openai-python, openai-node, LangChain, LlamaIndex.
| Endpoint | OpenAI equivalente | Modelo local |
|---|---|---|
POST /v1/embeddings | Create Embeddings | all-MiniLM-L6-v2 (384-dim) |
POST /v1/chat/completions | Chat Completions | GPT-2 (local ONNX) |
POST /v1/completions | Completions | GPT-2 (local ONNX) |
Endpoints extra (no-OpenAI)
/v1/classify/v1/entities/v1/predict/v1/similarity9 Federated Learning con Differential Privacy
El concepto de Federated Learning (introducido por McMahan et al., 2017) permite entrenar modelos colaborativamente sin compartir datos crudos. Cada cliente entrena localmente y comparte solo gradientes — nunca los datos originales. Implementamos el framework completo en el browser:
Gradient Extraction con Noise
extractPrivateGradients() extrae pesos del modelo TensorFlow.js y añade ruido gaussiano antes de enviarlos.
Federated Averaging
aggregateGradients() promedia gradientes de múltiples clientes. Actualiza el modelo global: FedAvg.
Laplace Noise (ε-DP)
addDifferentialPrivacyNoise() implementa el mecanismo de Laplace con sensitivity / epsilon. Menor ε = más privacidad, más ruido.
Privacy Budget Tracking
calculatePrivacyBudget() trackea el gasto de privacidad acumulado via composition theorem. Umbral mínimo: 100 filas de datos.
Estado actual: El framework cliente está completo — gradient extraction, differential privacy, privacy budget, verify dataset size. El hook useFederatedLearning intenta POST /api/federated/join, /contribute, /sync — pero los endpoints del servidor aún no existen. En modo fallback, opera como local-only. El diseño es deliberado: primero el framework privacy-safe, luego la coordinación multi-tenant.
10 AutoML: recomendación automática de modelo
autoMLUtils.js analiza los datos de un proyecto de Cadences y recomienda automáticamente qué tipo de modelo entrenar — sin que el usuario necesite saber ML. Inspirado en Google AutoML y AutoGluon, pero corriendo 100% en el browser.
Pipeline de análisis
Column profiling: clasifica cada columna como numérica, categórica, texto, fecha o booleana. Detecta cardinalidad y columnas binarias.
Target detection: identifica columnas candidatas — baja cardinalidad categórica para clasificación, numérica con varianza para regresión.
Model scoring: evalúa viabilidad de classifier, regression, clustering, embeddings y anomaly detection. Estima accuracy, R², silhouette score y training time.
Data quality score: completeness × type coverage. score = 0.6 × completeness + 0.4 × (typed/total).
11 El patrón PythonBridge: child_process.spawn + JSON over stdio
Aunque ML Trainer Hub es 100% Node.js, tenemos un patrón probado de interop Node↔Python en Audio Hub — otra app Electron que usa OpenAI Whisper para STT y edge-tts / gTTS para síntesis de voz. El patrón es reutilizable para cualquier caso donde necesites Python desde Electron.
Node.js → Python
child_process.spawn() arranca python main.py con PYTHONUNBUFFERED=1. Comandos se envían como JSON Lines a stdin. Cada request lleva un ID para correlacionar respuestas.
Python → Node.js
Python lee stdin en bucle, parsea JSON, ejecuta el comando, escribe la respuesta como JSON a stdout. Soporta 4 tipos de mensaje: ready, response, event, log.
12 El stack completo
| Capa | Tecnología | Ubicación |
|---|---|---|
| Desktop shell | Electron 33 | ml-trainer-local/src/main.js |
| HTTP API | Fastify 5 | server.js + api/routes.js |
| Database | SQLite (better-sqlite3, WAL) | db/index.js |
| ML inference | Transformers.js (ONNX) | inference/server.js |
| Regression training | JavaScript puro (SGD) | training/regression.js |
| Browser ML | TensorFlow.js (CDN) | src/contexts/ml/ |
| Browser storage | IndexedDB | src/contexts/ml/services/ |
| Federated Learning | Differential Privacy + FL | src/contexts/ml/utils/ |
| Audio Python | Whisper + edge-tts | audio-hub/python/ |
| Python bridge | JSON Lines over stdio | audio-hub/src/python/bridge.js |
| React frontend | React hooks + Lucide | src/features/ml-trainer/ |
13 Lo que aprendimos
1. ONNX hace que "ML en Node" sea viable hoy
Hace 3 años, correr un modelo de NLP en Node significaba TensorFlow C++ bindings o una pesadilla de compilación. Transformers.js con ONNX Runtime resuelve esto completamente. Los modelos de Hugging Face funcionan directamente. El coste: primera carga tarda ~10s por la descarga del modelo. Las siguientes son instantáneas desde cache.
2. SGD en JavaScript funciona mejor de lo esperado
Para regresión tabular con menos de 10K filas, gradient descent puro en JS entrena en milisegundos. No necesitas PyTorch ni NumPy. Con Xavier init + L2 reg + early stopping, los resultados son comparables a scikit-learn LinearRegression.
3. La API OpenAI-compatible es un multiplicador de adopción
Al seguir el formato exacto de respuesta de OpenAI, cualquier herramienta del ecosistema (LangChain, LlamaIndex, openai-python) funciona apuntando a localhost. Zero integration cost.
4. child_process.spawn + JSON Lines = el mejor bridge JS↔Python
Ni gRPC, ni sockets, ni HTTP interno. Un spawn con JSON Lines sobre stdin/stdout es el protocolo más simple y fiable. Request IDs para correlación, timeout simple por request, PYTHONUNBUFFERED=1 para evitar deadlocks. Funciona en Windows, macOS y Linux sin cambios.
5. Differential privacy primero, coordinación después
Implementar el framework de federated learning con garantías de privacidad antes de tener el servidor de coordinación fue la decisión correcta. El código del cliente es la parte difícil — Laplace noise, composition theorem, privacy budget tracking. El servidor de agregación es trivial en comparación.
📋 ML sin Cloud en cifras
Cadences Engineering
Documentación técnica del equipo de ingeniería