arcane-os 0.63.0 → 0.64.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.
Files changed (41) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +1 -1
  3. package/browser-runtime/core/client.mjs +7 -2
  4. package/docs/reference/README.md +2 -0
  5. package/docs/reference/core-client.md +11 -2
  6. package/docs/reference/core-native-packaging.md +35 -6
  7. package/docs/reference/core-repositories.md +149 -0
  8. package/docs/reference/inventory/package-api.json +59 -3
  9. package/docs/reference/local-ai.md +5 -2
  10. package/docs/reference/local-image-flux.md +1 -1
  11. package/docs/reference/local-image-generation.md +6 -0
  12. package/docs/reference/local-image-sdxl.md +139 -0
  13. package/docs/reference/local-whisper.md +219 -0
  14. package/docs/reference/native-speech.md +51 -2
  15. package/docs/reference/runtime-modules.md +16 -2
  16. package/docs/reference/sdk-api.md +162 -2
  17. package/package.json +4 -1
  18. package/runtime/arcane/components/speech.html +15 -3
  19. package/runtime/arcane/components/voice-transcription.html +19 -3
  20. package/runtime/arcane/modules/AI.js +250 -27
  21. package/runtime/arcane/modules/AIProviderRuntime.js +20 -3
  22. package/runtime/arcane/modules/AIRuntimeState.js +15 -2
  23. package/runtime/arcane/modules/ComponentContracts.js +3 -0
  24. package/schemas/arcane-app.schema.json +53 -2
  25. package/schemas/arcane-package.schema.json +53 -2
  26. package/src/core/repositories.mjs +210 -0
  27. package/src/core/services/speech.mjs +1 -0
  28. package/src/local-ai/config.mjs +7 -3
  29. package/src/local-ai/dev.mjs +32 -2
  30. package/src/local-ai/image-library.mjs +9 -0
  31. package/src/local-ai/image-models/sdxl.mjs +18 -0
  32. package/src/local-ai/install.mjs +26 -7
  33. package/src/local-ai/whisper/build.mjs +48 -0
  34. package/src/local-ai/whisper/config.mjs +61 -0
  35. package/src/local-ai/whisper/index.mjs +515 -0
  36. package/src/local-ai/whisper/install.mjs +274 -0
  37. package/src/local-ai/whisper/native/CMakeLists.txt +98 -0
  38. package/src/local-ai/whisper/native/NOTICE +18 -0
  39. package/src/local-ai/whisper/native/README.md +104 -0
  40. package/src/local-ai/whisper/native/main.cpp +489 -0
  41. package/src/native/portable-layout.mjs +37 -7
@@ -0,0 +1,219 @@
1
+ # Native Whisper transcription
2
+
3
+ Arcane runs `whisper.cpp` through a persistent native helper. A selected model
4
+ stays loaded across recordings. The helper reports actual model readiness,
5
+ native progress and timestamped segments, and completes cancellation before
6
+ the model or recording files are reused. The browser's existing
7
+ `AI.fetchSTT(audioFile, signal)` and Core `speech.transcribe` routes retain the
8
+ application's selected model and cancellation signal.
9
+
10
+ ## Select the runtime and model
11
+
12
+ An application's `arcane-app.json` selects native speech alongside its other
13
+ independent local runtimes:
14
+
15
+ ```json
16
+ {
17
+ "native": {
18
+ "localAI": {
19
+ "runtimes": [
20
+ {
21
+ "id": "whisper.cpp",
22
+ "backend": "auto",
23
+ "models": ["whisper-small"]
24
+ }
25
+ ]
26
+ }
27
+ }
28
+ }
29
+ ```
30
+
31
+ The equivalent package configuration is `arcane-package.json`'s `localAI`.
32
+ Existing unrelated descriptor fields remain part of the application's file.
33
+ `models` defaults to an empty array, so selecting the runtime alone acquires no
34
+ model. A single selected model becomes `modelId`; multiple models require an
35
+ explicit selected `modelId` before transcription. The built-in `whisper-small`
36
+ descriptor selects the official `ggerganov/whisper.cpp` `ggml-small.bin` model.
37
+ An explicit model descriptor is `{id, path}` or `{id, url, filename?}`.
38
+
39
+ Default Windows x64 preparation acquires the selected complete upstream
40
+ Whisper 1.9.4/b5130 CUDA 12.4 and CPU distributions, FFmpeg 9.0.2 essentials,
41
+ the selected model, and Arcane's helper asset from the matching SDK numeric
42
+ release. The Windows helper includes its selected Visual C++ release runtime
43
+ DLLs beside the executable. Preparation shares matching installation jobs, observes cancellation,
44
+ and preserves the complete selected runtime trees. Model-selection changes
45
+ can reuse the already prepared runtime and matching models.
46
+
47
+ With `backend: "auto"`, accelerated runtimes are attempted before CPU. Failure
48
+ to load an accelerated runtime falls back to the prepared CPU runtime. If
49
+ native accelerated inference fails, Arcane drains that context and retries the
50
+ complete decoded recording once on CPU with the same model. Cancellation,
51
+ input errors and observer errors do not trigger this retry. Explicit `cuda`,
52
+ `metal` or `cpu` selections retain that chosen backend.
53
+
54
+ During that accepted request's internal recovery, the engine reports
55
+ `state: "recovering"`, `loaded: false`, and `busy: true`, with the same selected
56
+ provider and model. It accepts no new transcription while the old context
57
+ drains and the CPU context loads. Only actual CPU readiness restores `ready`.
58
+ An explicitly unloaded, replaced, or closed model remains a cancellation
59
+ boundary; internal recovery preserves the original request and its signal.
60
+
61
+ `requestedBackend` identifies a preference. `observedBackend` and
62
+ `backendEvidence` report native initialization evidence when available; they
63
+ do not claim that every computation runs on the GPU. No driver or compiler is
64
+ installed on application startup.
65
+
66
+ ## Core and development integration
67
+
68
+ Development and generated native Core entries compose a
69
+ [`createSpeechService`](./native-speech.md) with the selected Whisper engine.
70
+ Speech preparation and model loading proceed independently of the page,
71
+ chat and image services. Core publishes retained `speech.state` and per-request
72
+ `speech.progress`; transcription availability follows the selected engine's
73
+ actual loaded state. The shared service remains usable when only STT is
74
+ selected, through `transcriptionAvailable` and `roles.stt`.
75
+
76
+ The native bundle includes the helper, runtime libraries, decoder and selected
77
+ model under its runtime directory beside the application executable. Those
78
+ resources stay in the application bundle. They are not embedded into one
79
+ Windows executable. The compiler and matching upstream headers are build-time
80
+ requirements for the first-party helper only.
81
+
82
+ Applications may continue using the existing Core call:
83
+
84
+ ```js
85
+ const transcript = await client.speech.transcribe({
86
+ audioBase64,
87
+ mimeType: 'audio/webm',
88
+ model: 'whisper-small'
89
+ }, {signal});
90
+ console.log(transcript.text);
91
+ ```
92
+
93
+ FFmpeg decodes the complete recorded media into mono 16 kHz PCM at the native
94
+ transport boundary. The original recording remains untouched during the
95
+ operation. Arcane imposes no recording-duration cap and does not trim the
96
+ transcript. The upstream inference API's signed-integer sample argument is
97
+ reported honestly if a recording cannot fit that native call; no audio is
98
+ silently shortened. Complete operation files are removed only after decoder
99
+ and native inference cleanup have completed.
100
+
101
+ ## Public engine API
102
+
103
+ ```js
104
+ import {createWhisperRuntime} from 'arcane-os/local-ai/whisper';
105
+
106
+ const stt = createWhisperRuntime({
107
+ runtime: preparedRuntime,
108
+ modelId: 'whisper-small',
109
+ temporaryDirectory: applicationSpeechDirectory,
110
+ onEvent: reportDiagnostic
111
+ });
112
+ const unsubscribe = stt.subscribe(renderModelState);
113
+ await stt.load({signal});
114
+ try {
115
+ const result = await stt.transcribe({audioBase64, mimeType, model: 'whisper-small'}, {
116
+ signal,
117
+ onProgress: renderTranscriptionProgress
118
+ });
119
+ console.log(result.text, result.segments);
120
+ } finally {
121
+ unsubscribe();
122
+ await stt.close();
123
+ }
124
+ ```
125
+
126
+ `createWhisperRuntime(options)` performs no model load. `options.runtime` is a
127
+ prepared descriptor returned by `ensureLocalAIRuntimes`; alternatively,
128
+ `options.prepare({signal, onEvent})` lazily returns that descriptor during
129
+ `load`. `temporaryDirectory` is required and belongs to the application.
130
+
131
+ | Member | Contract |
132
+ | --- | --- |
133
+ | `current()` | Returns `providerId`, selected `modelId`, lifecycle `state`, `loaded`, `busy`, correlated `requestId` or null, requested/observed backend, evidence and any error. |
134
+ | `subscribe(listener, {replay = true, signal} = {})` | Immediately replays current state by default and returns an unsubscribe function. |
135
+ | `load({modelId, signal} = {})` | Prepares the runtime if necessary, then waits for actual native model readiness. With no selected model it remains unloaded. |
136
+ | `transcribe(request, {signal, onProgress, requestId} = {})` | Consumes the complete `audioBase64` recording. Optional `language` defaults to native automatic detection; `translate: true` selects Whisper's translation operation. `model` must match the loaded model. Optional `requestId` carries the caller's existing operation identity through lifecycle snapshots as separate control information; it does not alter the recording or request payload. |
137
+ | `unload()` | Cancels and joins active load/transcription, releases the native context, and permits a later explicit load. |
138
+ | `close()` | Joins shutdown and disposes this engine permanently. Repeated calls share the same shutdown. |
139
+
140
+ One retained model processes one recording at a time; a simultaneous request
141
+ receives `WHISPER_BUSY`. Model changes use `await stt.unload()` followed by
142
+ `stt.load({modelId})`. The `ready` state remains `loaded: true` with `busy: true`
143
+ during ordinary inference. Progress records include the native event plus
144
+ `backend` and `attempt`, so a CPU retry remains distinguishable. Segment text
145
+ is preserved in order. The final result is
146
+ `{text, language, duration, segments: [{index, start, end, text}]}`; times are
147
+ seconds and are native segment timestamps, not invented word timestamps.
148
+
149
+ Full process diagnostics stay with `onEvent` and native process failures.
150
+ Applications should use their selected inspection surface for those details
151
+ and keep ordinary user status concise.
152
+ An error snapshot retains the failed operation's `requestId` after `busy`
153
+ becomes false, allowing the caller to correlate its actual pending response.
154
+ A new request or model load supersedes that identity; unload and close clear
155
+ it. No recording or request payload is retained by this correlation field.
156
+ Progress callbacks may cancel through the supplied controller and return;
157
+ they must not await the same transcription or its shutdown from inside its
158
+ own output callback. Await those operations in the initiating owner instead.
159
+
160
+ ## Explicit runtimes and helper builds
161
+
162
+ Windows, Linux and macOS host integration uses the same public engine and
163
+ portable helper source. Automatic archive selection currently provides the
164
+ Windows x64 distribution above. Other targets supply their complete selected
165
+ runtime, decoder and compiled helper paths; their native execution requires
166
+ verification on that actual platform. Android requires a host adapter for
167
+ its process and library lifecycle.
168
+
169
+ An explicit runtime requirement can supply:
170
+
171
+ ```js
172
+ {
173
+ id: 'whisper.cpp',
174
+ version: '1.9.4',
175
+ backend: 'auto',
176
+ variants: [
177
+ {backend: 'cuda', root: cudaRuntimeDirectory, libraryDirectory: cudaLibraryDirectory},
178
+ {backend: 'cpu', root: cpuRuntimeDirectory, libraryDirectory: cpuLibraryDirectory}
179
+ ],
180
+ helperExecutable,
181
+ helperRoot,
182
+ decoderExecutable,
183
+ decoderRoot,
184
+ models: [{id: 'whisper-small', path: selectedModelPath}],
185
+ modelId: 'whisper-small'
186
+ }
187
+ ```
188
+
189
+ `libraryDirectory` belongs to its complete variant root. `decoderExecutable`
190
+ belongs to `decoderRoot` (which defaults to its containing directory).
191
+ `helperExecutable` similarly belongs to `helperRoot`; its complete directory
192
+ tree is retained, including app-local native dependencies.
193
+ Preparation copies those selected resources into its owned installation, and
194
+ native packaging projects their paths relative to the application bundle.
195
+ `helperUrl`, `helperVersion`, and `decoderUrl` allow an explicitly selected
196
+ published distribution instead of local paths. Dependency permissions remain
197
+ the caller's responsibility.
198
+
199
+ ```js
200
+ import {buildWhisperHelper} from 'arcane-os/local-ai/whisper/build';
201
+
202
+ const helper = await buildWhisperHelper({
203
+ runtime: {directory: runtimeLibraryDirectory, sourceDirectory: matchingUpstreamSource},
204
+ outputRoot: helperBuildDirectory,
205
+ signal,
206
+ onEvent: reportDiagnostic
207
+ });
208
+ ```
209
+
210
+ The builder runs CMake configure/build/install against the selected matching
211
+ public upstream headers and runtime. Optional `cmake`, `generator`,
212
+ `cmakeArgs` and `env` select the existing toolchain. It returns the helper
213
+ `executable`, platform, architecture, runtime directory and complete build
214
+ diagnostics. The helper's documented process contract is in
215
+ [`native/README.md`](../../src/local-ai/whisper/native/README.md).
216
+ On MSVC, CMake's installed runtime-library module adds release CRT files to
217
+ the helper's `bin/` directory. `-DMSVC_REDIST_DIR=...` selects an existing
218
+ redistributable directory when needed. Windows provides the Universal CRT;
219
+ compiler tools and debug runtimes stay outside the application bundle.
@@ -82,8 +82,9 @@ Each supplied engine implements:
82
82
  function.
