arcane-os 0.92.0 → 0.94.0
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 +32 -0
- package/README.md +1 -1
- package/browser-runtime/ai/twin-cloud.mjs +109 -46
- package/browser-runtime/core/client.mjs +20 -0
- package/browser-runtime/core/contracts.mjs +20 -0
- package/browser-runtime/core/native-diagnostics.mjs +217 -0
- package/docs/reference/ai/twin-cloud.md +35 -14
- package/docs/reference/availability-and-normalization.md +1 -1
- package/docs/reference/core/arcane-api.md +32 -0
- package/docs/reference/core/arcane-events.md +25 -0
- package/docs/reference/core-client.md +99 -0
- package/docs/reference/inventory/package-api.json +97 -13
- package/docs/reference/local-ai.md +81 -5
- package/docs/reference/native-diagnostics.md +167 -0
- package/docs/reference/native-speech.md +220 -11
- package/docs/reference/runtime-modules.md +21 -18
- package/docs/reference/sdk-api.md +152 -11
- package/package.json +4 -1
- package/schemas/arcane-app.schema.json +2 -2
- package/schemas/arcane-package.schema.json +2 -2
- package/src/core/app-control.mjs +1 -0
- package/src/core/hosts/windows/ArcaneAppControl.cs +1 -1
- package/src/core/hosts/windows/ArcaneHost.cs +167 -0
- package/src/core/hosts/windows/ArcaneLauncher.cs +1 -0
- package/src/core/hosts/windows/ArcaneNativeDiagnostics.cs +137 -0
- package/src/core/hosts/windows/ArcaneNotifications.cs +822 -0
- package/src/core/hosts/windows/ArcaneWindowControl.cs +3 -2
- package/src/core/services/speech.mjs +85 -23
- package/src/core-native-diagnostics-source.mjs +13 -0
- package/src/import-map.mjs +1 -0
- package/src/local-ai/config.mjs +12 -1
- package/src/local-ai/dev.mjs +71 -4
- package/src/local-ai/install.mjs +113 -5
- package/src/local-ai/kokoro/frontend.mjs +219 -0
- package/src/local-ai/kokoro/helper.mjs +168 -0
- package/src/local-ai/kokoro/index.mjs +645 -0
- package/src/local-ai/kokoro/native/CMakeLists.txt +45 -0
- package/src/local-ai/kokoro/native/main.cpp +249 -0
- package/src/native/portable-layout.mjs +67 -9
|
@@ -82,6 +82,7 @@ runtime layouts.
|
|
|
82
82
|
| `arcane-os/core/image` | Native image Core service with independent runtime preparation and retained model ownership. |
|
|
83
83
|
| `arcane-os/core/speech` | Independent injected STT/TTS engine loading, request cancellation and joined Core service lifetime. |
|
|
84
84
|
| `arcane-os/local-ai/whisper` | Retained native Whisper model, complete recordings and joined helper lifetime. |
|
|
85
|
+
| `arcane-os/local-ai/kokoro` | Retained native Kokoro synthesis using the existing ONNX/model-resource owners and a compiled phonemization/Opus helper. |
|
|
85
86
|
| `arcane-os/local-ai/whisper/build` | First-party persistent helper build against selected installed Whisper headers and libraries. |
|
|
86
87
|
| `arcane-os/local-ai/whisper/openvino-build` | Optional Windows x64 Intel NPU encoder runtime producer using selected installed sources and toolchain. |
|
|
87
88
|
| `arcane-os/ai/core-model-assets` | Complete stored browser model members projected through Core into native working files. |
|
|
@@ -469,6 +470,12 @@ The remaining data-only subpaths are eight JSON Schemas and package metadata.
|
|
|
469
470
|
| `createLocalImageService()` | function | `arcane-os/core/image` | Core local images | Node Core with selected stable-diffusion.cpp requirements |
|
|
470
471
|
| `createLocalImageService default export` | function | `arcane-os/core/image` | Core local images | Same native image service factory |
|
|
471
472
|
| `createSpeechService()` | function | `arcane-os/core/speech` | Core native speech | Node Core with explicitly supplied speech engines |
|
|
473
|
+
| `createNativeKokoroRuntime()` | function | `arcane-os/local-ai/kokoro` | Core native speech | Node with matching prepared helper/data, selected model resources and an existing ONNX owner |
|
|
474
|
+
| `createNativeKokoroRuntime default export` | function | `arcane-os/local-ai/kokoro` | Core native speech | Same retained native Kokoro factory |
|
|
475
|
+
| `KOKORO_MODEL` | constant | `arcane-os/local-ai/kokoro` | Core native speech | Selected upstream model identifier |
|
|
476
|
+
| `KOKORO_REVISION` | constant | `arcane-os/local-ai/kokoro` | Core native speech | Selected upstream model revision |
|
|
477
|
+
| `KOKORO_SAMPLE_RATE` | constant | `arcane-os/local-ai/kokoro` | Core native speech | Native PCM sample rate |
|
|
478
|
+
| `KOKORO_VOICES` | constant | `arcane-os/local-ai/kokoro` | Core native speech | Complete 28-voice US/GB selection |
|
|
472
479
|
| `createWhisperRuntime()` | function | `arcane-os/local-ai/whisper` | Core native speech | Node with a prepared matching helper, runtime, decoder and selected model |
|
|
473
480
|
| `createWhisperRuntime default export` | function | `arcane-os/local-ai/whisper` | Core native speech | Same retained native engine factory |
|
|
474
481
|
| `buildWhisperHelper()` | function | `arcane-os/local-ai/whisper/build` | Core native speech | Node with installed CMake/C++17 toolchain and matching Whisper source/runtime |
|
|
@@ -8842,6 +8849,118 @@ selected model and native engine.
|
|
|
8842
8849
|
The default export from `arcane-os/core/image` is the same
|
|
8843
8850
|
`createLocalImageService` factory and lifecycle described above.
|
|
8844
8851
|
|
|
8852
|
+
## createNativeKokoroRuntime()
|
|
8853
|
+
|
|
8854
|
+
### Overview and signature
|
|
8855
|
+
|
|
8856
|
+
The named export from `arcane-os/local-ai/kokoro` composes the selected Kokoro
|
|
8857
|
+
FP32 graph with the host's existing native ONNX owner, model-resource owner
|
|
8858
|
+
and prepared persistent phonemization/Opus helper. Construction starts no
|
|
8859
|
+
native work and acquires no model or runtime.
|
|
8860
|
+
|
|
8861
|
+
```javascript
|
|
8862
|
+
createNativeKokoroRuntime({
|
|
8863
|
+
onnx, modelAssets, runtime, prepare, modelId = 'kokoro',
|
|
8864
|
+
model = KOKORO_MODEL, revision = KOKORO_REVISION, dtype = 'fp32',
|
|
8865
|
+
paths, assetProjectionId, resourcePaths, sessionOptions,
|
|
8866
|
+
executionPreference = 'gpu', executionTarget, signal, onEvent
|
|
8867
|
+
} = {})
|
|
8868
|
+
```
|
|
8869
|
+
|
|
8870
|
+
Supply the existing `onnx` runtime or a `prepare` callback that provides it.
|
|
8871
|
+
`runtime` describes the matching native helper and data. `paths` supplies
|
|
8872
|
+
complete prepared graph/tokenizer/voice resources; a completed
|
|
8873
|
+
`assetProjectionId` instead retains projected resources through `modelAssets`.
|
|
8874
|
+
The selected model/revision/FP32 contract is fixed by this frontend.
|
|
8875
|
+
See [native Kokoro](native-speech.md) for exact resource mappings, preparation,
|
|
8876
|
+
runtime distribution and model ownership.
|
|
8877
|
+
|
|
8878
|
+
### Members and results
|
|
8879
|
+
|
|
8880
|
+
- `current()` returns complete model/lifecycle state, execution configuration,
|
|
8881
|
+
current request, queue, progress, error, default voice and supported voices.
|
|
8882
|
+
- `subscribe(listener,{replay=true,signal}={})` returns an unsubscribe function
|
|
8883
|
+
and replays current state by default.
|
|
8884
|
+
- `load(options={})` activates the selected resources, retaining omitted
|
|
8885
|
+
selections. Explicit projection/resource mapping or execution-target changes
|
|
8886
|
+
join prior native retirement before loading the new activation.
|
|
8887
|
+
- `synthesize(request,{signal,onProgress,requestId=null}={})` requires a loaded
|
|
8888
|
+
activation. `request` contains the complete `input` string, optional `model`
|
|
8889
|
+
matching `modelId`, `voice` (default `af_heart`), positive finite `speed`
|
|
8890
|
+
(default `1`) and `responseFormat` (`wav` by default, `opus` or `ogg`).
|
|
8891
|
+
The result is `{audioBase64,contentType,sampleRate,channels,model,voice,speed}`;
|
|
8892
|
+
content type is `audio/wav` or `audio/ogg; codecs=opus`, with mono 24kHz PCM
|
|
8893
|
+
synthesis. The full input is processed in ordered model segments.
|
|
8894
|
+
- `unload()` retires the activation and joins owned native work. `close()`
|
|
8895
|
+
permanently closes the engine and releases its state subscriptions.
|
|
8896
|
+
|
|
8897
|
+
Queued requests retain order. Cancelling a queued request removes only that
|
|
8898
|
+
request. Active cancellation waits for actual helper/session retirement;
|
|
8899
|
+
surviving requests resume with the same accepted provider configuration.
|
|
8900
|
+
The ONNX owner may need a running native call to return before its worker exits.
|
|
8901
|
+
Readiness is lost while that retirement/reload occurs. Errors retain the actual
|
|
8902
|
+
engine and cleanup failures; unsupported explicit voices or formats are never
|
|
8903
|
+
substituted. No audio is played by this engine.
|
|
8904
|
+
|
|
8905
|
+
### Example
|
|
8906
|
+
|
|
8907
|
+
```javascript
|
|
8908
|
+
import {createNativeKokoroRuntime} from 'arcane-os/local-ai/kokoro';
|
|
8909
|
+
|
|
8910
|
+
// The host supplies its existing ONNX owner and already prepared resources.
|
|
8911
|
+
async function narrateMoonGoat({onnx, runtime, paths}) {
|
|
8912
|
+
const voice = createNativeKokoroRuntime({
|
|
8913
|
+
onnx, runtime, paths, executionPreference: 'cpu'
|
|
8914
|
+
});
|
|
8915
|
+
try {
|
|
8916
|
+
await voice.load();
|
|
8917
|
+
return await voice.synthesize({
|
|
8918
|
+
input: 'Captain Goat has landed. Guard the moon cheese!',
|
|
8919
|
+
voice: 'af_heart', responseFormat: 'wav'
|
|
8920
|
+
});
|
|
8921
|
+
} finally {
|
|
8922
|
+
await voice.close();
|
|
8923
|
+
}
|
|
8924
|
+
}
|
|
8925
|
+
```
|
|
8926
|
+
|
|
8927
|
+
Changing `voice` to `bf_emma` selects British English. `responseFormat: 'opus'`
|
|
8928
|
+
selects Ogg Opus encoding. GPU/physical-device selection uses the existing
|
|
8929
|
+
[ONNX execution contract](local-ai.md#physical-execution-targets); an accepted
|
|
8930
|
+
provider configuration is separate from observed hardware execution.
|
|
8931
|
+
The SDK publishes a Windows x64 helper archive. Other platforms need a matching
|
|
8932
|
+
custom helper distribution and supported native ONNX host. Native helper
|
|
8933
|
+
production and platform execution evidence remain separate.
|
|
8934
|
+
|
|
8935
|
+
## createNativeKokoroRuntime default export
|
|
8936
|
+
|
|
8937
|
+
The default export from `arcane-os/local-ai/kokoro` is the same
|
|
8938
|
+
`createNativeKokoroRuntime` factory with identical inputs, results and lifetime.
|
|
8939
|
+
|
|
8940
|
+
## KOKORO_MODEL
|
|
8941
|
+
|
|
8942
|
+
Named string export from `arcane-os/local-ai/kokoro`, with value
|
|
8943
|
+
`onnx-community/Kokoro-82M-v1.0-ONNX`. It identifies this frontend's model family
|
|
8944
|
+
for matching graph, tokenizer and voice resource selection.
|
|
8945
|
+
|
|
8946
|
+
## KOKORO_REVISION
|
|
8947
|
+
|
|
8948
|
+
Named string export from `arcane-os/local-ai/kokoro`, with value
|
|
8949
|
+
`1939ad2a8e416c0acfeecc08a694d14ef25f2231`. Resource preparation uses this
|
|
8950
|
+
upstream revision with `KOKORO_MODEL`.
|
|
8951
|
+
|
|
8952
|
+
## KOKORO_SAMPLE_RATE
|
|
8953
|
+
|
|
8954
|
+
Named number export from `arcane-os/local-ai/kokoro`, with value `24000`.
|
|
8955
|
+
It describes the engine's mono PCM synthesis rate in samples per second.
|
|
8956
|
+
|
|
8957
|
+
## KOKORO_VOICES
|
|
8958
|
+
|
|
8959
|
+
Named array export from `arcane-os/local-ai/kokoro` containing all 28 supported
|
|
8960
|
+
US/GB voice identifiers. The engine uses `af_heart` when the caller omits
|
|
8961
|
+
`voice`; an explicit unsupported selection reports an error. Use this complete
|
|
8962
|
+
array to populate a native Kokoro voice selector.
|
|
8963
|
+
|
|
8845
8964
|
## createWhisperRuntime()
|
|
8846
8965
|
|
|
8847
8966
|
### Overview
|
|
@@ -9018,6 +9137,13 @@ The returned object has `name: 'speech'`, `current`, `subscribe`, `close`,
|
|
|
9018
9137
|
Core RPC methods. The context supplies `signal`, `requestId` and `emit`.
|
|
9019
9138
|
The service publishes synchronous accepted `Thinking` before waiting for
|
|
9020
9139
|
its role's load, then forwards the engine's complete progress and result.
|
|
9140
|
+
- `methods['speech.load'](request,context)` and
|
|
9141
|
+
`methods['speech.unload'](request,context)` route explicit activation to the
|
|
9142
|
+
explicit `role: 'stt' | 'tts'` and forward the remaining activation
|
|
9143
|
+
fields and result through that role's configured engine. A startup load remains owned independently of callers
|
|
9144
|
+
waiting for it. The browser facade exposes `Arcane.speech.load(request,options)`
|
|
9145
|
+
and `unload(request,options)` with `signal`, `onRequest` and no elapsed-time
|
|
9146
|
+
cutoff. See [activation routing](native-speech.md) for selection and errors.
|
|
9021
9147
|
- `close()` and its `drain()` alias stop acceptance, abort the service lifetime,
|
|
9022
9148
|
close both engines concurrently and await startup and request work. Repeated
|
|
9023
9149
|
calls share one promise. `dispose()` also releases event subscriptions.
|
|
@@ -9123,7 +9249,8 @@ import createLocalAIService from 'arcane-os/core/local-ai';
|
|
|
9123
9249
|
### Overview
|
|
9124
9250
|
|
|
9125
9251
|
Reuses matching managed runtimes or installs complete selected
|
|
9126
|
-
llama.cpp/Ollama/ONNX/image distributions
|
|
9252
|
+
llama.cpp/Ollama/ONNX/image distributions, NeMo libraries or native Kokoro
|
|
9253
|
+
helper/data trees in an explicitly supplied directory.
|
|
9127
9254
|
|
|
9128
9255
|
### Signature and result
|
|
9129
9256
|
|
|
@@ -9146,6 +9273,12 @@ trees remain intact while a fresh installation is prepared. See
|
|
|
9146
9273
|
Image requirements also accept `backend` and
|
|
9147
9274
|
`models`; their pinned default runtime, CPU/Metal selection and separate model
|
|
9148
9275
|
preparation are described in [local image generation](local-image-generation.md).
|
|
9276
|
+
`kokoro-native` returns `helperExecutable`, `espeakDataDirectory` and
|
|
9277
|
+
`libraryDirectory`. Windows x64 defaults to the helper archive from the
|
|
9278
|
+
installed SDK's numeric release; an explicit `url` or `helperRoot` selects a
|
|
9279
|
+
complete custom distribution. The data directory is the parent of
|
|
9280
|
+
`espeak-ng-data`. Helper preparation loads no Kokoro graph and keeps the
|
|
9281
|
+
existing ONNX/model-resource owners separate. See [native speech](native-speech.md).
|
|
9149
9282
|
Matching concurrent
|
|
9150
9283
|
installs share their work. Downloads retain runtime libraries and surface
|
|
9151
9284
|
network/archive failures; cancellation and operation events remain observable.
|
|
@@ -9189,6 +9322,10 @@ The corrected Windows x64 ONNX selection retains its
|
|
|
9189
9322
|
is relocated with the other runtime paths; bundling introduces no separate
|
|
9190
9323
|
runtime selection. See [ONNX runtime preparation](local-ai.md#development).
|
|
9191
9324
|
|
|
9325
|
+
Kokoro bundling retains the complete helper/data tree and relocates
|
|
9326
|
+
`helperExecutable`, `espeakDataDirectory` and `libraryDirectory`. Its selected
|
|
9327
|
+
ONNX runtime and model resources retain their existing independent ownership.
|
|
9328
|
+
|
|
9192
9329
|
Image records relocate the complete library, binding and variant paths. Model
|
|
9193
9330
|
URL descriptors retain their original acquisition metadata for preparation
|
|
9194
9331
|
after launch. Runtime-contained native model paths become artifact-relative;
|
|
@@ -11683,10 +11820,11 @@ the message or response callback payload.
|
|
|
11683
11820
|
|
|
11684
11821
|
**Node and Browser; Cloud transport.** Uses standard Fetch and cancellation,
|
|
11685
11822
|
without DOM, browser profiles, user singletons, or storage initialization.
|
|
11686
|
-
Import starts no request. HTTP `429`
|
|
11687
|
-
|
|
11688
|
-
`
|
|
11689
|
-
|
|
11823
|
+
Import starts no request. Rejected Fetch and all HTTP `429`/`529` share three
|
|
11824
|
+
retries. Quota recovery honors `Retry-After`, otherwise waits `3000`, `6000`,
|
|
11825
|
+
or `12000` milliseconds by retry number. Active same-endpoint/credential calls
|
|
11826
|
+
share quota cooldown and paced dispatch. Complete error bodies remain unchanged
|
|
11827
|
+
on exhaustion; other HTTP failures are not retried. A missing key throws
|
|
11690
11828
|
`AI_PROVIDER_NOT_CONFIGURED`; a missing model throws `TypeError`. `signal`
|
|
11691
11829
|
cancellation during transport, body reading, retry waiting,
|
|
11692
11830
|
or callback settlement prevents successful return and uses
|
|
@@ -11836,21 +11974,24 @@ Ordinary TWiN callers use `fetchRequest()` instead.
|
|
|
11836
11974
|
### Signature and result
|
|
11837
11975
|
|
|
11838
11976
|
```text
|
|
11839
|
-
async fetchHTTPResponse(url,options)
|
|
11977
|
+
async fetchHTTPResponse(url,options,{onRetry=null}={})
|
|
11840
11978
|
```
|
|
11841
11979
|
|
|
11842
11980
|
Returns the successful Fetch `Response` without consuming its body. Both URL
|
|
11843
11981
|
and Fetch options are caller-supplied; this helper adds no key, model, or
|
|
11844
11982
|
request envelope. Non-success responses are read completely as JSON when the
|
|
11845
|
-
content type contains `application/json`, otherwise as text.
|
|
11846
|
-
|
|
11983
|
+
content type contains `application/json`, otherwise as text. Rejected Fetch
|
|
11984
|
+
and all `429`/`529` share three retries with complete terminal errors.
|
|
11985
|
+
`429` honors `Retry-After` or exponential delay and coordinates a transient
|
|
11986
|
+
same-endpoint/credential cooldown. See [shared request behavior](ai/twin-cloud.md#shared-request-behavior).
|
|
11847
11987
|
|
|
11848
11988
|
### Availability and normalization
|
|
11849
11989
|
|
|
11850
11990
|
**Node and Browser.** Exported from `arcane-os/ai/twin-cloud` for shared SDK
|
|
11851
|
-
integration. `options.signal` cancels Fetch and
|
|
11991
|
+
integration. `options.signal` cancels Fetch and recovery/pacing waits and is checked
|
|
11852
11992
|
after response/error-body reads. Cancellation uses
|
|
11853
|
-
`ARCANE_AI_REQUEST_ABORTED`.
|
|
11993
|
+
`ARCANE_AI_REQUEST_ABORTED`. Retry warnings use the existing shared logger;
|
|
11994
|
+
`onRetry` observes complete waiting/requesting records outside the payload.
|
|
11854
11995
|
|
|
11855
11996
|
### Example
|
|
11856
11997
|
|
|
@@ -11870,7 +12011,7 @@ Shared complete JSON-body reader built on `fetchHTTPResponse()`.
|
|
|
11870
12011
|
### Signature and result
|
|
11871
12012
|
|
|
11872
12013
|
```text
|
|
11873
|
-
async fetchJSONResponse(url,options)
|
|
12014
|
+
async fetchJSONResponse(url,options,{onRetry=null}={})
|
|
11874
12015
|
```
|
|
11875
12016
|
|
|
11876
12017
|
Returns the full parsed JSON value. A successful response whose content type
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "arcane-os",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.94.0",
|
|
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",
|
|
@@ -65,6 +65,8 @@
|
|
|
65
65
|
"./core/stdio": "./src/core/stdio.mjs",
|
|
66
66
|
"./core/host": "./src/core/host.mjs",
|
|
67
67
|
"./core/app-control": "./src/core/app-control.mjs",
|
|
68
|
+
"./core/native-diagnostics": "./browser-runtime/core/native-diagnostics.mjs",
|
|
69
|
+
"./core/native-diagnostics-source": "./src/core-native-diagnostics-source.mjs",
|
|
68
70
|
"./codex/app-server": "./src/codex/app-server.mjs",
|
|
69
71
|
"./mcp/stdio": "./src/mcp/stdio-server.mjs",
|
|
70
72
|
"./core/repositories": "./src/core/repositories.mjs",
|
|
@@ -87,6 +89,7 @@
|
|
|
87
89
|
"./local-ai/decisions": "./src/local-ai/decisions.mjs",
|
|
88
90
|
"./local-ai/image": "./src/local-ai/image-runtime.mjs",
|
|
89
91
|
"./local-ai/whisper": "./src/local-ai/whisper/index.mjs",
|
|
92
|
+
"./local-ai/kokoro": "./src/local-ai/kokoro/index.mjs",
|
|
90
93
|
"./local-ai/whisper/build": "./src/local-ai/whisper/build.mjs",
|
|
91
94
|
"./local-ai/whisper/openvino-build": "./src/local-ai/whisper/openvino-build.mjs",
|
|
92
95
|
"./integrated-provider": "./src/integrated-provider-loader.mjs",
|
|
@@ -277,12 +277,12 @@
|
|
|
277
277
|
"type": "array",
|
|
278
278
|
"items": {
|
|
279
279
|
"oneOf": [
|
|
280
|
-
{ "enum": ["llama.cpp", "ollama", "onnx", "stable-diffusion.cpp", "whisper.cpp"] },
|
|
280
|
+
{ "enum": ["llama.cpp", "ollama", "onnx", "stable-diffusion.cpp", "whisper.cpp", "kokoro-native"] },
|
|
281
281
|
{
|
|
282
282
|
"type": "object",
|
|
283
283
|
"required": ["id"],
|
|
284
284
|
"properties": {
|
|
285
|
-
"id": { "enum": ["llama.cpp", "ollama", "onnx", "stable-diffusion.cpp"] },
|
|
285
|
+
"id": { "enum": ["llama.cpp", "ollama", "onnx", "stable-diffusion.cpp", "kokoro-native"] },
|
|
286
286
|
"version": { "type": "string" },
|
|
287
287
|
"url": { "type": "string" },
|
|
288
288
|
"backend": { "type": "string", "description": "llama.cpp: auto, cpu or cuda. Image runtimes: auto, metal or cpu." },
|
|
@@ -156,12 +156,12 @@
|
|
|
156
156
|
"type": "array",
|
|
157
157
|
"items": {
|
|
158
158
|
"oneOf": [
|
|
159
|
-
{ "enum": ["llama.cpp", "ollama", "onnx", "stable-diffusion.cpp", "whisper.cpp"] },
|
|
159
|
+
{ "enum": ["llama.cpp", "ollama", "onnx", "stable-diffusion.cpp", "whisper.cpp", "kokoro-native"] },
|
|
160
160
|
{
|
|
161
161
|
"type": "object",
|
|
162
162
|
"required": ["id"],
|
|
163
163
|
"properties": {
|
|
164
|
-
"id": { "enum": ["llama.cpp", "ollama", "onnx", "stable-diffusion.cpp"] },
|
|
164
|
+
"id": { "enum": ["llama.cpp", "ollama", "onnx", "stable-diffusion.cpp", "kokoro-native"] },
|
|
165
165
|
"version": { "type": "string" },
|
|
166
166
|
"url": { "type": "string" },
|
|
167
167
|
"backend": { "type": "string", "description": "llama.cpp: auto, cpu or cuda. Image runtimes: auto, metal or cpu." },
|
package/src/core/app-control.mjs
CHANGED
|
@@ -15,6 +15,7 @@ export async function connectAppControl({endpoint, signal, onError = reportAppCo
|
|
|
15
15
|
endpoint,
|
|
16
16
|
closed: connection.closed,
|
|
17
17
|
status(options) { return client.invoke('app.control.status', {}, options); },
|
|
18
|
+
diagnostics(parameters = {}, options) { return client.invoke('app.control.diagnostics', parameters, options); },
|
|
18
19
|
inspect(parameters = {}, options) { return client.invoke('app.control.inspect', parameters, options); },
|
|
19
20
|
capture(parameters = {}, options) { return client.invoke('app.control.capture', parameters, options); },
|
|
20
21
|
act(parameters, options) { return client.invoke('app.control.act', parameters, options); },
|
|
@@ -452,7 +452,7 @@ namespace Arcane.Core.Hosts.Windows
|
|
|
452
452
|
try
|
|
453
453
|
{
|
|
454
454
|
cancellation.Token.ThrowIfCancellationRequested();
|
|
455
|
-
if (method != "app.control.status" && method != "app.control.inspect"
|
|
455
|
+
if (method != "app.control.status" && method != "app.control.diagnostics" && method != "app.control.inspect"
|
|
456
456
|
&& method != "app.control.capture" && method != "app.control.act"
|
|
457
457
|
&& method != "app.control.key" && method != "app.control.resize")
|
|
458
458
|
throw new NotSupportedException("The app does not expose " + method + ".");
|
|
@@ -24,6 +24,7 @@ namespace Arcane.Core.Hosts.Windows
|
|
|
24
24
|
public string Title { get; set; }
|
|
25
25
|
public string IconPath { get; set; }
|
|
26
26
|
public string ClassicClientSource { get; set; }
|
|
27
|
+
public string NativeDiagnosticsSource { get; set; }
|
|
27
28
|
public string AppControlEndpoint { get; set; }
|
|
28
29
|
public string AppControlSource { get; set; }
|
|
29
30
|
public string CoreExecutable { get; set; }
|
|
@@ -102,15 +103,20 @@ namespace Arcane.Core.Hosts.Windows
|
|
|
102
103
|
private readonly Action<string> onDiagnostic;
|
|
103
104
|
private readonly Action<Exception> onError;
|
|
104
105
|
private readonly WebView2 webView;
|
|
106
|
+
private readonly ArcaneNativeDiagnostics nativeDiagnostics;
|
|
105
107
|
private readonly object stateLock = new object();
|
|
106
108
|
private readonly Queue<CoreDelivery> pending = new Queue<CoreDelivery>();
|
|
107
109
|
private readonly Dictionary<string, long> requestGenerations = new Dictionary<string, long>();
|
|
110
|
+
private readonly Dictionary<string, DesktopNotificationRequest> desktopNotificationRequests =
|
|
111
|
+
new Dictionary<string, DesktopNotificationRequest>();
|
|
108
112
|
private readonly List<Exception> failures = new List<Exception>();
|
|
109
113
|
private readonly List<Task> failureNotifications = new List<Task>();
|
|
110
114
|
private readonly TaskCompletionSource<object> ready = new TaskCompletionSource<object>();
|
|
111
115
|
private readonly TaskCompletionSource<object> initialized = new TaskCompletionSource<object>();
|
|
112
116
|
private readonly TaskCompletionSource<object> completion = new TaskCompletionSource<object>();
|
|
113
117
|
private ArcaneCoreProcess core;
|
|
118
|
+
private ArcaneNotifications desktopNotifications;
|
|
119
|
+
private Task desktopNotificationsClosing;
|
|
114
120
|
private ArcaneBridge bridge;
|
|
115
121
|
private ArcaneBridge activeBridge;
|
|
116
122
|
private Task initialization;
|
|
@@ -119,6 +125,8 @@ namespace Arcane.Core.Hosts.Windows
|
|
|
119
125
|
private Task iconLoading;
|
|
120
126
|
private Icon applicationIcon;
|
|
121
127
|
private string injectedScript;
|
|
128
|
+
private string nativeDiagnosticsScript;
|
|
129
|
+
private bool nativeDiagnosticsRegistered;
|
|
122
130
|
private Exception transportFailure;
|
|
123
131
|
private long generation;
|
|
124
132
|
private long nextGeneration;
|
|
@@ -144,6 +152,7 @@ namespace Arcane.Core.Hosts.Windows
|
|
|
144
152
|
this.options = options;
|
|
145
153
|
this.onDiagnostic = onDiagnostic;
|
|
146
154
|
this.onError = onError;
|
|
155
|
+
nativeDiagnostics = new ArcaneNativeDiagnostics(Report);
|
|
147
156
|
ready.Task.ContinueWith(ObserveReportedFailure, TaskContinuationOptions.OnlyOnFaulted);
|
|
148
157
|
completion.Task.ContinueWith(ObserveReportedFailure, TaskContinuationOptions.OnlyOnFaulted);
|
|
149
158
|
Text = options.Title;
|
|
@@ -494,6 +503,10 @@ namespace Arcane.Core.Hosts.Windows
|
|
|
494
503
|
browser.SetVirtualHostNameToFolderMapping(options.OriginHost, applicationRoot,
|
|
495
504
|
CoreWebView2HostResourceAccessKind.Allow);
|
|
496
505
|
InstallDocumentBridge(generation);
|
|
506
|
+
// Register the diagnostic observer first so failures in the
|
|
507
|
+
// ordinary client itself use an ingress independent of Core.
|
|
508
|
+
await InstallNativeDiagnosticsAsync(browser);
|
|
509
|
+
if (closing) return;
|
|
497
510
|
// The selected generator uses replayRuntimeState:true. Both RPC
|
|
498
511
|
// installation and the shared event owner remain SDK-owned.
|
|
499
512
|
Task scripts = Task.WhenAll(InstallClassicClientAsync(browser), InstallAppControlAsync(browser));
|
|
@@ -523,6 +536,15 @@ namespace Arcane.Core.Hosts.Windows
|
|
|
523
536
|
injectedScript = await browser.AddScriptToExecuteOnDocumentCreatedAsync(options.ClassicClientSource);
|
|
524
537
|
}
|
|
525
538
|
|
|
539
|
+
private async Task InstallNativeDiagnosticsAsync(CoreWebView2 browser)
|
|
540
|
+
{
|
|
541
|
+
if (String.IsNullOrEmpty(options.NativeDiagnosticsSource)) return;
|
|
542
|
+
browser.AddHostObjectToScript("arcaneDiagnostics", nativeDiagnostics);
|
|
543
|
+
nativeDiagnosticsRegistered = true;
|
|
544
|
+
nativeDiagnosticsScript = await browser.AddScriptToExecuteOnDocumentCreatedAsync(options.NativeDiagnosticsSource);
|
|
545
|
+
nativeDiagnostics.Installed();
|
|
546
|
+
}
|
|
547
|
+
|
|
526
548
|
private void NavigationStarting(object sender, CoreWebView2NavigationStartingEventArgs args)
|
|
527
549
|
{
|
|
528
550
|
if (nextGeneration != 0 && navigationId == args.NavigationId) return;
|
|
@@ -561,6 +583,33 @@ namespace Arcane.Core.Hosts.Windows
|
|
|
561
583
|
if (documentGeneration != generation || closing)
|
|
562
584
|
throw new InvalidOperationException("The requesting document is no longer connected.");
|
|
563
585
|
string method = EnvelopeString(envelope, "method");
|
|
586
|
+
if (EnvelopeString(envelope, "protocol") == "arcane/1")
|
|
587
|
+
{
|
|
588
|
+
if (requestId != null && (method == "notifications.status" || method == "notifications.show"
|
|
589
|
+
|| method == "notifications.state" || method == "notifications.close"))
|
|
590
|
+
{
|
|
591
|
+
DesktopNotificationRequest operation = new DesktopNotificationRequest(documentGeneration);
|
|
592
|
+
desktopNotificationRequests.Add(requestId, operation);
|
|
593
|
+
operation.Completion = CompleteDesktopNotificationAsync(envelope, requestId, method, operation);
|
|
594
|
+
operation.Completion.ContinueWith(ObserveUnexpectedFailure, TaskContinuationOptions.OnlyOnFaulted);
|
|
595
|
+
// Bridge acceptance means this host owns the request;
|
|
596
|
+
// the correlated response carries its native outcome.
|
|
597
|
+
return Task.FromResult<object>(null);
|
|
598
|
+
}
|
|
599
|
+
if (EnvelopeString(envelope, "type") == "control")
|
|
600
|
+
{
|
|
601
|
+
string control = EnvelopeString(envelope, "control");
|
|
602
|
+
DesktopNotificationRequest selected;
|
|
603
|
+
if (control == "request.cancel" && desktopNotificationRequests.TryGetValue(
|
|
604
|
+
EnvelopeString(envelope, "requestId") ?? String.Empty, out selected)
|
|
605
|
+
&& selected.Generation == documentGeneration)
|
|
606
|
+
{
|
|
607
|
+
selected.Cancellation.Cancel();
|
|
608
|
+
return Task.FromResult<object>(null);
|
|
609
|
+
}
|
|
610
|
+
if (control == "requests.cancelAll") CancelDesktopNotificationRequests(documentGeneration);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
564
613
|
if (requestId != null && EnvelopeString(envelope, "protocol") == "arcane/1"
|
|
565
614
|
&& (method == "window.setTheme" || method == "window.state" || method == "window.setState"))
|
|
566
615
|
{
|
|
@@ -584,6 +633,115 @@ namespace Arcane.Core.Hosts.Windows
|
|
|
584
633
|
}
|
|
585
634
|
}
|
|
586
635
|
|
|
636
|
+
private async Task CompleteDesktopNotificationAsync(Dictionary<string, object> request,
|
|
637
|
+
string id, string method, DesktopNotificationRequest operation)
|
|
638
|
+
{
|
|
639
|
+
try
|
|
640
|
+
{
|
|
641
|
+
Dictionary<string, object> response = new Dictionary<string, object>
|
|
642
|
+
{
|
|
643
|
+
{ "protocol", "arcane/1" }, { "type", "response" }, { "id", id }
|
|
644
|
+
};
|
|
645
|
+
try
|
|
646
|
+
{
|
|
647
|
+
object value;
|
|
648
|
+
Dictionary<string, object> parameters = request.TryGetValue("parameters", out value)
|
|
649
|
+
? value as Dictionary<string, object> : new Dictionary<string, object>();
|
|
650
|
+
if (parameters == null) throw new ArgumentException("Supply a notification request object.");
|
|
651
|
+
if (desktopNotifications == null)
|
|
652
|
+
desktopNotifications = new ArcaneNotifications(options.ApplicationId, options.ProfileDirectory,
|
|
653
|
+
options.Title, options.IconPath, DesktopNotificationChanged);
|
|
654
|
+
Dictionary<string, object> result = await desktopNotifications.InvokeAsync(
|
|
655
|
+
method, parameters, operation.Cancellation.Token).ConfigureAwait(false);
|
|
656
|
+
// Cancellation after native submission does not rewrite an
|
|
657
|
+
// accepted result. Its record also remains in host state.
|
|
658
|
+
response["ok"] = true;
|
|
659
|
+
response["result"] = result;
|
|
660
|
+
}
|
|
661
|
+
catch (Exception error)
|
|
662
|
+
{
|
|
663
|
+
string code = error.Data["code"] as string;
|
|
664
|
+
if (code == null) code = error is OperationCanceledException ? "CANCELLED"
|
|
665
|
+
: error is ArgumentException ? "INVALID_ARGUMENT" : "ARCANE_NOTIFICATION_FAILED";
|
|
666
|
+
response["ok"] = false;
|
|
667
|
+
response["error"] = ArcaneHost.ErrorRecord(error, code);
|
|
668
|
+
}
|
|
669
|
+
response["time"] = DateTime.UtcNow.ToString("o");
|
|
670
|
+
string json = ArcaneHost.Serializer().Serialize(response);
|
|
671
|
+
lock (stateLock) pending.Enqueue(new CoreDelivery(operation.Generation, json));
|
|
672
|
+
ScheduleDelivery();
|
|
673
|
+
}
|
|
674
|
+
catch (Exception error)
|
|
675
|
+
{
|
|
676
|
+
// A bridge-accepted request must receive its response or the
|
|
677
|
+
// owning transport failure, including serialization failures.
|
|
678
|
+
CoreFailed(error);
|
|
679
|
+
}
|
|
680
|
+
finally
|
|
681
|
+
{
|
|
682
|
+
lock (stateLock)
|
|
683
|
+
{
|
|
684
|
+
desktopNotificationRequests.Remove(id);
|
|
685
|
+
operation.Cancellation.Dispose();
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
private void DesktopNotificationChanged(Dictionary<string, object> record)
|
|
691
|
+
{
|
|
692
|
+
string json = ArcaneHost.Serializer().Serialize(new Dictionary<string, object>
|
|
693
|
+
{
|
|
694
|
+
{ "protocol", "arcane/1" }, { "type", "event" },
|
|
695
|
+
{ "event", "notifications.state" }, { "data", record }, { "time", DateTime.UtcNow.ToString("o") }
|
|
696
|
+
});
|
|
697
|
+
lock (stateLock) pending.Enqueue(new CoreDelivery(generation, json));
|
|
698
|
+
ScheduleDelivery();
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
private void CancelDesktopNotificationRequests(long? documentGeneration)
|
|
702
|
+
{
|
|
703
|
+
lock (stateLock)
|
|
704
|
+
{
|
|
705
|
+
// Cancellation can settle a queued operation synchronously;
|
|
706
|
+
// iterate a snapshot while its completion removes correlation.
|
|
707
|
+
foreach (DesktopNotificationRequest operation in
|
|
708
|
+
new List<DesktopNotificationRequest>(desktopNotificationRequests.Values))
|
|
709
|
+
if (!documentGeneration.HasValue || operation.Generation == documentGeneration.Value)
|
|
710
|
+
operation.Cancellation.Cancel();
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
private void StopDesktopNotifications()
|
|
715
|
+
{
|
|
716
|
+
CancelDesktopNotificationRequests(null);
|
|
717
|
+
if (desktopNotifications == null || desktopNotificationsClosing != null) return;
|
|
718
|
+
try { desktopNotificationsClosing = desktopNotifications.CloseAsync(); }
|
|
719
|
+
catch (Exception error) { Report(error); }
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
private async Task DrainDesktopNotificationsAsync()
|
|
723
|
+
{
|
|
724
|
+
List<Task> operations = new List<Task>();
|
|
725
|
+
lock (stateLock)
|
|
726
|
+
{
|
|
727
|
+
foreach (DesktopNotificationRequest operation in desktopNotificationRequests.Values)
|
|
728
|
+
operations.Add(operation.Completion);
|
|
729
|
+
}
|
|
730
|
+
if (desktopNotificationsClosing != null) operations.Add(desktopNotificationsClosing);
|
|
731
|
+
Task settled = Task.WhenAll(operations);
|
|
732
|
+
try { await settled; }
|
|
733
|
+
catch (Exception error) { ReportTaskFailure(settled, error); }
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
private sealed class DesktopNotificationRequest
|
|
737
|
+
{
|
|
738
|
+
internal readonly long Generation;
|
|
739
|
+
internal readonly CancellationTokenSource Cancellation = new CancellationTokenSource();
|
|
740
|
+
internal Task Completion;
|
|
741
|
+
|
|
742
|
+
internal DesktopNotificationRequest(long generation) { Generation = generation; }
|
|
743
|
+
}
|
|
744
|
+
|
|
587
745
|
private async Task LoadApplicationIconAsync()
|
|
588
746
|
{
|
|
589
747
|
if (String.IsNullOrEmpty(options.IconPath)) return;
|
|
@@ -762,12 +920,14 @@ namespace Arcane.Core.Hosts.Windows
|
|
|
762
920
|
private void DocumentConnected(long documentGeneration)
|
|
763
921
|
{
|
|
764
922
|
ArcaneBridge previous;
|
|
923
|
+
long previousGeneration;
|
|
765
924
|
lock (stateLock)
|
|
766
925
|
{
|
|
767
926
|
if (closing) return;
|
|
768
927
|
if (documentGeneration == generation && documentReady) return;
|
|
769
928
|
if (documentGeneration != registeredGeneration) return;
|
|
770
929
|
previous = activeBridge;
|
|
930
|
+
previousGeneration = generation;
|
|
771
931
|
generation = documentGeneration;
|
|
772
932
|
activeBridge = bridge;
|
|
773
933
|
documentReady = true;
|
|
@@ -775,6 +935,7 @@ namespace Arcane.Core.Hosts.Windows
|
|
|
775
935
|
if (previous != null && previous != activeBridge)
|
|
776
936
|
{
|
|
777
937
|
previous.Stop(new OperationCanceledException("The previous document has navigated away."));
|
|
938
|
+
CancelDesktopNotificationRequests(previousGeneration);
|
|
778
939
|
if (transportFailure == null)
|
|
779
940
|
{
|
|
780
941
|
try { ObserveSend(core.SendAsync(CancelRendererRequests)); }
|
|
@@ -960,6 +1121,7 @@ namespace Arcane.Core.Hosts.Windows
|
|
|
960
1121
|
if (IsDisposed) return;
|
|
961
1122
|
if (bridge != null) bridge.Stop(transportFailure);
|
|
962
1123
|
if (activeBridge != null) activeBridge.Stop(transportFailure);
|
|
1124
|
+
CancelDesktopNotificationRequests(null);
|
|
963
1125
|
NotifyTransportFailure();
|
|
964
1126
|
if (core != null) core.CloseAsync();
|
|
965
1127
|
ScheduleDelivery();
|
|
@@ -1010,10 +1172,12 @@ namespace Arcane.Core.Hosts.Windows
|
|
|
1010
1172
|
{
|
|
1011
1173
|
if (closing) return;
|
|
1012
1174
|
lock (stateLock) closing = true;
|
|
1175
|
+
nativeDiagnostics.Stop();
|
|
1013
1176
|
if (!started) initialized.TrySetResult(null);
|
|
1014
1177
|
if (bridge != null) bridge.Stop(new InvalidOperationException("The application window is closing."));
|
|
1015
1178
|
if (activeBridge != null) activeBridge.Stop(new InvalidOperationException("The application window is closing."));
|
|
1016
1179
|
StopAppControl();
|
|
1180
|
+
StopDesktopNotifications();
|
|
1017
1181
|
WindowStateTransition.ReleasePending(this);
|
|
1018
1182
|
shutdown = DrainAndCloseAsync();
|
|
1019
1183
|
shutdown.ContinueWith(ObserveUnexpectedFailure, TaskContinuationOptions.OnlyOnFaulted);
|
|
@@ -1042,6 +1206,7 @@ namespace Arcane.Core.Hosts.Windows
|
|
|
1042
1206
|
}
|
|
1043
1207
|
}
|
|
1044
1208
|
await DrainAppControlAsync();
|
|
1209
|
+
await DrainDesktopNotificationsAsync();
|
|
1045
1210
|
// Joining the independent image owner belongs after Core input has
|
|
1046
1211
|
// closed; image I/O must never delay accepted service work draining.
|
|
1047
1212
|
if (iconLoading != null)
|
|
@@ -1069,6 +1234,8 @@ namespace Arcane.Core.Hosts.Windows
|
|
|
1069
1234
|
if (!browserProcessExited && webView.CoreWebView2 != null)
|
|
1070
1235
|
{
|
|
1071
1236
|
webView.CoreWebView2.RemoveHostObjectFromScript("arcaneBridge");
|
|
1237
|
+
if (nativeDiagnosticsRegistered) webView.CoreWebView2.RemoveHostObjectFromScript("arcaneDiagnostics");
|
|
1238
|
+
if (nativeDiagnosticsScript != null) webView.CoreWebView2.RemoveScriptToExecuteOnDocumentCreated(nativeDiagnosticsScript);
|
|
1072
1239
|
if (injectedScript != null) webView.CoreWebView2.RemoveScriptToExecuteOnDocumentCreated(injectedScript);
|
|
1073
1240
|
if (appControlScript != null) webView.CoreWebView2.RemoveScriptToExecuteOnDocumentCreated(appControlScript);
|
|
1074
1241
|
}
|
|
@@ -101,6 +101,7 @@ namespace Arcane.Core.Hosts.Windows
|
|
|
101
101
|
InitialWindowState = OptionalWindowState(windowOptions),
|
|
102
102
|
IconPath = String.IsNullOrEmpty(icon) ? null : Path.Combine(directory, icon),
|
|
103
103
|
ClassicClientSource = File.ReadAllText(Path.Combine(directory, RequiredString(client, "source"))),
|
|
104
|
+
NativeDiagnosticsSource = File.ReadAllText(Path.Combine(directory, "arcane-native-diagnostics.js")),
|
|
104
105
|
AppControlEndpoint = appControl == null ? null : RequiredString(appControl, "endpoint"),
|
|
105
106
|
AppControlSource = appControl == null ? null : File.ReadAllText(Path.Combine(directory, "arcane-app-control.js")),
|
|
106
107
|
CoreExecutable = Path.Combine(directory, "runtime", "ArcaneCore.exe"),
|