arcane-os 0.59.1 → 0.61.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 +43 -0
- package/README.md +1 -1
- package/browser-runtime/ai/core-image.mjs +360 -0
- package/browser-runtime/ai/core-model-assets.mjs +152 -0
- package/browser-runtime/core/client.mjs +8 -0
- package/browser-runtime/core/contracts.mjs +5 -0
- package/docs/reference/core/arcane-ai-contracts.md +17 -1
- package/docs/reference/core/arcane-api.md +12 -1
- package/docs/reference/core/reference/arcane-api/ai-and-ollama.md +20 -11
- package/docs/reference/core/reference/arcane-api/namespaces.md +14 -0
- package/docs/reference/core-client.md +67 -0
- package/docs/reference/core-native-packaging.md +66 -2
- package/docs/reference/inventory/package-api.json +2 -2
- package/docs/reference/inventory/runtime-modules.json +2 -2
- package/docs/reference/local-image-editing.md +79 -0
- package/docs/reference/local-image-generation.md +267 -0
- package/docs/reference/model-assets.md +147 -0
- package/docs/reference/runtime-modules.md +37 -5
- package/docs/reference/sdk-api.md +41 -5
- package/package.json +8 -3
- package/runtime/arcane/modules/ThemeBootstrap.js +217 -2
- package/schemas/arcane-app.schema.json +2 -2
- package/schemas/arcane-package.schema.json +2 -2
- package/src/core/hosts/windows/ArcaneHost.cs +211 -0
- package/src/core/hosts/windows/ArcaneLauncher.cs +5 -0
- package/src/core/services/image.mjs +563 -0
- package/src/core/services/local-ai.mjs +8 -3
- package/src/core/services/model-assets.mjs +267 -0
- package/src/import-map.mjs +2 -0
- package/src/local-ai/config.mjs +41 -1
- package/src/local-ai/dev.mjs +46 -9
- package/src/local-ai/image-editing.mjs +159 -0
- package/src/local-ai/image-library.mjs +985 -0
- package/src/local-ai/image-models/sd14.mjs +15 -0
- package/src/local-ai/image-png.mjs +74 -0
- package/src/local-ai/image-runtime.mjs +336 -0
- package/src/local-ai/image-worker.mjs +218 -0
- package/src/local-ai/install.mjs +326 -10
- package/src/native/portable-layout.mjs +28 -2
- package/src/native/windows-icon.mjs +582 -0
- package/src/native/windows-provider.mjs +60 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,48 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.61.0
|
|
4
|
+
|
|
5
|
+
- Add retained local image generation through Core, with public native runtime,
|
|
6
|
+
Core service and browser accessor entrypoints. Applications keep ownership of
|
|
7
|
+
prompts, styles, saved images and associations.
|
|
8
|
+
- Select the approved stable-diffusion.cpp runtime and Stable Diffusion 1.4
|
|
9
|
+
checkpoint through existing local-AI configuration. Runtime installation and
|
|
10
|
+
model storage/preparation remain separate; model setup uses the existing
|
|
11
|
+
DBOPFS model store and a retained Core working-file projection.
|
|
12
|
+
- Preserve complete prompts and PNG results, synchronous transient Thinking
|
|
13
|
+
acknowledgement, observable model state and progress, actual cancellation
|
|
14
|
+
joining, and native context/file cleanup. Preparation does not block page
|
|
15
|
+
rendering or unrelated Core services.
|
|
16
|
+
- Expose model-assets preparation and service entrypoints alongside image
|
|
17
|
+
generation. Native contexts retain working files until actual unload; the
|
|
18
|
+
browser accessor closes only its own requests and subscriptions.
|
|
19
|
+
- Built-in distributions select CPU on Windows and Linux and the same upstream
|
|
20
|
+
Metal/CPU archive on macOS. Windows CUDA preparation remains separate pending
|
|
21
|
+
work; publication does not claim GPU-first Windows or PM execution.
|
|
22
|
+
- Keep optional engine/binding acquisition in the selected application's
|
|
23
|
+
existing installer workflow. No engine binaries or model checkpoint are added
|
|
24
|
+
to the npm package, and its declared dependency tree is unchanged.
|
|
25
|
+
|
|
26
|
+
## 0.60.0
|
|
27
|
+
|
|
28
|
+
- Add `Arcane.window.setTheme(presentation, {signal})` for the current native
|
|
29
|
+
window. Optional sRGB RGBA background/text colors, null resets and omitted
|
|
30
|
+
fields retain their explicit semantics; results distinguish accepted and
|
|
31
|
+
unsupported fields. This does not change system-wide appearance.
|
|
32
|
+
- Forward application-computed theme colors without blocking rendering.
|
|
33
|
+
Observe existing theme and page lifecycles, cancel superseded requests, and
|
|
34
|
+
preserve ordinary browser behavior and native window controls.
|
|
35
|
+
- Apply app-owned Windows icons to the window/taskbar and copied executable.
|
|
36
|
+
Portable Node assembly handles PNG/ICO resources without an application
|
|
37
|
+
compiler, preserving source images and other executable resources. Other
|
|
38
|
+
formats retain assembly with an explicit unsupported-branding diagnostic.
|
|
39
|
+
- Windows caption/text colors use the host's supported per-window DWM
|
|
40
|
+
attributes. Using the new Windows behavior requires the matching host asset;
|
|
41
|
+
platform acceptance is reported rather than inferred from a request.
|
|
42
|
+
- Include completed internal PNG-decoding and stored-model working-file
|
|
43
|
+
preparation primitives. Image-service registration and engine integration
|
|
44
|
+
remain separate work; this release does not claim app-ready image generation.
|
|
45
|
+
|
|
3
46
|
## 0.59.1
|
|
4
47
|
|
|
5
48
|
- Forward the existing AI synthesis signal into native Core synthesis and
|
package/README.md
CHANGED
|
@@ -20,7 +20,7 @@ selected SDK projection or an explicit live SDK source mount. Both profiles
|
|
|
20
20
|
share the theme, packaging, event, cancellation,
|
|
21
21
|
and browser run contracts while retaining their selected application layout.
|
|
22
22
|
|
|
23
|
-
This checkout defines the `0.
|
|
23
|
+
This checkout defines the `0.61.0` SDK contract. Development applications declare
|
|
24
24
|
`arcane-os` as `latest` and retain their lockfile. Exact SDK pins begin when Roshi
|
|
25
25
|
designates a production build. Follow [Consumer updates](docs/consumer-updates.md)
|
|
26
26
|
to refresh and compare the installed SDK and lockfile with the published channel;
|
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
import Is from '../dependencies/strong-type/index.js';
|
|
2
|
+
import {getInstalledCoreClient} from '../core/client.mjs';
|
|
3
|
+
import {CoreError, serializeCoreError} from '../core/contracts.mjs';
|
|
4
|
+
import {createArcaneEventSource} from '../event-manager.mjs';
|
|
5
|
+
|
|
6
|
+
const is = new Is(false);
|
|
7
|
+
|
|
8
|
+
function unavailable(message) {
|
|
9
|
+
return new CoreError(
|
|
10
|
+
{code: 'ARCANE_IMAGE_CORE_UNAVAILABLE', message}
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Decode the complete JSON transport representation without retaining it. */
|
|
15
|
+
function decodeImage(image) {
|
|
16
|
+
const {data, encoding, ...metadata} = image;
|
|
17
|
+
if (encoding !== 'base64') {
|
|
18
|
+
throw new TypeError('Core image results require base64 transport encoding.');
|
|
19
|
+
}
|
|
20
|
+
const decoded = Uint8Array.from(
|
|
21
|
+
globalThis.atob(data),
|
|
22
|
+
function decodeImageCharacter(character) {
|
|
23
|
+
return character.charCodeAt(0);
|
|
24
|
+
}
|
|
25
|
+
);
|
|
26
|
+
const blob = new Blob(
|
|
27
|
+
[decoded],
|
|
28
|
+
{type: metadata.mediaType}
|
|
29
|
+
);
|
|
30
|
+
return {...metadata, blob};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Browser access to the selected native image service; no result persistence. */
|
|
34
|
+
export function createCoreImageRuntime(
|
|
35
|
+
{client = getInstalledCoreClient(), signal, onEvent} = {}
|
|
36
|
+
) {
|
|
37
|
+
const owner = {};
|
|
38
|
+
const events = createArcaneEventSource(
|
|
39
|
+
owner,
|
|
40
|
+
{source: 'core-image', eventTypes: ['image.state']}
|
|
41
|
+
);
|
|
42
|
+
const lifetime = new AbortController();
|
|
43
|
+
const lifetimeSignal = signal ? AbortSignal.any([signal, lifetime.signal]) : lifetime.signal;
|
|
44
|
+
const pending = new Set();
|
|
45
|
+
let state = {
|
|
46
|
+
id: 'stable-diffusion.cpp',
|
|
47
|
+
installed: false,
|
|
48
|
+
available: false,
|
|
49
|
+
selectedModel: null,
|
|
50
|
+
loaded: false,
|
|
51
|
+
state: client ? 'unknown' : 'unavailable',
|
|
52
|
+
backend: null,
|
|
53
|
+
devices: [],
|
|
54
|
+
resources: {},
|
|
55
|
+
progress: null,
|
|
56
|
+
error: null,
|
|
57
|
+
models: []
|
|
58
|
+
};
|
|
59
|
+
let revision = 0;
|
|
60
|
+
let closed = false;
|
|
61
|
+
let closing;
|
|
62
|
+
|
|
63
|
+
function current() {
|
|
64
|
+
const requests = [...pending].map(
|
|
65
|
+
function requestState(request) {
|
|
66
|
+
return {
|
|
67
|
+
method: request.method,
|
|
68
|
+
streamId: request.streamId,
|
|
69
|
+
status: request.status,
|
|
70
|
+
progress: request.progress
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
);
|
|
74
|
+
return {
|
|
75
|
+
...state,
|
|
76
|
+
requests,
|
|
77
|
+
status: !closed && requests.some(
|
|
78
|
+
function generationPending(request) {
|
|
79
|
+
return request.status === 'Thinking';
|
|
80
|
+
}
|
|
81
|
+
) ? 'Thinking' : null,
|
|
82
|
+
busy: pending.size > 0,
|
|
83
|
+
closed
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function reportObserverFailure(error) {
|
|
88
|
+
globalThis.console?.error('Core image observer failed.', error);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function notifyListener(listener, value) {
|
|
92
|
+
const result = listener(value);
|
|
93
|
+
if (result && is.function(result.then)) {
|
|
94
|
+
// Observers own their callback lifetimes and may await close().
|
|
95
|
+
Promise.resolve(result).catch(reportObserverFailure);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function observe(callback, value) {
|
|
100
|
+
if (!is.function(callback)) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
try {
|
|
104
|
+
notifyListener(callback, value);
|
|
105
|
+
} catch (error) {
|
|
106
|
+
reportObserverFailure(error);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function publish() {
|
|
111
|
+
const snapshot = current();
|
|
112
|
+
events.dispatch('image.state', snapshot);
|
|
113
|
+
if (snapshot.closed === closed) {
|
|
114
|
+
observe(
|
|
115
|
+
onEvent,
|
|
116
|
+
{type: 'image.state', data: snapshot}
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
return snapshot;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function accept(value) {
|
|
123
|
+
if (closed) {
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
state = value;
|
|
127
|
+
revision += 1;
|
|
128
|
+
publish();
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const unsubscribe = client?.events.on('image.state', accept)
|
|
132
|
+
?? function noCoreSubscription() {};
|
|
133
|
+
|
|
134
|
+
function invoke(method, parameters, {signal: requestSignal, onProgress} = {}) {
|
|
135
|
+
if (closed) {
|
|
136
|
+
throw unavailable('The Core image accessor is closed.');
|
|
137
|
+
}
|
|
138
|
+
if (!client) {
|
|
139
|
+
throw unavailable('Local image generation requires an available Core connection.');
|
|
140
|
+
}
|
|
141
|
+
const operationSignal = requestSignal
|
|
142
|
+
? AbortSignal.any([requestSignal, lifetimeSignal]) : lifetimeSignal;
|
|
143
|
+
operationSignal.throwIfAborted();
|
|
144
|
+
const request = {
|
|
145
|
+
method,
|
|
146
|
+
revision,
|
|
147
|
+
streamId: parameters.streamId,
|
|
148
|
+
status: method === 'image.generate' ? 'Thinking' : null,
|
|
149
|
+
progress: null,
|
|
150
|
+
task: null
|
|
151
|
+
};
|
|
152
|
+
let stopProgress;
|
|
153
|
+
if (request.streamId) {
|
|
154
|
+
stopProgress = client.events.on(
|
|
155
|
+
'image.progress',
|
|
156
|
+
function imageProgress(value) {
|
|
157
|
+
if (closed || operationSignal.aborted || value.streamId !== request.streamId) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
request.progress = value.progress;
|
|
161
|
+
publish();
|
|
162
|
+
if (closed || operationSignal.aborted) {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
observe(onProgress, value.progress);
|
|
166
|
+
if (closed || operationSignal.aborted) {
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
observe(
|
|
170
|
+
onEvent,
|
|
171
|
+
{type: 'image.progress', data: value}
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
request.task = Promise.resolve().then(
|
|
177
|
+
async function executeImageRequest() {
|
|
178
|
+
operationSignal.throwIfAborted();
|
|
179
|
+
observe(
|
|
180
|
+
onEvent,
|
|
181
|
+
{type: 'image.request', data: {method, parameters}}
|
|
182
|
+
);
|
|
183
|
+
operationSignal.throwIfAborted();
|
|
184
|
+
const result = await client.invoke(
|
|
185
|
+
method,
|
|
186
|
+
parameters,
|
|
187
|
+
{signal: operationSignal, timeoutMs: 0}
|
|
188
|
+
);
|
|
189
|
+
operationSignal.throwIfAborted();
|
|
190
|
+
if (method === 'image.generate') {
|
|
191
|
+
const images = result.images.map(decodeImage);
|
|
192
|
+
operationSignal.throwIfAborted();
|
|
193
|
+
const decoded = {...result, images};
|
|
194
|
+
observe(
|
|
195
|
+
onEvent,
|
|
196
|
+
{type: 'image.result', data: decoded}
|
|
197
|
+
);
|
|
198
|
+
operationSignal.throwIfAborted();
|
|
199
|
+
return decoded;
|
|
200
|
+
}
|
|
201
|
+
return result;
|
|
202
|
+
}
|
|
203
|
+
).catch(
|
|
204
|
+
function imageRequestFailed(error) {
|
|
205
|
+
if (!closed && !operationSignal.aborted) {
|
|
206
|
+
if (method !== 'image.status' || revision === request.revision) {
|
|
207
|
+
state = {...state, error: serializeCoreError(error)};
|
|
208
|
+
}
|
|
209
|
+
observe(
|
|
210
|
+
onEvent,
|
|
211
|
+
{type: 'image.error', data: {method, error: serializeCoreError(error)}}
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
throw error;
|
|
215
|
+
}
|
|
216
|
+
).finally(
|
|
217
|
+
function releaseImageRequest() {
|
|
218
|
+
stopProgress?.();
|
|
219
|
+
pending.delete(request);
|
|
220
|
+
if (!closed) {
|
|
221
|
+
publish();
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
);
|
|
225
|
+
pending.add(request);
|
|
226
|
+
// Acknowledgement precedes transport, preparation, model loading and inference.
|
|
227
|
+
publish();
|
|
228
|
+
return request.task;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
async function inspect({signal: requestSignal} = {}) {
|
|
232
|
+
const before = revision;
|
|
233
|
+
try {
|
|
234
|
+
const result = await invoke(
|
|
235
|
+
'image.status',
|
|
236
|
+
{},
|
|
237
|
+
{signal: requestSignal}
|
|
238
|
+
);
|
|
239
|
+
if (!closed && revision === before) {
|
|
240
|
+
accept(result);
|
|
241
|
+
}
|
|
242
|
+
return current();
|
|
243
|
+
} catch (error) {
|
|
244
|
+
if (!closed && !requestSignal?.aborted && revision === before) {
|
|
245
|
+
accept(
|
|
246
|
+
{...state, available: false, state: 'unavailable', error: serializeCoreError(error)}
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
throw error;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
async function load({model, context, assetProjectionId, resourcePaths, signal: requestSignal} = {}) {
|
|
254
|
+
const before = revision;
|
|
255
|
+
const result = await invoke(
|
|
256
|
+
'image.load',
|
|
257
|
+
{model, context, assetProjectionId, resourcePaths},
|
|
258
|
+
{signal: requestSignal}
|
|
259
|
+
);
|
|
260
|
+
if (!closed && revision === before) {
|
|
261
|
+
accept(result);
|
|
262
|
+
}
|
|
263
|
+
return result;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function generate(
|
|
267
|
+
{model, prompt, parameters, assetProjectionId, resourcePaths, signal: requestSignal, onProgress} = {}
|
|
268
|
+
) {
|
|
269
|
+
const streamId = client?.uuid?.() ?? globalThis.crypto.randomUUID();
|
|
270
|
+
return invoke(
|
|
271
|
+
'image.generate',
|
|
272
|
+
{model, prompt, parameters, streamId, assetProjectionId, resourcePaths},
|
|
273
|
+
{signal: requestSignal, onProgress}
|
|
274
|
+
);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
async function unload({signal: requestSignal} = {}) {
|
|
278
|
+
const before = revision;
|
|
279
|
+
const result = await invoke(
|
|
280
|
+
'image.unload',
|
|
281
|
+
{},
|
|
282
|
+
{signal: requestSignal}
|
|
283
|
+
);
|
|
284
|
+
if (!closed && revision === before) {
|
|
285
|
+
accept(result);
|
|
286
|
+
}
|
|
287
|
+
return result;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function subscribe(listener, {replay = true, signal: subscriptionSignal} = {}) {
|
|
291
|
+
if (!is.function(listener)) {
|
|
292
|
+
throw new TypeError('An image state listener must be a function.');
|
|
293
|
+
}
|
|
294
|
+
if (closed) {
|
|
295
|
+
if (replay && !subscriptionSignal?.aborted) {
|
|
296
|
+
notifyListener(listener, current());
|
|
297
|
+
}
|
|
298
|
+
return function closedSubscription() {};
|
|
299
|
+
}
|
|
300
|
+
const stop = events.on(
|
|
301
|
+
'image.state',
|
|
302
|
+
function imageStateChanged(event) {
|
|
303
|
+
notifyListener(listener, event.detail);
|
|
304
|
+
},
|
|
305
|
+
{signal: subscriptionSignal}
|
|
306
|
+
);
|
|
307
|
+
try {
|
|
308
|
+
if (replay && !subscriptionSignal?.aborted) {
|
|
309
|
+
notifyListener(listener, current());
|
|
310
|
+
}
|
|
311
|
+
} catch (error) {
|
|
312
|
+
stop();
|
|
313
|
+
throw error;
|
|
314
|
+
}
|
|
315
|
+
return stop;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function close() {
|
|
319
|
+
if (closing) {
|
|
320
|
+
return closing;
|
|
321
|
+
}
|
|
322
|
+
closed = true;
|
|
323
|
+
closing = Promise.resolve().then(
|
|
324
|
+
async function closeImageAccessor() {
|
|
325
|
+
await Promise.allSettled(
|
|
326
|
+
[...pending].map(
|
|
327
|
+
function requestCompletion(request) {
|
|
328
|
+
return request.task;
|
|
329
|
+
}
|
|
330
|
+
)
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
);
|
|
334
|
+
lifetime.abort();
|
|
335
|
+
lifetimeSignal.removeEventListener('abort', close);
|
|
336
|
+
unsubscribe();
|
|
337
|
+
state = {...state, available: false, state: 'closed'};
|
|
338
|
+
publish();
|
|
339
|
+
events.dispose();
|
|
340
|
+
return closing;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
lifetimeSignal.addEventListener(
|
|
344
|
+
'abort',
|
|
345
|
+
close,
|
|
346
|
+
{once: true}
|
|
347
|
+
);
|
|
348
|
+
if (lifetimeSignal.aborted) {
|
|
349
|
+
close();
|
|
350
|
+
} else if (client) {
|
|
351
|
+
// Subscribe before the snapshot request so a later lifecycle event wins.
|
|
352
|
+
inspect().catch(
|
|
353
|
+
function initialImageStateUnavailable(error) {
|
|
354
|
+
globalThis.console?.error('Core image initial state unavailable.', error);
|
|
355
|
+
}
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
return {load, generate, unload, inspect, current, subscribe, close};
|
|
360
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import Is from '../dependencies/strong-type/index.js';
|
|
2
|
+
import {getInstalledCoreClient} from '../core/client.mjs';
|
|
3
|
+
|
|
4
|
+
const is = new Is(false);
|
|
5
|
+
|
|
6
|
+
/** Projects complete caller-owned assets through Core without changing their store. */
|
|
7
|
+
export async function prepareCoreModelAssets({
|
|
8
|
+
client = getInstalledCoreClient(), workingDirectory, members, signal, onProgress
|
|
9
|
+
} = {}) {
|
|
10
|
+
if (!is.function(client?.invoke) || !is.function(client?.uuid)) {
|
|
11
|
+
const error = new Error('Native model assets require an available Core connection.');
|
|
12
|
+
error.code = 'ARCANE_MODEL_ASSETS_CORE_UNAVAILABLE';
|
|
13
|
+
throw error;
|
|
14
|
+
}
|
|
15
|
+
if (!is.string(workingDirectory) || workingDirectory === '') {
|
|
16
|
+
throw new TypeError('Model asset workingDirectory must be a nonempty string.');
|
|
17
|
+
}
|
|
18
|
+
if (!is.array(members) || members.length === 0) {
|
|
19
|
+
throw new TypeError('Model assets require at least one complete file.');
|
|
20
|
+
}
|
|
21
|
+
for (const member of members) {
|
|
22
|
+
if (!is.string(member?.path) || member.path === '' || !is.function(member?.file?.stream)) {
|
|
23
|
+
throw new TypeError('Each model asset requires a path and a complete Blob or File.');
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
if (onProgress !== undefined && !is.function(onProgress)) {
|
|
27
|
+
throw new TypeError('Model asset onProgress must be a function.');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const controller = new AbortController();
|
|
31
|
+
const operationSignal = signal ? AbortSignal.any([signal, controller.signal]) : controller.signal;
|
|
32
|
+
operationSignal.throwIfAborted();
|
|
33
|
+
const id = client.uuid();
|
|
34
|
+
const cleanupErrors = [];
|
|
35
|
+
let completed = 0;
|
|
36
|
+
let openingStarted = false;
|
|
37
|
+
let failed = false;
|
|
38
|
+
let failure;
|
|
39
|
+
let releaseTask;
|
|
40
|
+
|
|
41
|
+
function retainFailure(error) {
|
|
42
|
+
if (failed) return;
|
|
43
|
+
failed = true;
|
|
44
|
+
failure = error;
|
|
45
|
+
controller.abort(error);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function progress(phase, memberIndex) {
|
|
49
|
+
operationSignal.throwIfAborted();
|
|
50
|
+
onProgress?.({
|
|
51
|
+
phase, completed, total: members.length, unit: 'files',
|
|
52
|
+
...(memberIndex === undefined ? {} : {memberIndex, path: members[memberIndex].path})
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function release() {
|
|
57
|
+
releaseTask ??= Promise.resolve().then(function releaseNativeProjection() {
|
|
58
|
+
return client.invoke('modelAssets.release', {id}, {timeoutMs: 0});
|
|
59
|
+
});
|
|
60
|
+
return releaseTask;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function transferMember(member, memberIndex) {
|
|
64
|
+
let reader;
|
|
65
|
+
let cancellation;
|
|
66
|
+
|
|
67
|
+
function cancelReader() {
|
|
68
|
+
if (!reader || cancellation) return;
|
|
69
|
+
cancellation = Promise.resolve().then(function cancelMemberStream() {
|
|
70
|
+
return reader.cancel(operationSignal.reason);
|
|
71
|
+
}).catch(function retainReaderCleanupFailure(error) {
|
|
72
|
+
cleanupErrors.push(error);
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
operationSignal.throwIfAborted();
|
|
78
|
+
reader = member.file.stream().getReader();
|
|
79
|
+
operationSignal.addEventListener('abort', cancelReader, {once: true});
|
|
80
|
+
if (operationSignal.aborted) cancelReader();
|
|
81
|
+
progress('write', memberIndex);
|
|
82
|
+
while (true) {
|
|
83
|
+
operationSignal.throwIfAborted();
|
|
84
|
+
const {done, value} = await reader.read();
|
|
85
|
+
operationSignal.throwIfAborted();
|
|
86
|
+
if (done) break;
|
|
87
|
+
// Base64 belongs only to the existing JSON transport boundary.
|
|
88
|
+
const contentBase64 = encodeChunk(value);
|
|
89
|
+
await client.invoke('modelAssets.write', {id, memberIndex, contentBase64}, {
|
|
90
|
+
signal: operationSignal, timeoutMs: 0
|
|
91
|
+
});
|
|
92
|
+
progress('write', memberIndex);
|
|
93
|
+
}
|
|
94
|
+
completed += 1;
|
|
95
|
+
progress('write', memberIndex);
|
|
96
|
+
} catch (error) {
|
|
97
|
+
retainFailure(error);
|
|
98
|
+
throw error;
|
|
99
|
+
} finally {
|
|
100
|
+
operationSignal.removeEventListener('abort', cancelReader);
|
|
101
|
+
if (operationSignal.aborted) cancelReader();
|
|
102
|
+
if (cancellation) await cancellation;
|
|
103
|
+
try { reader?.releaseLock(); } catch (error) {
|
|
104
|
+
if (failed) cleanupErrors.push(error);
|
|
105
|
+
else retainFailure(error);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
progress('open');
|
|
112
|
+
openingStarted = true;
|
|
113
|
+
await client.invoke('modelAssets.open', {
|
|
114
|
+
id, workingDirectory,
|
|
115
|
+
members: members.map(function describeMember(member) { return {path: member.path}; })
|
|
116
|
+
}, {signal: operationSignal, timeoutMs: 0});
|
|
117
|
+
operationSignal.throwIfAborted();
|
|
118
|
+
const results = await Promise.allSettled(members.map(transferMember));
|
|
119
|
+
for (const result of results) {
|
|
120
|
+
if (result.status === 'rejected') retainFailure(result.reason);
|
|
121
|
+
}
|
|
122
|
+
if (failed) throw failure;
|
|
123
|
+
if (cleanupErrors.length) {
|
|
124
|
+
throw new AggregateError(cleanupErrors.splice(0), 'Model asset streams could not be released.');
|
|
125
|
+
}
|
|
126
|
+
progress('complete');
|
|
127
|
+
const projection = await client.invoke('modelAssets.complete', {id}, {
|
|
128
|
+
signal: operationSignal, timeoutMs: 0
|
|
129
|
+
});
|
|
130
|
+
progress('ready');
|
|
131
|
+
operationSignal.throwIfAborted();
|
|
132
|
+
// The caller releases ownership after native consumers retain this projection.
|
|
133
|
+
return {id, directory: projection.directory, members: projection.members, release};
|
|
134
|
+
} catch (error) {
|
|
135
|
+
retainFailure(error);
|
|
136
|
+
if (openingStarted) {
|
|
137
|
+
try { await release(); } catch (cleanupError) { cleanupErrors.push(cleanupError); }
|
|
138
|
+
}
|
|
139
|
+
if (cleanupErrors.length) {
|
|
140
|
+
throw new AggregateError([failure, ...cleanupErrors],
|
|
141
|
+
'Model asset preparation and cleanup failed.', {cause: failure});
|
|
142
|
+
}
|
|
143
|
+
throw failure;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function encodeChunk(content) {
|
|
148
|
+
if (is.function(content.toBase64)) return content.toBase64();
|
|
149
|
+
let binary = '';
|
|
150
|
+
for (const value of content) binary += String.fromCharCode(value);
|
|
151
|
+
return btoa(binary);
|
|
152
|
+
}
|
|
@@ -616,6 +616,14 @@ export function createCoreFacade(client){
|
|
|
616
616
|
storage:{list:()=>invoke('storage.list'),get:key=>invoke('storage.get',{key}),set:(key,value)=>invoke('storage.set',{key,value}),delete:key=>invoke('storage.delete',{key})},
|
|
617
617
|
preferences:{list:()=>invoke('preferences.list'),get:key=>invoke('preferences.get',{key}),set:(key,value)=>invoke('preferences.set',{key,value}),
|
|
618
618
|
setMany:entries=>invoke('preferences.setMany',{entries}),delete:key=>invoke('preferences.delete',{key})},
|
|
619
|
+
window: {
|
|
620
|
+
setTheme(presentation, options) {
|
|
621
|
+
return invoke(
|
|
622
|
+
'window.setTheme', presentation,
|
|
623
|
+
{signal: options?.signal}
|
|
624
|
+
);
|
|
625
|
+
}
|
|
626
|
+
},
|
|
619
627
|
appearance:{current:()=>invoke('appearance.current'),apply:appearance=>invoke('appearance.apply',appearance??{})},session:{logout:()=>invoke('session.logout')},
|
|
620
628
|
provisioning:{plan:usernames=>invoke('provisioning.plan',{usernames:Array.isArray(usernames)?usernames:[usernames].filter(Boolean)})},
|
|
621
629
|
diagnostics:{recentErrors:()=>invoke('diagnostics.recent'),get:diagnosticId=>invoke('diagnostics.get',{diagnosticId})},
|
|
@@ -32,6 +32,11 @@ export const CORE_METHOD_CONTRACTS={
|
|
|
32
32
|
'preferences.set':{input:'{key,value}',output:'{key,value}'},
|
|
33
33
|
'preferences.setMany':{input:'{entries}',output:'preference-batch-result'},
|
|
34
34
|
'preferences.delete':{input:'{key}',output:'{key,deleted}'},
|
|
35
|
+
'window.setTheme': {
|
|
36
|
+
input: '{backgroundColor?,textColor?}: null reset or {red,green,blue,alpha}',
|
|
37
|
+
output: '{platform,supported,applied,unsupported}',
|
|
38
|
+
meaning: 'Current host window only. Omitted colors stay unchanged; applied records platform-accepted values, not measured pixels or an atomic operation.'
|
|
39
|
+
},
|
|
35
40
|
'terminal.start':{input:'terminal-start-v1',output:'terminal-session-v1'},
|
|
36
41
|
'terminal.list':{input:'empty-object-v1',output:'terminal-list-v1'},
|
|
37
42
|
'terminal.write':{input:'terminal-write-v1',output:'terminal-write-result-v1'},
|
|
@@ -542,7 +542,7 @@ validate the speech health response.
|
|
|
542
542
|
|
|
543
543
|
### Speech synthesis request
|
|
544
544
|
|
|
545
|
-
`Arcane.speech.synthesize(request)` accepts the portable
|
|
545
|
+
`Arcane.speech.synthesize(request, options?)` accepts the portable
|
|
546
546
|
`SpeechSynthesisRequest` fields below. Supply the documented primitive types and
|
|
547
547
|
lowercase values; do not rely on host-specific coercion or empty-string
|
|
548
548
|
defaulting.
|
|
@@ -555,6 +555,22 @@ defaulting.
|
|
|
555
555
|
| `responseFormat` | `"opus" \| "wav"` | No | Default `"opus"` | Audio format. |
|
|
556
556
|
| `speed` | `number` | No | Default `1`; 0.5-2 inclusive | Speaking speed. |
|
|
557
557
|
|
|
558
|
+
### Speech synthesis controls
|
|
559
|
+
|
|
560
|
+
The optional second argument is `SpeechSynthesisControls`:
|
|
561
|
+
|
|
562
|
+
| Field | Type | Required | Default | Description |
|
|
563
|
+
|---|---|---:|---|---|
|
|
564
|
+
| `signal` | `AbortSignal` | No | No caller signal | Cancels this request through Core, separately from the complete synthesis payload. The registered service owns actual engine interruption. |
|
|
565
|
+
|
|
566
|
+
Existing one-argument calls retain their behavior and the 180-second timeout.
|
|
567
|
+
A pre-aborted signal dispatches no request. An in-flight abort sends
|
|
568
|
+
`request.cancel`, rejects with `ARCANE_REQUEST_ABORTED` and suppresses late
|
|
569
|
+
responses while unrelated requests continue. Other controls properties are
|
|
570
|
+
unused and do not enter the payload. `AI.fetchTTS(payload, signal)` forwards
|
|
571
|
+
the signal into this native call and reports cancellation as `AbortError` with
|
|
572
|
+
`ARCANE_AI_REQUEST_ABORTED`.
|
|
573
|
+
|
|
558
574
|
### Speech synthesis result
|
|
559
575
|
|
|
560
576
|
| Field | Type | Required | Constraints / default | Description |
|
|
@@ -48,6 +48,7 @@ This inventory covers every enumerable, non-method member exposed directly or th
|
|
|
48
48
|
| `Arcane.filesystem` | Namespace | Native filesystem selection operations. |
|
|
49
49
|
| `Arcane.storage` | Namespace | App-scoped native key/value storage. |
|
|
50
50
|
| `Arcane.preferences` | Namespace | App-scoped preference reads and atomic writes. |
|
|
51
|
+
| `Arcane.window` | Namespace | SDK host window presentation for the calling document; adapter availability is independent of the OS-user appearance service. |
|
|
51
52
|
| `Arcane.appearance` | Namespace | Native appearance state and application. |
|
|
52
53
|
| `Arcane.session` | Namespace | Current Arcane session lifecycle. |
|
|
53
54
|
| `Arcane.provisioning` | Namespace | Provisioning-plan operations. |
|
|
@@ -104,7 +105,7 @@ directly.
|
|
|
104
105
|
| `Arcane.localAI.recover(request)` | `request`: [`LocalAIRecoveryRequest`](arcane-ai-contracts.md#local-ai-recovery-request) | [`Promise<LocalAIRecoveryResult>`](arcane-ai-contracts.md#local-ai-recovery-result) | Requests one privileged, exclusive, start-existing-service-only recovery of selected Arcane-managed local services. Requires `ai.inference` and explicit application admission. |
|
|
105
106
|
| `Arcane.localAI.setParallelRequests(request)` | `request`: [`LocalAIParallelRequestsRequest`](arcane-ai-contracts.md#local-ai-parallel-request-configuration) | [`Promise<LocalAIParallelRequestsResult>`](arcane-ai-contracts.md#local-ai-parallel-request-configuration) | Solves one app-owned verified model/context's maximum directly from native model and memory/resource evidence without a product ceiling, clamps a positive request to that result (`0` requests the maximum), and changes the machine-wide managed Ollama setting only when needed. A tentative change uses one fresh stopped-service snapshot before the final value is written, then Core ensures, loads, and confirms the exact model. A restart unloads resident models. Automatic mutation currently requires the dedicated `ai.runtime.manage` capability on Microsoft NT desktop Core and operating-system authorization; Linux returns administrator-managed systemd guidance, and Android user-managed loopback sessions do not invoke it. |
|
|
106
107
|
| `Arcane.speech.status()` | None | [`Promise<SpeechStatus>`](arcane-ai-contracts.md#speech-status) | Reads bounded, independent Kokoro and Whisper readiness through Core or an admitted Android native host. Requires `ai.inference`. |
|
|
107
|
-
| `Arcane.speech.synthesize(request)` | `request`: [`SpeechSynthesisRequest`](arcane-ai-contracts.md#speech-synthesis-request) | [`Promise<SpeechSynthesisResult>`](arcane-ai-contracts.md#speech-synthesis-result) |
|
|
108
|
+
| `Arcane.speech.synthesize(request, options?)` | `request`: [`SpeechSynthesisRequest`](arcane-ai-contracts.md#speech-synthesis-request); `options?`: [`SpeechSynthesisControls`](arcane-ai-contracts.md#speech-synthesis-controls) | [`Promise<SpeechSynthesisResult>`](arcane-ai-contracts.md#speech-synthesis-result) | Sends the complete request to the selected native synthesis service; optional cancellation reaches Core's request lifetime. The registered host service owns audio generation and actual engine interruption. |
|
|
108
109
|
| `Arcane.speech.transcribe(request, options?)` | `request`: [`SpeechTranscriptionRequest`](arcane-ai-contracts.md#speech-transcription-request); `options?`: [`SpeechTranscriptionControls`](arcane-ai-contracts.md#speech-transcription-controls) | [`Promise<SpeechTranscriptionResult>`](arcane-ai-contracts.md#speech-transcription-result) | Sends the complete request to the selected native transcription service; optional cancellation reaches Core's request lifetime. The registered host service owns media decoding and actual engine interruption. |
|
|
109
110
|
| `Arcane.ollama.version()` | None | [`Promise<OllamaVersionResponse>`](arcane-ai-contracts.md#ollama-provider-responses) | Gets the managed Ollama version for Settings, Terminal, and Shell diagnostics. |
|
|
110
111
|
| `Arcane.ollama.models()` | None | [`Promise<OllamaModelsResponse>`](arcane-ai-contracts.md#ollama-provider-responses) | Lists the raw provider inventory envelope; it is not the application admission catalog. |
|
|
@@ -438,6 +439,16 @@ The canonical authority registry in `machine_bundles/arcane-os-machine-bundle/sr
|
|
|
438
439
|
|
|
439
440
|
## Filesystem, storage, preferences, and appearance
|
|
440
441
|
|
|
442
|
+
The SDK's additive `Arcane.window.setTheme(presentation, {signal}?)` controls
|
|
443
|
+
the current host window's `backgroundColor` and `textColor`, with per-field
|
|
444
|
+
accepted/unsupported results. See the [window contract](../core-client.md#current-window-theme)
|
|
445
|
+
for numeric RGBA, reset, cancellation, navigation and platform availability.
|
|
446
|
+
It does not call the OS-user `appearance.apply` service described below.
|
|
447
|
+
|
|
448
|
+
| SDK window method | Parameters | Return | Description |
|
|
449
|
+
|---|---|---|---|
|
|
450
|
+
| `Arcane.window.setTheme(presentation, options?)` | Optional `backgroundColor`/`textColor`: `{red,green,blue,alpha}` or `null`; `options.signal?` | `Promise<{platform,supported,applied,unsupported}>` | Applies supported colors only to the caller's native window, preserving omitted fields. Uses ordinary request cancellation and reports actual per-field acceptance. See the linked contract for adapter availability. |
|
|
451
|
+
|
|
441
452
|
| Method | Parameters | Return | Description |
|
|
442
453
|
|---|---|---|---|
|
|
443
454
|
| `Arcane.filesystem.selectDirectory(options?)` | `options?`: `{title?, initialPath?}`; title up to 200 plain-text characters; initial path must be an existing absolute directory | `Promise<{cancelled:boolean, path:string\|null}>` | Opens the native directory picker after a user action. It returns one canonical existing directory or explicit cancellation; it does not enumerate or grant access to directory contents. Requires `filesystem.directory.select`. |
|