arcane-os 0.6.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/README.md +8 -8
- package/browser-runtime/ai/browser-device-settings.mjs +83 -0
- package/browser-runtime/ai/browser-wasm-llm-provider.mjs +4 -48
- package/docs/architecture.md +1 -1
- package/docs/reference/ai/browser-speech.md +30 -0
- package/docs/reference/runtime-components.md +53 -0
- package/package.json +1 -1
- package/runtime/arcane/components/browser-ai-setup.html +212 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.6.1
|
|
4
|
+
|
|
5
|
+
- Add a reusable profile component for browser NPU setup and WebNN/WebGPU API
|
|
6
|
+
availability. Its setup button uses the existing GPU guidance approach:
|
|
7
|
+
attempt the appropriate Chrome or Edge flags page, then show an alert with
|
|
8
|
+
instructions and the address to paste if the browser blocks navigation.
|
|
9
|
+
- Share browser identification and the flags-opening helper with the existing
|
|
10
|
+
high-performance GPU notice. Preserve that notice's targets and wording.
|
|
11
|
+
Setup does not change saved preferences, browser flags, selected models, or
|
|
12
|
+
inference; API availability is not reported as physical NPU execution.
|
|
13
|
+
|
|
3
14
|
## 0.6.0
|
|
4
15
|
|
|
5
16
|
- Prefer WebNN NPU, then WebGPU, then CPU through WASM for browser Whisper
|
package/README.md
CHANGED
|
@@ -19,7 +19,7 @@ version-locked SDK runtime, while an integrated Arcane checkout uses its live
|
|
|
19
19
|
`arcane/` runtime. Both profiles preserve the same app URLs, theme, packaging,
|
|
20
20
|
event, cancellation, and browser run contracts.
|
|
21
21
|
|
|
22
|
-
This checkout defines the `0.6.
|
|
22
|
+
This checkout defines the `0.6.1` SDK contract. Applications pin one exact npm
|
|
23
23
|
version and lockfile; registry state is deliberately not baked into application
|
|
24
24
|
artifacts.
|
|
25
25
|
|
|
@@ -35,7 +35,7 @@ Create one browser application, install its pinned SDK, and start its source
|
|
|
35
35
|
server:
|
|
36
36
|
|
|
37
37
|
```bash
|
|
38
|
-
npx arcane-os@0.6.
|
|
38
|
+
npx arcane-os@0.6.1 new hello-speech --path ./hello-speech --target browser
|
|
39
39
|
cd hello-speech
|
|
40
40
|
npm install
|
|
41
41
|
npm run dev
|
|
@@ -363,7 +363,7 @@ uses the same controller for automatic memory extraction.
|
|
|
363
363
|
Create a new repository-shaped Arcane application with the exact stable SDK:
|
|
364
364
|
|
|
365
365
|
```bash
|
|
366
|
-
npx arcane-os@0.6.
|
|
366
|
+
npx arcane-os@0.6.1 new my-app --path ./my-app --target portable --git
|
|
367
367
|
cd my-app
|
|
368
368
|
npm install
|
|
369
369
|
npm run dev
|
|
@@ -373,7 +373,7 @@ To enroll an existing repository, install the exact SDK and initialize only
|
|
|
373
373
|
missing Arcane files:
|
|
374
374
|
|
|
375
375
|
```bash
|
|
376
|
-
npm install --save-dev --save-exact arcane-os@0.6.
|
|
376
|
+
npm install --save-dev --save-exact arcane-os@0.6.1
|
|
377
377
|
npm exec -- arcane init my-app --target portable
|
|
378
378
|
```
|
|
379
379
|
|
|
@@ -389,7 +389,7 @@ npm exec -- arcane-os targets
|
|
|
389
389
|
No global SDK install or standalone Arcane CLI is required. The application
|
|
390
390
|
repository's exact npm dependency and lockfile own the CLI and toolchain version.
|
|
391
391
|
|
|
392
|
-
Use `npx arcane-os@0.6.
|
|
392
|
+
Use `npx arcane-os@0.6.1` for the initial bootstrap because it names this npm
|
|
393
393
|
package explicitly; bare `npx arcane` outside an installed project could resolve
|
|
394
394
|
a different package. Both installed commands invoke the same headless toolchain.
|
|
395
395
|
Project-local npm scripts use the SDK pinned by that app's `package-lock.json`,
|
|
@@ -409,7 +409,7 @@ node ./bin/arcane.mjs new local-app --path ../local-app --target portable --git
|
|
|
409
409
|
|
|
410
410
|
# From the generated app repository
|
|
411
411
|
cd ../local-app
|
|
412
|
-
npm install --save-dev --save-exact ../arcane-os-sdk/arcane-os-0.6.
|
|
412
|
+
npm install --save-dev --save-exact ../arcane-os-sdk/arcane-os-0.6.1.tgz
|
|
413
413
|
npm ci
|
|
414
414
|
```
|
|
415
415
|
|
|
@@ -418,7 +418,7 @@ same location. The lockfile retains the selected package dependency while
|
|
|
418
418
|
Arcane uses the installed package name and version. Local directory `file:` dependencies are not
|
|
419
419
|
accepted because npm may install them as links; use a packed `.tgz`. A GitHub
|
|
420
420
|
runner also needs that tarball at the locked path. After publication, replace
|
|
421
|
-
the local declaration with the exact `arcane-os@0.6.
|
|
421
|
+
the local declaration with the exact `arcane-os@0.6.1` registry package and
|
|
422
422
|
commit the regenerated lock.
|
|
423
423
|
|
|
424
424
|
Generated repositories use `npm ci --ignore-scripts` in CI. Run dependency
|
|
@@ -557,7 +557,7 @@ package installation, or assertions.
|
|
|
557
557
|
|
|
558
558
|
## Current target support
|
|
559
559
|
|
|
560
|
-
Version `0.6.
|
|
560
|
+
Version `0.6.1` exposes one browser target and five explicitly paired
|
|
561
561
|
native development targets: a non-runnable portable directory, a
|
|
562
562
|
Windows x64 unsigned-local-test EXE bundle, Linux x64 and Linux ARM64
|
|
563
563
|
unsigned-local-test DEBs, and an Android development-signed APK. The
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import Is from "../dependencies/strong-type/index.js";
|
|
2
|
+
|
|
3
|
+
const is = new Is(false);
|
|
4
|
+
|
|
5
|
+
export const WEBNN_BROWSER_SETTINGS = [
|
|
6
|
+
{ browserId: "chrome", name: "Google Chrome", url: "chrome://flags/#web-machine-learning-neural-network" },
|
|
7
|
+
{ browserId: "edge", name: "Microsoft Edge", url: "edge://flags/#web-machine-learning-neural-network" },
|
|
8
|
+
];
|
|
9
|
+
|
|
10
|
+
// Internal-page navigation may be blocked. The instructions remain available
|
|
11
|
+
// regardless; returning from this function is not evidence that settings opened.
|
|
12
|
+
export function openBrowserDeviceSettings(target, message) {
|
|
13
|
+
try {
|
|
14
|
+
globalThis.open?.(target.url, "_blank", "noopener,noreferrer");
|
|
15
|
+
} catch {
|
|
16
|
+
// Continue to the address-bar instructions when the browser rejects the URL.
|
|
17
|
+
}
|
|
18
|
+
globalThis.alert?.(message);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Browser identity selects setup instructions, never hardware or model readiness.
|
|
22
|
+
export function getBrowserDeviceSettings(navigatorObject = globalThis.navigator) {
|
|
23
|
+
const userAgent = String(navigatorObject?.userAgent ?? "");
|
|
24
|
+
const clientHints = navigatorObject?.userAgentData;
|
|
25
|
+
const platform = String(clientHints?.platform || navigatorObject?.platform || userAgent);
|
|
26
|
+
const brands = new Set();
|
|
27
|
+
for (const entry of clientHints?.brands ?? []) brands.add(entry.brand);
|
|
28
|
+
const desktopWindows = /\b(?:Windows|Win32|Win64)\b/iu.test(platform)
|
|
29
|
+
&& clientHints?.mobile !== true
|
|
30
|
+
&& !/\b(?:Android|iPhone|iPad|iPod|Mobile)\b/iu.test(userAgent);
|
|
31
|
+
let browserId = "unknown";
|
|
32
|
+
let name = "this browser";
|
|
33
|
+
let highPerformanceGpu = null;
|
|
34
|
+
|
|
35
|
+
// Preserve the existing GPU guidance for browsers that also identify as Chrome.
|
|
36
|
+
if (brands.has("Vivaldi") || /\bVivaldi\//u.test(userAgent)) {
|
|
37
|
+
browserId = "vivaldi";
|
|
38
|
+
name = "Vivaldi";
|
|
39
|
+
highPerformanceGpu = { name, url: "vivaldi://flags/#force-high-performance-gpu" };
|
|
40
|
+
} else if (brands.has("Brave") || is.function(navigatorObject?.brave?.isBrave)) {
|
|
41
|
+
browserId = "brave";
|
|
42
|
+
name = "Brave";
|
|
43
|
+
highPerformanceGpu = { name, url: "brave://flags/#force-high-performance-gpu" };
|
|
44
|
+
} else if (brands.has("Opera") || /\bOPR\//u.test(userAgent)) {
|
|
45
|
+
browserId = "opera";
|
|
46
|
+
name = "Opera";
|
|
47
|
+
highPerformanceGpu = { name, url: "opera://flags/#force-high-performance-gpu" };
|
|
48
|
+
} else if (brands.has("Microsoft Edge") || /\bEdg\//u.test(userAgent)) {
|
|
49
|
+
browserId = "edge";
|
|
50
|
+
name = "Microsoft Edge";
|
|
51
|
+
highPerformanceGpu = { name, url: "edge://flags/#force-high-performance-gpu" };
|
|
52
|
+
} else if (
|
|
53
|
+
brands.has("Chromium")
|
|
54
|
+
|| brands.has("Google Chrome")
|
|
55
|
+
|| /\b(?:Chrome|Chromium)\//u.test(userAgent)
|
|
56
|
+
) {
|
|
57
|
+
// Keep the established generic GPU address for Chromium browsers that mask their brand.
|
|
58
|
+
highPerformanceGpu = { name: "your browser", url: "about://flags/#force-high-performance-gpu" };
|
|
59
|
+
if (
|
|
60
|
+
brands.has("Google Chrome")
|
|
61
|
+
|| (!clientHints?.brands && /\bChrome\//u.test(userAgent)
|
|
62
|
+
&& !/\b(?:SamsungBrowser|UCBrowser|YaBrowser)\//u.test(userAgent))
|
|
63
|
+
) {
|
|
64
|
+
browserId = "chrome";
|
|
65
|
+
name = "Google Chrome";
|
|
66
|
+
} else {
|
|
67
|
+
browserId = "chromium";
|
|
68
|
+
name = "your Chromium browser";
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const webnnFlagsURL = WEBNN_BROWSER_SETTINGS.find(function matchesBrowser(entry) {
|
|
73
|
+
return entry.browserId === browserId;
|
|
74
|
+
})?.url ?? null;
|
|
75
|
+
return {
|
|
76
|
+
browserId,
|
|
77
|
+
name,
|
|
78
|
+
webnnFlagsURL,
|
|
79
|
+
highPerformanceGpu: desktopWindows ? highPerformanceGpu : null,
|
|
80
|
+
webnnAvailable: is.function(navigatorObject?.ml?.createContext),
|
|
81
|
+
webgpuAvailable: Boolean(navigatorObject?.gpu),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
resolveModelSecurity,
|
|
9
9
|
} from "./model-controller.mjs";
|
|
10
10
|
import { createPackagedWllamaRuntime } from "./browser-wllama-runtime.mjs";
|
|
11
|
+
import { getBrowserDeviceSettings, openBrowserDeviceSettings } from "./browser-device-settings.mjs";
|
|
11
12
|
import { arcaneEvents } from "../event-manager.mjs";
|
|
12
13
|
|
|
13
14
|
const is = new Is(false);
|
|
@@ -264,65 +265,20 @@ function publicDescriptor(source) {
|
|
|
264
265
|
return modelDescriptor(source);
|
|
265
266
|
}
|
|
266
267
|
|
|
267
|
-
function highPerformanceGpuBrowser() {
|
|
268
|
-
const navigatorObject = globalThis.navigator;
|
|
269
|
-
const userAgent = String(navigatorObject?.userAgent ?? "");
|
|
270
|
-
const clientHints = navigatorObject?.userAgentData;
|
|
271
|
-
const platform = String(clientHints?.platform || navigatorObject?.platform || userAgent);
|
|
272
|
-
if (
|
|
273
|
-
!/\b(?:Windows|Win32|Win64)\b/iu.test(platform)
|
|
274
|
-
|| clientHints?.mobile === true
|
|
275
|
-
|| /\b(?:Android|iPhone|iPad|iPod|Mobile)\b/iu.test(userAgent)
|
|
276
|
-
) {
|
|
277
|
-
return null;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
const brands = new Set();
|
|
281
|
-
for (const entry of clientHints?.brands ?? []) {
|
|
282
|
-
brands.add(entry.brand);
|
|
283
|
-
}
|
|
284
|
-
// Vivaldi can append its identity while also identifying as Edge or Chrome.
|
|
285
|
-
if (brands.has("Vivaldi") || /\bVivaldi\//u.test(userAgent)) {
|
|
286
|
-
return { name: "Vivaldi", url: "vivaldi://flags/#force-high-performance-gpu" };
|
|
287
|
-
}
|
|
288
|
-
if (brands.has("Brave") || is.function(navigatorObject?.brave?.isBrave)) {
|
|
289
|
-
return { name: "Brave", url: "brave://flags/#force-high-performance-gpu" };
|
|
290
|
-
}
|
|
291
|
-
if (brands.has("Opera") || /\bOPR\//u.test(userAgent)) {
|
|
292
|
-
return { name: "Opera", url: "opera://flags/#force-high-performance-gpu" };
|
|
293
|
-
}
|
|
294
|
-
if (brands.has("Microsoft Edge") || /\bEdg\//u.test(userAgent)) {
|
|
295
|
-
return { name: "Microsoft Edge", url: "edge://flags/#force-high-performance-gpu" };
|
|
296
|
-
}
|
|
297
|
-
if (
|
|
298
|
-
brands.has("Chromium")
|
|
299
|
-
|| brands.has("Google Chrome")
|
|
300
|
-
|| /\b(?:Chrome|Chromium)\//u.test(userAgent)
|
|
301
|
-
) {
|
|
302
|
-
// Some Chromium browsers mask their brand. about:// uses their own flags page.
|
|
303
|
-
return { name: "your browser", url: "about://flags/#force-high-performance-gpu" };
|
|
304
|
-
}
|
|
305
|
-
return null;
|
|
306
|
-
}
|
|
307
|
-
|
|
308
268
|
function notifyHighPerformanceGpu(adapter) {
|
|
309
269
|
if (highPerformanceGpuNoticeShown) {
|
|
310
270
|
return;
|
|
311
271
|
}
|
|
312
|
-
const browser =
|
|
272
|
+
const browser = getBrowserDeviceSettings().highPerformanceGpu;
|
|
313
273
|
if (!browser) {
|
|
314
274
|
return;
|
|
315
275
|
}
|
|
316
276
|
highPerformanceGpuNoticeShown = true;
|
|
317
|
-
try {
|
|
318
|
-
globalThis.open?.(browser.url, "_blank", "noopener,noreferrer");
|
|
319
|
-
} catch {
|
|
320
|
-
// Browsers may reject internal-page navigation from web content.
|
|
321
|
-
}
|
|
322
277
|
const adapterName = adapter.description || adapter.name
|
|
323
278
|
|| [adapter.vendor, adapter.architecture].filter(Boolean).join(" ")
|
|
324
279
|
|| "the available WebGPU adapter";
|
|
325
|
-
|
|
280
|
+
openBrowserDeviceSettings(
|
|
281
|
+
browser,
|
|
326
282
|
`Selected WebGPU adapter: ${adapterName}.\n\n`
|
|
327
283
|
+ `If this computer has multiple GPUs, enable “Force High Performance GPU” in ${browser.name} `
|
|
328
284
|
+ "to request the high-performance GPU when available. "
|
package/docs/architecture.md
CHANGED
|
@@ -279,7 +279,7 @@ paths are withheld from the native provider. The provider copies the complete
|
|
|
279
279
|
selected release rather than accepting an unrelated source path. Verification
|
|
280
280
|
is a separate explicit operation for a selected release artifact.
|
|
281
281
|
|
|
282
|
-
The SDK `0.6.
|
|
282
|
+
The SDK `0.6.1` runtime requires Arcane `0.8.12` or newer. Compatibility
|
|
283
283
|
is contractual rather than exact-version pinning: the prepared Core must meet
|
|
284
284
|
the highest minimum declared by the runtime, selected app, and bundled app
|
|
285
285
|
dependencies; keep each app's Arcane protocol generation; and provide every
|
|
@@ -130,6 +130,36 @@ window.addEventListener('ai-tts-failure', function reportSpeechFailure(event) {
|
|
|
130
130
|
|
|
131
131
|
Call `speechEvents.abort()` when disposing that interface to remove the listener.
|
|
132
132
|
|
|
133
|
+
## Browser NPU setup
|
|
134
|
+
|
|
135
|
+
Applications can place the shared `browser-ai-setup.html` component in their
|
|
136
|
+
profile or settings page. It reports whether this page exposes WebNN and
|
|
137
|
+
WebGPU, without loading a model or creating an accelerator context:
|
|
138
|
+
|
|
139
|
+
```html
|
|
140
|
+
<html-import
|
|
141
|
+
id="browserAISetup"
|
|
142
|
+
href="/arcane/components/browser-ai-setup.html">
|
|
143
|
+
</html-import>
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
**Set up NPU** uses the same browser-settings approach as the existing
|
|
147
|
+
high-performance GPU notice: attempt to open the browser's flags page, then
|
|
148
|
+
show instructions including the full address to paste if navigation was
|
|
149
|
+
blocked. Chrome uses `chrome://flags/#web-machine-learning-neural-network`;
|
|
150
|
+
Edge uses `edge://flags/#web-machine-learning-neural-network`. Unrecognized
|
|
151
|
+
browsers receive explicit Chrome and Edge choices instead of an assumed target.
|
|
152
|
+
The [ONNX Runtime WebNN guide](https://onnxruntime.ai/docs/tutorials/web/ep-webnn.html)
|
|
153
|
+
documents the **Enables WebNN API** flag and model/operator requirements.
|
|
154
|
+
|
|
155
|
+
This control does not save an execution preference, change browser settings,
|
|
156
|
+
restart the browser, or report that the NPU is active. A WebNN API presence
|
|
157
|
+
result is not proof of NPU hardware, a compatible model, or physical execution.
|
|
158
|
+
The profile also cannot report a different chat page's loaded runtime. The
|
|
159
|
+
existing automatic speech route remains NPU, then GPU, then CPU; upstream
|
|
160
|
+
sessions can still place unsupported operators on CPU. Use the owning model
|
|
161
|
+
runtime's evidence to determine actual accelerator execution.
|
|
162
|
+
|
|
133
163
|
## Developer diagnostics
|
|
134
164
|
|
|
135
165
|
The shared logging API and speech traces are available in SDK `0.5.14`.
|
|
@@ -60,6 +60,7 @@ appropriate.
|
|
|
60
60
|
| --- | --- | --- | --- | --- |
|
|
61
61
|
| [`app-bar.html`](#app-barhtml) | Responsive application navigation, route state, status, and trailing actions. | `setNavigation()`<br>`setActiveRoute()`<br>`setStatus()`<br>`refresh()`<br>`destroy()` | `app-bar-ready` | DOM-normalized |
|
|
62
62
|
| [`assistant-panel.html`](#assistant-panelhtml) | Reusable assistant drawer, message area, composer, pending/streaming/empty/error state, and actions. | `open()`<br>`close()`<br>`toggle()`<br>`send()`<br>`clear()`<br>`setState()`<br>`focusComposer()`<br>`scrollToEnd()`<br>`destroy()` | `assistant-ready`<br>`assistant-opened`<br>`assistant-closed`<br>`assistant-send`<br>`assistant-clear` | DOM-normalized; caller/provider results remain external |
|
|
63
|
+
| [`browser-ai-setup.html`](#browser-ai-setuphtml) | Browser API availability and NPU setup instructions with browser-specific flags actions. | `refresh()`<br>`open()`<br>`destroy()`<br>`ready` | `browser-ai-setup-ready` | API presence only; settings navigation and hardware execution remain browser-owned |
|
|
63
64
|
| [`calculator.html`](#calculatorhtml) | Calculator keypad and result/error event surface backed by CalculatorEngine. | `calculate()`<br>`destroy()` | `calculator-ready`<br>`calculation-complete`<br>`calculation-error` | Normalized Calculation/error events |
|
|
64
65
|
| [`chart.html`](#charthtml) | Accessible uPlot line, area, or point chart with normalized options and rows. | `configure()`<br>`populate()`<br>`setData()`<br>`addData()`<br>`update()`<br>`destroy()` | `chart-ready`<br>`chart-remove` | Options/rows normalized; uPlot rendering is vendor-native |
|
|
65
66
|
| [`chat.html`](#chathtml) | Shared chat, visible selected-model activation request, file upload, streaming, structural tool settlement, speech, language, availability, and conversation-timebox surface. | `streamMessage()`<br>`setMessageProgress()`<br>`setAIAvailability()`<br>`setInitialSpeechMuted()`<br>`setConversationComplete()`<br>`bindConversationTimebox()`<br>`bindSession()`<br>`submitMessage()`<br>`submitToolResult()`<br>`submitToolResults()`<br>`sendMessage()`<br>`languageChanged()`<br>`requestAIActivation()`<br>`destroy()` | `chat-ready`<br>`chat-session-bound`<br>`chat-session-message`<br>`chat-session-error`<br>`chat-send-message`<br>`chat-send-error`<br>`chat-file-uploaded`<br>`chat-file-upload-error`<br>`chat-language-changed`<br>`chat-language-change-error`<br>`chat-ai-activation-request`<br>`chat-ai-activation-error`<br>`chat-speech-synthesis-error`<br>`conversation-timebox-error` | UI/runtime state, explicit user activation intent, and honest structural-call settlement normalized; AI/storage/media behavior mixed |
|
|
@@ -153,6 +154,58 @@ Slots: `title`, `subtitle`, `identity`, `messages/message`, `composer`, `actions
|
|
|
153
154
|
</html-import>
|
|
154
155
|
```
|
|
155
156
|
|
|
157
|
+
## browser-ai-setup.html
|
|
158
|
+
|
|
159
|
+
### Overview
|
|
160
|
+
|
|
161
|
+
Displays WebNN and WebGPU API availability for the current page and explains how
|
|
162
|
+
to enable WebNN for NPU use. Its setup action uses the same browser-flags opening
|
|
163
|
+
attempt and alert fallback as the existing high-performance GPU notice. Chrome
|
|
164
|
+
and Edge receive their own flags addresses; other or unidentified browsers show
|
|
165
|
+
both explicit choices.
|
|
166
|
+
|
|
167
|
+
### Public surface
|
|
168
|
+
|
|
169
|
+
Methods/properties: `refresh()`, `open(browserId?)`, `destroy()`, `ready`.
|
|
170
|
+
|
|
171
|
+
Events: `browser-ai-setup-ready`.
|
|
172
|
+
|
|
173
|
+
`refresh()` synchronously updates API presence and browser guidance and returns
|
|
174
|
+
the current settings record: `browserId`, `name`, `webnnFlagsURL`,
|
|
175
|
+
`highPerformanceGpu`, `webnnAvailable`, and `webgpuAvailable`. It returns `false`
|
|
176
|
+
after destruction. Availability means that the browser exposes the API to this
|
|
177
|
+
page; it does not confirm a working adapter, loaded model, or hardware execution.
|
|
178
|
+
|
|
179
|
+
`open()` refreshes the display, attempts to open the detected Chrome or Edge flags
|
|
180
|
+
page, and presents an alert containing the full address and enable/relaunch
|
|
181
|
+
instructions. Pass `"chrome"` or `"edge"` to choose explicitly. Invoke it from a
|
|
182
|
+
user action. It returns `false` when destroyed or when no supported target is
|
|
183
|
+
selected, and otherwise returns `undefined`; it never reports that navigation
|
|
184
|
+
succeeded. Browsers may block internal-page navigation, so the full addresses
|
|
185
|
+
also remain visible and copyable in the component.
|
|
186
|
+
|
|
187
|
+
`destroy()` aborts owned listeners, disposes the event source, marks `ready`
|
|
188
|
+
false, and suppresses UI updates from a pending clipboard operation. It returns
|
|
189
|
+
`true` the first time and `false` thereafter. A BFCache-persisted `pagehide`
|
|
190
|
+
preserves the component; nonpersisted `pagehide` destroys it.
|
|
191
|
+
|
|
192
|
+
### Availability and normalization
|
|
193
|
+
|
|
194
|
+
**Browser and supported native WebViews.** API presence and setup instructions
|
|
195
|
+
are normalized; browser flags, clipboard support, and device execution remain
|
|
196
|
+
platform-owned. Mounting or refreshing creates no model, GPU adapter, or WebNN
|
|
197
|
+
context. The component saves no preferences, changes no browser settings, and
|
|
198
|
+
does not restart the browser. It cannot report model state owned by another page.
|
|
199
|
+
|
|
200
|
+
### Example
|
|
201
|
+
|
|
202
|
+
```html
|
|
203
|
+
<html-import
|
|
204
|
+
id="browser-ai-setup"
|
|
205
|
+
href="/arcane/components/browser-ai-setup.html">
|
|
206
|
+
</html-import>
|
|
207
|
+
```
|
|
208
|
+
|
|
156
209
|
## calculator.html
|
|
157
210
|
|
|
158
211
|
### Overview
|
package/package.json
CHANGED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
<link rel="stylesheet" href="./arcane/css/primitives.css?v=1">
|
|
2
|
+
<style>
|
|
3
|
+
:host{display:block;min-width:0}
|
|
4
|
+
.availability{display:grid;gap:var(--arcane-space-2);margin:0}
|
|
5
|
+
.availability div{display:grid;gap:var(--arcane-space-1)}
|
|
6
|
+
.availability dt{font-weight:700}
|
|
7
|
+
.availability dd{margin:0}
|
|
8
|
+
.flag-choice{margin-block:var(--arcane-space-4)}
|
|
9
|
+
.flag-choice h3{margin-block:var(--arcane-space-2)}
|
|
10
|
+
.actions{display:flex;flex-wrap:wrap;gap:var(--arcane-space-2)}
|
|
11
|
+
p,dt,dd,li,button{overflow-wrap:anywhere}
|
|
12
|
+
button{white-space:normal}
|
|
13
|
+
code{display:block;white-space:pre-wrap;overflow-wrap:anywhere;user-select:text;margin-block:var(--arcane-space-2)}
|
|
14
|
+
li{margin-block:var(--arcane-space-2)}
|
|
15
|
+
</style>
|
|
16
|
+
|
|
17
|
+
<section class="arcane-card" aria-labelledby="setupTitle">
|
|
18
|
+
<header class="arcane-card__header">
|
|
19
|
+
<h2 id="setupTitle" class="arcane-section-heading">Browser AI acceleration</h2>
|
|
20
|
+
</header>
|
|
21
|
+
<div class="arcane-card__body">
|
|
22
|
+
<p id="browserName"></p>
|
|
23
|
+
<dl class="availability" aria-live="polite">
|
|
24
|
+
<div><dt>WebNN API</dt><dd id="webnnAvailability"></dd></div>
|
|
25
|
+
<div><dt>WebGPU API</dt><dd id="webgpuAvailability"></dd></div>
|
|
26
|
+
</dl>
|
|
27
|
+
<p class="arcane-help">These checks show browser APIs only. They do not report whether a model is running on an NPU or GPU.</p>
|
|
28
|
+
<h3>NPU setup</h3>
|
|
29
|
+
<p id="browserInstructions"></p>
|
|
30
|
+
<div id="flagAddresses"></div>
|
|
31
|
+
<ol id="setupSteps"></ol>
|
|
32
|
+
<p>Browser API availability does not confirm that an NPU or a particular model is supported. You choose whether to change the browser setting.</p>
|
|
33
|
+
<p><a href="https://onnxruntime.ai/docs/tutorials/web/ep-webnn.html">ONNX Runtime WebNN setup documentation</a></p>
|
|
34
|
+
<p id="copyStatus" class="arcane-help" role="status" aria-live="polite"></p>
|
|
35
|
+
</div>
|
|
36
|
+
<footer class="arcane-card__footer">
|
|
37
|
+
<button id="refreshAvailability" class="arcane-button arcane-button--secondary" type="button">Refresh browser availability</button>
|
|
38
|
+
<p id="setupStatus" class="arcane-help" role="status" aria-live="polite"></p>
|
|
39
|
+
</footer>
|
|
40
|
+
</section>
|
|
41
|
+
|
|
42
|
+
<script type="module">
|
|
43
|
+
const host=this;
|
|
44
|
+
const root=host.shadowRoot;
|
|
45
|
+
host.ready=false;
|
|
46
|
+
|
|
47
|
+
const {
|
|
48
|
+
getBrowserDeviceSettings,
|
|
49
|
+
openBrowserDeviceSettings,
|
|
50
|
+
WEBNN_BROWSER_SETTINGS
|
|
51
|
+
}=await import('../sdk/ai/browser-device-settings.mjs');
|
|
52
|
+
const {arcaneLogging}=await import('arcane-os/logging');
|
|
53
|
+
const {createArcaneEventSource,projectArcaneDOMEvent}=await import('arcane-os/event-manager');
|
|
54
|
+
const events=createArcaneEventSource(host,{
|
|
55
|
+
source:'arcane.component.browser-ai-setup',
|
|
56
|
+
eventTypes:['browser-ai-setup-ready']
|
|
57
|
+
});
|
|
58
|
+
const lifecycleController=new AbortController();
|
|
59
|
+
const status=root.querySelector('#setupStatus');
|
|
60
|
+
const copyStatus=root.querySelector('#copyStatus');
|
|
61
|
+
const choices=[];
|
|
62
|
+
const setupSteps=[
|
|
63
|
+
'If the flags page does not open, copy the matching address into your browser\'s address bar and open it.',
|
|
64
|
+
'Find “Enables WebNN API” and choose Enabled.',
|
|
65
|
+
'Save your work, then use the browser\'s Relaunch control.',
|
|
66
|
+
'Return to this page and select Refresh browser availability.'
|
|
67
|
+
];
|
|
68
|
+
let copyTask=null;
|
|
69
|
+
let destroyed=false;
|
|
70
|
+
|
|
71
|
+
host.refresh=refresh;
|
|
72
|
+
host.open=open;
|
|
73
|
+
host.destroy=destroy;
|
|
74
|
+
|
|
75
|
+
function refresh(){
|
|
76
|
+
if(destroyed)return false;
|
|
77
|
+
const settings=getBrowserDeviceSettings();
|
|
78
|
+
root.querySelector('#browserName').textContent=settings.browserId==='unknown'
|
|
79
|
+
?'Browser not identified.'
|
|
80
|
+
:`Browser: ${settings.name}.`;
|
|
81
|
+
root.querySelector('#webnnAvailability').textContent=settings.webnnAvailable
|
|
82
|
+
?'Exposed to this page.'
|
|
83
|
+
:'Unavailable to this page.';
|
|
84
|
+
root.querySelector('#webgpuAvailability').textContent=settings.webgpuAvailable
|
|
85
|
+
?'Exposed to this page.'
|
|
86
|
+
:'Unavailable to this page.';
|
|
87
|
+
root.querySelector('#browserInstructions').textContent=settings.webnnFlagsURL
|
|
88
|
+
?`Set up NPU attempts to open the WebNN flags page in ${settings.name} and shows the full instructions.`
|
|
89
|
+
:'A matching WebNN flags address is not known for this browser. Choose Google Chrome or Microsoft Edge below if that is the browser you are using.';
|
|
90
|
+
for(const choice of choices){
|
|
91
|
+
choice.row.hidden=Boolean(settings.webnnFlagsURL)
|
|
92
|
+
&&choice.target.url!==settings.webnnFlagsURL;
|
|
93
|
+
choice.setupButton.textContent=settings.webnnFlagsURL
|
|
94
|
+
?'Set up NPU'
|
|
95
|
+
:`Set up NPU in ${choice.target.name}`;
|
|
96
|
+
}
|
|
97
|
+
return settings;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function open(browserId){
|
|
101
|
+
if(destroyed)return false;
|
|
102
|
+
const current=refresh();
|
|
103
|
+
const selectedId=browserId??current.browserId;
|
|
104
|
+
const target=WEBNN_BROWSER_SETTINGS.find(function matchSetupBrowser(browser){
|
|
105
|
+
return browser.browserId===selectedId;
|
|
106
|
+
});
|
|
107
|
+
if(!target){
|
|
108
|
+
status.textContent='Choose the Google Chrome or Microsoft Edge setup button that matches your browser.';
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
const instructions=setupSteps.map(function numberSetupStep(step,index){
|
|
112
|
+
return `${index+1}. ${step}`;
|
|
113
|
+
}).join('\n');
|
|
114
|
+
openBrowserDeviceSettings(
|
|
115
|
+
target,
|
|
116
|
+
`NPU setup for ${target.name}\n\n${instructions}\n\n${target.url}`
|
|
117
|
+
);
|
|
118
|
+
status.textContent='If the flags page did not open, copy its address above and paste it into your browser\'s address bar.';
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function copyAddress(address){
|
|
122
|
+
if(destroyed)return Promise.resolve(false);
|
|
123
|
+
if(copyTask)return copyTask;
|
|
124
|
+
copyStatus.textContent='Copying address...';
|
|
125
|
+
for(const choice of choices)choice.copyButton.disabled=true;
|
|
126
|
+
copyTask=writeAddress().finally(function finishAddressCopy(){
|
|
127
|
+
copyTask=null;
|
|
128
|
+
if(!destroyed){
|
|
129
|
+
for(const choice of choices)choice.copyButton.disabled=false;
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
return copyTask;
|
|
133
|
+
|
|
134
|
+
async function writeAddress(){
|
|
135
|
+
try{
|
|
136
|
+
await globalThis.navigator.clipboard.writeText(address);
|
|
137
|
+
if(destroyed)return false;
|
|
138
|
+
copyStatus.textContent='Address copied. Paste it into your browser\'s address bar.';
|
|
139
|
+
return true;
|
|
140
|
+
}catch(error){
|
|
141
|
+
if(!destroyed){
|
|
142
|
+
copyStatus.textContent='The address could not be copied automatically. Select the complete address above, copy it manually, and paste it into your browser\'s address bar.';
|
|
143
|
+
arcaneLogging.error('Unable to copy the browser WebNN flags address.',error);
|
|
144
|
+
}
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const addressList=root.querySelector('#flagAddresses');
|
|
151
|
+
for(const target of WEBNN_BROWSER_SETTINGS){
|
|
152
|
+
const row=document.createElement('section');
|
|
153
|
+
const heading=document.createElement('h3');
|
|
154
|
+
const address=document.createElement('code');
|
|
155
|
+
const actions=document.createElement('div');
|
|
156
|
+
const setupButton=document.createElement('button');
|
|
157
|
+
const copyButton=document.createElement('button');
|
|
158
|
+
row.className='flag-choice';
|
|
159
|
+
heading.textContent=target.name;
|
|
160
|
+
address.textContent=target.url;
|
|
161
|
+
address.tabIndex=0;
|
|
162
|
+
address.setAttribute('aria-label',`${target.name} flags address`);
|
|
163
|
+
actions.className='actions';
|
|
164
|
+
setupButton.type='button';
|
|
165
|
+
setupButton.className='arcane-button';
|
|
166
|
+
setupButton.addEventListener('click',function openSelectedBrowserSettings(){
|
|
167
|
+
return open(target.browserId);
|
|
168
|
+
},{signal:lifecycleController.signal});
|
|
169
|
+
copyButton.type='button';
|
|
170
|
+
copyButton.className='arcane-button arcane-button--secondary';
|
|
171
|
+
copyButton.textContent=`Copy ${target.name} address`;
|
|
172
|
+
copyButton.addEventListener('click',function copySelectedBrowserAddress(){
|
|
173
|
+
return copyAddress(target.url);
|
|
174
|
+
},{signal:lifecycleController.signal});
|
|
175
|
+
actions.append(setupButton,copyButton);
|
|
176
|
+
row.append(heading,address,actions);
|
|
177
|
+
addressList.append(row);
|
|
178
|
+
choices.push({row,target,setupButton,copyButton});
|
|
179
|
+
}
|
|
180
|
+
const stepList=root.querySelector('#setupSteps');
|
|
181
|
+
for(const step of setupSteps){
|
|
182
|
+
const item=document.createElement('li');
|
|
183
|
+
item.textContent=step;
|
|
184
|
+
stepList.append(item);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
root.querySelector('#refreshAvailability').addEventListener('click',function refreshBrowserAvailability(){
|
|
188
|
+
refresh();
|
|
189
|
+
status.textContent='Browser API availability refreshed.';
|
|
190
|
+
},{signal:lifecycleController.signal});
|
|
191
|
+
host.ownerDocument.defaultView.addEventListener('pagehide',function disposeBeforePageUnload(event){
|
|
192
|
+
if(!event.persisted)destroy();
|
|
193
|
+
},{
|
|
194
|
+
signal:lifecycleController.signal
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
function destroy(){
|
|
198
|
+
if(destroyed)return false;
|
|
199
|
+
destroyed=true;
|
|
200
|
+
host.ready=false;
|
|
201
|
+
lifecycleController.abort('browser-ai-setup-destroyed');
|
|
202
|
+
events.dispose();
|
|
203
|
+
return true;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
refresh();
|
|
207
|
+
host.ready=true;
|
|
208
|
+
const publication=events.dispatch('browser-ai-setup-ready',{}, {
|
|
209
|
+
publicDetail:{ready:true}
|
|
210
|
+
});
|
|
211
|
+
projectArcaneDOMEvent(host,publication.occurrence);
|
|
212
|
+
</script>
|