arcane-os 0.7.0 → 0.7.2
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 +23 -0
- package/NOTICE +6 -11
- package/README.md +8 -8
- package/browser-runtime/ai/browser-device-settings.mjs +24 -20
- package/browser-runtime/ai/browser-speech-artifacts.mjs +86 -0
- package/browser-runtime/ai/browser-speech-providers.mjs +71 -7
- package/browser-runtime/ai/browser-wasm-llm-provider.mjs +15 -12
- package/browser-runtime/ai/browser-wllama-runtime.mjs +41 -2
- package/browser-runtime/ai/speech-worker-client.mjs +10 -2
- package/browser-runtime/ai/speech-worker-runtime.mjs +67 -3
- package/browser-runtime/ai/wllama/index.mjs +9 -9
- package/docs/architecture.md +1 -1
- package/docs/reference/ai/browser-speech.md +33 -15
- package/docs/reference/ai/browser-wasm.md +36 -28
- package/docs/reference/inventory/runtime-components.json +4 -4
- package/docs/reference/protocols.md +12 -5
- package/docs/reference/runtime-components.md +33 -22
- package/docs/reference/runtime-modules.md +7 -1
- package/docs/reference/sdk-api.md +5 -3
- package/package.json +1 -1
- package/runtime/arcane/components/browser-ai-setup.html +189 -179
- package/runtime/arcane/components/speech.html +34 -3
- package/runtime/arcane/components/voice-transcription.html +30 -3
- package/runtime/arcane/modules/ComponentContracts.js +86 -15
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.7.2
|
|
4
|
+
|
|
5
|
+
- Keep both supported browser NPU and GPU flag address copy controls available
|
|
6
|
+
regardless of API availability, GPU classification, or detection failure.
|
|
7
|
+
Use one short explanation per flag and one shared paste/enable/relaunch
|
|
8
|
+
instruction. Preserve browser-reported statuses and the GPU-only chat notice.
|
|
9
|
+
|
|
10
|
+
## 0.7.1
|
|
11
|
+
|
|
12
|
+
- Make Profile NPU and GPU setup concise and copy-only, with **Copy NPU flag
|
|
13
|
+
address** as the NPU action. Confirm a reported discrete GPU with **Already
|
|
14
|
+
using the performance GPU.** and show GPU setup only for explicitly reported
|
|
15
|
+
integrated or software adapters. Unknown GPU classes receive no flag advice.
|
|
16
|
+
- Retain the loaded Wllama Worker's adapter details for the GPU-only chat alert.
|
|
17
|
+
Show that alert only for an explicitly reported integrated or software adapter,
|
|
18
|
+
with manual browser-address instructions and no attempted navigation.
|
|
19
|
+
- Preserve complete adapter descriptions and optional class/fallback metadata
|
|
20
|
+
through the Wllama projection. Remove obsolete projection identity gates.
|
|
21
|
+
- Forward speech artifact and upstream model loading progress through sticky AI
|
|
22
|
+
state. Show the current file, completed files, elapsed time, and initialization
|
|
23
|
+
in both transcription controls, with indeterminate progress for unknown totals.
|
|
24
|
+
Preserve cancellation, complete diagnostics, models, and inference precision.
|
|
25
|
+
|
|
3
26
|
## 0.7.0
|
|
4
27
|
|
|
5
28
|
- Add `arcane dev --public`, including forwarding through
|
package/NOTICE
CHANGED
|
@@ -8,27 +8,24 @@ informational notice and is not itself a grant of commercial rights.
|
|
|
8
8
|
Third-party material retains its own terms:
|
|
9
9
|
|
|
10
10
|
- event-pubsub 6.1.0: MIT License; see its installed `licence` file.
|
|
11
|
-
- strong-type 2.0.
|
|
11
|
+
- strong-type 2.0.1: MIT License; see its installed `licence` file.
|
|
12
12
|
- vanilla-test 2.1.3: MIT License; see its installed `licence` file.
|
|
13
13
|
- ansi-colors-es6 5.0.0: MIT License; see its installed `LICENSE` file.
|
|
14
14
|
- vanilla-test's strong-type 2.0.1 dependency: MIT License; see its installed
|
|
15
15
|
`licence` file.
|
|
16
|
-
- bundled strong-type
|
|
16
|
+
- bundled strong-type 2.0.1: MIT License; see runtime/strong-type/licence.
|
|
17
17
|
- uPlot: MIT License; see runtime/arcane/modules/uPlot.LICENSE.txt.
|
|
18
18
|
- @wllama/wllama 3.6.0 (source revision
|
|
19
19
|
f16050d8d51a00602c6a2a6b8ac9c09f490eea7f): MIT License; see
|
|
20
20
|
browser-runtime/ai/wllama/LICENCE.
|
|
21
21
|
- llama.cpp b10454 (source revision
|
|
22
|
-
4df29be4f4c3673f428170fda944a5b19f743bb8), embedded in the
|
|
22
|
+
4df29be4f4c3673f428170fda944a5b19f743bb8), embedded in the
|
|
23
23
|
Wllama runtime: MIT License; see browser-runtime/ai/wllama/llama.cpp-LICENSE.
|
|
24
24
|
- Marked: MIT License; complete notice and terms reproduced below.
|
|
25
25
|
- QRCode.js: MIT License; complete notice and terms reproduced below.
|
|
26
26
|
|
|
27
|
-
No model weights, speech runtimes, or speech model/voice
|
|
28
|
-
this package. Browser
|
|
29
|
-
authorities. Byte-length and SHA-256 checks inherit the app security policy,
|
|
30
|
-
default to disabled, and may be enabled or overridden for one provider/load;
|
|
31
|
-
an enabled check requires its expected value before cache admission.
|
|
27
|
+
No model weights, speech runtimes, or speech model/voice files are included in
|
|
28
|
+
this package. Browser models and speech resources use their upstream downloads.
|
|
32
29
|
|
|
33
30
|
Marked
|
|
34
31
|
------
|
|
@@ -57,9 +54,7 @@ SOFTWARE.
|
|
|
57
54
|
QRCode.js
|
|
58
55
|
---------
|
|
59
56
|
|
|
60
|
-
runtime/arcane/modules/QRCode.min.js
|
|
61
|
-
from the davidshimjs/qrcodejs master branch. Its SHA-256 is
|
|
62
|
-
c541ef06327885a8415bca8df6071e14189b4855336def4f36db54bde8484f36.
|
|
57
|
+
runtime/arcane/modules/QRCode.min.js comes from davidshimjs/qrcodejs.
|
|
63
58
|
|
|
64
59
|
Copyright (c) 2012 davidshimjs
|
|
65
60
|
|
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.7.
|
|
22
|
+
This checkout defines the `0.7.2` 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.7.
|
|
38
|
+
npx arcane-os@0.7.2 new hello-speech --path ./hello-speech --target browser
|
|
39
39
|
cd hello-speech
|
|
40
40
|
npm install
|
|
41
41
|
npm run dev
|
|
@@ -378,7 +378,7 @@ uses the same controller for automatic memory extraction.
|
|
|
378
378
|
Create a new repository-shaped Arcane application with the exact stable SDK:
|
|
379
379
|
|
|
380
380
|
```bash
|
|
381
|
-
npx arcane-os@0.7.
|
|
381
|
+
npx arcane-os@0.7.2 new my-app --path ./my-app --target portable --git
|
|
382
382
|
cd my-app
|
|
383
383
|
npm install
|
|
384
384
|
npm run dev
|
|
@@ -388,7 +388,7 @@ To enroll an existing repository, install the exact SDK and initialize only
|
|
|
388
388
|
missing Arcane files:
|
|
389
389
|
|
|
390
390
|
```bash
|
|
391
|
-
npm install --save-dev --save-exact arcane-os@0.7.
|
|
391
|
+
npm install --save-dev --save-exact arcane-os@0.7.2
|
|
392
392
|
npm exec -- arcane init my-app --target portable
|
|
393
393
|
```
|
|
394
394
|
|
|
@@ -404,7 +404,7 @@ npm exec -- arcane-os targets
|
|
|
404
404
|
No global SDK install or standalone Arcane CLI is required. The application
|
|
405
405
|
repository's exact npm dependency and lockfile own the CLI and toolchain version.
|
|
406
406
|
|
|
407
|
-
Use `npx arcane-os@0.7.
|
|
407
|
+
Use `npx arcane-os@0.7.2` for the initial bootstrap because it names this npm
|
|
408
408
|
package explicitly; bare `npx arcane` outside an installed project could resolve
|
|
409
409
|
a different package. Both installed commands invoke the same headless toolchain.
|
|
410
410
|
Project-local npm scripts use the SDK pinned by that app's `package-lock.json`,
|
|
@@ -424,7 +424,7 @@ node ./bin/arcane.mjs new local-app --path ../local-app --target portable --git
|
|
|
424
424
|
|
|
425
425
|
# From the generated app repository
|
|
426
426
|
cd ../local-app
|
|
427
|
-
npm install --save-dev --save-exact ../arcane-os-sdk/arcane-os-0.7.
|
|
427
|
+
npm install --save-dev --save-exact ../arcane-os-sdk/arcane-os-0.7.2.tgz
|
|
428
428
|
npm ci
|
|
429
429
|
```
|
|
430
430
|
|
|
@@ -433,7 +433,7 @@ same location. The lockfile retains the selected package dependency while
|
|
|
433
433
|
Arcane uses the installed package name and version. Local directory `file:` dependencies are not
|
|
434
434
|
accepted because npm may install them as links; use a packed `.tgz`. A GitHub
|
|
435
435
|
runner also needs that tarball at the locked path. After publication, replace
|
|
436
|
-
the local declaration with the exact `arcane-os@0.7.
|
|
436
|
+
the local declaration with the exact `arcane-os@0.7.2` registry package and
|
|
437
437
|
commit the regenerated lock.
|
|
438
438
|
|
|
439
439
|
Generated repositories use `npm ci --ignore-scripts` in CI. Run dependency
|
|
@@ -572,7 +572,7 @@ package installation, or assertions.
|
|
|
572
572
|
|
|
573
573
|
## Current target support
|
|
574
574
|
|
|
575
|
-
Version `0.7.
|
|
575
|
+
Version `0.7.2` exposes one browser target and five explicitly paired
|
|
576
576
|
native development targets: a non-runnable portable directory, a
|
|
577
577
|
Windows x64 unsigned-local-test EXE bundle, Linux x64 and Linux ARM64
|
|
578
578
|
unsigned-local-test DEBs, and an Android development-signed APK. The
|
|
@@ -7,17 +7,6 @@ export const WEBNN_BROWSER_SETTINGS = [
|
|
|
7
7
|
{ browserId: "edge", name: "Microsoft Edge", url: "edge://flags/#web-machine-learning-neural-network" },
|
|
8
8
|
];
|
|
9
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
10
|
// Browser identity selects setup instructions, never hardware or model readiness.
|
|
22
11
|
export function getBrowserDeviceSettings(navigatorObject = globalThis.navigator) {
|
|
23
12
|
const userAgent = String(navigatorObject?.userAgent ?? "");
|
|
@@ -83,22 +72,37 @@ export function getBrowserDeviceSettings(navigatorObject = globalThis.navigator)
|
|
|
83
72
|
};
|
|
84
73
|
}
|
|
85
74
|
|
|
86
|
-
//
|
|
87
|
-
//
|
|
75
|
+
// Only an explicit browser adapter type or fallback flag establishes its class.
|
|
76
|
+
// Vendor names and powerPreference (including Chromium's echoed request) do not.
|
|
77
|
+
export function describeBrowserGpu(info, legacyFallbackAdapter) {
|
|
78
|
+
const adapterType = is.string(info?.type) ? info.type : null;
|
|
79
|
+
const isFallbackAdapter = is.boolean(info?.isFallbackAdapter)
|
|
80
|
+
? info.isFallbackAdapter
|
|
81
|
+
: is.boolean(legacyFallbackAdapter) ? legacyFallbackAdapter : null;
|
|
82
|
+
let performanceStatus = 'unknown';
|
|
83
|
+
if (isFallbackAdapter === true || adapterType === 'CPU') performanceStatus = 'fallback';
|
|
84
|
+
else if (adapterType === 'discrete GPU') performanceStatus = 'discrete';
|
|
85
|
+
else if (adapterType === 'integrated GPU') performanceStatus = 'integrated';
|
|
86
|
+
return {
|
|
87
|
+
name: info?.description || info?.name
|
|
88
|
+
|| [info?.vendor, info?.architecture, info?.device].filter(Boolean).join(' ')
|
|
89
|
+
|| 'WebGPU adapter',
|
|
90
|
+
adapterType,
|
|
91
|
+
isFallbackAdapter,
|
|
92
|
+
performanceStatus
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// This is the adapter returned to this page, not another page's model adapter.
|
|
97
|
+
// Requesting high performance is a hint, not evidence that it was honored.
|
|
88
98
|
export async function detectBrowserGpu(navigatorObject = globalThis.navigator) {
|
|
89
99
|
if (!is.function(navigatorObject?.gpu?.requestAdapter)) {
|
|
90
100
|
return { available: false, reason: "api-unavailable" };
|
|
91
101
|
}
|
|
92
102
|
const adapter = await navigatorObject.gpu.requestAdapter({ powerPreference: "high-performance" });
|
|
93
103
|
if (!adapter) return { available: false, reason: "adapter-unavailable" };
|
|
94
|
-
const info = adapter.info;
|
|
95
104
|
return {
|
|
96
105
|
available: true,
|
|
97
|
-
|
|
98
|
-
|| [info?.vendor, info?.architecture, info?.device].filter(Boolean).join(" ")
|
|
99
|
-
|| "WebGPU adapter",
|
|
100
|
-
isFallbackAdapter: is.boolean(info?.isFallbackAdapter)
|
|
101
|
-
? info.isFallbackAdapter
|
|
102
|
-
: is.boolean(adapter.isFallbackAdapter) ? adapter.isFallbackAdapter : null,
|
|
106
|
+
...describeBrowserGpu(adapter.info, adapter.isFallbackAdapter),
|
|
103
107
|
};
|
|
104
108
|
}
|
|
@@ -1962,6 +1962,10 @@ export function createDbopfsSpeechArtifactStore({
|
|
|
1962
1962
|
return artifactGraphError(graphFileReason(descriptor, boundary), message);
|
|
1963
1963
|
}
|
|
1964
1964
|
|
|
1965
|
+
function reportArtifactProgress(onProgress, signal, progress) {
|
|
1966
|
+
if (!signal?.aborted && is.function(onProgress)) onProgress(progress);
|
|
1967
|
+
}
|
|
1968
|
+
|
|
1965
1969
|
async function openCached(authority, { signal, onProgress } = {}) {
|
|
1966
1970
|
const graph = ARTIFACT_GRAPHS.has(authority);
|
|
1967
1971
|
const metadata = artifactMetadata(authority);
|
|
@@ -1975,14 +1979,50 @@ export function createDbopfsSpeechArtifactStore({
|
|
|
1975
1979
|
for (let index = 0; index < metadata.files.length; index += 1) {
|
|
1976
1980
|
throwIfAborted(signal);
|
|
1977
1981
|
const descriptor = metadata.files[index];
|
|
1982
|
+
reportArtifactProgress(
|
|
1983
|
+
onProgress,
|
|
1984
|
+
signal,
|
|
1985
|
+
{
|
|
1986
|
+
phase: 'prepare',
|
|
1987
|
+
stage: 'cache',
|
|
1988
|
+
message: 'Opening cached speech file',
|
|
1989
|
+
file: descriptor.path,
|
|
1990
|
+
completed: index,
|
|
1991
|
+
total: metadata.files.length,
|
|
1992
|
+
unit: 'files',
|
|
1993
|
+
}
|
|
1994
|
+
);
|
|
1978
1995
|
const file = await readFile(names.files[index]);
|
|
1979
1996
|
if (!file) {
|
|
1980
1997
|
await removeUnlocked(authority);
|
|
1981
1998
|
return null;
|
|
1982
1999
|
}
|
|
1983
2000
|
files.push({ descriptor, file });
|
|
2001
|
+
reportArtifactProgress(
|
|
2002
|
+
onProgress,
|
|
2003
|
+
signal,
|
|
2004
|
+
{
|
|
2005
|
+
phase: 'prepare',
|
|
2006
|
+
stage: 'cache',
|
|
2007
|
+
message: 'Opened cached speech file',
|
|
2008
|
+
file: descriptor.path,
|
|
2009
|
+
completed: files.length,
|
|
2010
|
+
total: metadata.files.length,
|
|
2011
|
+
unit: 'files',
|
|
2012
|
+
}
|
|
2013
|
+
);
|
|
1984
2014
|
}
|
|
1985
2015
|
try {
|
|
2016
|
+
reportArtifactProgress(
|
|
2017
|
+
onProgress,
|
|
2018
|
+
signal,
|
|
2019
|
+
{
|
|
2020
|
+
phase: 'prepare',
|
|
2021
|
+
stage: 'runtime',
|
|
2022
|
+
message: 'Preparing speech runtime modules',
|
|
2023
|
+
total: null,
|
|
2024
|
+
}
|
|
2025
|
+
);
|
|
1986
2026
|
const routing = graph
|
|
1987
2027
|
? await planOrdinaryMaterializedRuntime({ files }, metadata, signal)
|
|
1988
2028
|
: null;
|
|
@@ -2020,6 +2060,19 @@ export function createDbopfsSpeechArtifactStore({
|
|
|
2020
2060
|
throwIfAborted(signal);
|
|
2021
2061
|
const descriptor = metadata.files[index];
|
|
2022
2062
|
const sourceUrl = graph ? descriptor.sourceUrl : descriptor.url;
|
|
2063
|
+
reportArtifactProgress(
|
|
2064
|
+
onProgress,
|
|
2065
|
+
signal,
|
|
2066
|
+
{
|
|
2067
|
+
phase: 'download',
|
|
2068
|
+
stage: 'artifacts',
|
|
2069
|
+
message: 'Downloading speech file',
|
|
2070
|
+
file: descriptor.path,
|
|
2071
|
+
completed: index,
|
|
2072
|
+
total: metadata.files.length,
|
|
2073
|
+
unit: 'files',
|
|
2074
|
+
}
|
|
2075
|
+
);
|
|
2023
2076
|
let response;
|
|
2024
2077
|
try {
|
|
2025
2078
|
response = await fetchFunction(sourceUrl, {
|
|
@@ -2095,7 +2148,30 @@ export function createDbopfsSpeechArtifactStore({
|
|
|
2095
2148
|
throw speechError("ARCANE_AI_ARTIFACT_CACHE_REJECTED", "DBOPFS did not preserve a speech artifact.");
|
|
2096
2149
|
}
|
|
2097
2150
|
installed.push({ descriptor, file });
|
|
2151
|
+
reportArtifactProgress(
|
|
2152
|
+
onProgress,
|
|
2153
|
+
signal,
|
|
2154
|
+
{
|
|
2155
|
+
phase: 'download',
|
|
2156
|
+
stage: 'artifacts',
|
|
2157
|
+
message: 'Stored speech file',
|
|
2158
|
+
file: descriptor.path,
|
|
2159
|
+
completed: installed.length,
|
|
2160
|
+
total: metadata.files.length,
|
|
2161
|
+
unit: 'files',
|
|
2162
|
+
}
|
|
2163
|
+
);
|
|
2098
2164
|
}
|
|
2165
|
+
reportArtifactProgress(
|
|
2166
|
+
onProgress,
|
|
2167
|
+
signal,
|
|
2168
|
+
{
|
|
2169
|
+
phase: 'prepare',
|
|
2170
|
+
stage: 'runtime',
|
|
2171
|
+
message: 'Preparing speech runtime modules',
|
|
2172
|
+
total: null,
|
|
2173
|
+
}
|
|
2174
|
+
);
|
|
2099
2175
|
const routing = graph
|
|
2100
2176
|
? await planOrdinaryMaterializedRuntime({ files: installed }, metadata, signal)
|
|
2101
2177
|
: null;
|
|
@@ -2145,6 +2221,16 @@ export function createDbopfsSpeechArtifactStore({
|
|
|
2145
2221
|
}
|
|
2146
2222
|
throw speechError("ARCANE_AI_ARTIFACT_OFFLINE_MISS", "No cached offline speech artifacts are available.");
|
|
2147
2223
|
}
|
|
2224
|
+
reportArtifactProgress(
|
|
2225
|
+
onProgress,
|
|
2226
|
+
signal,
|
|
2227
|
+
{
|
|
2228
|
+
phase: 'prepare',
|
|
2229
|
+
stage: 'runtime',
|
|
2230
|
+
message: 'Opening prepared speech files',
|
|
2231
|
+
total: null,
|
|
2232
|
+
}
|
|
2233
|
+
);
|
|
2148
2234
|
if (graph) {
|
|
2149
2235
|
const materialized = await createOrdinaryArtifactObjectUrls(
|
|
2150
2236
|
admitted,
|
|
@@ -621,6 +621,7 @@ function observeLoadOperation(record, { signal, progress }, role) {
|
|
|
621
621
|
);
|
|
622
622
|
return;
|
|
623
623
|
}
|
|
624
|
+
if (record.lastProgress !== null) observer.progress(record.lastProgress);
|
|
624
625
|
});
|
|
625
626
|
}
|
|
626
627
|
|
|
@@ -1322,7 +1323,7 @@ function createBrowserSpeechProvider({
|
|
|
1322
1323
|
return pool;
|
|
1323
1324
|
}
|
|
1324
1325
|
|
|
1325
|
-
async function loadWorkerPool(preparation, device, warnings, signal) {
|
|
1326
|
+
async function loadWorkerPool(preparation, device, warnings, signal, onProgress) {
|
|
1326
1327
|
const pool = createWorkerPool(preparation, device, warnings);
|
|
1327
1328
|
const configuration = completeValue({
|
|
1328
1329
|
role,
|
|
@@ -1336,15 +1337,46 @@ function createBrowserSpeechProvider({
|
|
|
1336
1337
|
let failure = pool.failure;
|
|
1337
1338
|
if (!failure) {
|
|
1338
1339
|
try {
|
|
1339
|
-
await pool.slots[0].client.request(
|
|
1340
|
+
await pool.slots[0].client.request(
|
|
1341
|
+
'load',
|
|
1342
|
+
{ configuration },
|
|
1343
|
+
{ signal, onProgress }
|
|
1344
|
+
);
|
|
1340
1345
|
} catch (error) {
|
|
1341
1346
|
failure = error;
|
|
1342
1347
|
}
|
|
1343
1348
|
}
|
|
1344
1349
|
if (!failure) {
|
|
1345
1350
|
const remainingLoads = [];
|
|
1351
|
+
let completedWorkers = 1;
|
|
1352
|
+
function reportPoolProgress(detail) {
|
|
1353
|
+
onProgress(
|
|
1354
|
+
{
|
|
1355
|
+
phase: 'initialize',
|
|
1356
|
+
stage: 'workers',
|
|
1357
|
+
message: `Opening speech model sessions on ${device}`,
|
|
1358
|
+
completed: completedWorkers,
|
|
1359
|
+
total: pool.slots.length,
|
|
1360
|
+
unit: 'sessions',
|
|
1361
|
+
detail,
|
|
1362
|
+
}
|
|
1363
|
+
);
|
|
1364
|
+
}
|
|
1365
|
+
async function loadRemainingWorker(slot) {
|
|
1366
|
+
await slot.client.request(
|
|
1367
|
+
'load',
|
|
1368
|
+
{ configuration },
|
|
1369
|
+
{
|
|
1370
|
+
signal,
|
|
1371
|
+
onProgress: reportPoolProgress,
|
|
1372
|
+
}
|
|
1373
|
+
);
|
|
1374
|
+
completedWorkers += 1;
|
|
1375
|
+
reportPoolProgress(null);
|
|
1376
|
+
}
|
|
1377
|
+
if (pool.slots.length > 1) reportPoolProgress(null);
|
|
1346
1378
|
for (const slot of pool.slots.slice(1)) {
|
|
1347
|
-
remainingLoads.push(slot
|
|
1379
|
+
remainingLoads.push(loadRemainingWorker(slot));
|
|
1348
1380
|
}
|
|
1349
1381
|
const settlements = await Promise.allSettled(remainingLoads);
|
|
1350
1382
|
failure = pool.failure;
|
|
@@ -1488,6 +1520,7 @@ function createBrowserSpeechProvider({
|
|
|
1488
1520
|
errorCode = null;
|
|
1489
1521
|
const record = {
|
|
1490
1522
|
promise: null,
|
|
1523
|
+
lastProgress: null,
|
|
1491
1524
|
warnings: NO_PROVIDER_WARNINGS,
|
|
1492
1525
|
observers: new Set(),
|
|
1493
1526
|
settled: false,
|
|
@@ -1498,6 +1531,24 @@ function createBrowserSpeechProvider({
|
|
|
1498
1531
|
linked.abort(reason, code);
|
|
1499
1532
|
},
|
|
1500
1533
|
};
|
|
1534
|
+
const loadStartedAt = Date.now();
|
|
1535
|
+
function reportLoadProgress(progress) {
|
|
1536
|
+
if (
|
|
1537
|
+
record.settled
|
|
1538
|
+
|| linked.controller.signal.aborted
|
|
1539
|
+
|| operationGeneration !== generation
|
|
1540
|
+
|| state !== 'loading'
|
|
1541
|
+
) return;
|
|
1542
|
+
const value = {
|
|
1543
|
+
...progress,
|
|
1544
|
+
elapsedMs: Math.max(
|
|
1545
|
+
0,
|
|
1546
|
+
Date.now() - loadStartedAt
|
|
1547
|
+
),
|
|
1548
|
+
};
|
|
1549
|
+
record.lastProgress = value;
|
|
1550
|
+
for (const observer of [...record.observers]) observer.progress(value);
|
|
1551
|
+
}
|
|
1501
1552
|
lastWarnings = NO_PROVIDER_WARNINGS;
|
|
1502
1553
|
selectedDevice = null;
|
|
1503
1554
|
const promise = Promise.resolve().then(async function loadBrowserSpeechProviderPool() {
|
|
@@ -1505,10 +1556,22 @@ function createBrowserSpeechProvider({
|
|
|
1505
1556
|
let preparation = null;
|
|
1506
1557
|
let pool = null;
|
|
1507
1558
|
try {
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1559
|
+
reportLoadProgress(
|
|
1560
|
+
{
|
|
1561
|
+
phase: 'prepare',
|
|
1562
|
+
stage: 'cache',
|
|
1563
|
+
message: 'Opening stored speech runtime files',
|
|
1564
|
+
total: null,
|
|
1565
|
+
}
|
|
1566
|
+
);
|
|
1567
|
+
prepared = await store.prepare(
|
|
1568
|
+
authority.graph ?? authority,
|
|
1569
|
+
{
|
|
1570
|
+
signal: linked.controller.signal,
|
|
1571
|
+
offline,
|
|
1572
|
+
onProgress: reportLoadProgress,
|
|
1573
|
+
}
|
|
1574
|
+
);
|
|
1512
1575
|
preparation = {
|
|
1513
1576
|
prepared,
|
|
1514
1577
|
released: false,
|
|
@@ -1548,6 +1611,7 @@ function createBrowserSpeechProvider({
|
|
|
1548
1611
|
device,
|
|
1549
1612
|
record.warnings,
|
|
1550
1613
|
linked.controller.signal,
|
|
1614
|
+
reportLoadProgress,
|
|
1551
1615
|
);
|
|
1552
1616
|
break;
|
|
1553
1617
|
} catch (error) {
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
resolveModelSecurity,
|
|
9
9
|
} from "./model-controller.mjs";
|
|
10
10
|
import { createPackagedWllamaRuntime } from "./browser-wllama-runtime.mjs";
|
|
11
|
-
import { getBrowserDeviceSettings,
|
|
11
|
+
import { getBrowserDeviceSettings, describeBrowserGpu } from "./browser-device-settings.mjs";
|
|
12
12
|
import { arcaneEvents } from "../event-manager.mjs";
|
|
13
13
|
|
|
14
14
|
const is = new Is(false);
|
|
@@ -269,21 +269,22 @@ function notifyHighPerformanceGpu(adapter) {
|
|
|
269
269
|
if (highPerformanceGpuNoticeShown) {
|
|
270
270
|
return;
|
|
271
271
|
}
|
|
272
|
+
const selection = describeBrowserGpu(adapter);
|
|
273
|
+
if (selection.performanceStatus !== "integrated" && selection.performanceStatus !== "fallback") {
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
272
276
|
const browser = getBrowserDeviceSettings().highPerformanceGpu;
|
|
273
277
|
if (!browser) {
|
|
274
278
|
return;
|
|
275
279
|
}
|
|
276
280
|
highPerformanceGpuNoticeShown = true;
|
|
277
|
-
const
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
browser
|
|
282
|
-
|
|
283
|
-
+ `
|
|
284
|
-
+ "to request the high-performance GPU when available. "
|
|
285
|
-
+ `Then completely close and reopen ${browser.name} before loading the model again.\n\n`
|
|
286
|
-
+ `If the flags page did not open, paste ${browser.url} into the address bar.`,
|
|
281
|
+
const selectedKind = selection.performanceStatus === "fallback" ? "Software/fallback" : "Integrated";
|
|
282
|
+
globalThis.alert?.(
|
|
283
|
+
`${selectedKind} WebGPU adapter selected: ${selection.name}.\n\n`
|
|
284
|
+
+ "If this computer also has a discrete GPU, the browser may be able to use it. "
|
|
285
|
+
+ `Copy ${browser.url} into the address bar in ${browser.name} `
|
|
286
|
+
+ "and enable “Force High Performance GPU” to request it when available. "
|
|
287
|
+
+ `Save your work, then completely close and reopen ${browser.name} before loading the model again.`,
|
|
287
288
|
);
|
|
288
289
|
}
|
|
289
290
|
|
|
@@ -3057,7 +3058,9 @@ export function createBrowserWasmLlmProvider({
|
|
|
3057
3058
|
"Wllama did not confirm that the model loaded successfully.",
|
|
3058
3059
|
);
|
|
3059
3060
|
}
|
|
3060
|
-
|
|
3061
|
+
if (!signal?.aborted && generation === lifecycleGeneration && state === 'loading') {
|
|
3062
|
+
emitWebgpuAdapterSelection(activeSource, runtime);
|
|
3063
|
+
}
|
|
3061
3064
|
throwIfAborted(signal, "load");
|
|
3062
3065
|
if (generation !== lifecycleGeneration || state !== "loading") {
|
|
3063
3066
|
await runtime.exit();
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import Is from "../dependencies/strong-type/index.js";
|
|
2
2
|
import { arcaneLogging } from '../logging.mjs';
|
|
3
|
-
import {
|
|
3
|
+
import {Wllama, WllamaRuntimeError} from './wllama/index.mjs';
|
|
4
4
|
|
|
5
5
|
const is = new Is(false);
|
|
6
6
|
|
|
@@ -567,7 +567,46 @@ export function createPackagedWllamaRuntime({ logger = arcaneLogging } = {}) {
|
|
|
567
567
|
"Wllama did not confirm a successfully loaded model.",
|
|
568
568
|
);
|
|
569
569
|
}
|
|
570
|
-
|
|
570
|
+
let adapter = null;
|
|
571
|
+
const telemetryOperation = trackOperation(
|
|
572
|
+
Promise.resolve().then(
|
|
573
|
+
function observeLoadedGpu() {
|
|
574
|
+
return next.arcaneTelemetry();
|
|
575
|
+
}
|
|
576
|
+
)
|
|
577
|
+
);
|
|
578
|
+
function cancelGpuObservation() {
|
|
579
|
+
telemetryOperation.cancel(loadController.signal.reason);
|
|
580
|
+
}
|
|
581
|
+
loadController.signal.addEventListener(
|
|
582
|
+
'abort',
|
|
583
|
+
cancelGpuObservation,
|
|
584
|
+
{once: true}
|
|
585
|
+
);
|
|
586
|
+
if (loadController.signal.aborted) cancelGpuObservation();
|
|
587
|
+
try {
|
|
588
|
+
// Observe the adapter this loaded Worker already selected; do not probe
|
|
589
|
+
// a second adapter in the page and attribute it to the model.
|
|
590
|
+
const telemetry = await telemetryOperation.result;
|
|
591
|
+
if (telemetry?.worker?.invalid === true) {
|
|
592
|
+
arcaneLogging.warn("The loaded model's GPU adapter observation is inconsistent.", telemetry.worker);
|
|
593
|
+
} else {
|
|
594
|
+
adapter = telemetry?.worker?.adapter ?? null;
|
|
595
|
+
}
|
|
596
|
+
} catch (error) {
|
|
597
|
+
if (loadController.signal.aborted || error instanceof WllamaRuntimeError) throw error;
|
|
598
|
+
arcaneLogging.warn("The loaded model's GPU adapter details are unavailable.", error);
|
|
599
|
+
} finally {
|
|
600
|
+
loadController.signal.removeEventListener('abort', cancelGpuObservation);
|
|
601
|
+
}
|
|
602
|
+
if (progressFailure) throw progressFailure;
|
|
603
|
+
if (loadController.signal.aborted) throw cancellationError(loadController.signal.reason);
|
|
604
|
+
if (pending?.engine !== next) throw new Error('Wllama load was cancelled.');
|
|
605
|
+
const webgpu = {
|
|
606
|
+
observed: true,
|
|
607
|
+
apiPresent: true,
|
|
608
|
+
adapter
|
|
609
|
+
};
|
|
571
610
|
pending = null;
|
|
572
611
|
engine = next;
|
|
573
612
|
publishEvidence({ state: "ready", webgpu, cancellation: null, cleanup: null });
|
|
@@ -166,6 +166,11 @@ class SpeechWorkerClient {
|
|
|
166
166
|
), { intentional: false }).catch(() => undefined);
|
|
167
167
|
return;
|
|
168
168
|
}
|
|
169
|
+
if (is.safeInteger(message.id) && message.type === 'progress') {
|
|
170
|
+
const pending = this.#pending.get(message.id);
|
|
171
|
+
if (pending?.op === 'load') pending.onProgress?.(message.progress);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
169
174
|
if (!is.safeInteger(message.id) || !is.boolean(message.ok)) {
|
|
170
175
|
void this.terminate(clientError(
|
|
171
176
|
"ARCANE_AI_WORKER_MESSAGE_ERROR",
|
|
@@ -207,7 +212,7 @@ class SpeechWorkerClient {
|
|
|
207
212
|
));
|
|
208
213
|
}
|
|
209
214
|
|
|
210
|
-
request(op, payload, { signal = null } = {}) {
|
|
215
|
+
request(op, payload, { signal = null, onProgress = null } = {}) {
|
|
211
216
|
this.#trace("request.call", { op, payload, aborted: signal?.aborted });
|
|
212
217
|
if (!PUBLIC_WORKER_OPERATIONS.has(op)) {
|
|
213
218
|
this.#trace("request.rejected", { op, reason: "unknown-operation" });
|
|
@@ -283,7 +288,10 @@ class SpeechWorkerClient {
|
|
|
283
288
|
});
|
|
284
289
|
}
|
|
285
290
|
signal?.addEventListener?.("abort", onAbort, { once: true });
|
|
286
|
-
client.#pending.set(
|
|
291
|
+
client.#pending.set(
|
|
292
|
+
id,
|
|
293
|
+
{ resolve, reject, cleanup, op, onProgress }
|
|
294
|
+
);
|
|
287
295
|
const message = {
|
|
288
296
|
protocol: SPEECH_WORKER_PROTOCOL,
|
|
289
297
|
id,
|