Ejecutar WinML desde JavaScript (vinculaciones de JavaScript)

En esta guía se muestra cómo ejecutar la inferencia de un modelo ONNX desde Electron mediante enlaces de JS para las API de ML de SDK de Aplicaciones para Windows (detección de proveedores de ejecución a través de ExecutionProviderCatalog y descarga del modelo mediante ModelCatalog), combinados con onnxruntime-node para la inferencia, sin necesidad de ningún complemento en C#. La inferencia se ejecuta en un proceso utilitario de Electron para que no bloquee el proceso principal.

Prerequisites

Antes de iniciar esta guía, asegúrese de que tiene:

Instale ONNX Runtime para Node:

npm install onnxruntime-node@1.24.3

Important

El Aplicación de Windows Runtime carga previamente su propio onnxruntime.dll en procesos secundarios. La versión onnxruntime-node debe coincidir con la ORT ABI incluida en su versión de SDK de Aplicaciones para Windows. Para SDK de Aplicaciones para Windows 2.x, utilice onnxruntime-node@1.24.x.

Paso 1: Confirmar vinculaciones de WinML

El SDK de Aplicaciones para Windows depende transitivamente de Microsoft.WindowsAppSDK.ML, por lo que las API de WinML ya están en los enlaces generados. Compruebe lo siguiente:

Requiere @microsoft/dynwinrt-codegen0.1.0-preview.8 — consulta Primeros pasos con Electron para soluciones alternativas para proyectos antiguos.

node -e "console.log(Object.keys(require('#winapp/bindings')).filter(k => k.startsWith('ExecutionProvider')))"

Deberías ver [ 'ExecutionProvider', 'ExecutionProviderCatalog', 'ExecutionProviderReadyState' ].

Paso 2: Descargar el modelo a través del catálogo de modelos (proceso principal)

Usa ModelCatalog desde los bindings de JS para descargar el modelo y almacenarlo en caché localmente. El catálogo lee un manifiesto JSON (hospedado de forma remota o local) que describe los modelos disponibles y sus direcciones URL de descarga. Después de la primera descarga, las ejecuciones posteriores usan la copia almacenada en caché:

Creación de src/winml-model.js:

const { ModelCatalog, ModelCatalogSource, Uri } = require('#winapp/bindings');
const fs = require('node:fs');
const path = require('node:path');

// Remote catalog JSON hosted in the WindowsAppSDK-Samples repo
const MODEL_CATALOG_URL =
  'https://raw.githubusercontent.com/microsoft/WindowsAppSDK-Samples/main/Samples/WindowsML/Resources/SqueezeNetModelCatalog.json';

async function downloadModel(modelId, onProgress) {
  const uri = Uri.createUri(MODEL_CATALOG_URL);
  const source = await ModelCatalogSource.createFromUriAsync(uri);
  const catalog = ModelCatalog.createInstance([source]);
  const model = await catalog.findModelAsync(modelId);

  const op = model.getInstanceAsync();
  if (onProgress) {
    op.progress((value) => {
      try { onProgress(value); } catch {}
    });
  }
  const result = await op;

  const instance = result.getInstance();
  if (!instance) return undefined;

  const paths = instance.modelPaths;
  // modelPaths returns directories containing model files
  for (let i = 0; i < paths.size; i++) {
    const dir = paths.getAt(i);
    if (fs.existsSync(dir) && fs.statSync(dir).isDirectory()) {
      const onnx = fs.readdirSync(dir).find((f) => f.endsWith('.onnx'));
      if (onnx) {
        instance.close();
        return path.join(dir, onnx);
      }
    } else if (dir.endsWith('.onnx')) {
      instance.close();
      return dir;
    }
  }
  instance.close();
  return undefined;
}

module.exports = { downloadModel };

Paso 3: Detectar y garantizar proveedores de ejecución (proceso principal)

Use ExecutionProviderCatalog para enumerar los proveedores disponibles (CPU, DirectML, QNN/NPU) y ensureReadyAsync para descargar su tiempo de ejecución si es necesario:

Creación de src/winml-ep.js:

const { ExecutionProviderCatalog, ExecutionProviderReadyState, ExecutionProviderReadyResultState } = require('#winapp/bindings');

function listProviders() {
  const catalog = ExecutionProviderCatalog.getDefault();
  return catalog.findAllProviders().map((p) => ({
    name: p.name,
    readyState: p.readyState,
    libraryPath: p.libraryPath,
  }));
}

async function ensureProviderReady(providerName, onProgress) {
  const catalog = ExecutionProviderCatalog.getDefault();
  const providers = catalog.findAllProviders();
  const provider = providers.find((p) => p.name === providerName);
  if (!provider) {
    throw new Error(`Execution provider not found: ${providerName}`);
  }

  if (provider.readyState === ExecutionProviderReadyState.Ready) {
    return { name: provider.name, readyState: 'Ready', libraryPath: provider.libraryPath };
  }

  const op = provider.ensureReadyAsync();
  if (onProgress) {
    op.progress((value) => {
      try { onProgress(value); } catch {}
    });
  }
  const result = await op;
  let readyState;
  if (result.status === ExecutionProviderReadyResultState.Success) readyState = 'Ready';
  else if (result.status === ExecutionProviderReadyResultState.Failure) readyState = 'Failed';
  else readyState = 'InProgress';
  return {
    name: provider.name,
    readyState,
    diagnosticText: result.diagnosticText,
    libraryPath: provider.libraryPath,
  };
}

