Criação de uma Extensão WinML

Este guia mostra-lhe como criar um addon nativo de C# que utilize Windows Machine Learning (WinML) na sua aplicação Electron. O WinML permite-lhe executar modelos de machine learning (formato ONNX) localmente em dispositivos Windows para tarefas como classificação de imagens, deteção de objetos e mais.

Pré-requisitos

Antes de começar este guia, certifique-se de que:

Observação

O WinML funciona em qualquer dispositivo Windows 10 (1809+) ou Windows 11. Para melhor desempenho, recomendam-se dispositivos com GPUs ou NPUs, mas a API também funciona no CPU.

Importante

O addon WinML requer o experimental SDK de Aplicações Windows. Se selecionaste "SDKs estáveis" no winapp init guia de configuração, terás de atualizar a versão do SDK. Edita winapp.yaml e altera a versão Microsoft.WindowsAppSDK para 2.0.0-experimental3, depois executa npx winapp restore para atualizar.

Passo 1: Criar um addon nativo de C#

Vamos criar um addon nativo que utilize APIs do WinML. Vamos usar um template em C# que aproveita node-api-dotnet para fazer a ponte entre JavaScript e C#.

npx winapp node create-addon --template cs --name winMlAddon

Isto cria uma winMlAddon/ pasta com:

  • addon.cs - O seu código C# que irá chamar as APIs do WinML
  • winMlAddon.csproj - ficheiro Project com referências ao Windows SDK e SDK de Aplicações Windows
  • README.md - Documentação sobre como usar o addon

O comando também adiciona um build-winMlAddon script ao teu package.json para construir o addon e um clean-winMlAddon script para limpar artefactos de build:

{
  "scripts": {
    "build-winMlAddon": "dotnet publish ./winMlAddon/winMlAddon.csproj -c Release",
    "clean-winMlAddon": "dotnet clean ./winMlAddon/winMlAddon.csproj"
  }
}

O modelo inclui automaticamente referências a ambos os SDKs, para que possa começar imediatamente a chamar APIs do Windows!

Vamos verificar se tudo está configurado corretamente ao construir o adddon:

# Build the C# addon
npm run build-winMlAddon

Observação

Também podes criar um addon em C++ usando npx winapp node create-addon (sem o --template flag). Os addons C++ utilizam node-addon-api e fornecem acesso direto a APIs Windows com desempenho máximo. Consulte o guia do Adicionar de Notificações em C++ para uma explicação detalhada ou a documentação completa dos comandos para mais opções.

Passo 2: Descarregue o modelo SqueezeNet e obtenha código de exemplo

Vamos usar a amostra Classify Image da AI Dev Gallery como referência. Esta amostra utiliza o modelo SqueezeNet 1.1 para classificação de imagens.

2.1. Descarregue o modelo

  1. Instalar a Galeria de Desenvolvimento de IA
  2. Vá para o exemplo Classify Image
  3. Descarregue o modelo SqueezeNet 1.1 (suporta CPU, GPU e NPU)
  4. Clique em Abrir Pasta Contendo para localizar o .onnx ficheiro

Descarregar o SqueezeNet da AI Dev Gallery

  1. Copie o ficheiro squeezenet1.1.onnx para uma pasta models/ na raiz do seu projeto

Observação

O modelo pode também ser descarregado diretamente do ONNX Model Zoo GitHub repositório

Passo 3: Adicionar os Pacotes NuGet Obrigatórios

Antes de adicionar o código WinML, precisamos de adicionar pacotes NuGet adicionais necessários para processamento de imagem, ONNX Runtime e suporte a GenAI.

3.1. Atualize o arquivo Directory.packages.props

Adicione as seguintes versões de pacotes ao Directory.packages.props ficheiro na raiz do seu projeto (deveria ter sido criada quando criou o adddon):

<Project>
  <PropertyGroup>
    <!-- Enable central package versioning -->
    <ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
  </PropertyGroup>
  <ItemGroup>
    <PackageVersion Include="Microsoft.JavaScript.NodeApi" Version="0.9.17" />
    <PackageVersion Include="Microsoft.JavaScript.NodeApi.Generator" Version="0.9.17" />
    <!-- Add these packages for WinML -->
+   <PackageVersion Include="Microsoft.ML.OnnxRuntime.Extensions" Version="0.14.0" />
+   <PackageVersion Include="System.Drawing.Common" Version="9.0.9" />
+   <PackageVersion Include="Microsoft.Extensions.AI" Version="9.9.1" />
+   <PackageVersion Include="Microsoft.ML.OnnxRuntimeGenAI.Managed" Version="0.10.1" />
+   <PackageVersion Include="Microsoft.ML.OnnxRuntimeGenAI.WinML" Version="0.10.1" />
    
    <!-- These versions may be updated automatically during restore to match yaml -->
    <PackageVersion Include="Microsoft.WindowsAppSDK" Version="2.0.0-experimental3" />
    <PackageVersion Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.7175" />
  </ItemGroup>