83
83
  - `load({signal})`, resolving only after the constructor-selected model has
84
84
  loaded. Independent engines may load concurrently.
85
- - `transcribe(request, {signal, onProgress})` for STT, or
86
- `synthesize(request, {signal, onProgress})` for TTS.
85
+ - `transcribe(request, {signal, onProgress, requestId})` for STT, or
86
+ `synthesize(request, {signal, onProgress, requestId})` for TTS. `requestId` is
87
+ the actual Core request's control metadata, separate from the unchanged payload.
87
88
  - `close()`, cancelling and joining its native work and releasing its resources.
88
89
 
89
90
  The service forwards each complete request and result unchanged. It applies
@@ -112,3 +113,51 @@ share one shutdown promise. Actual engine cleanup failures reject with an
112
113
  `AggregateError` and remain in role diagnostics. `dispose()` additionally
113
114
  releases the service's event subscriptions. New work after shutdown begins
114
115
  rejects with `CORE_CLOSING`.
116
+
117
+ ## AI built-in native speech readiness
118
+
119
+ The `AI` module's `LOCAL_SPEACH` adapter observes `speech.state` before reading
120
+ `Arcane.speech.status()`. A newer event takes precedence over an outstanding
121
+ initial status response. Each role loads independently: STT does not wait for
122
+ TTS, and unmuting waits for the selected TTS model rather than a callable facade.
123
+
124
+ For this service's role snapshots, readiness requires the exact selected
125
+ `modelId`, its engine identity, `loaded: true`, `available: true`, and a `ready`
126
+ or `running` state. Model loading remains an ordinary cancellable waiting state.
127
+ An engine error, unavailable role, or different selected model rejects the
128
+ adapter load. Later readiness loss or engine/model replacement invalidates the
129
+ AI role and cancels its queued and active requests through the existing provider
130
+ runtime. A new explicit role load or unmute can observe the next ready state.
131
+
132
+ An engine can report `state: 'recovering'`, `loaded: false`, `busy: true`, and
133
+ the continuing Core `requestId` while retrying an already accepted request on
134
+ the same selected provider and model. Availability remains false. The built-in
135
+ STT adapter preserves only its own correlated pending request through this
136
+ state; another caller's recovery supplies no continuation authority. The real
137
+ RPC ID comes from `Arcane.speech.transcribe`'s optional `onRequest` observer.
138
+ The engine retains that ID on a terminal error snapshot until the snapshot is
139
+ superseded, allowing the pending RPC to deliver its actual complete failure
140
+ rather than converting it into cancellation. True unload, close, replacement,
141
+ transport loss and caller cancellation retain their existing ownership.
142
+
143
+ The shared AI observation becomes `recovering` with `loaded: false`, `busy:
144
+ true`, and its existing runtime `operationId`. An error may likewise retain
145
+ `busy: true` until the actual operation settles. New inference still requires
146
+ genuine loaded readiness. The STT provider request context includes a
147
+ `refreshState()` callback which rereads that provider's status only while the
148
+ exact request remains owned; it changes observation, not execution authority.
149
+ Shared speech components retain their own real runtime operation through this
150
+ recovery, keep new dispatch readiness-gated, and preserve native microphone
151
+ capture's separate cancellation and final/interim lifetime.
152
+
153
+ Older fixed-model hosts expose the published aggregate-only `SpeechStatus`
154
+ contract instead of per-model lifecycle. The adapter preserves their independent
155
+ health-based readiness for `whisper-small` with `whisper.cpp` and `kokoro` with
156
+ `kokoro-onnx`, using `status: 'ok'` and the corresponding availability boolean.
157
+ That contract is a one-shot health observation, not evidence of a persistently
158
+ loaded native model or a continuous lifecycle event stream.
159
+
160
+ Cancellation, unload, disposal, and route retirement release adapter-owned
161
+ subscriptions. They do not close the host's shared engines or alter complete
162
+ speech requests and results. Browser and explicitly registered speech providers
163
+ retain their existing lifecycle contracts.
@@ -1121,6 +1121,14 @@ work. Promise settlement proves only that the provider's exposed request promise
1121
1121
  completed; provider-specific cancellation acknowledgement remains the selected
1122
1122
  provider's boundary.
1123
1123
 
1124
+ An STT provider's request context also supplies `refreshState()`. An engine
1125
+ lifecycle observer may call it to reread `provider.status()` while that exact
1126
+ request remains active. A recovering or failed pending request is observed as
1127
+ `loaded: false`, `busy: true`, with its existing runtime operation ID; the
1128
+ callback does not make a new request ready or change its supplied payload.
1129
+ After cancellation, unload, disposal, replacement, or settlement it has no
1130
+ effect on the retired request.
1131
+
1124
1132
  Direct LLM `request()`, `chat()`, and `stream()` use the same message history,
1125
1133
  tool-declaration, emitted-call, all-choice, and ordered parallel-call contracts
1126
1134
  as the high-level AI API module, including one nonblank matching result for
@@ -1229,13 +1237,17 @@ observable without exposing provider transports in application code.
1229
1237
 
1230
1238
  Exact exports: `AI_RUNTIME_INTENT_EVENT`, `AI_RUNTIME_PROTOCOL`,
1231
1239
  `AI_RUNTIME_ROLES`, `AI_RUNTIME_STARTUP_EVENT`, `AI_RUNTIME_STATES`,
1232
- `AI_RUNTIME_STATE_EVENT`, `getAIRuntimeState`,
1240
+ `AI_RUNTIME_STATE_EVENT`, `continuesAIRuntimeOperation`, `getAIRuntimeState`,
1233
1241
  `publishAIRuntimeRoleState`, `publishAIRuntimeRolesState`,
1234
1242
  `requestAIRuntimeIntent`, `startAIRuntime`, `subscribeAIRuntimeIntents`, and
1235
1243
  `subscribeAIRuntimeState`.
1236
1244
 
1237
1245
  Each role record is exactly `{role,state,providerId,modelId,localOnly,loaded,
1238
1246
  busy,operationId,progress,error}`.
1247
+ `continuesAIRuntimeOperation(previous,current,operationId)` identifies the same
1248
+ already-owned provider/model/runtime operation in a busy, unloaded `recovering`
1249
+ or `error` observation. Pass the actual runtime operation ID, not a component's
1250
+ local counter. It grants neither readiness nor permission to start new work.
1239
1251
  `subscribeAIRuntimeState(listener,{signal=null,emitCurrent=true})` installs its
1240
1252
  subscription and synchronously replays the current mutable snapshot by default;
1241
1253
  `subscribeAIRuntimeIntents(listener,{signal=null})` is future-only. Both return
@@ -1251,7 +1263,9 @@ transcription-model load.
1251
1263
  ### Availability and normalization
1252
1264
 
1253
1265
  **Cross-host state contract.** States are `unavailable`, `unloaded`, `loading`,
1254
- `ready`, `unloading`, `error`, and `disposed`. Revisions increase monotonically.
1266
+ `recovering`, `ready`, `unloading`, `error`, and `disposed`. Recovery remains
1267
+ `loaded: false`; `recovering` and `error` may retain `busy: true` with an actual
1268
+ `operationId` until the pending request settles. Revisions increase monotonically.
1255
1269
  The events `arcane-ai-runtime-state`, `arcane-ai-runtime-intent`, and
1256
1270
  `arcane-ai-runtime-startup-settled` normalize observation only: receiving one
1257
1271
  does not grant a native capability, prove browser support, or load a provider.
@@ -57,6 +57,7 @@ runtime layouts.
57
57
  | `arcane-os/core/runtime` | App-neutral native Core dispatcher and service lifecycle. |
58
58
  | `arcane-os/core/stdio` | Framed Node stdio transport with graceful runtime drain. |
59
59
  | `arcane-os/core/host` | Node-only reusable Core host lifecycle with explicitly supplied services. |
60
+ | `arcane-os/core/repositories` | Native persistent ArcaneData paths and explicitly selected connected working repositories. |
60
61
  | `arcane-os/core/packaged-web` | Core-owned packaged web listener with a persisted loopback origin and ready/failure events. |
61
62
  | `arcane-os/core/preferences` | Existing preference RPC methods with an application-selected file, ordered writes and owned drain. |
62
63
  | `arcane-os/core/diarization` | Lazy native speaker-diarization service, explicit stream methods and lifecycle/result events. |
@@ -69,6 +70,7 @@ runtime layouts.
69
70
  | `arcane-os/local-ai/image` | Retained native stable-diffusion.cpp context, complete PNG generation and owned worker lifetime. |
70
71
  | `arcane-os/ai/core-image` | Browser image lifecycle and complete PNG Blobs through an available Core service. |
71
72
  | `arcane-os/core/image` | Native image Core service with independent runtime preparation and retained model ownership. |
73
+ | `arcane-os/core/speech` | Independent injected STT/TTS engine loading, request cancellation and joined Core service lifetime. |
72
74
  | `arcane-os/ai/core-model-assets` | Complete stored browser model members projected through Core into native working files. |
73
75
  | `arcane-os/core/model-assets` | Native working-file preparation, engine retain handles and joined cleanup. |
74
76
  | `arcane-os/integrated-provider` | Fixed integrated shared-development provider. |
@@ -391,6 +393,8 @@ The remaining data-only subpaths are eight JSON Schemas and package metadata.
391
393
  | `arcaneNativeBuilderProvider default export` | singleton | `arcane-os/native/portable-provider` | Portable native provider | Node; produces a portable payload rather than an executable host |
392
394
  | `startCoreHost()` | function | `arcane-os/core/host` | Native Core host composition | Node with readable input and writable output streams |
393
395
  | `readCoreLaunchContext()` | function | `arcane-os/core/host` | Native Core host composition | Node; reads explicitly selected launch JSON |
396
+ | `resolveArcaneDataPaths()` | function | `arcane-os/core/repositories` | Native repository workspaces | Node on Windows, Linux and macOS; Android supplies its data root |
397
+ | `createRepositoryWorkspace()` | function | `arcane-os/core/repositories` | Native repository workspaces | Node with Git; Android requires host process adaptation |
394
398
  | `createWindowsNativeProvider()` | function | `arcane-os/native/windows-provider` | Windows native provider | Node assembly; Windows x64/WebView2 execution |
395
399
  | `arcaneNativeBuilderProvider` | singleton | `arcane-os/native/windows-provider` | Windows native provider | Node assembly; Windows x64/WebView2 execution |
396
400
  | `arcaneNativeBuilderProvider default export` | singleton | `arcane-os/native/windows-provider` | Windows native provider | Node assembly; Windows x64/WebView2 execution |
@@ -412,6 +416,8 @@ The remaining data-only subpaths are eight JSON Schemas and package metadata.
412
416
  | `createCoreImageRuntime()` | function | `arcane-os/ai/core-image` | Core local images | Browser with an available Core image service |
413
417
  | `createLocalImageService()` | function | `arcane-os/core/image` | Core local images | Node Core with selected stable-diffusion.cpp requirements |
414
418
  | `createLocalImageService default export` | function | `arcane-os/core/image` | Core local images | Same native image service factory |
419
+ | `createSpeechService()` | function | `arcane-os/core/speech` | Core native speech | Node Core with explicitly supplied speech engines |
420
+ | `createSpeechService default export` | function | `arcane-os/core/speech` | Core native speech | Same shared speech service factory |
415
421
  | `prepareCoreModelAssets()` | function | `arcane-os/ai/core-model-assets` | Core model assets | Browser with complete Blob/File members and available Core service |