module.exports = { listProviders, ensureProviderReady };

Paso 4: Ejecución de la inferencia en un proceso de utilidad

La creación de la sesión de ONNX Runtime y la inferencia son operaciones bloqueantes; ejecútalas en un proceso de utilidad de Electron para que el proceso principal siga respondiendo.

4.1. Creación del trabajo

Crear src/winml-worker.js (este archivo se ejecuta en un proceso de utilidad):

const { roInitialize } = require('@microsoft/dynwinrt');

// When dynwinrt and onnxruntime-node share the same process, ORT's native
// init can leave the COM apartment uninitialized. Explicitly init MTA first.
roInitialize(1);

const ort = require('onnxruntime-node');

async function runModel(modelPath, inputData, inputShape, ep) {
  const providers = ep === 'dml'
    ? [{ name: 'dml', deviceId: 0 }, 'cpu']
    : ['cpu'];

  const session = await ort.InferenceSession.create(modelPath, {
    executionProviders: providers,
    graphOptimizationLevel: 'all',
  });

  const inputName = session.inputNames[0];
  const input = new ort.Tensor('float32', inputData, inputShape);
  const outputs = await session.run({ [inputName]: input });
  return Array.from(outputs[session.outputNames[0]].data);
}

process.parentPort.on('message', async (e) => {
  const { id, method, args } = e.data;
  try {
    if (method === 'classify') {
      const [modelPath, inputData, inputShape, ep] = args;
      const result = await runModel(modelPath, new Float32Array(inputData), inputShape, ep);
      process.parentPort.postMessage({ id, ok: true, result });
    }
  } catch (err) {
    process.parentPort.postMessage({ id, ok: false, error: err.message });
  }
});

4.2. Iniciar y llamar al trabajador desde main

Agregue lo siguiente a src/index.js:

const { utilityProcess } = require('electron');
const path = require('node:path');
const { listProviders, ensureProviderReady } = require('./winml-ep.js');
const { downloadModel } = require('./winml-model.js');

let worker = null;
let workerReady = null;
const pending = new Map();
let nextId = 1;

function startWinmlWorker() {
  worker = utilityProcess.fork(path.join(__dirname, 'winml-worker.js'), [], {
    stdio: 'pipe',
    serviceName: 'winml-worker',
  });
  worker.on('message', (msg) => {
    const entry = pending.get(msg.id);
    if (!entry) return;
    pending.delete(msg.id);
    if (msg.ok) entry.resolve(msg.result);
    else entry.reject(new Error(msg.error));
  });
  workerReady = new Promise((resolve) => worker.once('spawn', resolve));
}

async function classify(modelPath, inputData, inputShape, ep) {
  if (!worker) startWinmlWorker();
  await workerReady;
  return new Promise((resolve, reject) => {
    const id = nextId++;
    pending.set(id, { resolve, reject });
    worker.postMessage({ id, method: 'classify', args: [modelPath, Array.from(inputData), inputShape, ep] });
  });
}

4.3. Úsalo

Asegúrese de que la createWindow función es asyncy agregue:

const createWindow = async () => {
  // ... existing window creation code ...

  // List and ensure all execution providers are ready
  const providers = listProviders();
  console.log('Available providers:', providers);

  for (const ep of providers) {
    console.log(`Ensuring ${ep.name} is ready...`);
    const result = await ensureProviderReady(ep.name, (progress) => {
      const pct = progress <= 1 ? Math.round(progress * 100) : Math.round(progress);
      process.stdout.write(`\r  ${ep.name}: ${pct}%`);
    });
    process.stdout.write('\n');
    console.log(`  ${ep.name}: ${result.readyState}`);
  }

  // Download model via Model Catalog (cached after first run)
  console.log('Downloading model...');
  const modelPath = await downloadModel('squeezenet', (progress) => {
    if (progress >= 0 && progress <= 100) {
      process.stdout.write(`\rDownloading model: ${Math.round(progress)}%`);
    }
  });
  process.stdout.write('\n');
  console.log('Model path:', modelPath);

  // Run inference in utility process (replace with real preprocessed data)
  const inputData = new Float32Array(1 * 3 * 224 * 224);
  const output = await classify(modelPath, inputData, [1, 3, 224, 224], 'dml');

  console.log('Model output (top 5 values):', output.slice(0, 5));
};

Paso 5: Ejecutarlo

npx winapp node add-electron-debug-identity
npm start

Debería ver los proveedores de ejecución disponibles y la salida del modelo en la consola.

Tip

Para obtener una canalización de clasificación de imágenes completa con descodificación de imágenes a través de enlaces JS (StorageFile, BitmapDecoder, BitmapTransform), consulte el ejemplo winML de la Galería de Electrones.

Pasos siguientes

¡Felicidades! Está ejecutando proveedores de ejecución de WinML y ONNX Runtime desde JavaScript: no se requiere ningún complemento de C#. 🎉

Ahora ya está listo para:

O explore otras guías: