← Blog | IA & ML 5 Feb 2026 · 22 min lectura

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

0 requests a cloud
5 tipos de modelo
7 endpoints /v1/
100% datos en local

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.

// MLTrainerClient — singleton HTTP apuntando a localhost class MLTrainerClient { constructor(baseURL = 'http://localhost:8765') { this.baseURL = baseURL; } async checkHealth() { const response = await fetch(`${this.baseURL}/api/health`, { signal: AbortSignal.timeout(3000) }); return response.json(); // { status, version, uptime, models_count } } }

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: falsesecurity 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
// Cargar modelo para inferencia — primera vez descarga, luego cache import { pipeline, env } from '@xenova/transformers'; env.cacheDir = join(__dirname, 'models/cache'); env.allowLocalModels = true; // Crear pipeline — ONNX runtime en Node.js const embedder = await pipeline( 'feature-extraction', 'Xenova/all-MiniLM-L6-v2' ); // Generar embeddings — sin cloud, sin API keys const result = await embedder('texto del usuario', { pooling: 'mean', normalize: true }); // → Float32Array[384]

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.

w ~ U(-√(2/n), √(2/n))

🔧 L2 Regularization

Ridge regression — penaliza pesos grandes para evitar overfitting. Lambda = 0.001 por defecto.

w ← w - α(∇L + λw)

🛑 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.

x' = (x - μ) / σ
// regression.js — mini-batch gradient descent en JS puro for (let epoch = 1; epoch <= epochs; epoch++) { const indices = shuffleArray([...Array(n_samples).keys()]); for (let i = 0; i < n_samples; i += batchSize) { const batch = indices.slice(i, i + batchSize); // Forward pass const predictions = batch.map(idx => predict(X[idx], weights, bias)); const errors = predictions.map((p, j) => p - y[batch[j]]); // Backward pass + L2 regularization for (let k = 0; k < n_features; k++) { gradW[k] += (2 / batch.length) * errors[j] * X[batch[j]][k]; weights[k] -= lr * (gradW[k] + λ * weights[k]); } } // Early stopping check if (epochLoss < bestLoss) { bestWeights = [...weights]; } else if (++patience >= 10) break; }

Métricas de evaluación

MSE Mean Squared Error
RMSE Root MSE
MAE Mean Absolute Error
Coef. determinación
MAPE % Error

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:8765openai-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/similarity
// Respuesta de /v1/embeddings — formato OpenAI exacto { "object": "list", "data": [{ "object": "embedding", "index": 0, "embedding": [0.0123, -0.0456, ...] // 384 floats }], "model": "all-MiniLM-L6-v2", "usage": { "prompt_tokens": 12, "total_tokens": 12 } } // Uso con openai-python — solo cambiar base_url // client = OpenAI(base_url="http://localhost:8765/v1", api_key="local")

9 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.

g' = g + N(0, σ²)

Federated Averaging

aggregateGradients() promedia gradientes de múltiples clientes. Actualiza el modelo global: FedAvg.

w_new = w_old - α · mean(∇₁, ∇₂, ..., ∇ₙ)

Laplace Noise (ε-DP)

addDifferentialPrivacyNoise() implementa el mecanismo de Laplace con sensitivity / epsilon. Menor ε = más privacidad, más ruido.

noise ~ Laplace(0, Δf/ε)

Privacy Budget Tracking

calculatePrivacyBudget() trackea el gasto de privacidad acumulado via composition theorem. Umbral mínimo: 100 filas de datos.

ε_total = ε · √(2n · ln(1/δ))

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

STEP 1

Column profiling: clasifica cada columna como numérica, categórica, texto, fecha o booleana. Detecta cardinalidad y columnas binarias.

STEP 2

Target detection: identifica columnas candidatas — baja cardinalidad categórica para clasificación, numérica con varianza para regresión.

STEP 3

Model scoring: evalúa viabilidad de classifier, regression, clustering, embeddings y anomaly detection. Estima accuracy, R², silhouette score y training time.

STEP 4

Data quality score: completeness × type coverage. score = 0.6 × completeness + 0.4 × (typed/total).

// AutoML — K óptimo para clustering const optimalK = Math.min( 10, Math.max(2, Math.ceil(Math.sqrt(n_samples / 2))) ); // getAutoMLRecommendations() → sorted array [ { type: 'regression', score: 0.87, target: 'price', estimatedR2: 0.73 }, { type: 'classifier', score: 0.72, target: 'category', estimatedAcc: 0.81 }, { type: 'anomaly_detection', score: 0.45 }, ]

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.

// PythonBridge — Node.js side this.process = spawn('python', [PYTHON_SCRIPT], { stdio: ['pipe', 'pipe', 'pipe'], env: { ...process.env, PYTHONUNBUFFERED: '1' } }); // Enviar comando const request = { id: ++this.requestId, command: 'transcribe', params: { audio } }; this.process.stdin.write(JSON.stringify(request) + '\n'); // Python responde // { "type": "response", "id": 1, "data": { "text": "..." }, "error": null } // Lifecycle: // 1. spawn → wait "ready" (10s timeout) // 2. send commands → 30s per-request timeout // 3. shutdown → write { command: "shutdown" }

🐍 Dependencias Python de Audio Hub

openai-whisper Speech-to-Text
edge-tts Microsoft Neural TTS
gTTS Google TTS
pyttsx3 TTS offline fallback
numpy Numerical computing
soundfile Audio file I/O

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

5 tipos de modelo
30+ API endpoints
7 endpoints /v1/ OpenAI
4 modelos HuggingFace ONNX
384 dims embedding local
ε-DP differential privacy
0 datos al cloud
3 apps Electron
C

Cadences Engineering

Documentación técnica del equipo de ingeniería