arcane-os 0.10.1 → 0.11.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 +18 -0
- package/README.md +8 -8
- package/browser-runtime/ai/browser-wasm-llm-provider.mjs +34 -11
- package/browser-runtime/ai/browser-wllama-runtime.mjs +48 -37
- package/docs/reference/ai/browser-wasm.md +26 -11
- package/docs/reference/availability-and-normalization.md +7 -4
- package/docs/reference/inventory/package-api.json +1 -1
- package/docs/reference/protocols.md +11 -4
- package/docs/reference/runtime-components.md +23 -2
- package/docs/reference/sdk-api.md +11 -8
- package/package.json +2 -2
- package/runtime/arcane/components/chat.html +18 -1
- package/runtime/arcane/components/speech.html +72 -28
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.11.1
|
|
4
|
+
|
|
5
|
+
- Keep the shared Chat transcript, composer, speech controls, language selector,
|
|
6
|
+
and timer hidden until the selected local language model is ready and loaded.
|
|
7
|
+
Cloud routes remain visible immediately, and completed transcripts remain readable.
|
|
8
|
+
|
|
9
|
+
## 0.11.0
|
|
10
|
+
|
|
11
|
+
- Add explicit browser chat CPU selection with `loadDefaults:{gpuLayers:0}`
|
|
12
|
+
or `load({gpuLayers:0})`. Use the packaged Wllama CPU path without WebGPU
|
|
13
|
+
requirements or adapter initialization. Omission retains full GPU offload;
|
|
14
|
+
model sources, cache, complete responses, streaming, and cancellation remain
|
|
15
|
+
on their existing owners.
|
|
16
|
+
- On mobile browsers and tablets, keep shared Speech focused on voice output:
|
|
17
|
+
hide and disable transcription activation and recording, omit transcription
|
|
18
|
+
status/progress, and retain mute/unmute. Chat retains End. Desktop speech and
|
|
19
|
+
the dedicated transcription component retain their existing behavior.
|
|
20
|
+
|
|
3
21
|
## 0.10.1
|
|
4
22
|
|
|
5
23
|
- Open developer error modals only for newly captured live incidents. Restoring
|
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.11.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
|
|
|
@@ -41,7 +41,7 @@ Create one browser application, install its pinned SDK, and start its source
|
|
|
41
41
|
server:
|
|
42
42
|
|
|
43
43
|
```bash
|
|
44
|
-
npx arcane-os@0.
|
|
44
|
+
npx arcane-os@0.11.1 new hello-speech --path ./hello-speech --target browser
|
|
45
45
|
cd hello-speech
|
|
46
46
|
npm install
|
|
47
47
|
npm run dev
|
|
@@ -392,7 +392,7 @@ uses the same controller for automatic memory extraction.
|
|
|
392
392
|
Create a new repository-shaped Arcane application with the exact stable SDK:
|
|
393
393
|
|
|
394
394
|
```bash
|
|
395
|
-
npx arcane-os@0.
|
|
395
|
+
npx arcane-os@0.11.1 new my-app --path ./my-app --target portable --git
|
|
396
396
|
cd my-app
|
|
397
397
|
npm install
|
|
398
398
|
npm run dev
|
|
@@ -402,7 +402,7 @@ To enroll an existing repository, install the exact SDK and initialize only
|
|
|
402
402
|
missing Arcane files:
|
|
403
403
|
|
|
404
404
|
```bash
|
|
405
|
-
npm install --save-dev --save-exact arcane-os@0.
|
|
405
|
+
npm install --save-dev --save-exact arcane-os@0.11.1
|
|
406
406
|
npm exec -- arcane init my-app --target portable
|
|
407
407
|
```
|
|
408
408
|
|
|
@@ -418,7 +418,7 @@ npm exec -- arcane-os targets
|
|
|
418
418
|
No global SDK install or standalone Arcane CLI is required. The application
|
|
419
419
|
repository's exact npm dependency and lockfile own the CLI and toolchain version.
|
|
420
420
|
|
|
421
|
-
Use `npx arcane-os@0.
|
|
421
|
+
Use `npx arcane-os@0.11.1` for the initial bootstrap because it names this npm
|
|
422
422
|
package explicitly; bare `npx arcane` outside an installed project could resolve
|
|
423
423
|
a different package. Both installed commands invoke the same headless toolchain.
|
|
424
424
|
Project-local npm scripts use the SDK pinned by that app's `package-lock.json`,
|
|
@@ -438,7 +438,7 @@ node ./bin/arcane.mjs new local-app --path ../local-app --target portable --git
|
|
|
438
438
|
|
|
439
439
|
# From the generated app repository
|
|
440
440
|
cd ../local-app
|
|
441
|
-
npm install --save-dev --save-exact ../arcane-os-sdk/arcane-os-0.
|
|
441
|
+
npm install --save-dev --save-exact ../arcane-os-sdk/arcane-os-0.11.1.tgz
|
|
442
442
|
npm ci
|
|
443
443
|
```
|
|
444
444
|
|
|
@@ -447,7 +447,7 @@ same location. The lockfile retains the selected package dependency while
|
|
|
447
447
|
Arcane uses the installed package name and version. Local directory `file:` dependencies are not
|
|
448
448
|
accepted because npm may install them as links; use a packed `.tgz`. A GitHub
|
|
449
449
|
runner also needs that tarball at the locked path. After publication, replace
|
|
450
|
-
the local declaration with the exact `arcane-os@0.
|
|
450
|
+
the local declaration with the exact `arcane-os@0.11.1` registry package and
|
|
451
451
|
commit the regenerated lock.
|
|
452
452
|
|
|
453
453
|
Generated repositories use `npm ci --ignore-scripts` in CI. Run dependency
|
|
@@ -586,7 +586,7 @@ package installation, or assertions.
|
|
|
586
586
|
|
|
587
587
|
## Current target support
|
|
588
588
|
|
|
589
|
-
Version `0.
|
|
589
|
+
Version `0.11.1` exposes one browser target and five explicitly paired
|
|
590
590
|
native development targets: a non-runnable portable directory, a
|
|
591
591
|
Windows x64 unsigned-local-test EXE bundle, Linux x64 and Linux ARM64
|
|
592
592
|
unsigned-local-test DEBs, and an Android development-signed APK. The
|
|
@@ -2605,7 +2605,7 @@ function capabilityLoadPlan(runtimeCapabilities, defaults, options = {}) {
|
|
|
2605
2605
|
contextTokens,
|
|
2606
2606
|
batchTokens,
|
|
2607
2607
|
microBatchTokens,
|
|
2608
|
-
gpuLayers: 99_999,
|
|
2608
|
+
gpuLayers: configured.gpuLayers === 0 ? 0 : 99_999,
|
|
2609
2609
|
...(reasoning!==undefined?{ reasoning }:{}),
|
|
2610
2610
|
...(chatTemplate!==undefined?{ chatTemplate }:{}),
|
|
2611
2611
|
...(jinja!==undefined?{ jinja }:{}),
|
|
@@ -2625,14 +2625,20 @@ function sameLoadPlan(left, right) {
|
|
|
2625
2625
|
&& JSON.stringify(left?.templateDefaults) === JSON.stringify(right?.templateDefaults);
|
|
2626
2626
|
}
|
|
2627
2627
|
|
|
2628
|
-
function stableModelFailure(error) {
|
|
2628
|
+
function stableModelFailure(error, loadPlan) {
|
|
2629
2629
|
const code = is.string(error?.code) ? error.code : "";
|
|
2630
2630
|
const message = is.string(error?.message) ? error.message : "";
|
|
2631
2631
|
if (code === "ARCANE_AI_MODEL_SHARD_TOO_LARGE") {
|
|
2632
2632
|
return completeValue({ code });
|
|
2633
2633
|
}
|
|
2634
2634
|
if (/(?:out of memory|allocation failed|failed to allocate|memory exhausted)/iu.test(message)) {
|
|
2635
|
-
return completeValue(
|
|
2635
|
+
return completeValue(
|
|
2636
|
+
{
|
|
2637
|
+
code: loadPlan.gpuLayers === 0
|
|
2638
|
+
? 'ARCANE_AI_MODEL_CPU_MEMORY_INSUFFICIENT'
|
|
2639
|
+
: 'ARCANE_AI_MODEL_GPU_MEMORY_INSUFFICIENT'
|
|
2640
|
+
}
|
|
2641
|
+
);
|
|
2636
2642
|
}
|
|
2637
2643
|
if (code === "ARCANE_AI_WEBGPU_EVIDENCE_INVALID") {
|
|
2638
2644
|
return completeValue({ code: "ARCANE_AI_MODEL_FULL_OFFLOAD_UNPROVEN" });
|
|
@@ -2667,10 +2673,10 @@ function capabilityPolicy(
|
|
|
2667
2673
|
if (runtimeCapabilities.secureContext !== true) {
|
|
2668
2674
|
add("ARCANE_AI_SECURE_CONTEXT_REQUIRED", "incompatible");
|
|
2669
2675
|
}
|
|
2670
|
-
if (runtimeCapabilities.webgpuApiPresent !== true) {
|
|
2676
|
+
if (loadPlan.gpuLayers > 0 && runtimeCapabilities.webgpuApiPresent !== true) {
|
|
2671
2677
|
add("ARCANE_AI_WEBGPU_API_UNAVAILABLE", "incompatible");
|
|
2672
2678
|
}
|
|
2673
|
-
if (failure) {
|
|
2679
|
+
if (failure && failure.gpuLayers === loadPlan.gpuLayers) {
|
|
2674
2680
|
add(failure.code, "incompatible");
|
|
2675
2681
|
}
|
|
2676
2682
|
if (storage?.compatibility === "incompatible") {
|
|
@@ -2679,7 +2685,13 @@ function capabilityPolicy(
|
|
|
2679
2685
|
add(storage?.code ?? "ARCANE_AI_STORAGE_NOT_MEASURED", "unknown");
|
|
2680
2686
|
}
|
|
2681
2687
|
const webgpu = runtimeEvidence?.webgpu;
|
|
2682
|
-
if (
|
|
2688
|
+
if (loadPlan.gpuLayers === 0) {
|
|
2689
|
+
const cpuLoaded = state === 'ready' && runtimeEvidence?.executionDevice === 'cpu';
|
|
2690
|
+
add(
|
|
2691
|
+
cpuLoaded ? 'ARCANE_AI_CPU_MODEL_LOADED' : 'ARCANE_AI_CPU_MODEL_NOT_LOADED',
|
|
2692
|
+
cpuLoaded ? 'compatible' : 'unknown',
|
|
2693
|
+
);
|
|
2694
|
+
} else if (state === "ready" && webgpu?.observed === true) {
|
|
2683
2695
|
add(
|
|
2684
2696
|
"ARCANE_AI_WEBGPU_EXECUTION_OBSERVED",
|
|
2685
2697
|
"compatible",
|
|
@@ -2799,6 +2811,8 @@ export function createBrowserWasmLlmProvider({
|
|
|
2799
2811
|
webgpu: runtimeCapabilities.webgpu,
|
|
2800
2812
|
webgpuApiPresent: runtimeCapabilities.webgpuApiPresent,
|
|
2801
2813
|
webgpuOperational: runtimeCapabilities.webgpuOperational,
|
|
2814
|
+
webgpuRequired: activeLoadPlan.gpuLayers > 0,
|
|
2815
|
+
executionDevice: activeLoadPlan.gpuLayers === 0 ? 'cpu' : 'webgpu',
|
|
2802
2816
|
webgpuEvidenceProtocol: runtimeCapabilities.webgpuEvidenceProtocol,
|
|
2803
2817
|
webgpuAdapterSelectionEvent: WEBGPU_ADAPTER_SELECTED_EVENT,
|
|
2804
2818
|
crossOriginIsolated: runtimeCapabilities.crossOriginIsolated,
|
|
@@ -2920,7 +2934,7 @@ export function createBrowserWasmLlmProvider({
|
|
|
2920
2934
|
if (!sameLoadPlan(activeLoadPlan, requestedLoadPlan)) {
|
|
2921
2935
|
throw fail(
|
|
2922
2936
|
"ARCANE_AI_LOAD_PLAN_RELOAD_REQUIRED",
|
|
2923
|
-
"Unload the browser-WASM model before changing its context or batch load plan.",
|
|
2937
|
+
"Unload the browser-WASM model before changing its execution device, context, or batch load plan.",
|
|
2924
2938
|
);
|
|
2925
2939
|
}
|
|
2926
2940
|
activeSecurity = effectiveSecurity;
|
|
@@ -2936,7 +2950,7 @@ export function createBrowserWasmLlmProvider({
|
|
|
2936
2950
|
if (!sameLoadPlan(activeLoadPlan, requestedLoadPlan)) {
|
|
2937
2951
|
throw fail(
|
|
2938
2952
|
"ARCANE_AI_LOAD_PLAN_RELOAD_REQUIRED",
|
|
2939
|
-
"The in-flight browser-WASM load uses a different context or batch plan.",
|
|
2953
|
+
"The in-flight browser-WASM load uses a different execution device, context, or batch plan.",
|
|
2940
2954
|
);
|
|
2941
2955
|
}
|
|
2942
2956
|
activeSecurity = effectiveSecurity;
|
|
@@ -3082,8 +3096,13 @@ export function createBrowserWasmLlmProvider({
|
|
|
3082
3096
|
operation: "load",
|
|
3083
3097
|
signal: normalizationSignal(surfaced, signal),
|
|
3084
3098
|
});
|
|
3085
|
-
const modelFailure = stableModelFailure(normalized);
|
|
3086
|
-
if (modelFailure)
|
|
3099
|
+
const modelFailure = stableModelFailure(normalized, activeLoadPlan);
|
|
3100
|
+
if (modelFailure) {
|
|
3101
|
+
modelFailures.set(
|
|
3102
|
+
activeSource.id,
|
|
3103
|
+
{...modelFailure, gpuLayers: activeLoadPlan.gpuLayers}
|
|
3104
|
+
);
|
|
3105
|
+
}
|
|
3087
3106
|
if (generation === lifecycleGeneration && state === "loading") {
|
|
3088
3107
|
state = "error";
|
|
3089
3108
|
errorState = completeValue({ code: normalized.code, message: normalized.message });
|
|
@@ -3196,6 +3215,10 @@ export function createBrowserWasmLlmProvider({
|
|
|
3196
3215
|
state = "unloaded";
|
|
3197
3216
|
errorState = null;
|
|
3198
3217
|
activeSecurity = null;
|
|
3218
|
+
activeLoadPlan = capabilityLoadPlan(
|
|
3219
|
+
measuredRuntimeCapabilities(runtime.capabilities()),
|
|
3220
|
+
runtimeLoadDefaults,
|
|
3221
|
+
);
|
|
3199
3222
|
return status();
|
|
3200
3223
|
} catch (error) {
|
|
3201
3224
|
const normalized = normalizeArcaneAIError(error, {
|
|
@@ -3394,7 +3417,7 @@ export function adaptV1LlmProvider(provider) {
|
|
|
3394
3417
|
[capabilities?.webAssembly === true, "WebAssembly"],
|
|
3395
3418
|
[capabilities?.opfs === true, "OPFS"],
|
|
3396
3419
|
[capabilities?.secureContext === true, "a secure context"],
|
|
3397
|
-
[capabilities?.webgpuApiPresent === true, "the WebGPU API"],
|
|
3420
|
+
[capabilities?.webgpuRequired === false || capabilities?.webgpuApiPresent === true, "the WebGPU API"],
|
|
3398
3421
|
];
|
|
3399
3422
|
const missing = requirements.find(([available]) => !available)?.[1] ?? null;
|
|
3400
3423
|
if (missing) {
|
|
@@ -20,7 +20,9 @@ export const BROWSER_WASM_RUNTIME_AUTHORITY = completeValue({
|
|
|
20
20
|
protocol: "arcane-ai-browser-wasm/2",
|
|
21
21
|
provider: "wllama",
|
|
22
22
|
executionPolicy: {
|
|
23
|
-
webgpuRequired:
|
|
23
|
+
webgpuRequired: false,
|
|
24
|
+
defaultGpuLayers: FULL_GPU_LAYERS,
|
|
25
|
+
cpuGpuLayers: 0,
|
|
24
26
|
cpuFallback: false,
|
|
25
27
|
cancellation: "abortSignal-plus-llama-cancel-acknowledgement",
|
|
26
28
|
cleanup: "worker-termination-only",
|
|
@@ -71,6 +73,7 @@ function runtimeCapabilitySnapshot(evidence) {
|
|
|
71
73
|
webgpu: webgpuOperational,
|
|
72
74
|
webgpuApiPresent: Boolean(navigatorObject?.gpu),
|
|
73
75
|
webgpuOperational,
|
|
76
|
+
executionDevice: evidence?.executionDevice ?? null,
|
|
74
77
|
webgpuEvidenceProtocol: RUNTIME_EVIDENCE_PROTOCOL,
|
|
75
78
|
crossOriginIsolated: globalThis.crossOriginIsolated === true,
|
|
76
79
|
secureContext: globalThis.isSecureContext === true,
|
|
@@ -329,6 +332,7 @@ function initialEvidence() {
|
|
|
329
332
|
return completeValue({
|
|
330
333
|
protocol: RUNTIME_EVIDENCE_PROTOCOL,
|
|
331
334
|
state: "unloaded",
|
|
335
|
+
executionDevice: null,
|
|
332
336
|
webgpu: {
|
|
333
337
|
observed: false,
|
|
334
338
|
apiPresent: Boolean(globalThis.navigator?.gpu),
|
|
@@ -339,7 +343,8 @@ function initialEvidence() {
|
|
|
339
343
|
}
|
|
340
344
|
|
|
341
345
|
/**
|
|
342
|
-
* Creates one packaged
|
|
346
|
+
* Creates one packaged Wllama session. GPU loading is the default; gpuLayers: 0
|
|
347
|
+
* selects the upstream CPU path. This factory has no
|
|
343
348
|
* network or browser side effects until load() is called. Runtime URLs are
|
|
344
349
|
* fixed relative to this module for npm and materialized /arcane/sdk trees.
|
|
345
350
|
*/
|
|
@@ -473,7 +478,12 @@ export function createPackagedWllamaRuntime({ logger = arcaneLogging } = {}) {
|
|
|
473
478
|
if (!is.object(globalThis.WebAssembly)) {
|
|
474
479
|
throw new Error("WebAssembly is unavailable in this browser.");
|
|
475
480
|
}
|
|
476
|
-
|
|
481
|
+
const gpuLayers = options.gpuLayers === 0
|
|
482
|
+
? 0
|
|
483
|
+
: normalizePositiveInteger(options.gpuLayers, FULL_GPU_LAYERS);
|
|
484
|
+
const webgpuRequired = gpuLayers > 0;
|
|
485
|
+
const executionDevice = webgpuRequired ? 'webgpu' : 'cpu';
|
|
486
|
+
if (webgpuRequired && !globalThis.navigator?.gpu) {
|
|
477
487
|
throw runtimeFailure(
|
|
478
488
|
"ARCANE_AI_WEBGPU_REQUIRED",
|
|
479
489
|
"A WebGPU API is required, but navigator presence alone will not establish operational execution.",
|
|
@@ -486,9 +496,6 @@ export function createPackagedWllamaRuntime({ logger = arcaneLogging } = {}) {
|
|
|
486
496
|
const next = newEngine();
|
|
487
497
|
const threads = normalizePositiveInteger(options.threads, 1);
|
|
488
498
|
const contextTokens = normalizePositiveInteger(options.contextTokens, 4_096);
|
|
489
|
-
const gpuLayers = options.gpuLayers === undefined
|
|
490
|
-
? FULL_GPU_LAYERS
|
|
491
|
-
: normalizePositiveInteger(options.gpuLayers, FULL_GPU_LAYERS);
|
|
492
499
|
const loadOptions = {
|
|
493
500
|
n_threads: threads,
|
|
494
501
|
n_ctx: contextTokens,
|
|
@@ -527,7 +534,8 @@ export function createPackagedWllamaRuntime({ logger = arcaneLogging } = {}) {
|
|
|
527
534
|
|
|
528
535
|
publishEvidence({
|
|
529
536
|
state: "loading",
|
|
530
|
-
|
|
537
|
+
executionDevice,
|
|
538
|
+
webgpu: { observed: false, apiPresent: Boolean(globalThis.navigator?.gpu) },
|
|
531
539
|
cancellation: null,
|
|
532
540
|
cleanup: null,
|
|
533
541
|
});
|
|
@@ -568,43 +576,45 @@ export function createPackagedWllamaRuntime({ logger = arcaneLogging } = {}) {
|
|
|
568
576
|
);
|
|
569
577
|
}
|
|
570
578
|
let adapter = null;
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
579
|
+
if (webgpuRequired) {
|
|
580
|
+
const telemetryOperation = trackOperation(
|
|
581
|
+
Promise.resolve().then(
|
|
582
|
+
function observeLoadedGpu() {
|
|
583
|
+
return next.arcaneTelemetry();
|
|
584
|
+
}
|
|
585
|
+
)
|
|
586
|
+
);
|
|
587
|
+
function cancelGpuObservation() {
|
|
588
|
+
telemetryOperation.cancel(loadController.signal.reason);
|
|
589
|
+
}
|
|
590
|
+
loadController.signal.addEventListener(
|
|
591
|
+
'abort',
|
|
592
|
+
cancelGpuObservation,
|
|
593
|
+
{once: true}
|
|
594
|
+
);
|
|
595
|
+
if (loadController.signal.aborted) cancelGpuObservation();
|
|
596
|
+
try {
|
|
597
|
+
// Observe the adapter this loaded Worker already selected; do not probe
|
|
598
|
+
// a second adapter in the page and attribute it to the model.
|
|
599
|
+
const telemetry = await telemetryOperation.result;
|
|
600
|
+
if (telemetry?.worker?.invalid === true) {
|
|
601
|
+
arcaneLogging.warn("The loaded model's GPU adapter observation is inconsistent.", telemetry.worker);
|
|
602
|
+
} else {
|
|
603
|
+
adapter = telemetry?.worker?.adapter ?? null;
|
|
575
604
|
}
|
|
576
|
-
)
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
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;
|
|
605
|
+
} catch (error) {
|
|
606
|
+
if (loadController.signal.aborted || error instanceof WllamaRuntimeError) throw error;
|
|
607
|
+
arcaneLogging.warn("The loaded model's GPU adapter details are unavailable.", error);
|
|
608
|
+
} finally {
|
|
609
|
+
loadController.signal.removeEventListener('abort', cancelGpuObservation);
|
|
595
610
|
}
|
|
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
611
|
}
|
|
602
612
|
if (progressFailure) throw progressFailure;
|
|
603
613
|
if (loadController.signal.aborted) throw cancellationError(loadController.signal.reason);
|
|
604
614
|
if (pending?.engine !== next) throw new Error('Wllama load was cancelled.');
|
|
605
615
|
const webgpu = {
|
|
606
|
-
observed:
|
|
607
|
-
apiPresent:
|
|
616
|
+
observed: webgpuRequired,
|
|
617
|
+
apiPresent: Boolean(globalThis.navigator?.gpu),
|
|
608
618
|
adapter
|
|
609
619
|
};
|
|
610
620
|
pending = null;
|
|
@@ -638,6 +648,7 @@ export function createPackagedWllamaRuntime({ logger = arcaneLogging } = {}) {
|
|
|
638
648
|
contextTokens,
|
|
639
649
|
threads,
|
|
640
650
|
gpuLayers,
|
|
651
|
+
executionDevice,
|
|
641
652
|
evidence: evidenceState,
|
|
642
653
|
metadata: engine.getModelMetadata?.() ?? null,
|
|
643
654
|
});
|
|
@@ -80,6 +80,19 @@ GPU request, model operation, listener, or viewport-dependent reload. Its
|
|
|
80
80
|
dependency-free entrypoint does not import the inference runtime. Profile
|
|
81
81
|
selection does not alter prompts, documents, history, or output limits.
|
|
82
82
|
|
|
83
|
+
## CPU selection
|
|
84
|
+
|
|
85
|
+
Set `loadDefaults:{gpuLayers:0}` when creating the provider, or pass
|
|
86
|
+
`gpuLayers:0` to `ai.load()` or `provider.load()`, to run the selected model on
|
|
87
|
+
CPU through the packaged Wllama WebAssembly runtime. CPU selection skips
|
|
88
|
+
WebGPU capability checks, adapter initialization, and GPU execution reporting.
|
|
89
|
+
It uses the same complete model sources, cache, request, streaming, and
|
|
90
|
+
cancellation contracts. Existing context, batch, micro-batch, and thread
|
|
91
|
+
settings remain independently configurable.
|
|
92
|
+
|
|
93
|
+
Omitting `gpuLayers` preserves full GPU offload (`99999`). CPU is an explicit
|
|
94
|
+
selection; a failed GPU load does not silently switch to CPU.
|
|
95
|
+
|
|
83
96
|
## Lifecycle at a glance
|
|
84
97
|
|
|
85
98
|
`createArcaneAI()` owns one LLM controller. Its default `loadPolicy` is
|
|
@@ -206,15 +219,16 @@ Applications remain responsible for model selection and license compliance.
|
|
|
206
219
|
cannot download. Source downloads use CORS, omit credentials and referrer,
|
|
207
220
|
disable HTTP caching, and honor `AbortSignal`.
|
|
208
221
|
|
|
209
|
-
After a model loads, its existing Worker reports the adapter it selected.
|
|
222
|
+
After a GPU model loads, its existing Worker reports the adapter it selected.
|
|
210
223
|
The runtime retains that adapter in `evidence().webgpu.adapter`, including
|
|
211
224
|
optional browser-reported `type` and `isFallbackAdapter` fields. This reads
|
|
212
225
|
the loaded Worker's metadata once; it does not request another adapter.
|
|
213
226
|
Missing fields remain unknown. Cancellation also cancels the metadata wait;
|
|
214
227
|
an unavailable observation is logged without rejecting an otherwise loaded model.
|
|
215
228
|
|
|
216
|
-
|
|
217
|
-
CPU, or fallback adapter triggers one GPU-only browser alert
|
|
229
|
+
For GPU loads on Windows desktop Chromium browsers, an explicitly reported
|
|
230
|
+
integrated GPU, CPU, or fallback adapter triggers one GPU-only browser alert
|
|
231
|
+
per page session.
|
|
218
232
|
The alert names the selected adapter and **Force High Performance GPU**
|
|
219
233
|
(`#force-high-performance-gpu`), conditional on the computer also having a
|
|
220
234
|
discrete GPU. It contains the address to paste manually and asks the user to
|
|
@@ -278,9 +292,10 @@ serialized; provider status exposes `busy` and `queued`. Supported request
|
|
|
278
292
|
generation fields include temperature, top-K, top-P, min-P, repeat penalty,
|
|
279
293
|
and seed. Load settings separately include
|
|
280
294
|
`contextTokens`, `batchTokens`, `microBatchTokens`, `threads`, and GPU-layer
|
|
281
|
-
count. The shipped runtime
|
|
282
|
-
|
|
283
|
-
|
|
295
|
+
count. The shipped runtime defaults to `gpuLayers:99999` for full GPU offload;
|
|
296
|
+
`gpuLayers:0` explicitly selects CPU. WebGPU/full-offload availability applies
|
|
297
|
+
only to the GPU route, which has no automatic CPU fallback. Secure context
|
|
298
|
+
remains a browser platform requirement;
|
|
284
299
|
cross-origin isolation and coarse hardware fields remain observations.
|
|
285
300
|
|
|
286
301
|
Tool definitions, tool choice, parallel-tool-call preference, and JSON or JSON
|
|
@@ -528,11 +543,11 @@ intent. They do not activate checking in the ordinary development contract.
|
|
|
528
543
|
|
|
529
544
|
### Availability and normalization
|
|
530
545
|
|
|
531
|
-
**Browser secure context with WebAssembly
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
storage/model compatibility, and lifecycle state; it does not claim that
|
|
546
|
+
**Browser secure context with WebAssembly and OPFS/DBOPFS.** The default GPU
|
|
547
|
+
route additionally requires WebGPU and full-offload support; `gpuLayers:0`
|
|
548
|
+
selects CPU without those GPU requirements. Inference is local after a
|
|
549
|
+
successful Wllama load. Status discloses the optional `secure` intent,
|
|
550
|
+
capability state, storage/model compatibility, and lifecycle state; it does not claim that
|
|
536
551
|
hardening ran.
|
|
537
552
|
|
|
538
553
|
### Example
|
|
@@ -70,7 +70,7 @@ version; WebKitGTK availability must not be generalized to macOS.
|
|
|
70
70
|
| Publish application events or review a complete event history | `arcane-os/event-manager` | **Node** and **Browser**; optional DOM capture needs a browser DOM or compatible host | Live listeners receive original arguments. Ordinary `secure:false` recording preserves complete URLs, public details, and captured stack text in deeply frozen `arcane-event-stack/1` snapshots while credential-named fields remain redacted. The stack format is local diagnostic data, not a host transport. |
|
|
71
71
|
| Build browser UI and app-local behavior | `/arcane/modules/*.js`, shared entities, and components | **Browser**; many modules also run inside every native renderer | Pure modules own their result contracts. Modules that call `Arcane` inherit the bridge boundary described below. |
|
|
72
72
|
| Select and observe independent LLM/STT/TTS roles | `/arcane/modules/AIProviderRuntime.js` and `AIRuntimeState.js` | **Cross-host** controller/state; registered providers retain their own host requirements | Required/projected provider members, route/configuration records, and status fields; per-role lifecycle, cancellation, stream cleanup, sticky state, and startup barriers are normalized. `localOnly` creates no fallback. |
|
|
73
|
-
| Run a caller-selected local LLM entirely in a browser renderer | `arcane-os/ai/browser-wasm` through `createArcaneAI()` | **Browser** only; secure context, WebAssembly, OPFS/DBOPFS
|
|
73
|
+
| Run a caller-selected local LLM entirely in a browser renderer | `arcane-os/ai/browser-wasm` through `createArcaneAI()` | **Browser** only; secure context, WebAssembly, and OPFS/DBOPFS; full WebGPU offload by default or explicit CPU with `gpuLayers:0` | The public AI API module normalizes multi-model lifecycle, status, complete all-choice streaming, cancellation, exact ordered structural tool-call visibility, and session persistence. Model sources are canonical ordered file descriptors; licenses and model choice remain application policy. |
|
|
74
74
|
| Run caller-selected Whisper or Kokoro in a browser renderer | `arcane-os/ai/browser-speech` registered with `AIProviderRuntime` | **Browser** only; DBOPFS, Web Locks, Workers, Fetch/object URLs, and a caller-supplied self-contained runtime/model closure are required | STT/TTS use independent provider/2 lifecycle and status. Kokoro adds bounded Worker/session concurrency and explicit `auto`, `webgpu`, or `wasm` execution. Complete model/runtime selection, offline behavior, cancellation, Worker teardown, and request/result shapes are normalized. No runtime/model content or cloud fallback is supplied. |
|
|
75
75
|
| Prepare ordered speech playback or reuse the speech-input formatting filter | `arcane-os/speech-playback` and `arcane-os/speech-text` | **Node** with injected media adapters, or **Browser / Native WebView** media; the text filter itself is **Cross-host** | Stored and caller-owned text stays exact. Only the outbound synthesis copy automatically loses repeated same formatting marks. A capacity-advertising provider receives complete segments immediately while retaining indexed playback; native/custom synthesis stays serialized. |
|
|
76
76
|
| Preserve complete chat history and memory | `/arcane/modules/PersistentAIChatSession.js` | **Browser / native WebView** with ChatEntity/DBOPFS and a configured chat function | Existing DBOPFS names and memory semantics are preserved. Live-context commit is atomic; durable persistence is explicit and coherent across user/assistant turns and atomic all-ID tool-result batches. |
|
|
@@ -133,9 +133,12 @@ surface does not require an Arcane Core method grant because it does not call a
|
|
|
133
133
|
Core host. Browser Fetch, CORS, storage policy, secure-context behavior, and
|
|
134
134
|
resource limits still apply.
|
|
135
135
|
|
|
136
|
-
The
|
|
137
|
-
|
|
138
|
-
|
|
136
|
+
The browser runtime defaults to full GPU offload (`gpuLayers:99999`). Explicit
|
|
137
|
+
`loadDefaults:{gpuLayers:0}` or `load({gpuLayers:0})` selects CPU and skips
|
|
138
|
+
WebGPU requirements and adapter work; GPU failure never selects CPU implicitly.
|
|
139
|
+
Both routes wait for Wllama to report the complete model loaded.
|
|
140
|
+
`navigator.gpu` presence by itself is not readiness. On the GPU route, the
|
|
141
|
+
provider emits the instrumented
|
|
139
142
|
`arcane.ai.browser-wasm.webgpu.adapter.selected` capability event after adapter
|
|
140
143
|
selection.
|
|
141
144
|
|
|
@@ -2932,7 +2932,7 @@
|
|
|
2932
2932
|
"primaryImport": "arcane-os/ai/browser-wasm",
|
|
2933
2933
|
"group": "Browser-WASM local AI",
|
|
2934
2934
|
"summary": "Creates the packaged Wllama browser provider with complete serialized inference, streaming, abort, unload, and probe operations.",
|
|
2935
|
-
"availability": "Browser context with WebAssembly
|
|
2935
|
+
"availability": "Browser context with WebAssembly and OPFS/DBOPFS; WebGPU by default or explicit CPU with gpuLayers:0",
|
|
2936
2936
|
"protocol": "arcane-ai-adapter/1 with OpenAI-like chat completion and streaming envelopes",
|
|
2937
2937
|
"normalization": "Mutable complete lifecycle/status records, declaration/history validation, atomic nonblank all-ID tool-result sequencing, complete all-choice nonstructural ordinary iterator projections, all-choice terminal validation, terminal-only structural-call acceptance, and exact structured tool-call data with required user-facing message and retained extension fields; the SDK never invokes application tools"
|
|
2938
2938
|
},
|
|
@@ -355,10 +355,12 @@ does not silently delete the app-owned cache. A complete whole member supersedes
|
|
|
355
355
|
its current resumable fragments. Cleanup failure is warned without hiding the
|
|
356
356
|
usable model.
|
|
357
357
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
358
|
+
Browser-WASM loads default to full WebGPU offload. Explicit
|
|
359
|
+
`loadDefaults:{gpuLayers:0}` or `load({gpuLayers:0})` selects CPU through the
|
|
360
|
+
same packaged Wllama runtime without GPU initialization or GPU-only
|
|
361
|
+
requirements. Both routes wait for the runtime to report the complete model
|
|
362
|
+
loaded. `navigator.gpu` presence alone is not readiness. There is no automatic
|
|
363
|
+
CPU fallback or silent switch to native/Core/cloud inference.
|
|
362
364
|
|
|
363
365
|
### Browser speech lifecycle
|
|
364
366
|
|
|
@@ -556,6 +558,11 @@ model or selects a fallback. `speech.html` and `voice-transcription.html` consum
|
|
|
556
558
|
one shared `createSTTActivationController()` contract for selected, unloaded,
|
|
557
559
|
loading, unloading, error, and ready presentation plus cancelable user intent.
|
|
558
560
|
Both keep capture unavailable until sticky STT state is exactly ready.
|
|
561
|
+
On mobile browsers, `speech.html` additionally omits transcription activation,
|
|
562
|
+
capture, and transcription status/progress while retaining its TTS control and
|
|
563
|
+
voice status. Its public activation callback returns `false` without dispatch.
|
|
564
|
+
This uses the SDK's browser-device classification, including tablets, and does
|
|
565
|
+
not change the dedicated `voice-transcription.html` workflow or provider APIs.
|
|
559
566
|
|
|
560
567
|
Each shared speech component owns an `AbortController` for its STT request and
|
|
561
568
|
passes its signal through `AI.fetchSTT()`. `voice-transcription.html` also adds
|
|
@@ -411,6 +411,18 @@ comes from sticky runtime role state; the method never forwards standalone
|
|
|
411
411
|
speech booleans or synthesizes ready speech roles without a selected, loaded
|
|
412
412
|
provider.
|
|
413
413
|
|
|
414
|
+
Before model selection is available, conversation surfaces remain hidden unless
|
|
415
|
+
the host supplies LLM availability through `setAIAvailability()`. For a selected
|
|
416
|
+
local LLM, the transcript, session status, composer, upload, speech panel
|
|
417
|
+
(including application-supplied controls), language selector, and timer stay
|
|
418
|
+
hidden until that exact sticky role is `ready` and `loaded`. The existing model
|
|
419
|
+
activation area remains available for Start, progress, cancellation, and retry.
|
|
420
|
+
A selected cloud route (`localOnly:false`) shows the conversation surfaces
|
|
421
|
+
immediately; Send still follows actual LLM availability. Later unload, failure,
|
|
422
|
+
or selection changes update visibility from the same runtime subscription
|
|
423
|
+
without starting model work or changing retained conversation content. A
|
|
424
|
+
completed conversation keeps its transcript visible and its controls hidden.
|
|
425
|
+
|
|
414
426
|
When a selected LLM route is `unloaded` or in `error`, the component exposes a
|
|
415
427
|
keyboard-operable Start/Try again control while Send stays disabled. During
|
|
416
428
|
`loading`, the control becomes Cancel loading and reflects sticky progress
|
|
@@ -1163,8 +1175,17 @@ Shared dependencies: [`AI.js`](runtime-modules.md#aijs),
|
|
|
1163
1175
|
[`ComponentContracts.js`](runtime-modules.md#componentcontractsjs), and
|
|
1164
1176
|
[`DBLS.js`](runtime-modules.md#dblsjs).
|
|
1165
1177
|
|
|
1166
|
-
|
|
1167
|
-
|
|
1178
|
+
On mobile browsers, including tablets, the shared browser-device classifier
|
|
1179
|
+
selects a Speech interface for voice output. Transcription activation,
|
|
1180
|
+
recording, sending recorded audio, and their status/progress are unavailable;
|
|
1181
|
+
`requestSTTActivation()` returns `false` without requesting a provider action.
|
|
1182
|
+
The keyboard remains the voice-input path. The existing TTS mute/unmute control
|
|
1183
|
+
and voice status remain available, and Chat retains its End control. Desktop
|
|
1184
|
+
speech and the dedicated `voice-transcription.html` component retain their
|
|
1185
|
+
existing behavior.
|
|
1186
|
+
|
|
1187
|
+
On desktop, the Hold to talk control remains disabled unless sticky STT state
|
|
1188
|
+
is `ready`, the role is not busy, and microphone capture is available. For an explicitly
|
|
1168
1189
|
selected STT route, a separate keyboard-operable control presents Start
|
|
1169
1190
|
transcription while `unloaded`, Cancel loading while `loading`, a disabled
|
|
1170
1191
|
Canceling state while `unloading`, and Try again with the sticky error while
|
|
@@ -122,7 +122,7 @@ browser map are cataloged separately in [Runtime modules](runtime-modules.md).
|
|
|
122
122
|
| `createBrowserModelSource()` | function | `arcane-os/ai/browser-wasm` | Browser-WASM local AI | Browser Fetch with a readable response body |
|
|
123
123
|
| `createBrowserSpeechArtifactGraph()` | function | `arcane-os/ai/browser-speech` | Browser speech providers | Browser metadata; construction starts no fetch, cache, Worker, provider, or event operation |
|
|
124
124
|
| `createBrowserSpeechAuthority()` | function | `arcane-os/ai/browser-speech` | Browser speech providers | Browser descriptor construction; use requires the selected storage and provider Web APIs |
|
|
125
|
-
| `createBrowserWasmLlmProvider()` | function | `arcane-os/ai/browser-wasm` | Browser-WASM local AI | Browser context with WebAssembly
|
|
125
|
+
| `createBrowserWasmLlmProvider()` | function | `arcane-os/ai/browser-wasm` | Browser-WASM local AI | Browser context with WebAssembly and OPFS/DBOPFS; WebGPU by default or explicit CPU with gpuLayers:0 |
|
|
126
126
|
| `createBrowserWhisperProvider()` | function | `arcane-os/ai/browser-speech` | Browser speech providers | Browser with Workers, object URLs, caller-selected Whisper runtime/model artifacts, and an SDK DBOPFS speech store |
|
|
127
127
|
| `createCanonicalUstarHeader()` | function | `arcane-os` | Packaging and release bundles | Node |
|
|
128
128
|
| `createDbopfsModelStore()` | function | `arcane-os/ai/browser-wasm` | Browser-WASM local AI | Browser with a ready DBOPFS instance and OPFS |
|
|
@@ -6112,8 +6112,9 @@ exposes `protocol`, `id`, default `model`, `catalog`, `capabilities`, `status`,
|
|
|
6112
6112
|
Direct `load()` selects a catalog model and returns `{model,status}`; the
|
|
6113
6113
|
public AI API module's `ai.load()` returns the flat controller status. Load settings include
|
|
6114
6114
|
offline mode, `AbortSignal`, progress, threads, and context/batch/micro-batch
|
|
6115
|
-
tokens.
|
|
6116
|
-
|
|
6115
|
+
tokens. `loadDefaults:{gpuLayers:0}` or `load({gpuLayers:0})` explicitly selects
|
|
6116
|
+
CPU. Omission retains full GPU offload (`gpuLayers:99999`); GPU failures never
|
|
6117
|
+
silently switch to CPU.
|
|
6117
6118
|
|
|
6118
6119
|
Chat supports OpenAI-like message/generation fields, tools, tool choice,
|
|
6119
6120
|
parallel tool-call preference, and JSON/JSON-Schema structured output.
|
|
@@ -6133,11 +6134,13 @@ turn.
|
|
|
6133
6134
|
|
|
6134
6135
|
### Availability and normalization
|
|
6135
6136
|
|
|
6136
|
-
**Browser context with WebAssembly
|
|
6137
|
-
|
|
6137
|
+
**Browser context with WebAssembly and OPFS/DBOPFS.** The default GPU route
|
|
6138
|
+
also requires WebGPU; explicit `gpuLayers:0` CPU loading skips GPU requirements
|
|
6139
|
+
and adapter work. A load succeeds after Wllama confirms the complete model is
|
|
6140
|
+
loaded. There is no automatic CPU fallback.
|
|
6138
6141
|
Cross-origin isolation and coarse hardware fields remain observations rather
|
|
6139
6142
|
than hard gates. Ordinary status reports complete catalog compatibility and
|
|
6140
|
-
lifecycle state.
|
|
6143
|
+
lifecycle state. GPU adapter selection is instrumented as
|
|
6141
6144
|
`arcane.ai.browser-wasm.webgpu.adapter.selected`.
|
|
6142
6145
|
Cancellation normalizes to `ARCANE_AI_REQUEST_ABORTED`; load or availability failures
|
|
6143
6146
|
surface stable `ARCANE_AI_*` codes such as `ARCANE_AI_WEBGPU_REQUIRED`,
|
|
@@ -6278,8 +6281,8 @@ Re-adapting the same provider returns the same adapter object.
|
|
|
6278
6281
|
### Availability and normalization
|
|
6279
6282
|
|
|
6280
6283
|
**Browser; the wrapped provider retains its own secure-context, WebAssembly,
|
|
6281
|
-
OPFS/DBOPFS, WebGPU, model, and lifecycle requirements.**
|
|
6282
|
-
provider/2 selection and lifecycle without adding Node, native, cloud, network,
|
|
6284
|
+
OPFS/DBOPFS, selected CPU or WebGPU route, model, and lifecycle requirements.**
|
|
6285
|
+
The adapter normalizes provider/2 selection and lifecycle without adding Node, native, cloud, network,
|
|
6283
6286
|
or CPU fallback behavior.
|
|
6284
6287
|
|
|
6285
6288
|
### Example
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "arcane-os",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.1",
|
|
4
4
|
"description": "Arcane OS JavaScript SDK, project-local CLI, browser runtime, and repository-portable application packager.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.mjs",
|
|
@@ -68,7 +68,7 @@
|
|
|
68
68
|
"test:release": "node ./bin/arcane-test.mjs test/npm-release.test.mjs",
|
|
69
69
|
"test:unit": "node ./bin/arcane-test.mjs test/app-descriptor.test.mjs test/app-schema.test.mjs test/contracts.test.mjs test/doctor.test.mjs test/mail-credentials.test.mjs test/mail-outbox.test.mjs test/mail-public-api.test.mjs test/mail-send.test.mjs test/mail-transport.test.mjs test/targets.test.mjs test/workspace-operation-lock.test.mjs",
|
|
70
70
|
"test:functional": "node ./bin/arcane-test.mjs test/browser-speech-providers.test.mjs test/browser-wasm-gpu-notice.test.mjs test/browser-wasm-download-resume.test.mjs test/cli.test.mjs test/dbopfs-document-library.test.mjs test/dev-server.test.mjs test/dev-pwa.test.mjs test/dom-event-instrumentation.test.mjs test/event-manager.test.mjs test/events.test.mjs test/import-map.test.mjs test/mail-cli.test.mjs test/mail-runtime.test.mjs test/mail-server.test.mjs test/packaging.test.mjs test/pwa-packaging.test.mjs test/pwa-client.test.mjs test/pwa-worker.test.mjs test/persistent-ai-chat-session.test.mjs test/reference-completeness.test.mjs test/runtime-api-behavior.test.mjs test/runtime.test.mjs test/scaffold.test.mjs test/speech-playback.test.mjs test/site.test.mjs test/update-check.test.mjs",
|
|
71
|
-
"test:integration": "node ./bin/arcane-test.mjs test/integrated-shared.test.mjs test/integrated-workspace.test.mjs test/mail-browser.test.mjs test/native-plan.test.mjs test/native-provider-loader.test.mjs test/npm-release.test.mjs test/release-bundle.test.mjs test/release-capability-smoke.test.mjs test/shared-payload-batch.test.mjs test/tarball.test.mjs test/wllama-webgpu-runtime.test.mjs",
|
|
71
|
+
"test:integration": "node ./bin/arcane-test.mjs test/integrated-shared.test.mjs test/integrated-workspace.test.mjs test/mail-browser.test.mjs test/native-plan.test.mjs test/native-provider-loader.test.mjs test/npm-release.test.mjs test/release-bundle.test.mjs test/release-capability-smoke.test.mjs test/shared-payload-batch.test.mjs test/tarball.test.mjs test/browser-wasm-cpu.test.mjs test/wllama-webgpu-runtime.test.mjs",
|
|
72
72
|
"test:regression": "node ./bin/arcane-test.mjs test/channel-workflows.test.mjs test/html-import-registration.test.mjs test/logging-regression.test.mjs test/markdown-speech.test.mjs test/prepared-speech.test.mjs test/native-provider-generation.test.mjs test/speech-queue-regression.test.mjs test/testing.test.mjs test/test-sets.test.mjs",
|
|
73
73
|
"check": "node tools/check-source.mjs && npm test",
|
|
74
74
|
"check:release": "node tools/check-source.mjs --package-only && npm run test:release",
|
|
@@ -112,6 +112,15 @@
|
|
|
112
112
|
display:none;
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
+
.chat_area[data-model-start-required="true"]:not([data-conversation-complete="true"]) > .conversation_timebox,
|
|
116
|
+
.chat_area[data-model-start-required="true"]:not([data-conversation-complete="true"]) > .chat_output,
|
|
117
|
+
.chat_area[data-model-start-required="true"]:not([data-conversation-complete="true"]) > .chat_session_status,
|
|
118
|
+
.chat_area[data-model-start-required="true"]:not([data-conversation-complete="true"]) > .chat_input_area,
|
|
119
|
+
.chat_area[data-model-start-required="true"]:not([data-conversation-complete="true"]) > #speech,
|
|
120
|
+
.chat_area[data-model-start-required="true"]:not([data-conversation-complete="true"]) > .controls {
|
|
121
|
+
display: none;
|
|
122
|
+
}
|
|
123
|
+
|
|
115
124
|
.conversation_timebox[data-error="true"]{
|
|
116
125
|
color:var(--danger-color,var(--text-color));
|
|
117
126
|
}
|
|
@@ -570,7 +579,7 @@
|
|
|
570
579
|
}
|
|
571
580
|
</style>
|
|
572
581
|
|
|
573
|
-
<section class="chat_area">
|
|
582
|
+
<section class="chat_area" data-model-start-required="true">
|
|
574
583
|
<section
|
|
575
584
|
id="conversation_timebox"
|
|
576
585
|
class="conversation_timebox"
|
|
@@ -1985,6 +1994,14 @@
|
|
|
1985
1994
|
|
|
1986
1995
|
function applyAIAvailability(availability){
|
|
1987
1996
|
host.aiAvailability={...availability};
|
|
1997
|
+
const selectedRole = hasSelectedAIRuntimeRole('llm')
|
|
1998
|
+
? latestAIRuntimeRoles.llm
|
|
1999
|
+
: null;
|
|
2000
|
+
const conversationVisible = selectedRole
|
|
2001
|
+
? selectedRole.localOnly === false
|
|
2002
|
+
|| (selectedRole.state === 'ready' && selectedRole.loaded)
|
|
2003
|
+
: availability.llm;
|
|
2004
|
+
chatArea.dataset.modelStartRequired = String(!conversationVisible);
|
|
1988
2005
|
send.disabled=host.conversationComplete
|
|
1989
2006
|
||!availability.llm
|
|
1990
2007
|
||sessionMessagePending
|
|
@@ -137,10 +137,24 @@
|
|
|
137
137
|
grid-column: 3;
|
|
138
138
|
}
|
|
139
139
|
}
|
|
140
|
+
|
|
141
|
+
:host([data-device-class="mobile"]) {
|
|
142
|
+
grid-template-columns: minmax(0, 1fr) auto;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
:host([data-device-class="mobile"]) .speech_feedback {
|
|
146
|
+
grid-row: 1;
|
|
147
|
+
grid-column: 1;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
:host([data-device-class="mobile"]) #muteButton {
|
|
151
|
+
grid-row: 1;
|
|
152
|
+
grid-column: 2;
|
|
153
|
+
}
|
|
140
154
|
</style>
|
|
141
155
|
|
|
142
156
|
<p class="hidden">Will auto send after X seconds of silence set in code.</p>
|
|
143
|
-
<button id="recordButton" type="button" aria-label="Transcription unavailable" disabled>Hold to talk</button>
|
|
157
|
+
<button id="recordButton" type="button" aria-label="Transcription unavailable" hidden disabled>Hold to talk</button>
|
|
144
158
|
<button id="sttActivationButton" type="button" hidden disabled>Start transcription</button>
|
|
145
159
|
<button id="stopButton" type="button" class="hidden" disabled>Send</button>
|
|
146
160
|
<div class="speech_feedback">
|
|
@@ -155,6 +169,7 @@
|
|
|
155
169
|
<button id="muteButton" type="button" class="mute" aria-pressed="true" aria-label="Voice unavailable" disabled>Voice unavailable</button>
|
|
156
170
|
|
|
157
171
|
<script type="module">
|
|
172
|
+
const browserDeviceImport = import('arcane-os/browser-device');
|
|
158
173
|
const {default:Is}=await import('strong-type');
|
|
159
174
|
const is=new Is(false);
|
|
160
175
|
|
|
@@ -180,8 +195,12 @@
|
|
|
180
195
|
createSTTActivationController,
|
|
181
196
|
formatAIRuntimeProgress
|
|
182
197
|
} = await import('../modules/ComponentContracts.js');
|
|
198
|
+
const {getBrowserDeviceClass} = await browserDeviceImport;
|
|
183
199
|
|
|
184
200
|
const host = this;
|
|
201
|
+
const deviceClass = getBrowserDeviceClass();
|
|
202
|
+
const transcriptionEnabled = deviceClass !== 'mobile';
|
|
203
|
+
host.dataset.deviceClass = deviceClass;
|
|
185
204
|
const events = createArcaneEventSource(
|
|
186
205
|
host,
|
|
187
206
|
{
|
|
@@ -326,28 +345,36 @@
|
|
|
326
345
|
host.configure = configure;
|
|
327
346
|
host.destroy = destroy;
|
|
328
347
|
host.reportTTSError = reportTTSError;
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
348
|
+
if (transcriptionEnabled) {
|
|
349
|
+
host.requestSTTActivation = host.requestSTTActivation
|
|
350
|
+
|| function requestSTTActivation(intent) {
|
|
351
|
+
return requestAIRuntimeIntent(intent);
|
|
352
|
+
};
|
|
353
|
+
} else {
|
|
354
|
+
host.requestSTTActivation = function unavailableMobileSTTActivation() {
|
|
355
|
+
return false;
|
|
332
356
|
};
|
|
357
|
+
}
|
|
333
358
|
host.setMuted = setMuted;
|
|
334
359
|
host.availability = {
|
|
335
|
-
microphone: Boolean(
|
|
360
|
+
microphone: transcriptionEnabled && Boolean(
|
|
336
361
|
navigator.mediaDevices?.getUserMedia
|
|
337
362
|
&& globalThis.MediaRecorder
|
|
338
363
|
),
|
|
339
364
|
stt: false,
|
|
340
365
|
tts: false
|
|
341
366
|
};
|
|
342
|
-
const sttActivationController =
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
367
|
+
const sttActivationController = transcriptionEnabled
|
|
368
|
+
? createSTTActivationController(
|
|
369
|
+
{
|
|
370
|
+
host,
|
|
371
|
+
button: sttActivationButton,
|
|
372
|
+
progress: sttActivationProgress,
|
|
373
|
+
onChange: renderSTTActivationState,
|
|
374
|
+
eventSource: events
|
|
375
|
+
}
|
|
376
|
+
)
|
|
377
|
+
: null;
|
|
351
378
|
|
|
352
379
|
const lifecycleListenerOptions = completeValue(
|
|
353
380
|
{ signal: runtimeStateAbortController.signal }
|
|
@@ -516,13 +543,13 @@
|
|
|
516
543
|
}
|
|
517
544
|
|
|
518
545
|
function applyRoleTransitions(previousSTTRole, previousTTSRole) {
|
|
519
|
-
sttActivationController
|
|
546
|
+
sttActivationController?.synchronize(sttRole);
|
|
520
547
|
const sttBecameUnready = previousSTTRole.state === 'ready'
|
|
521
548
|
&& sttRole.state !== 'ready';
|
|
522
549
|
const ttsBecameUnready = previousTTSRole.state === 'ready'
|
|
523
550
|
&& ttsRole.state !== 'ready';
|
|
524
551
|
|
|
525
|
-
host.availability.stt = sttRole.state === 'ready';
|
|
552
|
+
host.availability.stt = transcriptionEnabled && sttRole.state === 'ready';
|
|
526
553
|
host.availability.tts = ttsRole.state === 'ready';
|
|
527
554
|
|
|
528
555
|
if (sttBecameUnready) {
|
|
@@ -571,6 +598,17 @@
|
|
|
571
598
|
}
|
|
572
599
|
|
|
573
600
|
function renderControls() {
|
|
601
|
+
if (!transcriptionEnabled) {
|
|
602
|
+
recordButton.hidden = true;
|
|
603
|
+
recordButton.disabled = true;
|
|
604
|
+
sttActivationButton.hidden = true;
|
|
605
|
+
sttActivationButton.disabled = true;
|
|
606
|
+
stopButton.hidden = true;
|
|
607
|
+
stopButton.disabled = true;
|
|
608
|
+
sttActivationProgress.hidden = true;
|
|
609
|
+
renderTTSControl();
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
574
612
|
const transcriptionReady = sttRole.state === 'ready';
|
|
575
613
|
const microphoneReady = host.availability.microphone
|
|
576
614
|
&& is.function(globalThis.MediaRecorder)
|
|
@@ -693,17 +731,19 @@
|
|
|
693
731
|
if (localStatus?.message) {
|
|
694
732
|
messages.push(localStatus.message);
|
|
695
733
|
tone = localStatus.tone;
|
|
696
|
-
if (
|
|
697
|
-
|
|
734
|
+
if (transcriptionEnabled
|
|
735
|
+
&& (sttActivationController.pending
|
|
736
|
+
|| ['loading', 'unloading'].includes(sttRole.state))) {
|
|
698
737
|
messages.push(
|
|
699
738
|
describeSTTRole()
|
|
700
739
|
);
|
|
701
740
|
}
|
|
702
|
-
} else {
|
|
741
|
+
} else if (transcriptionEnabled) {
|
|
703
742
|
messages.push(describeSTTRole());
|
|
704
743
|
}
|
|
705
744
|
messages.push(describeTTSRole());
|
|
706
|
-
if (
|
|
745
|
+
if ((transcriptionEnabled && sttRole.state === 'error')
|
|
746
|
+
|| ttsRole.state === 'error') {
|
|
707
747
|
tone = 'error';
|
|
708
748
|
}
|
|
709
749
|
|
|
@@ -712,13 +752,14 @@
|
|
|
712
752
|
speechStatus.setAttribute(
|
|
713
753
|
'aria-busy',
|
|
714
754
|
String(
|
|
715
|
-
|
|
716
|
-
|
|
755
|
+
(transcriptionEnabled
|
|
756
|
+
&& (sttRole.state === 'loading'
|
|
757
|
+
|| sttRole.state === 'unloading'
|
|
758
|
+
|| sttRole.busy
|
|
759
|
+
|| sttActivationController.pending
|
|
760
|
+
|| transcriptionActive))
|
|
717
761
|
|| ttsRole.state === 'loading'
|
|
718
|
-
|| sttRole.busy
|
|
719
762
|
|| ttsRole.busy
|
|
720
|
-
|| sttActivationController.pending
|
|
721
|
-
|| transcriptionActive
|
|
722
763
|
)
|
|
723
764
|
);
|
|
724
765
|
}
|
|
@@ -776,7 +817,9 @@
|
|
|
776
817
|
}
|
|
777
818
|
|
|
778
819
|
async function observeMicrophonePermission() {
|
|
779
|
-
if (!
|
|
820
|
+
if (!transcriptionEnabled
|
|
821
|
+
|| !navigator.permissions?.query
|
|
822
|
+
|| !navigator.mediaDevices?.getUserMedia) {
|
|
780
823
|
return;
|
|
781
824
|
}
|
|
782
825
|
|
|
@@ -1226,7 +1269,8 @@
|
|
|
1226
1269
|
|
|
1227
1270
|
async function record() {
|
|
1228
1271
|
if (
|
|
1229
|
-
|
|
1272
|
+
!transcriptionEnabled
|
|
1273
|
+
|| destroyed
|
|
1230
1274
|
|| sttRole.state !== 'ready'
|
|
1231
1275
|
|| sttRole.busy
|
|
1232
1276
|
|| !host.availability.microphone
|
|
@@ -1725,7 +1769,7 @@
|
|
|
1725
1769
|
|
|
1726
1770
|
destroyed = true;
|
|
1727
1771
|
runtimeStateAbortController.abort();
|
|
1728
|
-
sttActivationController
|
|
1772
|
+
sttActivationController?.destroy();
|
|
1729
1773
|
window.removeEventListener('pagehide', handlePageHide);
|
|
1730
1774
|
window.removeEventListener('pageshow', handlePageShow);
|
|
1731
1775
|
microphonePermissionStatus?.removeEventListener?.(
|