Skip to content

Commit 83a461b

Browse files
authored
Added example of using Phi with WinML in Electron (#211)
1 parent 48b674e commit 83a461b

File tree

12 files changed

+487
-22
lines changed

12 files changed

+487
-22
lines changed

samples/electron-winml/.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,4 +103,5 @@ obj/
103103
*.user
104104
*.suo
105105

106-
*.onnx
106+
*.onnx
107+
/models

samples/electron-winml/Directory.packages.props

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@
99
<PackageVersion Include="Microsoft.ML.OnnxRuntime.Extensions" Version="0.14.0" />
1010
<PackageVersion Include="System.Drawing.Common" Version="9.0.9" />
1111

12+
<PackageVersion Include="Microsoft.Extensions.AI" Version="9.9.1" />
13+
<PackageVersion Include="Microsoft.ML.OnnxRuntimeGenAI.Managed" Version="0.10.1" />
14+
<PackageVersion Include="Microsoft.ML.OnnxRuntimeGenAI.WinML" Version="0.10.1" />
15+
1216
<!-- These versions may be updated automatically during restore to match yaml -->
1317
<PackageVersion Include="Microsoft.WindowsAppSDK" Version="2.0.0-experimental3" />
1418
<PackageVersion Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.7175" />

samples/electron-winml/README.md

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Electron WinML Sample
22

3-
This sample demonstrates how to integrate Windows Machine Learning (WinML) into an Electron application using the Windows App Development CLI. The app uses the SqueezeNet 1.1 model to classify images directly on the user's device.
3+
This sample demonstrates how to integrate Windows Machine Learning (WinML) into an Electron application using the Windows App Development CLI. The app uses the SqueezeNet 1.1 model to classify images directly on the user's device, and the Phi model for text generation.
44

55
## What's Included
66

@@ -37,12 +37,21 @@ This automatically runs the `postinstall` script which:
3737
- Adds debug identity to Electron
3838

3939
### 2. Download the Model
40+
The models are available in the [AI Dev Gallery](https://aka.ms/aidevgallery). Install the gallery to download the models. You don't need both models if you only care about one or the other. The *models* folder will need to be created.
41+
42+
43+
#### SqueezeNet
44+
1. Navigate to the **Classify Image** sample
45+
2. Download the **SqueezeNet 1.1** model
46+
3. Click **Open Containing Folder** to locate the `.onnx` file
47+
4. Copy `squeezenet1.1-7.onnx` to the `models/` folder in this project
48+
49+
#### Phi
50+
1. Navigate to the **Generate Text** sample
51+
2. Download any of the Phi models from Custom models
52+
3. Click **Open Containing Folder** to locate model files
53+
4. Copy all the contents of the folder (should have .onnx and .json files) to the `models/phi` folder in this project.
4054

41-
1. Install the [AI Dev Gallery](https://aka.ms/aidevgallery)
42-
2. Navigate to the **Classify Image** sample
43-
3. Download the **SqueezeNet 1.1** model
44-
4. Click **Open Containing Folder** to locate the `.onnx` file
45-
5. Copy `squeezenet1.1-7.onnx` to the `models/` folder in this project
4655

4756
### 3. Build the C# Addon
4857

samples/electron-winml/forge.config.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ module.exports = {
55
packagerConfig: {
66
asar: {
77
// This tells Forge: "Put everything in the ASAR, EXCEPT these files"
8-
"unpack": "**/*.{dll,exe,node,onnx}"
8+
"unpack": "{**/*.{dll,exe,node,onnx},**/models/phi/**}"
99
},
1010
ignore: [
1111
/^\/.winapp\//,

samples/electron-winml/src/index.css

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,3 +166,69 @@ h2 {
166166
.hidden {
167167
display: none;
168168
}
169+
170+
/* Tabs */
171+
.tabs {
172+
display: flex;
173+
border-bottom: 1px solid #e0e0e0;
174+
margin-bottom: 24px;
175+
}
176+
177+
.tab-btn {
178+
padding: 12px 24px;
179+
background: none;
180+
border: none;
181+
border-bottom: 2px solid transparent;
182+
cursor: pointer;
183+
font-size: 16px;
184+
font-weight: 600;
185+
color: #605e5c;
186+
transition: all 0.2s;
187+
font-family: inherit;
188+
}
189+
190+
.tab-btn:hover {
191+
color: #0078d4;
192+
background: #f3f2f1;
193+
}
194+
195+
.tab-btn.active {
196+
color: #0078d4;
197+
border-bottom-color: #0078d4;
198+
}
199+
200+
.tab-content {
201+
animation: fadeIn 0.3s ease-in-out;
202+
}
203+
204+
@keyframes fadeIn {
205+
from { opacity: 0; }
206+
to { opacity: 1; }
207+
}
208+
209+
/* Text Generation Styles */
210+
.prompt-input {
211+
width: 100%;
212+
height: 100px;
213+
padding: 12px;
214+
border: 1px solid #e0e0e0;
215+
border-radius: 4px;
216+
margin-bottom: 16px;
217+
font-family: inherit;
218+
resize: vertical;
219+
}
220+
221+
.prompt-input:focus {
222+
outline: none;
223+
border-color: #0078d4;
224+
}
225+
226+
.text-output {
227+
margin-top: 20px;
228+
padding: 16px;
229+
background: #fafafa;
230+
border: 1px solid #e0e0e0;
231+
border-radius: 4px;
232+
min-height: 100px;
233+
white-space: pre-wrap;
234+
}

samples/electron-winml/src/index.html

Lines changed: 39 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -7,22 +7,47 @@
77
</head>
88
<body>
99
<div class="container">
10-
<h1>🖼️ Image Classification</h1>
11-
12-
<button id="selectImageBtn" class="select-btn">Select Image</button>
13-
14-
<div id="imagePreview" class="image-preview hidden">
15-
<img id="selectedImage" src="" alt="Selected image" />
10+
<div class="tabs">
11+
<button id="tabImage" class="tab-btn active">Image Classification</button>
12+
<button id="tabText" class="tab-btn">Text Generation</button>
1613
</div>
17-
18-
<div id="loadingView" class="loading hidden">
19-
<div class="spinner"></div>
20-
<p>Classifying image...</p>
14+
15+
<div id="contentImage" class="tab-content">
16+
<h1>🖼️ Image Classification</h1>
17+
18+
<button id="selectImageBtn" class="select-btn">Select Image</button>
19+
20+
<div id="imagePreview" class="image-preview hidden">
21+
<img id="selectedImage" src="" alt="Selected image" />
22+
</div>
23+
24+
<div id="loadingView" class="loading hidden">
25+
<div class="spinner"></div>
26+
<p>Classifying image...</p>
27+
</div>
28+
29+
<div id="resultsContainer" class="results hidden">
30+
<h2>Classification Results</h2>
31+
<ul id="resultsList"></ul>
32+
</div>
2133
</div>
22-
23-
<div id="resultsContainer" class="results hidden">
24-
<h2>Classification Results</h2>
25-
<ul id="resultsList"></ul>
34+
35+
<div id="contentText" class="tab-content hidden">
36+
<h1>📝 Text Generation</h1>
37+
<!-- Placeholder for Text Generation Implementation -->
38+
<div class="text-gen-container">
39+
<textarea id="promptInput" class="prompt-input" placeholder="Enter your prompt here..."></textarea>
40+
<button id="generateBtn" class="select-btn">Generate Text</button>
41+
42+
<div id="textLoadingView" class="loading hidden">
43+
<div class="spinner"></div>
44+
<p>Generating text...</p>
45+
</div>
46+
47+
<div id="textOutput" class="text-output hidden">
48+
<!-- Generated text will go here -->
49+
</div>
50+
</div>
2651
</div>
2752
</div>
2853

samples/electron-winml/src/index.js

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ const path = require('node:path');
33
const winMlAddon = require('../winMlAddon/dist/winMlAddon.node');
44

55
let addonInstance;
6+
let chatClientInstance;
67

78
// Handle creating/removing shortcuts on Windows when installing/uninstalling.
89
if (require('electron-squirrel-startup')) {
@@ -68,6 +69,27 @@ ipcMain.handle('classify-image', async (event, imagePath) => {
6869
}
6970
});
7071

72+
// IPC Handler: Generate text
73+
ipcMain.handle('generate-text', async (event, prompt) => {
74+
try {
75+
if (!chatClientInstance) {
76+
let rootDir = path.join(__dirname, '..');
77+
// if app.asar in path, adjust rootDir
78+
if (rootDir.includes('app.asar')) {
79+
rootDir = rootDir.replace('app.asar', 'app.asar.unpacked');
80+
}
81+
chatClientInstance = await winMlAddon.ChatClient.createAsync(rootDir);
82+
}
83+
84+
const systemPrompt = "You are a helpful assistant.";
85+
const response = await chatClientInstance.generateText(systemPrompt, prompt);
86+
return response;
87+
} catch (error) {
88+
console.error('Text generation error:', error);
89+
throw error;
90+
}
91+
});
92+
7193
// This method will be called when Electron has finished
7294
// initialization and is ready to create browser windows.
7395
// Some APIs can only be used after this event occurs.

samples/electron-winml/src/preload.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@ const { contextBridge, ipcRenderer } = require('electron');
44
contextBridge.exposeInMainWorld('electronAPI', {
55
selectImage: () => ipcRenderer.invoke('select-image'),
66
classifyImage: (imagePath) => ipcRenderer.invoke('classify-image', imagePath),
7+
generateText: (prompt) => ipcRenderer.invoke('generate-text', prompt),
78
});

samples/electron-winml/src/renderer.js

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,56 @@ const loadingView = document.getElementById('loadingView');
66
const resultsContainer = document.getElementById('resultsContainer');
77
const resultsList = document.getElementById('resultsList');
88

9+
// Tabs
10+
const tabImage = document.getElementById('tabImage');
11+
const tabText = document.getElementById('tabText');
12+
const contentImage = document.getElementById('contentImage');
13+
const contentText = document.getElementById('contentText');
14+
15+
// Text Gen Elements
16+
const promptInput = document.getElementById('promptInput');
17+
const generateBtn = document.getElementById('generateBtn');
18+
const textLoadingView = document.getElementById('textLoadingView');
19+
const textOutput = document.getElementById('textOutput');
20+
21+
// Tab switching logic
22+
tabImage.addEventListener('click', () => {
23+
tabImage.classList.add('active');
24+
tabText.classList.remove('active');
25+
contentImage.classList.remove('hidden');
26+
contentText.classList.add('hidden');
27+
});
28+
29+
tabText.addEventListener('click', () => {
30+
tabImage.classList.remove('active');
31+
tabText.classList.add('active');
32+
contentImage.classList.add('hidden');
33+
contentText.classList.remove('hidden');
34+
});
35+
36+
// Handle text generation
37+
generateBtn.addEventListener('click', async () => {
38+
const prompt = promptInput.value.trim();
39+
if (!prompt) return;
40+
41+
try {
42+
textOutput.classList.add('hidden');
43+
textLoadingView.classList.remove('hidden');
44+
generateBtn.disabled = true;
45+
46+
const response = await window.electronAPI.generateText(prompt);
47+
48+
textOutput.textContent = response;
49+
textOutput.classList.remove('hidden');
50+
} catch (error) {
51+
console.error('Error generating text:', error);
52+
alert('Error generating text: ' + error.message);
53+
} finally {
54+
textLoadingView.classList.add('hidden');
55+
generateBtn.disabled = false;
56+
}
57+
});
58+
959
// Handle image selection
1060
selectImageBtn.addEventListener('click', async () => {
1161
try {

0 commit comments

Comments
 (0)