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 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.5.19` SDK contract. Applications pin one exact npm
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.5.19 new hello-speech --path ./hello-speech --target browser
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
- silently falls back or prevents text chat. Kokoro defaults to a four-slot Worker
303
- and model-session pool: it selects WebGPU when the browser can load the complete
304
- pool and otherwise recreates that pool on WASM. Apps may select `webgpu` or
305
- `wasm` explicitly and may set the bounded TTS capacity from one through four.
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.5.19 new my-app --path ./my-app --target portable --git
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.5.19
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.5.19` for the initial bootstrap because it names this npm
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.5.19.tgz
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.5.19` registry package and
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.5.19` exposes one browser target and five explicitly paired
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 TTS_EXECUTION_DEVICES = new Set(["auto", "webgpu", "wasm"]);
27
- const DEFAULT_TTS_EXECUTION_DEVICE = "auto";
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
- if (role === "stt") {
154
- if (execution !== undefined) {
155
- throw new TypeError("Browser Whisper does not accept an execution option.");
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: DEFAULT_TTS_EXECUTION_DEVICE,
162
- maxConcurrentRequests: DEFAULT_TTS_MAX_CONCURRENT_REQUESTS,
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("Browser Kokoro execution must be a plain data record.");
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("Browser Kokoro execution must be a plain data record.");
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("Browser Kokoro execution contains an unsupported or accessor field.");
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
- : DEFAULT_TTS_EXECUTION_DEVICE;
180
+ : DEFAULT_SPEECH_EXECUTION_DEVICE;
184
181
  const maxConcurrentRequests = Object.hasOwn(descriptors, "maxConcurrentRequests")
185
182
  ? descriptors.maxConcurrentRequests.value
186
- : DEFAULT_TTS_MAX_CONCURRENT_REQUESTS;
187
- if (!TTS_EXECUTION_DEVICES.has(device)) {
188
- throw new TypeError('Browser Kokoro execution.device must be "auto", "webgpu", or "wasm".');
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 > MAX_TTS_CONCURRENT_REQUESTS
190
+ || maxConcurrentRequests > maximumConcurrency
194
191
  ) {
195
- throw new TypeError("Browser Kokoro execution.maxConcurrentRequests must be a safe integer from 1 through 4.");
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 navigatorHasWebGpu() {
201
- try {
202
- return Boolean(globalThis.navigator?.gpu);
203
- } catch {
204
- return false;
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: role === "tts"
1191
- ? completeValue({
1192
- requestedDevice: speechExecution.device,
1193
- selectedDevice,
1194
- maxConcurrentRequests: speechExecution.maxConcurrentRequests,
1195
- activeRequestCount: requestOperations.size,
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 primaryDevice = role === "stt"
1539
- ? "wasm"
1540
- : speechExecution.device === "auto"
1541
- ? navigatorHasWebGpu() ? "webgpu" : "wasm"
1542
- : speechExecution.device;
1543
- try {
1544
- pool = await loadWorkerPool(
1545
- preparation,
1546
- primaryDevice,
1547
- record.warnings,
1548
- linked.controller.signal,
1549
- );
1550
- } catch (error) {
1551
- if (
1552
- role !== "tts"
1553
- || speechExecution.device !== "auto"
1554
- || primaryDevice !== "webgpu"
1555
- || linked.controller.signal.aborted
1556
- || operationGeneration !== generation
1557
- ) {
1558
- throw error;
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 = highPerformanceGpuBrowser();
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
- globalThis.alert?.(
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
- || (role === "stt" && descriptors.device.value !== "wasm")
742
- || (role === "tts"
743
- && descriptors.device.value !== "wasm"
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,
@@ -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.5.19` runtime requires Arcane `0.8.12` or newer. Compatibility
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