</Project>

3.2. Atualizar winMlAddon.csproj

Abra winMlAddon/winMlAddon.csproj e adicione as referências do pacote ao <ItemGroup>:

<ItemGroup>
  <PackageReference Include="Microsoft.JavaScript.NodeApi" />
  <PackageReference Include="Microsoft.JavaScript.NodeApi.Generator" />
  <!-- Add these packages for WinML -->
+ <PackageReference Include="Microsoft.ML.OnnxRuntime.Extensions" />
+ <PackageReference Include="System.Drawing.Common" />
+ <PackageReference Include="Microsoft.Extensions.AI" />
+ <PackageReference Include="Microsoft.ML.OnnxRuntimeGenAI.Managed" />
+ <PackageReference Include="Microsoft.ML.OnnxRuntimeGenAI.WinML" />
  
  <PackageReference Include="Microsoft.Windows.SDK.BuildTools" />
  <PackageReference Include="Microsoft.WindowsAppSDK" />
</ItemGroup>

O que fazem estes pacotes:

  • Microsoft.ML.OnnxRuntime.Extensions - Fornece operadores e utilidades adicionais para o Runtime ONNX
  • System.Drawing.Common - Permite carregamento e manipulação de imagens para pré-processamento
  • Microsoft. Extensions.AI - Abstrações de IA para .NET
  • Microsoft.ML.OnnxRuntimeGenAI.Managed - Ligações geridas para ONNX Runtime GenAI
  • Microsoft.ML.OnnxRuntimeGenAI.WinML - Integração WinML para ONNX Runtime GenAI

Passo 4: Adicionar o Código de Exemplo

A AI Dev Gallery mostra a implementação completa para classificação de imagens com o SqueezeNet:

Código de Exemplo SqueezeNet

Adaptámos este código para o Electron e pode encontrar a implementação completa na amostra electron-winml. A winMlAddon/ pasta contém o código modificado da AI Dev Gallery.

Copie a pasta inteira winMlAddon/ de samples/electron-winml/winMlAddon/ para a raiz do seu projeto, substituindo a criada no Passo 1. O exemplo inclui vários ficheiros além addon.cs disso (classes auxiliares em Utils/, um cliente de chat, etc.) que são necessários para que o addon seja construído e executado.

Importante

Deve copiar a pasta inteira, não apenas addon.cs. O addon depende dos ficheiros auxiliares na Utils/ subpasta (Prediction.cs, ImageNet.cs, BitmapFunctions.cs, etc.).

Detalhes Chave da Implementação

Vamos destacar as partes importantes da implementação e as principais diferenças em relação ao código da AI Dev Gallery:

1. Requisito do Project Root Path

Ao contrário do código da AI Dev Gallery, o nosso addon Electron requer que o código JavaScript passe pelo caminho raiz do projeto. Isto é necessário porque:

  • O addon precisa de localizar o ficheiro do modelo ONNX na models/ pasta
  • Dependências nativas (DLLs) precisam de ser carregadas a partir de diretórios específicos
[JSExport]
public static async Task<Addon> CreateAsync(string projectRoot)
{
    if (!Path.Exists(projectRoot))
    {
        throw new Exception("Project root is invalid.");
    }

    var addon = new Addon(projectRoot);
    addon.PreloadNativeDependencies();

    string modelPath = Path.Join(projectRoot, "models", @"squeezenet1.1-7.onnx");
    await addon.InitModel(modelPath, ExecutionProviderDevicePolicy.DEFAULT, null, false, null);

    return addon;
}

Isto seleciona automaticamente o melhor fornecedor de execução (CPU, GPU ou NPU) com base nas capacidades do dispositivo.

2. Pré-carregamento de dependências nativas

O addon inclui um PreloadNativeDependencies() método para carregar as DLLs necessárias. Esta abordagem funciona tanto para cenários de desenvolvimento como de produção sem necessidade de copiar DLLs para a raiz do projeto:

private void PreloadNativeDependencies()
{
    // Loads required DLLs from the winMlAddon build output
    // This ensures dependencies are available regardless of the execution context
}

Isto é chamado durante a inicialização antes de carregar o modelo, garantindo que todas as bibliotecas nativas estão disponíveis.

