arcane-os 0.5.19 → 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 +26 -0
- package/README.md +18 -12
- package/browser-runtime/ai/browser-device-settings.mjs +83 -0
- package/browser-runtime/ai/browser-speech-providers.mjs +64 -60
- package/browser-runtime/ai/browser-wasm-llm-provider.mjs +4 -48
- package/browser-runtime/ai/speech-worker-runtime.mjs +4 -5
- package/docs/architecture.md +1 -1
- package/docs/reference/ai/browser-speech.md +95 -38
- package/docs/reference/behavioral-testing.md +1 -1
- package/docs/reference/inventory/runtime-modules.json +2 -2
- package/docs/reference/runtime-components.md +53 -0
- package/docs/reference/runtime-modules.md +32 -17
- package/docs/reference/sdk-api.md +32 -11
- package/examples/wasm-ai-demo/README.md +5 -3
- package/examples/wasm-ai-demo/app.js +1 -1
- package/package.json +1 -1
- package/runtime/arcane/components/browser-ai-setup.html +212 -0
- package/runtime/arcane/modules/AI.js +27 -29
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,31 @@
|
|
|
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
|
+
|
|
14
|
+
## 0.6.0
|
|
15
|
+
|
|
16
|
+
- Prefer WebNN NPU, then WebGPU, then CPU through WASM for browser Whisper
|
|
17
|
+
transcription and Kokoro synthesis. Skip absent accelerator APIs and replace
|
|
18
|
+
failed model-load Workers before trying the next backend with the same
|
|
19
|
+
application-selected model and precision.
|
|
20
|
+
- Add Whisper execution configuration and NPU selection for both speech roles.
|
|
21
|
+
Explicit `webnn-npu`, `webgpu`, and `wasm` choices report failure without
|
|
22
|
+
falling back. Whisper retains one slot; Kokoro retains four by default and
|
|
23
|
+
accepts capacities from one through four.
|
|
24
|
+
- Report requested and successfully selected backends for both roles through
|
|
25
|
+
provider execution status. Selection reports upstream session loading;
|
|
26
|
+
actual accelerator use and speech quality depend on the selected runtime,
|
|
27
|
+
model, browser, drivers, and hardware. Wllama remains on its WebGPU backend.
|
|
28
|
+
|
|
3
29
|
## 0.5.19
|
|
4
30
|
|
|
5
31
|
- Report observed Wllama initialization stages and runtime activity through
|
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.
|
|
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.
|
|
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
|
|
@@ -299,10 +299,16 @@ provider factories. The package contains the plain-JavaScript provider and
|
|
|
299
299
|
Worker machinery, not speech runtimes, models, voices, or a CDN default. An app
|
|
300
300
|
must supply each runtime/model selection explicitly. Speech roles
|
|
301
301
|
load, cancel, unload, fail, and recover independently, so speech failure never
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
302
|
+
switches to a cloud provider or prevents text chat. Both roles default to
|
|
303
|
+
NPU, GPU, then CPU automatic selection: WebNN NPU when its browser API is
|
|
304
|
+
exposed, then WebGPU, then WASM. Failed model loads release their Workers before
|
|
305
|
+
the next backend uses the same model and precision. Apps may select
|
|
306
|
+
`webnn-npu`, `webgpu`, or `wasm` explicitly without automatic fallback.
|
|
307
|
+
Whisper keeps one transcription slot; Kokoro defaults to four Worker/model
|
|
308
|
+
sessions and accepts capacities from one through four. Both roles expose
|
|
309
|
+
requested and selected devices through execution status. A selected backend
|
|
310
|
+
reports successful upstream loading, not physical accelerator use for every
|
|
311
|
+
operation; model and hardware compatibility remain upstream.
|
|
306
312
|
|
|
307
313
|
Applications that need faster spoken-response onset can configure the shared
|
|
308
314
|
TTS stream without taking over synthesis or playback:
|
|
@@ -357,7 +363,7 @@ uses the same controller for automatic memory extraction.
|
|
|
357
363
|
Create a new repository-shaped Arcane application with the exact stable SDK:
|
|
358
364
|
|
|
359
365
|
```bash
|
|
360
|
-
npx arcane-os@0.
|
|
366
|
+
npx arcane-os@0.6.1 new my-app --path ./my-app --target portable --git
|
|
361
367
|
cd my-app
|
|
362
368
|
npm install
|
|
363
369
|
npm run dev
|
|
@@ -367,7 +373,7 @@ To enroll an existing repository, install the exact SDK and initialize only
|
|
|
367
373
|
missing Arcane files:
|
|
368
374
|
|
|
369
375
|
```bash
|
|
370
|
-
npm install --save-dev --save-exact arcane-os@0.
|
|
376
|
+
npm install --save-dev --save-exact arcane-os@0.6.1
|
|
371
377
|
npm exec -- arcane init my-app --target portable
|
|
372
378
|
```
|
|
373
379
|
|
|
@@ -383,7 +389,7 @@ npm exec -- arcane-os targets
|
|
|
383
389
|
No global SDK install or standalone Arcane CLI is required. The application
|
|
384
390
|
repository's exact npm dependency and lockfile own the CLI and toolchain version.
|
|
385
391
|
|
|
386
|
-
Use `npx arcane-os@0.
|
|
392
|
+
Use `npx arcane-os@0.6.1` for the initial bootstrap because it names this npm
|
|
387
393
|
package explicitly; bare `npx arcane` outside an installed project could resolve
|
|
388
394
|
a different package. Both installed commands invoke the same headless toolchain.
|
|
389
395
|
Project-local npm scripts use the SDK pinned by that app's `package-lock.json`,
|
|
@@ -403,7 +409,7 @@ node ./bin/arcane.mjs new local-app --path ../local-app --target portable --git
|
|
|
403
409
|
|
|
404
410
|
# From the generated app repository
|
|
405
411
|
cd ../local-app
|
|
406
|
-
npm install --save-dev --save-exact ../arcane-os-sdk/arcane-os-0.
|
|
412
|
+
npm install --save-dev --save-exact ../arcane-os-sdk/arcane-os-0.6.1.tgz
|
|
407
413
|
npm ci
|
|
408
414
|
```
|
|
409
415
|
|
|
@@ -412,7 +418,7 @@ same location. The lockfile retains the selected package dependency while
|
|
|
412
418
|
Arcane uses the installed package name and version. Local directory `file:` dependencies are not
|
|
413
419
|
accepted because npm may install them as links; use a packed `.tgz`. A GitHub
|
|
414
420
|
runner also needs that tarball at the locked path. After publication, replace
|
|
415
|
-
the local declaration with the exact `arcane-os@0.
|
|
421
|
+
the local declaration with the exact `arcane-os@0.6.1` registry package and
|
|
416
422
|
commit the regenerated lock.
|
|
417
423
|
|
|
418
424
|
Generated repositories use `npm ci --ignore-scripts` in CI. Run dependency
|
|
@@ -551,7 +557,7 @@ package installation, or assertions.
|
|
|
551
557
|
|
|
552
558
|
## Current target support
|
|
553
559
|
|
|
554
|
-
Version `0.
|
|
560
|
+
Version `0.6.1` exposes one browser target and five explicitly paired
|
|
555
561
|
native development targets: a non-runnable portable directory, a
|
|
556
562
|
Windows x64 unsigned-local-test EXE bundle, Linux x64 and Linux ARM64
|
|
557
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
|
+
}
|
|
@@ -23,8 +23,8 @@ const ROLE_OPERATION = completeValue({ stt: "transcribe", tts: "synthesize" });
|
|
|
23
23
|
const STT_SAMPLE_RATE = 16_000;
|
|
24
24
|
const TTS_SAMPLE_RATE = 24_000;
|
|
25
25
|
const TTS_RESPONSE_FORMAT = "wav";
|
|
26
|
-
const
|
|
27
|
-
const
|
|
26
|
+
const SPEECH_EXECUTION_DEVICES = new Set(["auto", "webnn-npu", "webgpu", "wasm"]);
|
|
27
|
+
const DEFAULT_SPEECH_EXECUTION_DEVICE = "auto";
|
|
28
28
|
const DEFAULT_TTS_MAX_CONCURRENT_REQUESTS = 4;
|
|
29
29
|
const MAX_TTS_CONCURRENT_REQUESTS = 4;
|
|
30
30
|
const ROLE_REQUEST_REASON = completeValue({
|
|
@@ -150,24 +150,21 @@ function requiredIdentifier(value, label) {
|
|
|
150
150
|
}
|
|
151
151
|
|
|
152
152
|
function normalizeSpeechExecution(role, execution) {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
}
|
|
157
|
-
return completeValue({ device: "wasm", maxConcurrentRequests: 1 });
|
|
158
|
-
}
|
|
153
|
+
const label = role === "stt" ? "Browser Whisper" : "Browser Kokoro";
|
|
154
|
+
const defaultConcurrency = role === "stt" ? 1 : DEFAULT_TTS_MAX_CONCURRENT_REQUESTS;
|
|
155
|
+
const maximumConcurrency = role === "stt" ? 1 : MAX_TTS_CONCURRENT_REQUESTS;
|
|
159
156
|
if (execution === undefined) {
|
|
160
157
|
return completeValue({
|
|
161
|
-
device:
|
|
162
|
-
maxConcurrentRequests:
|
|
158
|
+
device: DEFAULT_SPEECH_EXECUTION_DEVICE,
|
|
159
|
+
maxConcurrentRequests: defaultConcurrency,
|
|
163
160
|
});
|
|
164
161
|
}
|
|
165
162
|
if (!execution || !is.object(execution) || is.array(execution)) {
|
|
166
|
-
throw new TypeError(
|
|
163
|
+
throw new TypeError(`${label} execution must be a plain data record.`);
|
|
167
164
|
}
|
|
168
165
|
const prototype = Object.getPrototypeOf(execution);
|
|
169
166
|
if (prototype !== Object.prototype && prototype !== null) {
|
|
170
|
-
throw new TypeError(
|
|
167
|
+
throw new TypeError(`${label} execution must be a plain data record.`);
|
|
171
168
|
}
|
|
172
169
|
const descriptors = Object.getOwnPropertyDescriptors(execution);
|
|
173
170
|
for (const key of Reflect.ownKeys(descriptors)) {
|
|
@@ -175,34 +172,35 @@ function normalizeSpeechExecution(role, execution) {
|
|
|
175
172
|
(key !== "device" && key !== "maxConcurrentRequests")
|
|
176
173
|
|| !Object.hasOwn(descriptors[key], "value")
|
|
177
174
|
) {
|
|
178
|
-
throw new TypeError(
|
|
175
|
+
throw new TypeError(`${label} execution contains an unsupported or accessor field.`);
|
|
179
176
|
}
|
|
180
177
|
}
|
|
181
178
|
const device = Object.hasOwn(descriptors, "device")
|
|
182
179
|
? descriptors.device.value
|
|
183
|
-
:
|
|
180
|
+
: DEFAULT_SPEECH_EXECUTION_DEVICE;
|
|
184
181
|
const maxConcurrentRequests = Object.hasOwn(descriptors, "maxConcurrentRequests")
|
|
185
182
|
? descriptors.maxConcurrentRequests.value
|
|
186
|
-
:
|
|
187
|
-
if (!
|
|
188
|
-
throw new TypeError(
|
|
183
|
+
: defaultConcurrency;
|
|
184
|
+
if (!SPEECH_EXECUTION_DEVICES.has(device)) {
|
|
185
|
+
throw new TypeError(`${label} execution.device must be "auto", "webnn-npu", "webgpu", or "wasm".`);
|
|
189
186
|
}
|
|
190
187
|
if (
|
|
191
188
|
!is.safeInteger(maxConcurrentRequests)
|
|
192
189
|
|| maxConcurrentRequests < 1
|
|
193
|
-
|| maxConcurrentRequests >
|
|
190
|
+
|| maxConcurrentRequests > maximumConcurrency
|
|
194
191
|
) {
|
|
195
|
-
throw new TypeError(
|
|
192
|
+
throw new TypeError(`${label} execution.maxConcurrentRequests must be a safe integer from 1 through ${maximumConcurrency}.`);
|
|
196
193
|
}
|
|
197
194
|
return completeValue({ device, maxConcurrentRequests });
|
|
198
195
|
}
|
|
199
196
|
|
|
200
|
-
function
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
197
|
+
function speechExecutionDevices(requestedDevice) {
|
|
198
|
+
if (requestedDevice !== "auto") return [requestedDevice];
|
|
199
|
+
const devices = [];
|
|
200
|
+
if (is.function(globalThis.navigator?.ml?.createContext)) devices.push("webnn-npu");
|
|
201
|
+
if (globalThis.navigator?.gpu) devices.push("webgpu");
|
|
202
|
+
devices.push("wasm");
|
|
203
|
+
return devices;
|
|
206
204
|
}
|
|
207
205
|
|
|
208
206
|
function createProviderAuthority({
|
|
@@ -1187,14 +1185,12 @@ function createBrowserSpeechProvider({
|
|
|
1187
1185
|
cache,
|
|
1188
1186
|
lifecycleReason,
|
|
1189
1187
|
activeOperation,
|
|
1190
|
-
execution:
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
})
|
|
1197
|
-
: null,
|
|
1188
|
+
execution: completeValue({
|
|
1189
|
+
requestedDevice: speechExecution.device,
|
|
1190
|
+
selectedDevice,
|
|
1191
|
+
maxConcurrentRequests: speechExecution.maxConcurrentRequests,
|
|
1192
|
+
activeRequestCount: requestOperations.size,
|
|
1193
|
+
}),
|
|
1198
1194
|
secureIntent,
|
|
1199
1195
|
warnings: providerWarnings(runtimeWarnings),
|
|
1200
1196
|
});
|
|
@@ -1535,34 +1531,42 @@ function createBrowserSpeechProvider({
|
|
|
1535
1531
|
`${role}-load-superseded-by-unload`,
|
|
1536
1532
|
);
|
|
1537
1533
|
}
|
|
1538
|
-
const
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
) {
|
|
1558
|
-
|
|
1534
|
+
const devices = speechExecutionDevices(speechExecution.device);
|
|
1535
|
+
for (const [index, device] of devices.entries()) {
|
|
1536
|
+
throwIfAborted(linked.controller.signal, `${role}-load-cancelled`);
|
|
1537
|
+
if (operationGeneration !== generation) {
|
|
1538
|
+
throw providerError(
|
|
1539
|
+
"ARCANE_AI_OPERATION_SUPERSEDED",
|
|
1540
|
+
"Browser speech loading was superseded.",
|
|
1541
|
+
undefined,
|
|
1542
|
+
`${role}-load-superseded-by-unload`,
|
|
1543
|
+
);
|
|
1544
|
+
}
|
|
1545
|
+
try {
|
|
1546
|
+
pool = await loadWorkerPool(
|
|
1547
|
+
preparation,
|
|
1548
|
+
device,
|
|
1549
|
+
record.warnings,
|
|
1550
|
+
linked.controller.signal,
|
|
1551
|
+
);
|
|
1552
|
+
break;
|
|
1553
|
+
} catch (error) {
|
|
1554
|
+
if (
|
|
1555
|
+
index === devices.length - 1
|
|
1556
|
+
|| linked.controller.signal.aborted
|
|
1557
|
+
|| operationGeneration !== generation
|
|
1558
|
+
|| error?.code === "ARCANE_AI_REQUEST_ABORTED"
|
|
1559
|
+
|| error?.code === "ARCANE_AI_OPERATION_SUPERSEDED"
|
|
1560
|
+
) {
|
|
1561
|
+
throw error;
|
|
1562
|
+
}
|
|
1563
|
+
// A fresh Worker releases a failed upstream initialization before
|
|
1564
|
+
// the next backend reuses the same prepared model and precision.
|
|
1565
|
+
const warning = `Browser ${role} ${device} loading failed; trying ${devices[index + 1]}.`;
|
|
1566
|
+
record.warnings = [...record.warnings, warning];
|
|
1567
|
+
lastWarnings = record.warnings;
|
|
1568
|
+
console.warn(warning, error);
|
|
1559
1569
|
}
|
|
1560
|
-
pool = await loadWorkerPool(
|
|
1561
|
-
preparation,
|
|
1562
|
-
"wasm",
|
|
1563
|
-
record.warnings,
|
|
1564
|
-
linked.controller.signal,
|
|
1565
|
-
);
|
|
1566
1570
|
}
|
|
1567
1571
|
throwIfAborted(
|
|
1568
1572
|
linked.controller.signal,
|
|
@@ -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. "
|
|
@@ -738,10 +738,9 @@ function validateConfiguration(configuration, role) {
|
|
|
738
738
|
if (
|
|
739
739
|
descriptorMismatch
|
|
740
740
|
|| !Object.hasOwn(descriptors, "device")
|
|
741
|
-
|| (
|
|
742
|
-
|
|
743
|
-
&& descriptors.device.value !== "
|
|
744
|
-
&& descriptors.device.value !== "webgpu")
|
|
741
|
+
|| (descriptors.device.value !== "wasm"
|
|
742
|
+
&& descriptors.device.value !== "webgpu"
|
|
743
|
+
&& descriptors.device.value !== "webnn-npu")
|
|
745
744
|
) {
|
|
746
745
|
throw workerError(
|
|
747
746
|
"ARCANE_AI_INVALID_REQUEST",
|
|
@@ -1367,7 +1366,7 @@ async function createWhisperEngine(namespace, configuration, signal, report) {
|
|
|
1367
1366
|
"automatic-speech-recognition",
|
|
1368
1367
|
configuration.model.repository,
|
|
1369
1368
|
{
|
|
1370
|
-
device: "wasm",
|
|
1369
|
+
device: configuration.execution?.device ?? "wasm",
|
|
1371
1370
|
dtype: configuration.model.dtype ?? "fp32",
|
|
1372
1371
|
revision: configuration.model.revision,
|
|
1373
1372
|
progress_callback: report,
|
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.
|
|
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
|