Ausführen von WinML aus JavaScript (JS-Bindungen)

Dieser Leitfaden zeigt, wie ONNX-Modellinferenz aus Electron mithilfe von JS-Bindungen für Windows App SDK ML-APIs ausgeführt wird (Ermittlung des Ausführungsanbieters über ExecutionProviderCatalog und Herunterladen des Modells über ModelCatalog), kombiniert mit onnxruntime-node für die Inferenz – kein C#-Add-On erforderlich. Inferenz wird in einem Electron-Dienstprozess ausgeführt, sodass der Hauptprozess nicht blockiert wird.

Voraussetzungen

Bevor Sie diesen Leitfaden starten, stellen Sie sicher, dass Sie folgendes haben:

ONNX-Runtime für Node installieren:

npm install onnxruntime-node@1.24.3

Important

Die Windows-App Runtime lädt ihre eigenen onnxruntime.dll in untergeordnete Prozesse vor. Die onnxruntime-node-Version muss mit der ORT-ABI übereinstimmen, die mit Ihrer Windows App SDK-Version gebündelt ist. Verwenden Sie onnxruntime-node@1.24.xfür Windows App SDK 2.x .

Schritt 1: Bestätigen von WinML-Bindungen

Das Windows App SDK ist transitiv von Microsoft.WindowsAppSDK.ML abhängig, sodass die WinML-APIs bereits in Ihren generierten Bindings enthalten sind. Überprüfen:

Erfordert @microsoft/dynwinrt-codegen0.1.0-preview.8 – siehe Erste Schritte mit Electron für Fallbacks bei älteren Projekten.

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

[ 'ExecutionProvider', 'ExecutionProviderCatalog', 'ExecutionProviderReadyState' ] sollte angezeigt werden.

Schritt 2: Herunterladen des Modells über den Modellkatalog (Hauptprozess)

Verwenden Sie ModelCatalog aus JS-Bindungen, um das Modell lokal herunterzuladen und zwischenzuspeichern. Der Katalog liest ein JSON-Manifest (remote oder lokal gehostet), das verfügbare Modelle und deren Download-URLs beschreibt. Nach dem ersten Download verwenden nachfolgende Ausführung die zwischengespeicherte Kopie:

Erstellen Sie 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 };

Schritt 3: Ermitteln und Sicherstellen von Ausführungsanbietern (Hauptprozess)

Dient ExecutionProviderCatalog zum Auflisten der verfügbaren Anbieter (CPU, DirectML, QNN/NPU) und ensureReadyAsync zum Herunterladen der Laufzeit bei Bedarf:

Erstellen Sie 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 };

Schritt 4: Ausführen von Rückschlüssen in einem Hilfsprogrammprozess

ONNX-Runtime-Sitzungserstellung und -ableitung blockieren – führen Sie sie in einem Electron-Hilfsprozess aus, um den Hauptprozess reaktionsfähig zu halten.

4.1. Erstellen Sie den Worker

Erstellen src/winml-worker.js (diese Datei wird in einem Hilfsprogrammprozess ausgeführt):

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. Worker aus main starten und aufrufen

Fügen Sie Folgendes zu Ihrem 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. Verwenden Sie es

Stellen Sie sicher, dass Ihre createWindow Funktion lautet async, und fügen Sie Folgendes hinzu:

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));
};

Schritt 5: Ausführen

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

In der Konsole sollten die verfügbaren Ausführungsanbieter und die Modellausgabe angezeigt werden.

Tip

Eine vollständige Bildklassifizierungspipeline mit Bilddecodierung über JS-Bindungen (StorageFile, BitmapDecoder, BitmapTransform), finden Sie im WinML-Beispiel der Elektronengalerie.

Nächste Schritte

Herzlichen Glückwunsch! Sie führen WinML-Ausführungsanbieter und ONNX-Runtime aus JavaScript aus – kein C#-Addon erforderlich. 🎉

Jetzt sind Sie bereit für:

Oder erkunden Sie andere Guides: