Criando um complemento WinML

Este guia mostra como criar um complemento nativo em C# que usa o WinML (Windows Machine Learning) em seu aplicativo Electron. O WinML permite que você execute machine learning modelos (formato ONNX) localmente em dispositivos Windows para tarefas como classificação de imagem, detecção de objetos e muito mais.

Pré-requisitos

Antes de iniciar este guia, verifique se você:

Observação

O WinML é executado em qualquer dispositivo Windows 10 (1809+) ou Windows 11. Para obter melhor desempenho, dispositivos com GPUs ou NPUs são recomendados, mas a API também funciona na CPU.

Importante

O complemento WinML requer o experimental SDK do Aplicativo Windows. Se você selecionou "SDKs estáveis" no guia de instalação durante winapp init, precisará atualizar sua versão do SDK. Edite winapp.yaml e altere a versão Microsoft.WindowsAppSDK para 2.0.0-experimental3 e execute npx winapp restore para atualizar.

Etapa 1: Criar um complemento nativo em C#

Vamos criar um complemento nativo que usará APIs WinML. Usaremos um modelo C# que aproveita o node-api-dotnet para fazer a ponte entre JavaScript e C#.

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

Isso cria uma winMlAddon/ pasta com:

  • addon.cs - Seu código C# que chamará APIs WinML
  • winMlAddon.csproj – arquivo Project com referências ao SDK do Windows e SDK do Aplicativo Windows
  • README.md - Documentação sobre como usar o complemento

O comando também adiciona um build-winMlAddon script à sua package.json criação do complemento e um clean-winMlAddon script para limpar artefatos 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 para ambos os SDKs, para que você possa iniciar imediatamente a chamada Windows APIs!

Vamos verificar se tudo está configurado corretamente criando o complemento:

# Build the C# addon
npm run build-winMlAddon

Observação

Você também pode criar um complemento C++ usando npx winapp node create-addon (sem o --template sinalizador). Os complementos do C++ usam node-addon-api e fornecem acesso direto às APIs Windows com desempenho máximo. Consulte o guia do Complemento de Notificação do C++ para obter um passo a passo ou a documentação completa do comando para obter mais opções.

Etapa 2: Baixar o modelo SqueezeNet e obter o código de exemplo

Usaremos o exemplo classificar imagem da Galeria de Desenvolvimento de IA como nossa referência. Este exemplo usa o modelo SqueezeNet 1.1 para classificação de imagem.

2.1. Baixar o modelo

  1. Instalar a Galeria de Desenvolvimento de IA
  2. Navegue até o exemplo Classificar Imagem
  3. Baixe o modelo SqueezeNet 1.1 (ele dá suporte a CPU, GPU e NPU)
  4. Clique em Abrir Pasta De Contenção para localizar o .onnx arquivo

Baixando o SqueezeNet da Galeria de Desenvolvimento de IA

  1. Copie o arquivo squeezenet1.1.onnx para uma pasta models/ na raiz do project

Observação

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

Etapa 3: Adicionar pacotes NuGet necessários

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

3.1. Atualizar Directory.packages.props

Adicione as seguintes versões de pacote ao Directory.packages.props arquivo na raiz do projeto (deveria ter sido criado quando você criou o complemento):

<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 de 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 esses pacotes fazem:

  • Microsoft.ML.OnnxRuntime.Extensions – fornece operadores e utilitários adicionais para o ONNX Runtime
  • System.Drawing.Common – Habilita o carregamento e a manipulação de imagem para pré-processamento
  • Microsoft. Extensions.AI – abstrações de IA para .NET
  • Microsoft.ML.OnnxRuntimeGenAI.Managed – Associações gerenciadas para o ONNX Runtime GenAI
  • Microsoft.ML.OnnxRuntimeGenAI.WinML – integração winml para ONNX Runtime GenAI

Etapa 4: Adicionar o código de exemplo

A Galeria de Desenvolvimento de IA mostra a implementação completa para classificação de imagem com SqueezeNet:

Código de exemplo SqueezeNet

Adaptamos esse código para o Electron e você pode encontrar a implementação completa no exemplo de electron-winml. A winMlAddon/ pasta contém o código modificado da Galeria de Desenvolvimento de IA.

Copie a pasta inteira winMlAddon/ de exemplos/electron-winml/winMlAddon/ para a raiz do projeto, substituindo a criada na Etapa 1. O exemplo inclui vários arquivos além addon.cs (classes auxiliares em Utils/, um cliente de chat etc.) que são necessários para o complemento criar e executar.

Importante

Você deve copiar a pasta inteira, não apenas addon.cs. O complemento depende de arquivos auxiliares na Utils/ subpasta (Prediction.cs, , ImageNet.csBitmapFunctions.csetc.).

Principais detalhes da implementação

Vamos realçar as partes importantes da implementação e as principais diferenças do código da Galeria de Desenvolvimento de IA:

1. Requisito de Caminho Raiz do Projeto

Ao contrário do código da Galeria de Desenvolvimento de IA, nosso addon Electron requer que o código JavaScript passe o caminho raiz do projeto. Isso é necessário porque:

  • O complemento precisa localizar o arquivo de modelo ONNX na models/ pasta
  • As DLLs (dependências nativas) precisam ser carregadas 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;
}

Isso seleciona automaticamente o melhor provedor de execução (CPU, GPU ou NPU) com base nos recursos do dispositivo.

2. Pré-carregamento de dependências nativas

O complemento inclui um PreloadNativeDependencies() método para carregar DLLs necessárias. Essa abordagem funciona para cenários de desenvolvimento e produção sem a 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
}

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

3. Configurando o Electron Forge para Empacotação

Para garantir que o complemento funcione corretamente em builds de produção, você precisa configurar o empacotador para:

  1. Desempacotar arquivos nativos – DLLs, modelos ONNX e arquivos .node devem estar acessíveis fora do arquivo ASAR
  2. Excluir arquivos desnecessários – Mantenha o tamanho do pacote pequeno excluindo artefatos de build e arquivos temporários

Para Electron Forge, atualize 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 isso faz:

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

    • Isso os torna acessíveis no runtime por meio de caminhos do sistema de arquivos
    • O código JavaScript ajusta caminhos automaticamente (consulte a app.asar substituição de → app.asar.unpacked acima)
  2. ignore – Exclui do pacote final:

    • .winapp/ - Pacotes e cabeçalhos do SDK (não necessários no runtime)
    • .msix arquivos – Saídas empacotadas
    • winMlAddon/ arquivos de origem – mantém apenas a dist/ pasta com binários compilados

Observação

Se você estiver usando uma ferramenta de empacotamento diferente (construtor de elétrons etc.), precisará definir configurações semelhantes para desempacotar dependências nativas e excluir arquivos de desenvolvimento. Verifique a documentação do empacotador para opções de descompactação ASAR.

4. Classificação de imagem

O ClassifyImage método processa uma imagem e retorna 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 lida com:

  • Carregamento e pré-processamento de imagem (redimensionamento, normalização)
  • Executando a inferência do modelo
  • Resultados pós-processamento para obter previsões principais com rótulos e pontuações de confiança

Observação

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

Noções básicas sobre o código

O complemento fornece estas funções principais:

  1. CreateAsync – Inicializa o complemento e carrega o modelo SqueezeNet
  2. ClassifyImage – Usa 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.

Etapa 5: Criar o complemento em C#

Agora, crie o complemento:

npm run build-winMlAddon

Isso compila seu código C# usando Native AOT (compilação em tempo antecipado), que:

  • Cria um .node binário (formato de complemento nativo)
  • Corta o código não utilizado para um tamanho de pacote menor
  • Não requer .NET runtime em computadores de destino
  • Fornece desempenho nativo

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

Etapa 6: Testar o complemento

Agora, vamos testar se o complemento funciona chamando-o do processo principal. Abra src/main.js e siga estas etapas:

6.1. Carregar o complemento

Adicione as declarações 'require' na parte superior:

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

6.2. Criar uma função de teste

Adicione esta função para testar a classificação de 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 funcione tanto em ambientes de desenvolvimento quanto em aplicativos empacotados.
  • Isso acessa os arquivos nativos desempacotados 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 imagem:

  1. Criar uma test-images/ pasta na raiz do projeto
  2. Adicionar uma imagem de teste chamada sample.jpg (o código espera esse nome de arquivo exato)
  3. O modelo SqueezeNet reconhece 1000 classes ImageNet diferentes (animais, objetos, cenas etc.)

Ao executar o aplicativo, você verá os resultados da classificação no console!

Dica

Para obter uma implementação completa com manipuladores de IPC, caixas de diálogo de seleção de arquivo e uma interface do usuário, consulte o exemplo de electron-winml.

Etapa 7: Atualizar identidade de depuração

Para garantir que o SDK do Aplicativo Windows esteja carregado e disponível para uso, precisamos garantir que configuremos a identidade de depuração, o que garantirá que a estrutura seja carregada sempre que nosso aplicativo for executado. Da mesma forma, sempre que você modificar Package.appxmanifest ou alterar recursos referenciados no manifesto (como ícones de aplicativo), será necessário atualizar a identidade de depuração do seu aplicativo. Execute:

npx winapp node add-electron-debug-identity

Este comando:

  1. Lê o seu Package.appxmanifest para obter detalhes e funcionalidades do aplicativo
  2. Registra electron.exe em sua node_modules com uma identidade temporária
  3. Permite testar APIs que requerem identidade sem o empacotamento MSIX completo

Observação

Esse comando já faz parte do postinstall script que adicionamos no guia de instalação, portanto, ele é executado automaticamente após npm install. No entanto, você precisa executá-lo manualmente sempre que:

  • Modificar Package.appxmanifest (alterar recursos, identidade ou propriedades)
  • Atualizar ativos do aplicativo (ícones, logotipos etc.)

Agora, execute seu aplicativo:

npm start

Verifique a saída do console – você deve ver os resultados do teste do WinML!

⚠️ Problema Conhecido: Falhas de aplicativo ou janela em branco (clique para expandir)

Há um bug de Windows conhecido com aplicativos Electron de empacotamento esparso que faz com que o aplicativo falhe no início ou não renderize o conteúdo da Web. O problema foi corrigido em Windows mas ainda não foi propagado para todos os dispositivos.

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

Próximas etapas

Parabéns! Você criou com êxito um complemento nativo que pode executar modelos de machine learning com o WinML! 🎉

Agora você está pronto para:

Ou explore outros guias:

Personalizando para seu modelo

Para integrar totalmente seu modelo ONNX, você precisará:

  1. Entenda as entradas do modelo – Imagens, tensores, sequências etc.
  2. Criar associações de entrada adequadas – Converter seus dados no formato que o WinML espera
  3. Processar as saídas – Analisar e interpretar as previsões do modelo
  4. Manipular erros de forma eficaz – o carregamento e a inferência do modelo podem falhar

Recursos adicionais

Solução de problemas

Falha na compilação com NU1010: os itens PackageReference não definem um PackageVersion correspondente

Verifique se todos os pacotes referenciados em winMlAddon.csproj têm entradas correspondentes em Directory.packages.props. Consulte a Etapa 3 para obter a lista completa de pacotes necessários.

"não é um aplicativo Win32 válido" ao carregar o complemento

Isso significa que o complemento foi criado para uma arquitetura diferente da Node.js/Electron runtime. Verifique sua arquitetura de Node.js:

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

Em seguida, recompile o complemento com o destino 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 você alterou recentemente sua instalação do Node.js, também reinstale node_modules para obter o binário Electron compatível.

rm -rf node_modules package-lock.json
npm install

Obter Ajuda

Feliz aprendizado de máquina! 🤖