Jalankan WinML dari JavaScript (pengikatan JS)

Panduan ini menunjukkan cara menjalankan inferensi model ONNX dari Electron menggunakan binding JS untuk API ML SDK Aplikasi Windows (penemuan penyedia eksekusi melalui ExecutionProviderCatalog dan pengunduhan model melalui ModelCatalog) yang dikombinasikan dengan onnxruntime-node untuk inferensi — tidak memerlukan add-on C#. Inferensi dijalankan di proses utilitas Electron sehingga tidak memblokir proses utama.

Prerequisites

Sebelum memulai panduan ini, pastikan Anda telah:

Instal ONNX Runtime untuk Node:

npm install onnxruntime-node@1.24.3

Important

Windows App Runtime memuat terlebih dahulu onnxruntime.dll miliknya sendiri ke proses turunan. Versi onnxruntime-node harus cocok dengan ORT ABI yang dibundel dengan versi SDK Aplikasi Windows Anda. Untuk SDK Aplikasi Windows 2.x, gunakan onnxruntime-node@1.24.x.

Langkah 1: Mengonfirmasi pengikatan WinML

SDK Aplikasi Windows secara transitif tergantung pada Microsoft.WindowsAppSDK.ML, sehingga API WinML sudah ada dalam pengikatan yang Anda buat. Verifikasi:

Memerlukan @microsoft/dynwinrt-codegen0.1.0-preview.8 — lihat Memulai dengan Electron untuk alternatif bagi proyek lama.

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

Anda seharusnya melihat [ 'ExecutionProvider', 'ExecutionProviderCatalog', 'ExecutionProviderReadyState' ].

Langkah 2: Unduh model melalui Katalog Model (proses utama)

Gunakan ModelCatalog dari pengikatan JS untuk mengunduh dan menyimpan cache model secara lokal. Katalog membaca manifes JSON (dihosting dari jarak jauh atau lokal) yang menjelaskan model yang tersedia dan URL unduhannya. Setelah pengunduhan pertama, eksekusi berikutnya menggunakan salinan yang di-cache:

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

Langkah 3: Temukan dan pastikan penyedia eksekusi (proses utama)

Gunakan ExecutionProviderCatalog untuk mencantumkan penyedia yang tersedia (CPU, DirectML, QNN/NPU) dan ensureReadyAsync untuk mengunduh runtime mereka jika diperlukan:

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

Langkah 4: Jalankan inferensi dalam proses utilitas

Pembuatan sesi dan inferensi ONNX Runtime bersifat memblokir — jalankan keduanya di proses utilitas Electron untuk menjaga proses utama tetap responsif.

4.1. Membuat pekerja

Buat src/winml-worker.js (file ini berjalan dalam proses utilitas):

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. Luncurkan dan panggil pekerja dari utama

Tambahkan yang berikut ini ke 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. Gunakan itu

Pastikan fungsi Anda createWindow adalah async, lalu tambahkan:

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

Langkah 5: Jalankan

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

Anda akan melihat penyedia eksekusi dan output model yang tersedia di konsol.

Tip

Untuk contoh end-to-end lainnya tentang pemanggilan API Windows dari Electron, lihat galeri Electron di Windows.

Langkah Selanjutnya

Selamat! Anda menjalankan penyedia eksekusi WinML dan ONNX Runtime dari JavaScript — tidak diperlukan addon C#. 🎉

Sekarang Anda siap untuk:

Atau jelajahi panduan lain: