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
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,45 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.62.0
|
|
4
|
+
|
|
5
|
+
- Add whole-image PNG editing through the existing local image runtime, Core
|
|
6
|
+
service and browser accessor: `edit({model, image, prompt, strength, ...})`
|
|
7
|
+
and `image.edit` share the retained generation context and lifecycle.
|
|
8
|
+
- Preserve complete original PNG input and prompts through lossless transport,
|
|
9
|
+
copy queued native input, and pass explicit finite strength without clamping.
|
|
10
|
+
Return a separate complete PNG result; applications own persistence.
|
|
11
|
+
- Preserve synchronous Thinking, progress, ordered context work, cancellation,
|
|
12
|
+
unload and close. SD1.4 now advertises `img2img` beside `txt2img`.
|
|
13
|
+
- Support static non-interlaced 8-bit RGB/RGBA PNG input using the existing
|
|
14
|
+
worker decoder. Unsupported formats report their actual limitation; model
|
|
15
|
+
cropping, resizing and alpha behavior remain explicit in the editing guide.
|
|
16
|
+
- Reuse the approved runtime and model workflow with no dependency change.
|
|
17
|
+
This increment does not add masks, reference editing, new models or GPU
|
|
18
|
+
installation, and does not claim exact visual instruction adherence.
|
|
19
|
+
|
|
20
|
+
## 0.61.0
|
|
21
|
+
|
|
22
|
+
- Add retained local image generation through Core, with public native runtime,
|
|
23
|
+
Core service and browser accessor entrypoints. Applications keep ownership of
|
|
24
|
+
prompts, styles, saved images and associations.
|
|
25
|
+
- Select the approved stable-diffusion.cpp runtime and Stable Diffusion 1.4
|
|
26
|
+
checkpoint through existing local-AI configuration. Runtime installation and
|
|
27
|
+
model storage/preparation remain separate; model setup uses the existing
|
|
28
|
+
DBOPFS model store and a retained Core working-file projection.
|
|
29
|
+
- Preserve complete prompts and PNG results, synchronous transient Thinking
|
|
30
|
+
acknowledgement, observable model state and progress, actual cancellation
|
|
31
|
+
joining, and native context/file cleanup. Preparation does not block page
|
|
32
|
+
rendering or unrelated Core services.
|
|
33
|
+
- Expose model-assets preparation and service entrypoints alongside image
|
|
34
|
+
generation. Native contexts retain working files until actual unload; the
|
|
35
|
+
browser accessor closes only its own requests and subscriptions.
|
|
36
|
+
- Built-in distributions select CPU on Windows and Linux and the same upstream
|
|
37
|
+
Metal/CPU archive on macOS. Windows CUDA preparation remains separate pending
|
|
38
|
+
work; publication does not claim GPU-first Windows or PM execution.
|
|
39
|
+
- Keep optional engine/binding acquisition in the selected application's
|
|
40
|
+
existing installer workflow. No engine binaries or model checkpoint are added
|
|
41
|
+
to the npm package, and its declared dependency tree is unchanged.
|
|
42
|
+
|
|
3
43
|
## 0.60.0
|
|
4
44
|
|
|
5
45
|
- 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.62.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,392 @@
|
|
|
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
|
+
/** Preserve the complete encoded input at the JSON transport boundary. */
|
|
34
|
+
async function encodeImage(image, signal) {
|
|
35
|
+
if (!(image instanceof Blob)) {
|
|
36
|
+
throw new TypeError('Image editing requires the original PNG Blob or File.');
|
|
37
|
+
}
|
|
38
|
+
const content = new Uint8Array(await image.arrayBuffer());
|
|
39
|
+
signal.throwIfAborted();
|
|
40
|
+
let data;
|
|
41
|
+
if (is.function(content.toBase64)) {
|
|
42
|
+
data = content.toBase64();
|
|
43
|
+
} else {
|
|
44
|
+
let binary = '';
|
|
45
|
+
for (const value of content) binary += String.fromCharCode(value);
|
|
46
|
+
data = globalThis.btoa(binary);
|
|
47
|
+
}
|
|
48
|
+
return {data, encoding: 'base64', mediaType: image.type};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Browser access to the selected native image service; no result persistence. */
|
|
52
|
+
export function createCoreImageRuntime(
|
|
53
|
+
{client = getInstalledCoreClient(), signal, onEvent} = {}
|
|
54
|
+
) {
|
|
55
|
+
const owner = {};
|
|
56
|
+
const events = createArcaneEventSource(
|
|
57
|
+
owner,
|
|
58
|
+
{source: 'core-image', eventTypes: ['image.state']}
|
|
59
|
+
);
|
|
60
|
+
const lifetime = new AbortController();
|
|
61
|
+
const lifetimeSignal = signal ? AbortSignal.any([signal, lifetime.signal]) : lifetime.signal;
|
|
62
|
+
const pending = new Set();
|
|
63
|
+
let state = {
|
|
64
|
+
id: 'stable-diffusion.cpp',
|
|
65
|
+
installed: false,
|
|
66
|
+
available: false,
|
|
67
|
+
selectedModel: null,
|
|
68
|
+
loaded: false,
|
|
69
|
+
state: client ? 'unknown' : 'unavailable',
|
|
70
|
+
backend: null,
|
|
71
|
+
devices: [],
|
|
72
|
+
resources: {},
|
|
73
|
+
progress: null,
|
|
74
|
+
error: null,
|
|
75
|
+
models: []
|
|
76
|
+
};
|
|
77
|
+
let revision = 0;
|
|
78
|
+
let closed = false;
|
|
79
|
+
let closing;
|
|
80
|
+
|
|
81
|
+
function current() {
|
|
82
|
+
const requests = [...pending].map(
|
|
83
|
+
function requestState(request) {
|
|
84
|
+
return {
|
|
85
|
+
method: request.method,
|
|
86
|
+
streamId: request.streamId,
|
|
87
|
+
status: request.status,
|
|
88
|
+
progress: request.progress
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
);
|
|
92
|
+
return {
|
|
93
|
+
...state,
|
|
94
|
+
requests,
|
|
95
|
+
status: !closed && requests.some(
|
|
96
|
+
function generationPending(request) {
|
|
97
|
+
return request.status === 'Thinking';
|
|
98
|
+
}
|
|
99
|
+
) ? 'Thinking' : null,
|
|
100
|
+
busy: pending.size > 0,
|
|
101
|
+
closed
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function reportObserverFailure(error) {
|
|
106
|
+
globalThis.console?.error('Core image observer failed.', error);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function notifyListener(listener, value) {
|
|
110
|
+
const result = listener(value);
|
|
111
|
+
if (result && is.function(result.then)) {
|
|
112
|
+
// Observers own their callback lifetimes and may await close().
|
|
113
|
+
Promise.resolve(result).catch(reportObserverFailure);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function observe(callback, value) {
|
|
118
|
+
if (!is.function(callback)) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
try {
|
|
122
|
+
notifyListener(callback, value);
|
|
123
|
+
} catch (error) {
|
|
124
|
+
reportObserverFailure(error);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function publish() {
|
|
129
|
+
const snapshot = current();
|
|
130
|
+
events.dispatch('image.state', snapshot);
|
|
131
|
+
if (snapshot.closed === closed) {
|
|
132
|
+
observe(
|
|
133
|
+
onEvent,
|
|
134
|
+
{type: 'image.state', data: snapshot}
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
return snapshot;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function accept(value) {
|
|
141
|
+
if (closed) {
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
state = value;
|
|
145
|
+
revision += 1;
|
|
146
|
+
publish();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const unsubscribe = client?.events.on('image.state', accept)
|
|
150
|
+
?? function noCoreSubscription() {};
|
|
151
|
+
|
|
152
|
+
function invoke(method, parameters, {signal: requestSignal, onProgress} = {}) {
|
|
153
|
+
if (closed) {
|
|
154
|
+
throw unavailable('The Core image accessor is closed.');
|
|
155
|
+
}
|
|
156
|
+
if (!client) {
|
|
157
|
+
throw unavailable('Local image generation requires an available Core connection.');
|
|
158
|
+
}
|
|
159
|
+
const operationSignal = requestSignal
|
|
160
|
+
? AbortSignal.any([requestSignal, lifetimeSignal]) : lifetimeSignal;
|
|
161
|
+
operationSignal.throwIfAborted();
|
|
162
|
+
const request = {
|
|
163
|
+
method,
|
|
164
|
+
revision,
|
|
165
|
+
streamId: parameters.streamId,
|
|
166
|
+
status: method === 'image.generate' || method === 'image.edit' ? 'Thinking' : null,
|
|
167
|
+
progress: null,
|
|
168
|
+
task: null
|
|
169
|
+
};
|
|
170
|
+
let stopProgress;
|
|
171
|
+
if (request.streamId) {
|
|
172
|
+
stopProgress = client.events.on(
|
|
173
|
+
'image.progress',
|
|
174
|
+
function imageProgress(value) {
|
|
175
|
+
if (closed || operationSignal.aborted || value.streamId !== request.streamId) {
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
request.progress = value.progress;
|
|
179
|
+
publish();
|
|
180
|
+
if (closed || operationSignal.aborted) {
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
observe(onProgress, value.progress);
|
|
184
|
+
if (closed || operationSignal.aborted) {
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
observe(
|
|
188
|
+
onEvent,
|
|
189
|
+
{type: 'image.progress', data: value}
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
);
|
|
193
|
+
}
|
|
194
|
+
request.task = Promise.resolve().then(
|
|
195
|
+
async function executeImageRequest() {
|
|
196
|
+
operationSignal.throwIfAborted();
|
|
197
|
+
const requestParameters = method === 'image.edit'
|
|
198
|
+
? {...parameters, image: await encodeImage(parameters.image, operationSignal)} : parameters;
|
|
199
|
+
operationSignal.throwIfAborted();
|
|
200
|
+
observe(
|
|
201
|
+
onEvent,
|
|
202
|
+
{type: 'image.request', data: {method, parameters: requestParameters}}
|
|
203
|
+
);
|
|
204
|
+
operationSignal.throwIfAborted();
|
|
205
|
+
const result = await client.invoke(
|
|
206
|
+
method,
|
|
207
|
+
requestParameters,
|
|
208
|
+
{signal: operationSignal, timeoutMs: 0}
|
|
209
|
+
);
|
|
210
|
+
operationSignal.throwIfAborted();
|
|
211
|
+
if (method === 'image.generate' || method === 'image.edit') {
|
|
212
|
+
const images = result.images.map(decodeImage);
|
|
213
|
+
operationSignal.throwIfAborted();
|
|
214
|
+
const decoded = {...result, images};
|
|
215
|
+
observe(
|
|
216
|
+
onEvent,
|
|
217
|
+
{type: 'image.result', data: decoded}
|
|
218
|
+
);
|
|
219
|
+
operationSignal.throwIfAborted();
|
|
220
|
+
return decoded;
|
|
221
|
+
}
|
|
222
|
+
return result;
|
|
223
|
+
}
|
|
224
|
+
).catch(
|
|
225
|
+
function imageRequestFailed(error) {
|
|
226
|
+
if (!closed && !operationSignal.aborted) {
|
|
227
|
+
if (method !== 'image.status' || revision === request.revision) {
|
|
228
|
+
state = {...state, error: serializeCoreError(error)};
|
|
229
|
+
}
|
|
230
|
+
observe(
|
|
231
|
+
onEvent,
|
|
232
|
+
{type: 'image.error', data: {method, error: serializeCoreError(error)}}
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
throw error;
|
|
236
|
+
}
|
|
237
|
+
).finally(
|
|
238
|
+
function releaseImageRequest() {
|
|
239
|
+
stopProgress?.();
|
|
240
|
+
pending.delete(request);
|
|
241
|
+
if (!closed) {
|
|
242
|
+
publish();
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
);
|
|
246
|
+
pending.add(request);
|
|
247
|
+
// Acknowledgement precedes transport, preparation, model loading and inference.
|
|
248
|
+
publish();
|
|
249
|
+
return request.task;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
async function inspect({signal: requestSignal} = {}) {
|
|
253
|
+
const before = revision;
|
|
254
|
+
try {
|
|
255
|
+
const result = await invoke(
|
|
256
|
+
'image.status',
|
|
257
|
+
{},
|
|
258
|
+
{signal: requestSignal}
|
|
259
|
+
);
|
|
260
|
+
if (!closed && revision === before) {
|
|
261
|
+
accept(result);
|
|
262
|
+
}
|
|
263
|
+
return current();
|
|
264
|
+
} catch (error) {
|
|
265
|
+
if (!closed && !requestSignal?.aborted && revision === before) {
|
|
266
|
+
accept(
|
|
267
|
+
{...state, available: false, state: 'unavailable', error: serializeCoreError(error)}
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
throw error;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
async function load({model, context, assetProjectionId, resourcePaths, signal: requestSignal} = {}) {
|
|
275
|
+
const before = revision;
|
|
276
|
+
const result = await invoke(
|
|
277
|
+
'image.load',
|
|
278
|
+
{model, context, assetProjectionId, resourcePaths},
|
|
279
|
+
{signal: requestSignal}
|
|
280
|
+
);
|
|
281
|
+
if (!closed && revision === before) {
|
|
282
|
+
accept(result);
|
|
283
|
+
}
|
|
284
|
+
return result;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function generate(
|
|
288
|
+
{model, prompt, parameters, assetProjectionId, resourcePaths, signal: requestSignal, onProgress} = {}
|
|
289
|
+
) {
|
|
290
|
+
const streamId = client?.uuid?.() ?? globalThis.crypto.randomUUID();
|
|
291
|
+
return invoke(
|
|
292
|
+
'image.generate',
|
|
293
|
+
{model, prompt, parameters, streamId, assetProjectionId, resourcePaths},
|
|
294
|
+
{signal: requestSignal, onProgress}
|
|
295
|
+
);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
function edit(
|
|
299
|
+
{model, image, prompt, strength, parameters, assetProjectionId, resourcePaths, signal: requestSignal, onProgress} = {}
|
|
300
|
+
) {
|
|
301
|
+
const streamId = client?.uuid?.() ?? globalThis.crypto.randomUUID();
|
|
302
|
+
return invoke(
|
|
303
|
+
'image.edit',
|
|
304
|
+
{model, image, prompt, strength, parameters, streamId, assetProjectionId, resourcePaths},
|
|
305
|
+
{signal: requestSignal, onProgress}
|
|
306
|
+
);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
async function unload({signal: requestSignal} = {}) {
|
|
310
|
+
const before = revision;
|
|
311
|
+
const result = await invoke(
|
|
312
|
+
'image.unload',
|
|
313
|
+
{},
|
|
314
|
+
{signal: requestSignal}
|
|
315
|
+
);
|
|
316
|
+
if (!closed && revision === before) {
|
|
317
|
+
accept(result);
|
|
318
|
+
}
|
|
319
|
+
return result;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function subscribe(listener, {replay = true, signal: subscriptionSignal} = {}) {
|
|
323
|
+
if (!is.function(listener)) {
|
|
324
|
+
throw new TypeError('An image state listener must be a function.');
|
|
325
|
+
}
|
|
326
|
+
if (closed) {
|
|
327
|
+
if (replay && !subscriptionSignal?.aborted) {
|
|
328
|
+
notifyListener(listener, current());
|
|
329
|
+
}
|
|
330
|
+
return function closedSubscription() {};
|
|
331
|
+
}
|
|
332
|
+
const stop = events.on(
|
|
333
|
+
'image.state',
|
|
334
|
+
function imageStateChanged(event) {
|
|
335
|
+
notifyListener(listener, event.detail);
|
|
336
|
+
},
|
|
337
|
+
{signal: subscriptionSignal}
|
|
338
|
+
);
|
|
339
|
+
try {
|
|
340
|
+
if (replay && !subscriptionSignal?.aborted) {
|
|
341
|
+
notifyListener(listener, current());
|
|
342
|
+
}
|
|
343
|
+
} catch (error) {
|
|
344
|
+
stop();
|
|
345
|
+
throw error;
|
|
346
|
+
}
|
|
347
|
+
return stop;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function close() {
|
|
351
|
+
if (closing) {
|
|
352
|
+
return closing;
|
|
353
|
+
}
|
|
354
|
+
closed = true;
|
|
355
|
+
closing = Promise.resolve().then(
|
|
356
|
+
async function closeImageAccessor() {
|
|
357
|
+
await Promise.allSettled(
|
|
358
|
+
[...pending].map(
|
|
359
|
+
function requestCompletion(request) {
|
|
360
|
+
return request.task;
|
|
361
|
+
}
|
|
362
|
+
)
|
|
363
|
+
);
|
|
364
|
+
}
|
|
365
|
+
);
|
|
366
|
+
lifetime.abort();
|
|
367
|
+
lifetimeSignal.removeEventListener('abort', close);
|
|
368
|
+
unsubscribe();
|
|
369
|
+
state = {...state, available: false, state: 'closed'};
|
|
370
|
+
publish();
|
|
371
|
+
events.dispose();
|
|
372
|
+
return closing;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
lifetimeSignal.addEventListener(
|
|
376
|
+
'abort',
|
|
377
|
+
close,
|
|
378
|
+
{once: true}
|
|
379
|
+
);
|
|
380
|
+
if (lifetimeSignal.aborted) {
|
|
381
|
+
close();
|
|
382
|
+
} else if (client) {
|
|
383
|
+
// Subscribe before the snapshot request so a later lifecycle event wins.
|
|
384
|
+
inspect().catch(
|
|
385
|
+
function initialImageStateUnavailable(error) {
|
|
386
|
+
globalThis.console?.error('Core image initial state unavailable.', error);
|
|
387
|
+
}
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
return {load, generate, edit, unload, inspect, current, subscribe, close};
|
|
392
|
+
}
|
|
@@ -1,10 +1,97 @@
|
|
|
1
|
-
# Local image
|
|
1
|
+
# Local image editing through Core
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
3
|
+
`createCoreImageRuntime()` exposes whole-image img2img through `edit()`, using
|
|
4
|
+
the same retained native context as generation. The selected SD1.4 model
|
|
5
|
+
advertises `txt2img` and `img2img`. Applications supply the original PNG, the
|
|
6
|
+
complete prompt and explicit strength, retain the original, and decide whether
|
|
7
|
+
to save the separate generated result.
|
|
8
|
+
|
|
9
|
+
## Browser accessor
|
|
10
|
+
|
|
11
|
+
Prepare and load the selected model using the
|
|
12
|
+
[shared generation and model-asset lifecycle](local-image-generation.md).
|
|
13
|
+
Then pass a generated PNG Blob or a supported PNG File directly:
|
|
14
|
+
|
|
15
|
+
```js
|
|
16
|
+
import {createCoreImageRuntime} from 'arcane-os/ai/core-image';
|
|
17
|
+
|
|
18
|
+
const images = createCoreImageRuntime({signal: page.signal});
|
|
19
|
+
images.subscribe(function showImageStatus(snapshot) {
|
|
20
|
+
statusElement.textContent = snapshot.status ?? snapshot.state;
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const result = await images.edit({
|
|
24
|
+
model: 'sd14',
|
|
25
|
+
image: originalPNG,
|
|
26
|
+
prompt: 'A dignified raccoon astronaut wearing a purple knitted helmet, holding a tiny moon cactus.',
|
|
27
|
+
strength: 0.6,
|
|
28
|
+
parameters: {
|
|
29
|
+
width: 512,
|
|
30
|
+
height: 512,
|
|
31
|
+
sample_params: {sample_steps: 20}
|
|
32
|
+
},
|
|
33
|
+
signal: page.signal,
|
|
34
|
+
onProgress(progress) {
|
|
35
|
+
console.log(progress);
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const editedPNG = result.images[0].blob;
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The example uses the application's existing `page` AbortController,
|
|
43
|
+
`statusElement` and original `originalPNG` Blob or File. It assumes that the
|
|
44
|
+
selected model has already been loaded. `edit()` can also prepare a selected
|
|
45
|
+
model on demand using the same `assetProjectionId` and `resourcePaths` fields
|
|
46
|
+
as `generate()`.
|
|
47
|
+
|
|
48
|
+
| Member or field | Contract |
|
|
49
|
+
| --- | --- |
|
|
50
|
+
| `edit({model, image, prompt, strength, parameters?, assetProjectionId?, resourcePaths?, signal?, onProgress?})` | Returns the complete result with `images: [{blob, mediaType, width, height}]`, preserving additional runtime result metadata. |
|
|
51
|
+
| `image` | Original complete PNG `Blob` or `File`, with `image/png` or an empty declared media type. Supported PNG encodings are listed below. |
|
|
52
|
+
| `prompt` | Complete caller-authored string, passed unchanged. Native C-string incompatibilities retain the generation error contract. |
|
|
53
|
+
| `strength` | Explicit finite number from `0` through `1`, mapped to native img2img strength. It takes precedence over a model default or `parameters.strength`; it is never clamped. |
|
|
54
|
+
| `parameters` | Native generation options, including canvas dimensions, seed, sampling and `image_preprocess`. The top-level image supplies native `init_image`; the top-level strength supplies native `strength`. Other options retain the generation contract. |
|
|
55
|
+
|
|
56
|
+
`Thinking` is published synchronously when the browser accepts the edit, before
|
|
57
|
+
reading the Blob, sending the request, preparing the runtime or loading the
|
|
58
|
+
model. Native sampling progress uses the existing progress surface. Complete
|
|
59
|
+
PNG results arrive after native completion; the SDK fabricates no image chunks
|
|
60
|
+
or assistant turns. Closing the accessor cancels and joins its requests while
|
|
61
|
+
preserving the shared Core client and model ownership.
|
|
62
|
+
|
|
63
|
+
The original encoded input is preserved through lossless Core JSON framing.
|
|
64
|
+
The browser does not decode, draw, re-encode, resize, crop, composite or save it.
|
|
65
|
+
The request signal reaches input preparation, worker decoding and cooperative
|
|
66
|
+
native cancellation. Cancellation during `Blob.arrayBuffer()` is observed
|
|
67
|
+
after that platform operation completes. An aborted edit produces no late
|
|
68
|
+
success or progress callback. Request invocation retains `timeoutMs: 0`.
|
|
69
|
+
|
|
70
|
+
## Core and native request
|
|
71
|
+
|
|
72
|
+
The `arcane-os/core/image` service adds `image.edit` with
|
|
73
|
+
`{model, image, prompt, strength, parameters?, streamId?, assetProjectionId?, resourcePaths?}`.
|
|
74
|
+
At this JSON boundary only, `image` is
|
|
75
|
+
`{data: completeBase64PNG, encoding: 'base64', mediaType}`. The service emits
|
|
76
|
+
the existing accepted `image.progress` event before preparation, then forwards
|
|
77
|
+
the decoded transport content to the shared image worker. Results use the
|
|
78
|
+
same complete PNG transport records as `image.generate`.
|
|
79
|
+
|
|
80
|
+
The native runtime's `edit()` takes the same operation fields, with
|
|
81
|
+
`image: {data: Uint8Array, mediaType}` and no Core projection arguments.
|
|
82
|
+
It copies the supplied data view at acceptance so caller reuse cannot change
|
|
83
|
+
an input waiting behind another operation.
|
|
84
|
+
The worker decodes once, loads or reuses the exact selected context, and passes
|
|
85
|
+
the raster as native `init_image` with explicit `strength`. The existing native
|
|
86
|
+
binding retains input memory through completion and copies complete output
|
|
87
|
+
rasters before native cleanup. The same queue, cancellation, unload and close
|
|
88
|
+
owners serve generation and editing; no second engine or model store is created.
|
|
89
|
+
|
|
90
|
+
Unreadable or unsupported input rejects the edit without substituting an
|
|
91
|
+
image. Errors retain their complete details on rejected promises and diagnostic
|
|
92
|
+
events. Applications keep those engineering diagnostics outside ordinary status
|
|
93
|
+
surfaces and durable chat history. No prompt, input or result is persisted by
|
|
94
|
+
this operation.
|
|
8
95
|
|
|
9
96
|
## Decoder contract
|
|
10
97
|
|
|
@@ -56,22 +143,21 @@ failure never returns partial imagery or substitutes a different image.
|
|
|
56
143
|
|
|
57
144
|
## Editing semantics at the native owner
|
|
58
145
|
|
|
59
|
-
Whole-image img2img uses an initial image, prompt and strength.
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
with its base SD1.4 model in the
|
|
146
|
+
Whole-image img2img uses an initial image, prompt and strength. This `edit()`
|
|
147
|
+
contract selects that operation. Masked and reference editing require their
|
|
148
|
+
own model-specific contracts and are outside this PNG img2img operation.
|
|
149
|
+
Ordinary img2img does not require LoRA training. The selected engine documents
|
|
150
|
+
this directly with its base SD1.4 model in the
|
|
65
151
|
[tagged img2img example](https://github.com/leejet/stable-diffusion.cpp/blob/master-929-3f8527a/docs/sd.md#img2img-example).
|
|
66
152
|
|
|
67
153
|
Source dimensions are decoder output, not an implicit request to change the
|
|
68
|
-
generation dimensions. The
|
|
69
|
-
and reports actual output dimensions.
|
|
70
|
-
|
|
154
|
+
generation dimensions. The engine center-crops and resizes temporary initial
|
|
155
|
+
images to the selected canvas by default and reports actual output dimensions.
|
|
156
|
+
Caller images remain unchanged;
|
|
71
157
|
even `mode=none` does not suppress all later model adaptation. See the
|
|
72
158
|
[native preprocessing contract](https://github.com/leejet/stable-diffusion.cpp/blob/master-929-3f8527a/docs/image_preprocessing.md).
|
|
73
159
|
|
|
74
|
-
An RGB-only model such as the selected SD1.4
|
|
160
|
+
An RGB-only model such as the selected SD1.4 consumes the stored RGB
|
|
75
161
|
and does not preserve alpha. The SDK must not silently choose a compositing
|
|
76
162
|
background or claim that passing RGBA preserves its displayed appearance.
|
|
77
163
|
Strength zero is also not an exact-copy operation: native img2img still uses
|