arcane-os 0.60.0 → 0.62.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 +40 -0
- package/README.md +1 -1
- package/browser-runtime/ai/core-image.mjs +392 -0
- package/docs/reference/local-image-editing.md +102 -16
- package/docs/reference/local-image-generation.md +267 -0
- package/docs/reference/model-assets.md +14 -11
- 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 +605 -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 +358 -0
- package/src/local-ai/image-worker.mjs +232 -0
- package/src/local-ai/install.mjs +326 -10
- package/src/native/portable-layout.mjs +28 -2
|
@@ -0,0 +1,605 @@
|
|
|
1
|
+
import {Buffer} from 'node:buffer';
|
|
2
|
+
import {isDeepStrictEqual} from 'node:util';
|
|
3
|
+
import Is from 'strong-type';
|
|
4
|
+
import {createArcaneEventSource} from '../../event-manager.mjs';
|
|
5
|
+
import {CoreError, serializeCoreError} from '../../../browser-runtime/core/contracts.mjs';
|
|
6
|
+
import {normalizeLocalAIConfig} from '../../local-ai/config.mjs';
|
|
7
|
+
import {createImageRuntime} from '../../local-ai/image-runtime.mjs';
|
|
8
|
+
|
|
9
|
+
const is = new Is(false);
|
|
10
|
+
|
|
11
|
+
function failure(code, message) {
|
|
12
|
+
return new CoreError(
|
|
13
|
+
{code, message}
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function waitForPreparation(task, signal) {
|
|
18
|
+
signal.throwIfAborted();
|
|
19
|
+
return new Promise(
|
|
20
|
+
function awaitImagePreparation(resolve, reject) {
|
|
21
|
+
function cancelled() {
|
|
22
|
+
signal.removeEventListener('abort', cancelled);
|
|
23
|
+
reject(signal.reason);
|
|
24
|
+
}
|
|
25
|
+
signal.addEventListener(
|
|
26
|
+
'abort',
|
|
27
|
+
cancelled,
|
|
28
|
+
{once: true}
|
|
29
|
+
);
|
|
30
|
+
task.then(
|
|
31
|
+
function prepared(runtime) {
|
|
32
|
+
signal.removeEventListener('abort', cancelled);
|
|
33
|
+
if (signal.aborted) {
|
|
34
|
+
reject(signal.reason);
|
|
35
|
+
} else {
|
|
36
|
+
resolve(runtime);
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
function preparationFailed(error) {
|
|
40
|
+
signal.removeEventListener('abort', cancelled);
|
|
41
|
+
reject(error);
|
|
42
|
+
}
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** PNG encoding here belongs exclusively to the JSON Core transport. */
|
|
49
|
+
function encodeImage(image) {
|
|
50
|
+
const {data, ...metadata} = image;
|
|
51
|
+
return {
|
|
52
|
+
...metadata,
|
|
53
|
+
encoding: 'base64',
|
|
54
|
+
data: Buffer.from(data).toString('base64')
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** One selected native image runtime; applications own prompts and persistence. */
|
|
59
|
+
export function createLocalImageService(
|
|
60
|
+
configuration,
|
|
61
|
+
{appRoot, runtimes = [], signal, onEvent, prepare, modelAssets} = {}
|
|
62
|
+
) {
|
|
63
|
+
const config = normalizeLocalAIConfig(configuration) ?? {runtimes: []};
|
|
64
|
+
const requirement = config.runtimes.find(
|
|
65
|
+
function selectedImageRuntime(record) {
|
|
66
|
+
return record.id === 'stable-diffusion.cpp';
|
|
67
|
+
}
|
|
68
|
+
);
|
|
69
|
+
const lifetime = new AbortController();
|
|
70
|
+
const lifetimeSignal = signal ? AbortSignal.any([signal, lifetime.signal]) : lifetime.signal;
|
|
71
|
+
const owner = {};
|
|
72
|
+
const events = createArcaneEventSource(
|
|
73
|
+
owner,
|
|
74
|
+
{source: 'core-image', eventTypes: ['image.state']}
|
|
75
|
+
);
|
|
76
|
+
const jobs = new Set();
|
|
77
|
+
const projections = new Map();
|
|
78
|
+
const releases = new Set();
|
|
79
|
+
const releaseFailures = [];
|
|
80
|
+
let record = runtimes.find(
|
|
81
|
+
function installedImageRuntime(value) {
|
|
82
|
+
return value.id === 'stable-diffusion.cpp';
|
|
83
|
+
}
|
|
84
|
+
);
|
|
85
|
+
let state = {
|
|
86
|
+
id: 'stable-diffusion.cpp',
|
|
87
|
+
installed: Boolean(record?.libraryPath && record?.bindingModulePath),
|
|
88
|
+
available: false,
|
|
89
|
+
selectedModel: null,
|
|
90
|
+
loaded: false,
|
|
91
|
+
state: requirement ? 'created' : 'unavailable',
|
|
92
|
+
backend: record?.backend ?? null,
|
|
93
|
+
devices: [],
|
|
94
|
+
resources: {},
|
|
95
|
+
progress: null,
|
|
96
|
+
error: null,
|
|
97
|
+
models: record?.models ?? requirement?.models ?? []
|
|
98
|
+
};
|
|
99
|
+
let context;
|
|
100
|
+
let runtime;
|
|
101
|
+
let preparation;
|
|
102
|
+
let stopRuntimeSubscription;
|
|
103
|
+
let closing;
|
|
104
|
+
let closed = false;
|
|
105
|
+
|
|
106
|
+
function current() {
|
|
107
|
+
return {...state, closed};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function publish() {
|
|
111
|
+
const snapshot = current();
|
|
112
|
+
events.dispatch('image.state', snapshot);
|
|
113
|
+
context?.emit('image.state', snapshot);
|
|
114
|
+
return snapshot;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function acceptRuntimeState(value) {
|
|
118
|
+
state = {
|
|
119
|
+
...state,
|
|
120
|
+
...value,
|
|
121
|
+
available: value.available ?? (!value.closed && value.state !== 'error'),
|
|
122
|
+
installed: Boolean(record?.libraryPath && record?.bindingModulePath),
|
|
123
|
+
models: record?.models ?? requirement?.models ?? []
|
|
124
|
+
};
|
|
125
|
+
if (closing) {
|
|
126
|
+
state.available = false;
|
|
127
|
+
state.state = closed ? 'closed' : 'closing';
|
|
128
|
+
}
|
|
129
|
+
publish();
|
|
130
|
+
releaseUnusedProjections();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function diagnostic(event) {
|
|
134
|
+
if (!onEvent) {
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
function reportDiagnosticFailure(error) {
|
|
138
|
+
console.error('Local image diagnostic observer failed.', error);
|
|
139
|
+
}
|
|
140
|
+
try {
|
|
141
|
+
Promise.resolve(onEvent(event)).catch(reportDiagnosticFailure);
|
|
142
|
+
} catch (error) {
|
|
143
|
+
reportDiagnosticFailure(error);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function notifyListener(listener, value) {
|
|
148
|
+
const result = listener(value);
|
|
149
|
+
if (result && is.function(result.then)) {
|
|
150
|
+
Promise.resolve(result).catch(
|
|
151
|
+
function reportImageListenerFailure(error) {
|
|
152
|
+
console.error('Local image state listener failed.', error);
|
|
153
|
+
}
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function preparationProgress(event) {
|
|
159
|
+
if (!closing) {
|
|
160
|
+
state = {...state, progress: event};
|
|
161
|
+
publish();
|
|
162
|
+
}
|
|
163
|
+
diagnostic(event);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function beginPreparation() {
|
|
167
|
+
if (preparation || !requirement || closing) {
|
|
168
|
+
return preparation;
|
|
169
|
+
}
|
|
170
|
+
state = {...state, state: 'preparing', error: null};
|
|
171
|
+
preparation = Promise.resolve().then(
|
|
172
|
+
async function prepareSelectedImageRuntime() {
|
|
173
|
+
lifetimeSignal.throwIfAborted();
|
|
174
|
+
if (prepare) {
|
|
175
|
+
record = await prepare(
|
|
176
|
+
{
|
|
177
|
+
appRoot,
|
|
178
|
+
configuration: config,
|
|
179
|
+
requirement,
|
|
180
|
+
signal: lifetimeSignal,
|
|
181
|
+
onEvent: preparationProgress
|
|
182
|
+
}
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
lifetimeSignal.throwIfAborted();
|
|
186
|
+
if (!record?.libraryPath || !record?.bindingModulePath) {
|
|
187
|
+
throw failure(
|
|
188
|
+
'LOCAL_AI_RUNTIME_UNAVAILABLE',
|
|
189
|
+
'The selected local image runtime has not been prepared.'
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
runtime = createImageRuntime(
|
|
193
|
+
{
|
|
194
|
+
libraryPath: record.libraryPath,
|
|
195
|
+
bindingModulePath: record.bindingModulePath,
|
|
196
|
+
models: record.models ?? [],
|
|
197
|
+
backend: record.backend,
|
|
198
|
+
variants: record.variants,
|
|
199
|
+
signal: lifetimeSignal,
|
|
200
|
+
onEvent: diagnostic
|
|
201
|
+
}
|
|
202
|
+
);
|
|
203
|
+
stopRuntimeSubscription = runtime.subscribe(
|
|
204
|
+
acceptRuntimeState,
|
|
205
|
+
{replay: true}
|
|
206
|
+
);
|
|
207
|
+
return runtime;
|
|
208
|
+
}
|
|
209
|
+
);
|
|
210
|
+
preparation.catch(
|
|
211
|
+
function preparationFailed(error) {
|
|
212
|
+
state = {
|
|
213
|
+
...state,
|
|
214
|
+
installed: Boolean(record?.libraryPath && record?.bindingModulePath),
|
|
215
|
+
available: false,
|
|
216
|
+
state: lifetimeSignal.aborted ? 'closing' : 'error',
|
|
217
|
+
error: lifetimeSignal.aborted ? null : serializeCoreError(error)
|
|
218
|
+
};
|
|
219
|
+
publish();
|
|
220
|
+
}
|
|
221
|
+
);
|
|
222
|
+
publish();
|
|
223
|
+
return preparation;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async function selectedRuntime(operationSignal) {
|
|
227
|
+
operationSignal.throwIfAborted();
|
|
228
|
+
if (!requirement) {
|
|
229
|
+
throw failure(
|
|
230
|
+
'LOCAL_AI_RUNTIME_NOT_SELECTED',
|
|
231
|
+
'stable-diffusion.cpp is not selected in this application configuration.'
|
|
232
|
+
);
|
|
233
|
+
}
|
|
234
|
+
const selected = await waitForPreparation(beginPreparation(), operationSignal);
|
|
235
|
+
operationSignal.throwIfAborted();
|
|
236
|
+
return selected;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
async function projectedModel(model, assetProjectionId, resourcePaths, operationSignal) {
|
|
240
|
+
if (assetProjectionId === undefined) {
|
|
241
|
+
return model;
|
|
242
|
+
}
|
|
243
|
+
if (!modelAssets) {
|
|
244
|
+
throw failure(
|
|
245
|
+
'LOCAL_AI_IMAGE_ASSETS_UNAVAILABLE',
|
|
246
|
+
'This Core has no model asset projection service.'
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
let projection = projections.get(assetProjectionId);
|
|
250
|
+
if (!projection) {
|
|
251
|
+
projection = {id: assetProjectionId, lease: null, task: null, models: new Map()};
|
|
252
|
+
projection.task = Promise.resolve().then(
|
|
253
|
+
async function retainModelAssets() {
|
|
254
|
+
projection.lease = await modelAssets.retain(assetProjectionId);
|
|
255
|
+
return projection.lease;
|
|
256
|
+
}
|
|
257
|
+
);
|
|
258
|
+
projections.set(assetProjectionId, projection);
|
|
259
|
+
}
|
|
260
|
+
const lease = await projection.task;
|
|
261
|
+
operationSignal.throwIfAborted();
|
|
262
|
+
if (!resourcePaths || !is.object(resourcePaths) || is.array(resourcePaths)) {
|
|
263
|
+
throw new TypeError('Projected image models require resourcePaths mapping native roles to projection members.');
|
|
264
|
+
}
|
|
265
|
+
const entries = Object.entries(resourcePaths);
|
|
266
|
+
const modelId = is.string(model) ? model : model?.id;
|
|
267
|
+
const cached = projection.models.get(modelId);
|
|
268
|
+
if (cached && isDeepStrictEqual(cached.model, model)
|
|
269
|
+
&& Object.keys(cached.resourcePaths).length === entries.length
|
|
270
|
+
&& entries.every(
|
|
271
|
+
function matchingResource([role, memberPath]) {
|
|
272
|
+
return cached.resourcePaths[role] === memberPath;
|
|
273
|
+
}
|
|
274
|
+
)) {
|
|
275
|
+
return cached.prepared;
|
|
276
|
+
}
|
|
277
|
+
const definition = is.string(model)
|
|
278
|
+
? record.models?.find(
|
|
279
|
+
function selectedModelDefinition(value) {
|
|
280
|
+
return value.id === model;
|
|
281
|
+
}
|
|
282
|
+
) : model;
|
|
283
|
+
if (!definition) {
|
|
284
|
+
throw failure(
|
|
285
|
+
'LOCAL_AI_IMAGE_MODEL_UNAVAILABLE',
|
|
286
|
+
`The selected image model ${String(model)} has no model definition.`
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
const resources = {...definition.resources};
|
|
290
|
+
for (const [role, memberPath] of entries) {
|
|
291
|
+
const member = lease.members.find(
|
|
292
|
+
function selectedProjectionMember(value) {
|
|
293
|
+
return value.path === memberPath;
|
|
294
|
+
}
|
|
295
|
+
);
|
|
296
|
+
if (!member) {
|
|
297
|
+
throw failure(
|
|
298
|
+
'LOCAL_AI_IMAGE_RESOURCE_UNAVAILABLE',
|
|
299
|
+
`The retained model projection has no member ${String(memberPath)} for ${role}.`
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
resources[role] = member.nativePath;
|
|
303
|
+
}
|
|
304
|
+
const prepared = {...definition, resources};
|
|
305
|
+
projection.models.set(
|
|
306
|
+
modelId,
|
|
307
|
+
{model, resourcePaths: {...resourcePaths}, prepared}
|
|
308
|
+
);
|
|
309
|
+
return prepared;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function releaseUnusedProjections(all = false) {
|
|
313
|
+
// Native calls share one context. Retain every provisional projection
|
|
314
|
+
// until those calls settle, then compare with the actual loaded inputs.
|
|
315
|
+
if (jobs.size > 0) {
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
const resources = all ? [] : Object.values(runtime?.current().resources ?? {});
|
|
319
|
+
for (const [id, projection] of projections) {
|
|
320
|
+
const active = projection.lease?.members.some(
|
|
321
|
+
function activeNativeResource(member) {
|
|
322
|
+
return resources.includes(member.nativePath);
|
|
323
|
+
}
|
|
324
|
+
);
|
|
325
|
+
if (active) {
|
|
326
|
+
continue;
|
|
327
|
+
}
|
|
328
|
+
projections.delete(id);
|
|
329
|
+
if (!projection.lease) {
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
const task = Promise.resolve().then(
|
|
333
|
+
function releaseModelProjection() {
|
|
334
|
+
return projection.lease.release();
|
|
335
|
+
}
|
|
336
|
+
);
|
|
337
|
+
releases.add(task);
|
|
338
|
+
task.then(
|
|
339
|
+
function projectionReleased() {
|
|
340
|
+
releases.delete(task);
|
|
341
|
+
},
|
|
342
|
+
function projectionReleaseFailed(error) {
|
|
343
|
+
releases.delete(task);
|
|
344
|
+
releaseFailures.push(error);
|
|
345
|
+
console.error('Local image model projection release failed.', error);
|
|
346
|
+
}
|
|
347
|
+
);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function track(task, operationSignal) {
|
|
352
|
+
jobs.add(task);
|
|
353
|
+
function settled() {
|
|
354
|
+
jobs.delete(task);
|
|
355
|
+
releaseUnusedProjections();
|
|
356
|
+
}
|
|
357
|
+
task.then(
|
|
358
|
+
settled,
|
|
359
|
+
function imageOperationFailed(error) {
|
|
360
|
+
if (!closing && !operationSignal.aborted) {
|
|
361
|
+
state = {...state, error: serializeCoreError(error)};
|
|
362
|
+
publish();
|
|
363
|
+
}
|
|
364
|
+
settled();
|
|
365
|
+
}
|
|
366
|
+
);
|
|
367
|
+
return task;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function subscribe(listener, {replay = true, signal: subscriptionSignal} = {}) {
|
|
371
|
+
if (!is.function(listener)) {
|
|
372
|
+
throw new TypeError('An image state listener must be a function.');
|
|
373
|
+
}
|
|
374
|
+
if (closed) {
|
|
375
|
+
if (replay && !subscriptionSignal?.aborted) {
|
|
376
|
+
notifyListener(listener, current());
|
|
377
|
+
}
|
|
378
|
+
return function closedSubscription() {};
|
|
379
|
+
}
|
|
380
|
+
const unsubscribe = events.on(
|
|
381
|
+
'image.state',
|
|
382
|
+
function imageStateChanged(event) {
|
|
383
|
+
notifyListener(listener, event.detail);
|
|
384
|
+
},
|
|
385
|
+
{signal: subscriptionSignal}
|
|
386
|
+
);
|
|
387
|
+
try {
|
|
388
|
+
if (replay && !subscriptionSignal?.aborted) {
|
|
389
|
+
notifyListener(listener, current());
|
|
390
|
+
}
|
|
391
|
+
} catch (error) {
|
|
392
|
+
unsubscribe();
|
|
393
|
+
throw error;
|
|
394
|
+
}
|
|
395
|
+
return unsubscribe;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function close() {
|
|
399
|
+
if (closing) {
|
|
400
|
+
return closing;
|
|
401
|
+
}
|
|
402
|
+
closing = Promise.resolve().then(
|
|
403
|
+
async function closeImageService() {
|
|
404
|
+
const runtimeClosing = Promise.resolve().then(
|
|
405
|
+
function closeNativeImageRuntime() {
|
|
406
|
+
return runtime?.close();
|
|
407
|
+
}
|
|
408
|
+
);
|
|
409
|
+
const results = await Promise.allSettled(
|
|
410
|
+
[runtimeClosing, ...jobs, ...(preparation ? [preparation] : [])]
|
|
411
|
+
);
|
|
412
|
+
try {
|
|
413
|
+
const errors = [];
|
|
414
|
+
if (results[0].status === 'rejected') {
|
|
415
|
+
errors.push(results[0].reason);
|
|
416
|
+
}
|
|
417
|
+
// Native close joins unload or worker exit even when it
|
|
418
|
+
// rejects. Its files can now complete their own cleanup.
|
|
419
|
+
try {
|
|
420
|
+
releaseUnusedProjections(true);
|
|
421
|
+
} catch (error) {
|
|
422
|
+
errors.push(error);
|
|
423
|
+
}
|
|
424
|
+
await Promise.allSettled([...releases]);
|
|
425
|
+
errors.push(...releaseFailures);
|
|
426
|
+
if (errors.length === 1) {
|
|
427
|
+
throw errors[0];
|
|
428
|
+
}
|
|
429
|
+
if (errors.length > 1) {
|
|
430
|
+
throw new AggregateError(
|
|
431
|
+
errors,
|
|
432
|
+
'Local image service cleanup failed.',
|
|
433
|
+
{cause: errors[0]}
|
|
434
|
+
);
|
|
435
|
+
}
|
|
436
|
+
} catch (error) {
|
|
437
|
+
state = {...state, error: serializeCoreError(error)};
|
|
438
|
+
throw error;
|
|
439
|
+
} finally {
|
|
440
|
+
stopRuntimeSubscription?.();
|
|
441
|
+
lifetimeSignal.removeEventListener('abort', abortService);
|
|
442
|
+
closed = true;
|
|
443
|
+
state = {...state, available: false, state: 'closed'};
|
|
444
|
+
publish();
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
);
|
|
448
|
+
lifetime.abort();
|
|
449
|
+
state = {...state, available: false, state: 'closing'};
|
|
450
|
+
publish();
|
|
451
|
+
return closing;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function abortService() {
|
|
455
|
+
close().catch(
|
|
456
|
+
function reportImageShutdownFailure(error) {
|
|
457
|
+
console.error('Local image service shutdown failed.', error);
|
|
458
|
+
}
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
lifetimeSignal.addEventListener(
|
|
463
|
+
'abort',
|
|
464
|
+
abortService,
|
|
465
|
+
{once: true}
|
|
466
|
+
);
|
|
467
|
+
|
|
468
|
+
return {
|
|
469
|
+
name: 'image',
|
|
470
|
+
current,
|
|
471
|
+
subscribe,
|
|
472
|
+
start(currentContext) {
|
|
473
|
+
context = currentContext;
|
|
474
|
+
if (lifetimeSignal.aborted) {
|
|
475
|
+
abortService();
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
beginPreparation();
|
|
479
|
+
publish();
|
|
480
|
+
},
|
|
481
|
+
methods: {
|
|
482
|
+
'image.status': current,
|
|
483
|
+
'image.load': function loadImage(
|
|
484
|
+
{model, context: modelContext, assetProjectionId, resourcePaths} = {},
|
|
485
|
+
request
|
|
486
|
+
) {
|
|
487
|
+
const operationSignal = AbortSignal.any([lifetimeSignal, request.signal]);
|
|
488
|
+
return track(
|
|
489
|
+
(async function loadSelectedImageModel() {
|
|
490
|
+
const selected = await selectedRuntime(operationSignal);
|
|
491
|
+
const prepared = await projectedModel(model, assetProjectionId, resourcePaths, operationSignal);
|
|
492
|
+
await selected.load(
|
|
493
|
+
{model: prepared, context: modelContext, signal: operationSignal}
|
|
494
|
+
);
|
|
495
|
+
operationSignal.throwIfAborted();
|
|
496
|
+
return current();
|
|
497
|
+
})(),
|
|
498
|
+
operationSignal
|
|
499
|
+
);
|
|
500
|
+
},
|
|
501
|
+
'image.generate': function generateImage(
|
|
502
|
+
{model, prompt, parameters, streamId, assetProjectionId, resourcePaths} = {},
|
|
503
|
+
request
|
|
504
|
+
) {
|
|
505
|
+
const operationSignal = AbortSignal.any([lifetimeSignal, request.signal]);
|
|
506
|
+
operationSignal.throwIfAborted();
|
|
507
|
+
request.emit(
|
|
508
|
+
'image.progress',
|
|
509
|
+
{streamId, requestId: request.requestId, status: 'Thinking', progress: {phase: 'accepted'}}
|
|
510
|
+
);
|
|
511
|
+
return track(
|
|
512
|
+
(async function generateSelectedImage() {
|
|
513
|
+
const selected = await selectedRuntime(operationSignal);
|
|
514
|
+
const prepared = await projectedModel(model, assetProjectionId, resourcePaths, operationSignal);
|
|
515
|
+
const result = await selected.generate(
|
|
516
|
+
{
|
|
517
|
+
model: prepared,
|
|
518
|
+
prompt,
|
|
519
|
+
parameters,
|
|
520
|
+
signal: operationSignal,
|
|
521
|
+
onProgress(progress) {
|
|
522
|
+
if (!operationSignal.aborted) {
|
|
523
|
+
request.emit(
|
|
524
|
+
'image.progress',
|
|
525
|
+
{streamId, requestId: request.requestId, status: 'Thinking', progress}
|
|
526
|
+
);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
);
|
|
531
|
+
operationSignal.throwIfAborted();
|
|
532
|
+
return {...result, images: result.images.map(encodeImage)};
|
|
533
|
+
})(),
|
|
534
|
+
operationSignal
|
|
535
|
+
);
|
|
536
|
+
},
|
|
537
|
+
'image.edit': function editImage(
|
|
538
|
+
{model, image, prompt, strength, parameters, streamId, assetProjectionId, resourcePaths} = {},
|
|
539
|
+
request
|
|
540
|
+
) {
|
|
541
|
+
const operationSignal = AbortSignal.any([lifetimeSignal, request.signal]);
|
|
542
|
+
operationSignal.throwIfAborted();
|
|
543
|
+
request.emit(
|
|
544
|
+
'image.progress',
|
|
545
|
+
{streamId, requestId: request.requestId, status: 'Thinking', progress: {phase: 'accepted'}}
|
|
546
|
+
);
|
|
547
|
+
return track(
|
|
548
|
+
(async function editSelectedImage() {
|
|
549
|
+
if (image?.encoding !== 'base64' || !is.string(image.data)) {
|
|
550
|
+
throw new TypeError('Core image input requires complete base64 transport encoding.');
|
|
551
|
+
}
|
|
552
|
+
const input = {data: Buffer.from(image.data, 'base64'), mediaType: image.mediaType};
|
|
553
|
+
const selected = await selectedRuntime(operationSignal);
|
|
554
|
+
const prepared = await projectedModel(model, assetProjectionId, resourcePaths, operationSignal);
|
|
555
|
+
const result = await selected.edit(
|
|
556
|
+
{
|
|
557
|
+
model: prepared,
|
|
558
|
+
image: input,
|
|
559
|
+
prompt,
|
|
560
|
+
strength,
|
|
561
|
+
parameters,
|
|
562
|
+
signal: operationSignal,
|
|
563
|
+
onProgress(progress) {
|
|
564
|
+
if (!operationSignal.aborted) {
|
|
565
|
+
request.emit(
|
|
566
|
+
'image.progress',
|
|
567
|
+
{streamId, requestId: request.requestId, status: 'Thinking', progress}
|
|
568
|
+
);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
);
|
|
573
|
+
operationSignal.throwIfAborted();
|
|
574
|
+
return {...result, images: result.images.map(encodeImage)};
|
|
575
|
+
})(),
|
|
576
|
+
operationSignal
|
|
577
|
+
);
|
|
578
|
+
},
|
|
579
|
+
'image.unload': function unloadImage(_parameters, request) {
|
|
580
|
+
const operationSignal = AbortSignal.any([lifetimeSignal, request.signal]);
|
|
581
|
+
return track(
|
|
582
|
+
(async function unloadSelectedImageModel() {
|
|
583
|
+
const selected = await selectedRuntime(operationSignal);
|
|
584
|
+
await selected.unload(
|
|
585
|
+
{signal: operationSignal}
|
|
586
|
+
);
|
|
587
|
+
operationSignal.throwIfAborted();
|
|
588
|
+
return current();
|
|
589
|
+
})(),
|
|
590
|
+
operationSignal
|
|
591
|
+
);
|
|
592
|
+
}
|
|
593
|
+
},
|
|
594
|
+
drain: close,
|
|
595
|
+
async dispose() {
|
|
596
|
+
try {
|
|
597
|
+
await close();
|
|
598
|
+
} finally {
|
|
599
|
+
events.dispose();
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
export default createLocalImageService;
|
|
@@ -121,7 +121,7 @@ function collectCompletion(completion, chunk) {
|
|
|
121
121
|
}
|
|
122
122
|
|
|
123
123
|
/** Core owns native processes; applications own runtime and model selection. */
|
|
124
|
-
export function createLocalAIService(configuration, {appRoot, runtimes = [], signal, onEvent} = {}) {
|
|
124
|
+
export function createLocalAIService(configuration, {appRoot, runtimes = [], signal, onEvent, prepare} = {}) {
|
|
125
125
|
const config = normalizeLocalAIConfig(configuration) ?? {runtimes: []};
|
|
126
126
|
const lifetime = new AbortController();
|
|
127
127
|
const lifetimeSignal = signal ? AbortSignal.any([signal, lifetime.signal]) : lifetime.signal;
|
|
@@ -136,7 +136,7 @@ export function createLocalAIService(configuration, {appRoot, runtimes = [], sig
|
|
|
136
136
|
let onnxRecoveryError;
|
|
137
137
|
let stopONNXSubscription;
|
|
138
138
|
const onnxSelected = config.runtimes.some(function selectedONNX(requirement) { return requirement.id === 'onnx'; });
|
|
139
|
-
|
|
139
|
+
let onnxRecord = runtimes.find(function installedONNX(runtime) { return runtime.id === 'onnx'; });
|
|
140
140
|
|
|
141
141
|
function onnxStatus() {
|
|
142
142
|
const current = onnx?.current();
|
|
@@ -493,8 +493,13 @@ export function createLocalAIService(configuration, {appRoot, runtimes = [], sig
|
|
|
493
493
|
const service = {
|
|
494
494
|
name: 'local-ai',
|
|
495
495
|
current: snapshot,
|
|
496
|
-
start(currentContext) {
|
|
496
|
+
async start(currentContext) {
|
|
497
497
|
context = currentContext;
|
|
498
|
+
if (prepare) {
|
|
499
|
+
runtimes = await prepare({signal: lifetimeSignal, onEvent});
|
|
500
|
+
lifetimeSignal.throwIfAborted();
|
|
501
|
+
onnxRecord = runtimes.find(function installedONNX(runtime) { return runtime.id === 'onnx'; });
|
|
502
|
+
}
|
|
498
503
|
for (const requirement of config.runtimes) {
|
|
499
504
|
if (!['llama.cpp', 'ollama', 'onnx'].includes(requirement.id)) {
|
|
500
505
|
throw failure('LOCAL_AI_RUNTIME_UNAVAILABLE', `Core runtime ${requirement.id} is not available in this service.`);
|
package/src/import-map.mjs
CHANGED
|
@@ -51,6 +51,8 @@ const SDK_BROWSER_SELF_IMPORTS=new Map([
|
|
|
51
51
|
['arcane-os/ai/browser-wasm',SDK_BROWSER_AI_ENTRY],
|
|
52
52
|
['arcane-os/ai/core-local','sdk/ai/core-local.mjs'],
|
|
53
53
|
['arcane-os/ai/core-onnx','sdk/ai/core-onnx.mjs'],
|
|
54
|
+
['arcane-os/ai/core-image','sdk/ai/core-image.mjs'],
|
|
55
|
+
['arcane-os/ai/core-model-assets','sdk/ai/core-model-assets.mjs'],
|
|
54
56
|
['arcane-os/ai/browser-decisions','sdk/ai/browser-decisions.mjs'],
|
|
55
57
|
['arcane-os/ai/tool-text-stream','sdk/ai/tool-text-stream.mjs'],
|
|
56
58
|
['arcane-os/ai/twin-cloud','sdk/ai/twin-cloud.mjs'],
|