416
422
  | `createModelAssetService()` | function | `arcane-os/core/model-assets` | Core model assets | Node Core with an application-selected working directory |
417
423
  | `createModelAssetService default export` | function | `arcane-os/core/model-assets` | Core model assets | Same native working-file service factory |
@@ -7421,12 +7427,23 @@ application builds. The build machine needs its normal Node/npm toolchain and
7421
7427
  The returned `arcane-native-builder/1` provider exposes `describe`, `doctor`,
7422
7428
  `prepare`, `build`, `verify` and `run`. Build returns `{app,target,manifest}`
7423
7429
  with the actual directory at `target.rootDir`. Verification reads the artifact;
7424
- it does not launch the window. Run launches `Arcane.exe` and observes complete
7425
- diagnostics and exit. Cancellation closes input and waits for host-owned drain,
7430
+ it does not launch the window. Run launches the executable recorded in the
7431
+ saved `manifest.host.executable` and observes complete diagnostics and exit.
7432
+ Cancellation closes input and waits for host-owned drain,
7426
7433
  including accepted service work, without a forced termination deadline. Missing
7427
7434
  prerequisites, download/extraction failures and host failures remain observable
7428
7435
  through the operation's error/event boundary.
7429
7436
 
7437
+ Assembly names the launcher from `appDescriptor.displayName` plus `.exe`,
7438
+ with a matching `.exe.config` file. Low-level compositions without a nonempty
7439
+ string display name use the app ID. Windows-reserved filename characters and
7440
+ control characters become underscores; reserved DOS device basenames receive
7441
+ an underscore prefix. Supported Unicode, spaces and case remain, as do the
7442
+ original descriptor and title. Icon embedding, file inventory, verification
7443
+ and run use this same name. Existing artifacts retain the executable recorded
7444
+ in their manifest; the reusable downloaded host retains `Arcane.exe` and
7445
+ `Arcane.exe.config`, and `runtime/ArcaneCore.exe` remains unchanged.
7446
+
7430
7447
  During assembly, `appDescriptor.native.icon` selects the app's existing image.
7431
7448
  PNG and ICO inputs produce a derived `runtime/arcane-app.ico` and update only
7432
7449
  the copied application's executable icon. Source assets, the selected host
@@ -7676,6 +7693,63 @@ separate argument; other composing hosts may use the same seam. This JavaScript
7676
7693
  contract does not establish a compiled host or matching release archive. See