3. Configuração do Electron Forge para Empacotamento

Para garantir que o addon funciona corretamente em builds de produção, precisa de configurar o seu empacotador para:

  1. Desembalar ficheiros nativos - DLLs, modelos ONNX e ficheiros .node devem estar acessíveis fora do arquivo ASAR
  2. Excluir ficheiros desnecessários - Mantenha o tamanho do pacote pequeno excluindo artefactos de compilação e ficheiros temporários

Para Electron Forge, atualize o seu forge.config.js:

// From samples/electron-winml/forge.config.js
module.exports = {
  packagerConfig: {
    asar: {
      // Unpack native files so they can be accessed by the addon
      unpack: "**/*.{dll,exe,node,onnx}"
    },
    ignore: [
      // Exclude .winapp folder (SDK packages and headers)
      /^\/.winapp\//,
      // Exclude MSIX packages
      "\\.msix$",
      // Exclude winMlAddon source files, but keep the dist folder
      /^\/winMlAddon\/(?!dist).+/
    ]
  },
  // ... rest of your config
};

O que isto faz:

  1. asar.unpack - Extrai DLLs, executáveis, binários .node e modelos ONNX para app.asar.unpacked/

    • Isto torna-as acessíveis em tempo de execução através de caminhos do sistema de ficheiros
    • O código JavaScript ajusta automaticamente os caminhos (ver a app.asar substituição → app.asar.unpacked acima)
  2. ignore - Exclui do pacote final:

    • .winapp/ - Pacotes e cabeçalhos SDK (não necessários em tempo de execução)
    • .msix ficheiros - Resultados empacotados
    • winMlAddon/ ficheiros de origem - Mantém apenas a dist/ pasta com binários compilados

Observação

Se estiveres a usar uma ferramenta de embalagem diferente (electron-builder, etc.), terás de configurar definições semelhantes para desempacotar dependências nativas e excluir ficheiros de desenvolvimento. Verifique a documentação do seu empacotador para as opções de descompactação ASAR.

4. Classificação da Imagem

O ClassifyImage método processa uma imagem e devolve previsões:

[JSExport]
public async Task<Prediction[]> ClassifyImage(string imagePath)
{
    // Loads the image, preprocesses it, and runs inference
    // Returns top predictions with labels and confidence scores
}

A implementação completa aborda:

  • Carregamento e pré-processamento de imagem (redimensionamento, normalização)
  • Execução da inferência do modelo
  • Pós-processamento de resultados para obter as melhores previsões com etiquetas e níveis de confiança

Observação

O código-fonte completo inclui pré-processamento de imagens, criação de tensores e análise de resultados. Verifique a implementação de exemplo para todos os detalhes.

Compreender o Código

O addon fornece as seguintes funções principais:

  1. CreateAsync - Inicializa o addon e carrega o modelo SqueezeNet
  2. ClassifyImage - Toma um caminho de imagem e retorna previsões de classificação

O WinML seleciona automaticamente o melhor dispositivo de execução (CPU, GPU ou NPU) com base na disponibilidade.

Passo 5: Constrói o addon C#

Agora constrói o adddon:

npm run build-winMlAddon

Isto compila o código C# usando Native AOT (compilação antecipada), que:

  • Cria um .node binário (formato nativo de addon)
  • Remove o código não utilizado para um tamanho de pacote menor
  • Não requer tempo de execução do .NET nas máquinas alvo
  • Proporciona desempenho nativo

O addon compilado estará em winMlAddon/dist/winMlAddon.node.

Passo 6: Testa o Addon

Agora vamos testar o funcionamento do addon chamando-o do processo principal. Abra src/main.js e siga estes passos:

6.1. Carregar o Addon

Adicione as instruções de requerimento no topo:

const winMlAddon = require('../winMlAddon/dist/winMlAddon.node');

6.2. Criar uma Função de Teste

Adicione esta função para testar a classificação da imagem:

const testWinML = async () => {
  console.log('Testing WinML addon...');
  
  try {
    let projectRoot = path.join(__dirname, '..');
    // Adjust path for packaged apps
    if (projectRoot.includes('app.asar')) {
      projectRoot = projectRoot.replace('app.asar', 'app.asar.unpacked');
    }
    
    const addon = await winMlAddon.Addon.createAsync(projectRoot);
    console.log('Model loaded successfully!');
    
    // Classify a sample image
    const imagePath = path.join(projectRoot, 'test-images', 'sample.jpg');
    const predictions = await addon.classifyImage(imagePath);
    
    console.log('Top predictions:');
    predictions.slice(0, 5).forEach((pred, i) => {
      console.log(`${i + 1}. ${pred.label}: ${(pred.confidence * 100).toFixed(2)}%`);
    });
  } catch (error) {
    console.error('Error testing WinML:', error.message);
  }
};

