arcane-os 0.60.0 → 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 +23 -0
- package/README.md +1 -1
- package/browser-runtime/ai/core-image.mjs +360 -0
- package/docs/reference/local-image-generation.md +267 -0
- package/docs/reference/model-assets.md +10 -7
- package/docs/reference/sdk-api.md +32 -0
- package/package.json +6 -1
- package/schemas/arcane-app.schema.json +2 -2
- package/schemas/arcane-package.schema.json +2 -2
- package/src/core/services/image.mjs +563 -0
- package/src/core/services/local-ai.mjs +8 -3
- 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-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/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,28 @@
|
|
|
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
|
+
|
|
3
26
|
## 0.60.0
|
|
4
27
|
|
|
5
28
|
- Add `Arcane.window.setTheme(presentation, {signal})` for the current native
|
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,267 @@
|
|
|
1
|
+
# Local image generation through Core
|
|
2
|
+
|
|
3
|
+
The SDK's local image service runs selected stable-diffusion.cpp models in a
|
|
4
|
+
retained native context. The browser uses Core for model lifecycle and complete
|
|
5
|
+
PNG results. Applications own image prompts, style choices, character identity,
|
|
6
|
+
associations and saved results.
|
|
7
|
+
|
|
8
|
+
Runtime installation, model asset preparation, model loading and image
|
|
9
|
+
generation are separate operations. The service starts promptly and retains
|
|
10
|
+
runtime preparation in the background. `image.status` remains callable during
|
|
11
|
+
preparation; rendering and unrelated Core services continue independently.
|
|
12
|
+
|
|
13
|
+
## Selection and model assets
|
|
14
|
+
|
|
15
|
+
Select the runtime in `arcane-app.json` under `native.localAI`, or in the
|
|
16
|
+
corresponding package projection's `localAI` record:
|
|
17
|
+
|
|
18
|
+
```json
|
|
19
|
+
{
|
|
20
|
+
"runtimes": [
|
|
21
|
+
{
|
|
22
|
+
"id": "stable-diffusion.cpp",
|
|
23
|
+
"version": "master-929-3f8527a",
|
|
24
|
+
"backend": "auto",
|
|
25
|
+
"models": ["sd14"]
|
|
26
|
+
}
|
|
27
|
+
]
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
`sd14` selects the Stable Diffusion 1.4 definition and its official
|
|
32
|
+
`sd-v1-4.ckpt` resource. A model definition contains `id`, `name`, `family`,
|
|
33
|
+
`resources`, `context`, `defaults` and `operations`. Selection supplies the
|
|
34
|
+
model's resource metadata; runtime installation does not independently download
|
|
35
|
+
or bundle model files. The shared model asset owner prepares selected browser
|
|
36
|
+
assets and supplies a Core working projection for native path-based loading.
|
|
37
|
+
|
|
38
|
+
The first published distribution path selects CPU on Windows and Linux.
|
|
39
|
+
On macOS, `auto` selects the same upstream universal runtime's built-in Metal
|
|
40
|
+
backend, with CPU fallback; explicit `cpu` remains available. A caller-supplied
|
|
41
|
+
runtime archive with `backend: 'auto'` uses its available accelerated device
|
|
42
|
+
with CPU fallback. Built-in Windows CUDA distribution preparation is a separate
|
|
43
|
+
pending addition. Actual backend/device availability and failures
|
|
44
|
+
come from the runtime, separately from hardware metadata or an installed archive.
|
|
45
|
+
Cancellation, invalid model input and inference failures retain their actual
|
|
46
|
+
errors. Model loading has no native cancellation handle before its constructor
|
|
47
|
+
returns; cancellation joins that call and releases its returned context before
|
|
48
|
+
the operation settles.
|
|
49
|
+
|
|
50
|
+
The complete prepared projection has an `id`, a `directory`, a `members` list
|
|
51
|
+
and `release()`. `resourcePaths` maps native model resource roles to exact
|
|
52
|
+
projection member paths. For the selected checkpoint, the role is `model`.
|
|
53
|
+
The member name must match the preparation result. See
|
|
54
|
+
[Model assets through Core](model-assets.md) for the shared preparation
|
|
55
|
+
lifecycle. Core acquires its own
|
|
56
|
+
retain before loading. The caller can release its preparation ownership after
|
|
57
|
+
successful `load()` acknowledgement: Core retains the files while its native
|
|
58
|
+
context needs them. Release caller ownership when loading fails too; native
|
|
59
|
+
cleanup keeps its own retain through actual completion. A later
|
|
60
|
+
`generate({model: 'sd14', ...})` reuses that prepared selection. Replacing or
|
|
61
|
+
unloading the context releases displaced projections
|
|
62
|
+
after actual native operations settle. Core shutdown joins owned operations
|
|
63
|
+
before releasing retained model files.
|
|
64
|
+
|
|
65
|
+
`model` also accepts a complete prepared model descriptor. Native callers can
|
|
66
|
+
supply actual resource paths directly. The browser projection arguments supply
|
|
67
|
+
the native paths through the shared asset service and preserve the remaining
|
|
68
|
+
descriptor fields. The image service creates no second model downloader or
|
|
69
|
+
durable model store.
|
|
70
|
+
|
|
71
|
+
## Browser accessor
|
|
72
|
+
|
|
73
|
+
```js
|
|
74
|
+
import {createCoreImageRuntime} from 'arcane-os/ai/core-image';
|
|
75
|
+
import {prepareCoreModelAssets} from 'arcane-os/ai/core-model-assets';
|
|
76
|
+
import {createBrowserModelSource, createDbopfsModelStore} from 'arcane-os/ai/browser-wasm';
|
|
77
|
+
|
|
78
|
+
const page = new AbortController();
|
|
79
|
+
window.addEventListener(
|
|
80
|
+
'pagehide',
|
|
81
|
+
function detachImagePage() {
|
|
82
|
+
page.abort();
|
|
83
|
+
},
|
|
84
|
+
{once: true}
|
|
85
|
+
);
|
|
86
|
+
|
|
87
|
+
const images = createCoreImageRuntime(
|
|
88
|
+
{signal: page.signal}
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
images.subscribe(
|
|
92
|
+
function showImageStatus(snapshot) {
|
|
93
|
+
statusElement.textContent = snapshot.status ?? snapshot.state;
|
|
94
|
+
}
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
const selected = (await images.inspect()).models.find(
|
|
98
|
+
function selectedCheckpoint(model) {
|
|
99
|
+
return model.id === 'sd14';
|
|
100
|
+
}
|
|
101
|
+
);
|
|
102
|
+
const resource = selected.resources.model;
|
|
103
|
+
const source = createBrowserModelSource(
|
|
104
|
+
{
|
|
105
|
+
id: selected.id,
|
|
106
|
+
files: [
|
|
107
|
+
{name: resource.filename, url: resource.url}
|
|
108
|
+
]
|
|
109
|
+
}
|
|
110
|
+
);
|
|
111
|
+
const store = createDbopfsModelStore(
|
|
112
|
+
{dbopfs, downloadConcurrency: 4}
|
|
113
|
+
);
|
|
114
|
+
const stored = await store.ensure(
|
|
115
|
+
source,
|
|
116
|
+
{signal: page.signal, offline: false}
|
|
117
|
+
);
|
|
118
|
+
const projection = await prepareCoreModelAssets(
|
|
119
|
+
{
|
|
120
|
+
workingDirectory: '.arcane/model-working',
|
|
121
|
+
members: source.files.map(
|
|
122
|
+
function preparedCheckpoint(member, index) {
|
|
123
|
+
return {path: member.name, file: stored.files[index]};
|
|
124
|
+
}
|
|
125
|
+
),
|
|
126
|
+
signal: page.signal
|
|
127
|
+
}
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
try {
|
|
131
|
+
await images.load(
|
|
132
|
+
{
|
|
133
|
+
model: 'sd14',
|
|
134
|
+
assetProjectionId: projection.id,
|
|
135
|
+
resourcePaths: {
|
|
136
|
+
model: 'sd-v1-4.ckpt'
|
|
137
|
+
},
|
|
138
|
+
signal: page.signal
|
|
139
|
+
}
|
|
140
|
+
);
|
|
141
|
+
} finally {
|
|
142
|
+
await projection.release();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const result = await images.generate(
|
|
146
|
+
{
|
|
147
|
+
model: 'sd14',
|
|
148
|
+
prompt: 'Portrait of a dignified raccoon astronaut holding a tiny moon cactus.',
|
|
149
|
+
signal: page.signal,
|
|
150
|
+
onProgress(progress) {
|
|
151
|
+
console.log(progress);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
for (const image of result.images) {
|
|
157
|
+
const url = URL.createObjectURL(image.blob);
|
|
158
|
+
const preview = new Image();
|
|
159
|
+
preview.alt = 'Generated raccoon astronaut portrait';
|
|
160
|
+
function releasePreviewURL() {
|
|
161
|
+
URL.revokeObjectURL(url);
|
|
162
|
+
}
|
|
163
|
+
preview.addEventListener(
|
|
164
|
+
'load',
|
|
165
|
+
releasePreviewURL,
|
|
166
|
+
{once: true}
|
|
167
|
+
);
|
|
168
|
+
preview.addEventListener(
|
|
169
|
+
'error',
|
|
170
|
+
releasePreviewURL,
|
|
171
|
+
{once: true}
|
|
172
|
+
);
|
|
173
|
+
preview.src = url;
|
|
174
|
+
imageContainer.append(preview);
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
The example uses the application's existing `dbopfs` instance and existing
|
|
179
|
+
`statusElement` and `imageContainer` elements. The published model store accepts
|
|
180
|
+
the selected checkpoint as an ordinary resource: `ensure()` returns complete
|
|
181
|
+
ordered `Blob` or `File` members after its resumable download. Range support
|
|
182
|
+
allows concurrent transfer within a member; those transfer parts remain the
|
|
183
|
+
store's concern. Use `offline: true` to require an already complete stored model.
|
|
184
|
+
The store does not select or run the image engine. In source development,
|
|
185
|
+
load the development Core bootstrap described
|
|
186
|
+
in [Local AI through Core](local-ai.md#development) before constructing the
|
|
187
|
+
accessor. Native hosts install their Core client through the existing host path.
|
|
188
|
+
An ordinary browser without Core reports local image generation as unavailable.
|
|
189
|
+
|
|
190
|
+
| Member | Contract |
|
|
191
|
+
|---|---|
|
|
192
|
+
| `createCoreImageRuntime({client?, signal?, onEvent?})` | Uses the installed Core client by default, subscribes to lifecycle events and asynchronously reads one initial status snapshot. It returns immediately. |
|
|
193
|
+
| `load({model, context?, assetProjectionId?, resourcePaths?, signal?})` | Loads the selected model and returns authoritative service state after native completion. `context` supplies native context options. |
|
|
194
|
+
| `generate({model, prompt, parameters?, assetProjectionId?, resourcePaths?, signal?, onProgress?})` | Returns the complete result with `images: [{blob, mediaType, width, height}]`. Additional runtime result metadata remains present. |
|
|
195
|
+
| `unload({signal?})` | Explicitly unloads the shared selected native context and returns its resulting state. |
|
|
196
|
+
| `inspect({signal?})` | Reads current Core status without waiting for model or runtime preparation. |
|
|
197
|
+
| `current()` | Synchronously returns the retained lifecycle snapshot and this accessor's transient request status. |
|
|
198
|
+
| `subscribe(listener, {replay = true, signal?})` | Observes `image.state` and immediately replays the current snapshot by default; returns an unsubscribe function. |
|
|
199
|
+
| `close()` | Cancels this accessor's requests, removes subscriptions and joins its request promises. It leaves the shared Core client and selected model under their existing owners. |
|
|
200
|
+
|
|
201
|
+
Generation publishes `status: 'Thinking'` synchronously when accepted, before
|
|
202
|
+
transport, preparation, model loading or inference waits. This remains transient
|
|
203
|
+
status until the complete image result arrives. Native sampling progress is
|
|
204
|
+
reported as progress, with no fabricated image chunks or conversation turns.
|
|
205
|
+
The accessor's `requests` records contain method, stream ID, status and progress;
|
|
206
|
+
they contain no saved prompt or image history.
|
|
207
|
+
|
|
208
|
+
The authoritative lifecycle snapshot includes `selectedModel`, `loaded`,
|
|
209
|
+
`state`, `backend`, `progress`, `error`, native resource paths and device state.
|
|
210
|
+
Service metadata adds runtime `id`, installation/availability state and selected
|
|
211
|
+
model definitions, including their resource metadata before engine preparation
|
|
212
|
+
finishes. Installed files do not imply a loaded model. A late status
|
|
213
|
+
response never overwrites a newer lifecycle event.
|
|
214
|
+
|
|
215
|
+
Prompts and parameters pass unchanged to the native runtime. Native option
|
|
216
|
+
names, supported model operations and defaults belong to that runtime and the
|
|
217
|
+
selected model definition. A prompt containing U+0000 or an unpaired UTF-16
|
|
218
|
+
surrogate cannot pass unchanged through the native UTF-8 C-string boundary and
|
|
219
|
+
reports that incompatibility.
|
|
220
|
+
|
|
221
|
+
The request signal propagates through Core to the owning native operation.
|
|
222
|
+
Aborted requests produce no late image result or progress callback. Request
|
|
223
|
+
invocations use `timeoutMs: 0`; there is no arbitrary image-generation timeout.
|
|
224
|
+
Errors retain their actual details on rejected promises and diagnostic events.
|
|
225
|
+
Applications choose concise ordinary status text and keep complete engineering
|
|
226
|
+
details in their diagnostic surface, outside durable chat history.
|
|
227
|
+
|
|
228
|
+
`onEvent` observes `image.state`, `image.progress`, `image.request`,
|
|
229
|
+
`image.result` and `image.error` records. Observer failures are reported to the
|
|
230
|
+
developer console and do not replace inference results. Observers own their
|
|
231
|
+
asynchronous callback lifetimes. The SDK persists no prompt, result, transcript
|
|
232
|
+
or application-owned image association.
|
|
233
|
+
|
|
234
|
+
## Core service and transport
|
|
235
|
+
|
|
236
|
+
`createLocalImageService(configuration, options)` is exported from
|
|
237
|
+
`arcane-os/core/image`. `configuration` is the selected `localAI` record.
|
|
238
|
+
Options are `appRoot`, installed `runtimes`, `signal`, `onEvent`, an optional
|
|
239
|
+
`prepare` callback and the existing `modelAssets` service instance.
|
|
240
|
+
|
|
241
|
+
`prepare({appRoot, configuration, requirement, signal, onEvent})` returns one
|
|
242
|
+
installed runtime record. Preparation runs once per service. That record carries
|
|
243
|
+
`libraryPath`, `bindingModulePath`, `backend`, `variants` and model definitions.
|
|
244
|
+
Runtime variants preserve their native backend, library path and runtime root.
|
|
245
|
+
The service retains one native runtime and subscribes to its current-state
|
|
246
|
+
replay. Expensive model execution belongs to that runtime's worker.
|
|
247
|
+
|
|
248
|
+
| Method/event | Parameters or result |
|
|
249
|
+
|---|---|
|
|
250
|
+
| `image.status` | Immediate lifecycle snapshot. |
|
|
251
|
+
| `image.load` | `{model, context?, assetProjectionId?, resourcePaths?}`; returns lifecycle snapshot. |
|
|
252
|
+
| `image.generate` | `{model, prompt, parameters?, streamId?, assetProjectionId?, resourcePaths?}`; returns complete encoded images. |
|
|
253
|
+
| `image.unload` | Empty parameters; returns lifecycle snapshot after unload. |
|
|
254
|
+
| `image.state` | Complete service lifecycle snapshot. Service and browser `subscribe()` provide current-state replay. |
|
|
255
|
+
| `image.progress` | `{streamId, requestId, status: 'Thinking', progress}` for the corresponding generation request. |
|
|
256
|
+
|
|
257
|
+
Core's JSON transport represents each complete PNG as
|
|
258
|
+
`{data, encoding: 'base64', mediaType: 'image/png', width, height}`. The browser
|
|
259
|
+
decodes that complete representation into a `Blob` and preserves its metadata.
|
|
260
|
+
There is no result resizing, clipping, storage write or application policy at
|
|
261
|
+
this transport boundary.
|
|
262
|
+
|
|
263
|
+
One service prepares one selected runtime, retains one selected native context
|
|
264
|
+
and performs one native generation per request. The native owner serializes
|
|
265
|
+
only competing operations on that context. Independent Core services and page
|
|
266
|
+
rendering remain concurrent. Native platform execution evidence and package
|
|
267
|
+
publication are separate from this source/API description.
|
|
@@ -42,13 +42,16 @@ try {
|
|
|
42
42
|
// Native loading takes its own retain handle before using these files.
|
|
43
43
|
await projection.release();
|
|
44
44
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
45
|
+
try {
|
|
46
|
+
const result = await image.generate({
|
|
47
|
+
model: 'sd14',
|
|
48
|
+
prompt: 'A dignified raccoon who has been put in charge of the moon.',
|
|
49
|
+
signal
|
|
50
|
+
});
|
|
51
|
+
// Display or save the complete returned image through the application owner.
|
|
52
|
+
} finally {
|
|
53
|
+
await image.unload();
|
|
54
|
+
}
|
|
52
55
|
```
|
|
53
56
|
|
|
54
57
|
This example assumes that the application selected the `sd14` model and its
|