7677
7694
  [launch-time locations](core-native-packaging.md#launch-time-locations).
7678
7695
 
7696
+ ## resolveArcaneDataPaths()
7697
+
7698
+ ### Overview
7699
+
7700
+ Resolves the native per-user `ArcaneData` directory and its `Repos` child without
7701
+ creating directories or changing application state.
7702
+
7703
+ ### Signature and result
7704
+
7705
+ ```text
7706
+ resolveArcaneDataPaths({dataRoot}={})
7707
+ ```
7708
+
7709
+ Returns absolute `{dataRoot,repositoriesRoot}`. Defaults use Windows LocalAppData,
7710
+ Linux XDG data home or `~/.local/share`, and macOS user Application Support.
7711
+ Android and other adapted hosts supply their persistent `dataRoot`. An explicit
7712
+ root is used directly, with relative paths resolved from the process working
7713
+ directory. See [native repository locations](core-repositories.md#persistent-locations).
7714
+
7715
+ ```javascript
7716
+ import {resolveArcaneDataPaths} from 'arcane-os/core/repositories';
7717
+
7718
+ const {repositoriesRoot} = resolveArcaneDataPaths();
7719
+ ```
7720
+
7721
+ ## createRepositoryWorkspace()
7722
+
7723
+ ### Overview
7724
+
7725
+ Owns one native connected working checkout under `ArcaneData/Repos/<name>` or an
7726
+ explicit existing path. The application owns the remote, branch and Core API.
7727
+
7728
+ ### Signature and result
7729
+
7730
+ ```text
7731
+ createRepositoryWorkspace({name,directory,dataRoot,remote,branch,onEvent,run=runProcess}={})
7732
+ ```
7733
+
7734
+ Returns `{directory,open,status,pull,push,close,drain,dispose}`. Construction does
7735
+ no I/O. Operations accept `{signal}`; a missing or empty destination clones once,
7736
+ while existing checkout files, branches, remotes and local changes remain intact.
7737
+ Same-directory calls are ordered within the process. Shutdown drains accepted
7738
+ work and preserves the checkout. No CLI cwd, launch state, preference, snapshot
7739
+ cache or existing user-data location changes. See the [complete inputs, results,
7740
+ errors and lifecycle](core-repositories.md#one-connected-working-checkout).
7741
+
7742
+ ```javascript
7743
+ import {createRepositoryWorkspace} from 'arcane-os/core/repositories';
7744
+
7745
+ const repository = createRepositoryWorkspace({
7746
+ name: 'moon-cheese-dispatches',
7747
+ remote: applicationConnection.remote
7748
+ });
7749
+ await repository.open({signal});
7750
+ await repository.close();
7751
+ ```
7752
+
7679
7753
  ## createCoreLocalAIProvider()
7680
7754
 
7681
7755
  ### Overview
@@ -7881,6 +7955,92 @@ selected model and native engine.
7881
7955
  The default export from `arcane-os/core/image` is the same
7882
7956
  `createLocalImageService` factory and lifecycle described above.
7883
7957
 
7958
+ ## createSpeechService()
7959
+
7960
+ ### Overview and usage
7961
+
7962
+ `createSpeechService({stt,tts,signal}={})` from `arcane-os/core/speech` composes
7963
+ independently selected STT and TTS engines as one Core service. Supply either
7964
+ role or both. Construction downloads, installs and loads nothing; the host
7965
+ owns engine creation, model/device selection and supported formats.
7966
+
7967
+ ```javascript
7968
+ import {createCoreRuntime} from 'arcane-os/core/runtime';
7969
+ import {createSpeechService} from 'arcane-os/core/speech';
7970
+
7971
+ // transcriptionEngine is the host's already constructed, selected engine.
7972
+ const speech = createSpeechService({stt: transcriptionEngine});
7973
+ const core = createCoreRuntime({services: [speech]});
7974
+ core.start();
7975
+ ```
7976
+
7977
+ Each supplied engine exposes `current()`, replaying `subscribe(listener)`,
7978
+ `load({signal})`, `close()` and its role's
7979
+ `transcribe(request,{signal,onProgress})` or
7980
+ `synthesize(request,{signal,onProgress})`. The service forwards complete
7981
+ requests and results unchanged. Format decoding, model processing and encoding
7982
+ remain with that engine. This factory supplies no speech engine or automatic
7983
+ browser/native provider selection.
7984
+
7985
+ ### Members, results and events
7986
+
7987
+ The returned object has `name: 'speech'`, `current`, `subscribe`, `close`,
7988
+ `start`, `methods`, `drain` and `dispose`:
7989
+
7990
+ - `start(context)` subscribes and starts both configured engine loads
7991
+ independently, returning immediately. Core and status remain responsive
7992
+ during loading; one role's failure leaves the other usable.
7993
+ - `current()` and `methods['speech.status']()` return the same snapshot:
7994
+ `ready`, `transcriptionAvailable`, `synthesisAvailable`, `status`,
7995
+ `sttEngine`, `ttsEngine`, `roles.stt`, `roles.tts` and `closed`.
7996
+ `ready` requires both roles; each independent availability flag requires
7997
+ its model loaded in `ready` or `running` state while the service accepts work.
7998
+ `status` is `created`, `ok`, `closing` or `closed`; per-role state carries
7999
+ model loading, progress and complete errors. An omitted role is unavailable.
8000
+ - `subscribe(listener,{replay=true,signal}={})` replays current state by
8001
+ default and returns an unsubscribe function. Its signal owns only that
8002
+ subscription; a closed service can replay its final snapshot.
8003
+ - `methods['speech.transcribe'](request,context)` and
8004
+ `methods['speech.synthesize'](request,context)` are registered as the matching
8005
+ Core RPC methods. The context supplies `signal`, `requestId` and `emit`.
8006
+ The service publishes synchronous accepted `Thinking` before waiting for
8007
+ its role's load, then forwards the engine's complete progress and result.
8008
+ - `close()` and its `drain()` alias stop acceptance, abort the service lifetime,
8009
+ close both engines concurrently and await startup and request work. Repeated
8010
+ calls share one promise. `dispose()` also releases event subscriptions.
8011
+
8012
+ `speech.state` carries the complete snapshot. `speech.progress` carries
8013
+ `{requestId,role,status:'Thinking',progress}` with initial
8014
+ `progress:{phase:'accepted'}`. The service's subscription replays state; Core's
8015
+ generic runtime replay does not replay this custom event. Browser consumers
8016
+ subscribe before requesting `Arcane.speech.status()` and preserve any newer
8017
+ state event received while that request is pending.
8018
+
8019
+ ### Cancellation, errors and availability
8020
+
8021
+ The optional factory signal owns the service lifetime. A request signal is
8022
+ combined with that lifetime and reaches its selected engine. Cancelling one
8023
+ request waiting for a shared load settles that request without cancelling
8024
+ the other requests or role. No later cancelled progress or result is delivered.
8025
+ Actual queued/running native interruption belongs to the engine; it must settle
8026
+ before Core reaches service shutdown, because Core first cancels and awaits
8027
+ active requests.
8028
+
8029
+ Invalid engine interfaces or listeners throw `TypeError`. Missing roles reject
8030
+ their operation with `SPEECH_ENGINE_UNAVAILABLE`; work after shutdown begins
8031
+ rejects with `CORE_CLOSING`. Engine errors remain observable, and engine cleanup
8032
+ failures reject shutdown with `AggregateError`. Asynchronous listener failures
8033
+ reach developer diagnostics. Engine platform support remains its own contract;
8034
+ this Node composition layer adds no engine dependency. Existing AI readiness
8035
+ consumers recognize `whisper.cpp` and `kokoro-onnx` IDs independently of the
8036
+ service's engine-neutral interface. See the [complete engine and lifetime
8037
+ contract](native-speech.md).
8038
+
8039
+ ## createSpeechService default export
8040
+
8041
+ The default export from `arcane-os/core/speech` is the same
8042
+ `createSpeechService` factory, with identical inputs, results and lifecycle.
8043
+
7884
8044
  ## prepareCoreModelAssets()
7885
8045
 
7886
8046
  `prepareCoreModelAssets({client=getInstalledCoreClient(),workingDirectory,members,signal?,onProgress?})`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arcane-os",
3
- "version": "0.63.0",
3
+ "version": "0.64.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",
@@ -61,6 +61,7 @@
61
61
  "./core/runtime": "./src/core/runtime.mjs",
62
62
  "./core/stdio": "./src/core/stdio.mjs",
63
63
  "./core/host": "./src/core/host.mjs",
64
+ "./core/repositories": "./src/core/repositories.mjs",
64
65
  "./core/packaged-web": "./src/core/services/packaged-web.mjs",
65
66
  "./core/preferences": "./src/core/services/preferences.mjs",
66
67
  "./core/diarization": "./src/diarization/service.mjs",
@@ -73,6 +74,8 @@
73
74
  "./local-ai": "./src/local-ai/install.mjs",
74
75
  "./local-ai/onnx": "./src/local-ai/onnx.mjs",
75
76
  "./local-ai/image": "./src/local-ai/image-runtime.mjs",
77
+ "./local-ai/whisper": "./src/local-ai/whisper/index.mjs",
78
+ "./local-ai/whisper/build": "./src/local-ai/whisper/build.mjs",
76
79
  "./integrated-provider": "./src/integrated-provider-loader.mjs",
77
80
  "./packager": "./src/packager/core.mjs",
78
81
  "./release-bundle": "./src/release-bundle.mjs",
@@ -295,6 +295,7 @@
295
295
  const aiModuleImport = import('../modules/AI.js');
296
296
  const providerRuntimeImport = import('../modules/AIProviderRuntime.js');
297
297
  const {
298
+ continuesAIRuntimeOperation,
298
299
  requestAIRuntimeIntent,
299
300
  subscribeAIRuntimeState
300
301
  } = await import('../modules/AIRuntimeState.js');
@@ -366,6 +367,7 @@
366
367
  let ttsIntentGeneration = 0;
367
368
  let eventOperationSequence = 0;
368
369
  let transcriptionOperationId = null;
370
+ let transcriptionRuntimeOperationId = null;
369
371
  let ttsOperationId = null;
370
372
  let destroyed = false;
371
373
  let recognitionLanguage = is.string(host.recognitionLanguage)
@@ -826,8 +828,10 @@
826
828
 
827
829
  function applyRoleTransitions(previousSTTRole, previousTTSRole) {
828
830
  sttActivationController?.synchronize(sttRole);
829
- const sttBecameUnready = previousSTTRole.state === 'ready'
830
- && sttRole.state !== 'ready';
831
+ const continuingTranscription = transcriptionActive && !nativeCaptureSession
832
+ && continuesAIRuntimeOperation(previousSTTRole, sttRole, transcriptionRuntimeOperationId);
833
+ const sttBecameUnready = ['ready', 'recovering'].includes(previousSTTRole.state)
834
+ && sttRole.state !== 'ready' && !continuingTranscription;
831
835
  const ttsBecameUnready = previousTTSRole.state === 'ready'
832
836
  && ttsRole.state !== 'ready';
833
837
 
@@ -2011,10 +2015,16 @@
2011
2015
  throw error;
2012
2016
  }
2013
2017
 
2014
- const transcription = await aiRuntime.fetchSTT(
2018
+ const previousRuntimeOperationId = providerRuntime.status('stt').operationId;
2019
+ const pendingTranscription = aiRuntime.fetchSTT(
2015
2020
  audioFile,
2016
2021
  controller.signal
2017
2022
  );
2023
+ const activeRole = providerRuntime.status('stt');
2024
+ transcriptionRuntimeOperationId = activeRole.busy
2025
+ && activeRole.operationId !== previousRuntimeOperationId
2026
+ ? activeRole.operationId : null;
2027
+ const transcription = await pendingTranscription;
2018
2028
  if (
2019
2029
  generation !== transcriptionGeneration
2020
2030
  || sttRole.state !== 'ready'
@@ -2044,6 +2054,7 @@
2044
2054
  } finally {
2045
2055
  if (transcriptionAbortController === controller) {
2046
2056
  transcriptionAbortController = null;
2057
+ transcriptionRuntimeOperationId = null;
2047
2058
  }
2048
2059
  if (generation === transcriptionGeneration) {
2049
2060
  transcriptionActive = false;
@@ -2144,6 +2155,7 @@
2144
2155
  transcriptionGeneration += 1;
2145
2156
  transcriptionAbortController?.abort();
2146
2157
  transcriptionAbortController = null;
2158
+ transcriptionRuntimeOperationId = null;
2147
2159
  recordingRequested = false;
2148
2160
  transcriptionActive = false;
2149
2161
  clearSilenceTimer();