Pontos principais:

  • O ajuste de caminho (app.asarapp.asar.unpacked) garante que o código funciona tanto em aplicações de desenvolvimento como em aplicações empacotadas
  • Isto acede aos ficheiros nativos descompactados configurados em forge.config.js

6.3. Chamar a Função de Teste

Adicione esta linha no final da createWindow() função:

testWinML();

6.4. Preparar Imagens de Teste

Para testar a classificação de imagens:

  1. Crie uma test-images/ pasta na raiz do seu projeto
  2. Adicione uma imagem de teste chamada sample.jpg (o código espera exatamente este nome de ficheiro)
  3. O modelo SqueezeNet reconhece 1000 classes diferentes de ImageNet (animais, objetos, cenas, etc.)

Quando executares a aplicação, vais ver os resultados da classificação na consola!

Sugestão

Para uma implementação completa com IPC handlers, diálogos para seleção de ficheiros e uma interface de utilizador, veja electron-winml sample.

Passo 7: Atualizar a Identidade de Depuração

Para garantir que o SDK de Aplicações Windows está carregado e disponível para uso, precisamos de garantir que configuramos a identidade de depuração que garantirá que o framework seja carregado sempre que a nossa aplicação for executada. Da mesma forma, sempre que modifica Package.appxmanifest ou altera os ativos referenciados no manifesto (como ícones da app), é necessário atualizar a identidade de depuração da sua aplicação. Corrida:

npx winapp node add-electron-debug-identity

Este comando:

  1. Lê o seu Package.appxmanifest para obter detalhes e funcionalidades da aplicação.
  2. Regista electron.exe no seu node_modules com uma identidade temporária
  3. Permite testar APIs que exigem identidade sem empacotamento MSIX completo

Observação

Este comando já faz parte do postinstall script que adicionámos no guia de configuração, por isso corre automaticamente após npm install. No entanto, é necessário executá-lo manualmente sempre que:

  • Modificar Package.appxmanifest (alterar capacidades, identidade ou propriedades)
  • Atualize os recursos da aplicação (ícones, logótipos, etc.)

Agora executa a tua aplicação:

npm start

Verifica a saída da consola – deves ver os resultados do teste do WinML!

⚠️ Problema Conhecido: A Aplicação Crasha ou Janela Em Branco (clique para expandir)

Existe um bug conhecido no Windows com aplicações Electron em empacotamento esparso que faz com que a aplicação crashe ao iniciar ou não renderize conteúdo web. O problema foi resolvido no Windows, mas ainda não se propagou a todos os dispositivos.

Consulte configuração do ambiente de desenvolvimento para solução alternativa.

Próximas Etapas

Parabéns! Criou com sucesso um addon nativo que pode correr modelos de aprendizagem automática com WinML! 🎉

Agora está pronto para:

Ou explore outros guias:

Personalização para o Seu Modelo

Para integrar totalmente o seu modelo ONNX, terá de:

  1. Compreende as entradas do seu modelo - imagens, tensores, sequências, etc.
  2. Crie bindings de entrada adequados - Converta os seus dados para o formato que o WinML espera
  3. Processar os resultados - Analisar e interpretar as previsões do modelo
  4. Lidar com erros de forma elegante - O carregamento e a inferência do modelo podem falhar

Recursos adicionais

Troubleshooting

Falhas na compilação com o NU1010: Os itens PackageReference não definem uma PackageVersion correspondente

Certifique-se de que todos os pacotes referenciados em winMlAddon.csproj têm entradas correspondentes em Directory.packages.props. Consulte o Passo 3 para a lista completa de pacotes obrigatórios.

"não é uma aplicação Win32 válida" ao carregar o addon

Isto significa que o addon foi criado para uma arquitetura diferente da do teu tempo de execução Node.js/Electron. Verifique a sua arquitetura Node.js:

node -e "console.log(process.arch)"

Depois reconstrói o addon com o alvo correspondente:

# For x64 Node.js:
dotnet publish ./winMlAddon/winMlAddon.csproj -c Release -r win-x64

# For ARM64 Node.js:
dotnet publish ./winMlAddon/winMlAddon.csproj -c Release -r win-arm64

Se mudaste recentemente a tua instalação Node.js, reinstala node_modules também para obter o binário Electron correspondente:

rm -rf node_modules package-lock.json
npm install

Obter Ajuda

Boa aprendizagem automática! 🤖