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
|
@@ -6936,6 +6936,19 @@ no engine. The native routes of `AI.fetchTTS(payload, signal)` and
|
|
|
6936
6936
|
`AI.fetchSTT(audio, signal)` forward their signals through these calls and
|
|
6937
6937
|
normalize cancellation to `AbortError` with `ARCANE_AI_REQUEST_ABORTED`.
|
|
6938
6938
|
|
|
6939
|
+
`facade.window.setTheme(presentation, {signal}?)` forwards the complete
|
|
6940
|
+
presentation to the current native window, independently of OS-user appearance.
|
|
6941
|
+
Optional `backgroundColor` and `textColor` accept `{red,green,blue,alpha}` or
|
|
6942
|
+
`null` for a platform-default reset; omitted fields remain unchanged. The
|
|
6943
|
+
result is `{platform,supported,applied,unsupported}` with actual per-field
|
|
6944
|
+
acceptance, rather than a measured-pixel or atomic-update claim. Cancellation
|
|
6945
|
+
uses the existing request lifetime and does not undo settings already accepted.
|
|
6946
|
+
Shared ThemeBootstrap forwards the app's computed semantic colors without
|
|
6947
|
+
blocking rendering; applications using it need no duplicate listener. See the
|
|
6948
|
+
[current-window contract](core-client.md#current-window-theme) for channel
|
|
6949
|
+
values, errors, navigation, disposal and current host support. The facade does
|
|
6950
|
+
not add this operation to an older executable or an unsupported host.
|
|
6951
|
+
|
|
6939
6952
|
### Availability and normalization
|
|
6940
6953
|
|
|
6941
6954
|
**Browser and native WebView JavaScript hosts with a Core client.** Creating the
|
|
@@ -7402,6 +7415,25 @@ including accepted service work, without a forced termination deadline. Missing
|
|
|
7402
7415
|
prerequisites, download/extraction failures and host failures remain observable
|
|
7403
7416
|
through the operation's error/event boundary.
|
|
7404
7417
|
|
|
7418
|
+
During assembly, `appDescriptor.native.icon` selects the app's existing image.
|
|
7419
|
+
PNG and ICO inputs produce a derived `runtime/arcane-app.ico` and update only
|
|
7420
|
+
the copied application's executable icon. Source assets, the selected host
|
|
7421
|
+
cache, other executable resources and the Core executable remain unchanged.
|
|
7422
|
+
`manifest.host.icon` identifies the selected runtime icon, while
|
|
7423
|
+
`manifest.host.executableIcon` records whether embedding succeeded.
|
|
7424
|
+
|
|
7425
|
+
The builder's `onEvent` receives `native.icon.completed` after successful
|
|
7426
|
+
embedding or `native.icon.unsupported` when the selected format or executable
|
|
7427
|
+
layout cannot provide it. Both carry `target`, `appId`, `icon` and `executable`;
|
|
7428
|
+
the unsupported event also carries the original `code` and complete `message`.
|
|
7429
|
+
Unsupported embedding preserves ordinary assembly and the original icon path.
|
|
7430
|
+
Unreadable files or malformed supported image data retain their ordinary errors.
|
|
7431
|
+
The matching host loads the window/taskbar icon independently of page and Core
|
|
7432
|
+
startup. PNG/ICO embedding and runtime codec support are separate boundaries;
|
|
7433
|
+
see [application icon and window colors](core-native-packaging.md#application-icon-and-window-colors)
|
|
7434
|
+
for formats, per-window colors, errors and platform limits. npm publication
|
|
7435
|
+
alone does not establish delivery or execution of the matching native archive.
|
|
7436
|
+
|
|
7405
7437
|
### Example
|
|
7406
7438
|
|
|
7407
7439
|
```javascript
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "arcane-os",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.61.0",
|
|
4
4
|
"description": "Arcane OS JavaScript SDK, project-local CLI, browser runtime, and repository-portable application packager.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./src/index.mjs",
|
|
@@ -41,6 +41,8 @@
|
|
|
41
41
|
"./ai/browser-wasm": "./browser-runtime/ai/browser-wasm.mjs",
|
|
42
42
|
"./ai/core-local": "./browser-runtime/ai/core-local.mjs",
|
|
43
43
|
"./ai/core-onnx": "./browser-runtime/ai/core-onnx.mjs",
|
|
44
|
+
"./ai/core-image": "./browser-runtime/ai/core-image.mjs",
|
|
45
|
+
"./ai/core-model-assets": "./browser-runtime/ai/core-model-assets.mjs",
|
|
44
46
|
"./ai/browser-decisions": "./browser-runtime/ai/browser-decisions.mjs",
|
|
45
47
|
"./ai/tool-text-stream": "./browser-runtime/ai/tool-text-stream.mjs",
|
|
46
48
|
"./ai/twin-cloud": "./browser-runtime/ai/twin-cloud.mjs",
|
|
@@ -63,10 +65,13 @@
|
|
|
63
65
|
"./core/preferences": "./src/core/services/preferences.mjs",
|
|
64
66
|
"./core/diarization": "./src/diarization/service.mjs",
|
|
65
67
|
"./core/local-ai": "./src/core/services/local-ai.mjs",
|
|
68
|
+
"./core/image": "./src/core/services/image.mjs",
|
|
69
|
+
"./core/model-assets": "./src/core/services/model-assets.mjs",
|
|
66
70
|
"./diarization": "./src/diarization/index.mjs",
|
|
67
71
|
"./diarization/build": "./src/diarization/build.mjs",
|
|
68
72
|
"./local-ai": "./src/local-ai/install.mjs",
|
|
69
73
|
"./local-ai/onnx": "./src/local-ai/onnx.mjs",
|
|
74
|
+
"./local-ai/image": "./src/local-ai/image-runtime.mjs",
|
|
70
75
|
"./integrated-provider": "./src/integrated-provider-loader.mjs",
|
|
71
76
|
"./packager": "./src/packager/core.mjs",
|
|
72
77
|
"./release-bundle": "./src/release-bundle.mjs",
|
|
@@ -262,12 +262,12 @@
|
|
|
262
262
|
"type": "array",
|
|
263
263
|
"items": {
|
|
264
264
|
"oneOf": [
|
|
265
|
-
{ "enum": ["llama.cpp", "ollama", "onnx"] },
|
|
265
|
+
{ "enum": ["llama.cpp", "ollama", "onnx", "stable-diffusion.cpp"] },
|
|
266
266
|
{
|
|
267
267
|
"type": "object",
|
|
268
268
|
"required": ["id"],
|
|
269
269
|
"properties": {
|
|
270
|
-
"id": { "enum": ["llama.cpp", "ollama", "onnx"] },
|
|
270
|
+
"id": { "enum": ["llama.cpp", "ollama", "onnx", "stable-diffusion.cpp"] },
|
|
271
271
|
"version": { "type": "string" },
|
|
272
272
|
"url": { "type": "string" }
|
|
273
273
|
}
|
|
@@ -156,12 +156,12 @@
|
|
|
156
156
|
"type": "array",
|
|
157
157
|
"items": {
|
|
158
158
|
"oneOf": [
|
|
159
|
-
{ "enum": ["llama.cpp", "ollama", "onnx"] },
|
|
159
|
+
{ "enum": ["llama.cpp", "ollama", "onnx", "stable-diffusion.cpp"] },
|
|
160
160
|
{
|
|
161
161
|
"type": "object",
|
|
162
162
|
"required": ["id"],
|
|
163
163
|
"properties": {
|
|
164
|
-
"id": { "enum": ["llama.cpp", "ollama", "onnx"] },
|
|
164
|
+
"id": { "enum": ["llama.cpp", "ollama", "onnx", "stable-diffusion.cpp"] },
|
|
165
165
|
"version": { "type": "string" },
|
|
166
166
|
"url": { "type": "string" }
|
|
167
167
|
}
|
|
@@ -0,0 +1,563 @@
|
|
|
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.unload': function unloadImage(_parameters, request) {
|
|
538
|
+
const operationSignal = AbortSignal.any([lifetimeSignal, request.signal]);
|
|
539
|
+
return track(
|
|
540
|
+
(async function unloadSelectedImageModel() {
|
|
541
|
+
const selected = await selectedRuntime(operationSignal);
|
|
542
|
+
await selected.unload(
|
|
543
|
+
{signal: operationSignal}
|
|
544
|
+
);
|
|
545
|
+
operationSignal.throwIfAborted();
|
|
546
|
+
return current();
|
|
547
|
+
})(),
|
|
548
|
+
operationSignal
|
|
549
|
+
);
|
|
550
|
+
}
|
|
551
|
+
},
|
|
552
|
+
drain: close,
|
|
553
|
+
async dispose() {
|
|
554
|
+
try {
|
|
555
|
+
await close();
|
|
556
|
+
} finally {
|
|
557
|
+
events.dispose();
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
export default createLocalImageService;
|