arcane-os 0.61.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 CHANGED
@@ -1,5 +1,22 @@
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
+
3
20
  ## 0.61.0
4
21
 
5
22
  - Add retained local image generation through Core, with public native runtime,
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.61.0` SDK contract. Development applications declare
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;
@@ -30,6 +30,24 @@ function decodeImage(image) {
30
30
  return {...metadata, blob};
31
31
  }
32
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
+
33
51
  /** Browser access to the selected native image service; no result persistence. */
34
52
  export function createCoreImageRuntime(
35
53
  {client = getInstalledCoreClient(), signal, onEvent} = {}
@@ -145,7 +163,7 @@ export function createCoreImageRuntime(
145
163
  method,
146
164
  revision,
147
165
  streamId: parameters.streamId,
148
- status: method === 'image.generate' ? 'Thinking' : null,
166
+ status: method === 'image.generate' || method === 'image.edit' ? 'Thinking' : null,
149
167
  progress: null,
150
168
  task: null
151
169
  };
@@ -176,18 +194,21 @@ export function createCoreImageRuntime(
176
194
  request.task = Promise.resolve().then(
177
195
  async function executeImageRequest() {
178
196
  operationSignal.throwIfAborted();
197
+ const requestParameters = method === 'image.edit'
198
+ ? {...parameters, image: await encodeImage(parameters.image, operationSignal)} : parameters;
199
+ operationSignal.throwIfAborted();
179
200
  observe(
180
201
  onEvent,
181
- {type: 'image.request', data: {method, parameters}}
202
+ {type: 'image.request', data: {method, parameters: requestParameters}}
182
203
  );
183
204
  operationSignal.throwIfAborted();
184
205
  const result = await client.invoke(
185
206
  method,
186
- parameters,
207
+ requestParameters,
187
208
  {signal: operationSignal, timeoutMs: 0}
188
209
  );
189
210
  operationSignal.throwIfAborted();
190
- if (method === 'image.generate') {
211
+ if (method === 'image.generate' || method === 'image.edit') {
191
212
  const images = result.images.map(decodeImage);
192
213
  operationSignal.throwIfAborted();
193
214
  const decoded = {...result, images};
@@ -274,6 +295,17 @@ export function createCoreImageRuntime(
274
295
  );
275
296
  }
276
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
+
277
309
  async function unload({signal: requestSignal} = {}) {
278
310
  const before = revision;
279
311
  const result = await invoke(
@@ -356,5 +388,5 @@ export function createCoreImageRuntime(
356
388
  );
357
389
  }
358
390
 
359
- return {load, generate, unload, inspect, current, subscribe, close};
391
+ return {load, generate, edit, unload, inspect, current, subscribe, close};
360
392
  }
@@ -1,10 +1,97 @@
1
- # Local image edit inputs
1
+ # Local image editing through Core
2
2
 
3
- The SDK input decoder prepares a PNG raster for the shared native image
4
- runtime. Decoding does not load a model or perform inference. The runtime owns
5
- model selection, supported editing operations, native calls and their complete
6
- results. Applications retain the original image and decide whether to save a
7
- newly generated result.
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. Masked editing
60
- adds a separate mask; instruction/reference editing uses a model that supports
61
- reference conditioning. These are distinct operations and must be advertised
62
- only when the selected model and native path implement them. Ordinary img2img
63
- does not require LoRA training. The candidate engine documents this directly
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 model owner documents its actual canvas adaptation
69
- and reports actual output dimensions. In the candidate engine, temporary
70
- initial images use model preprocessing while caller images remain unchanged;
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 candidate consumes the stored RGB
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
@@ -1,9 +1,9 @@
1
1
  # Native working files from stored model assets
2
2
 
3
- The selected `0.60.0` package contains the model-assets helpers as internal
4
- preparation. The public imports and Core registration shown below belong to
5
- the pending local-image integration and are unavailable through `0.60.0`.
6
- These examples document that integration's intended public contract.
3
+ These public APIs were first published in `arcane-os@0.61.0`. Browser
4
+ preparation is exported by `arcane-os/ai/core-model-assets`, and the native
5
+ service by `arcane-os/core/model-assets`. The image example below uses
6
+ `arcane-os/ai/core-image`, published in the same release.
7
7
 
8
8
  `prepareCoreModelAssets()` streams complete browser `File` or `Blob` members
9
9
  through the existing Core connection into native working files. DBOPFS keeps
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arcane-os",
3
- "version": "0.61.0",
3
+ "version": "0.62.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",
@@ -534,6 +534,48 @@ export function createLocalImageService(
534
534
  operationSignal
535
535
  );
536
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
+ },
537
579
  'image.unload': function unloadImage(_parameters, request) {
538
580
  const operationSignal = AbortSignal.any([lifetimeSignal, request.signal]);
539
581
  return track(
@@ -11,5 +11,5 @@ export const SD14_MODEL = {
11
11
  },
12
12
  context: {},
13
13
  defaults: {},
14
- operations: ['txt2img']
14
+ operations: ['txt2img', 'img2img']
15
15
  };
@@ -1,7 +1,9 @@
1
1
  import {Worker} from 'node:worker_threads';
2
2
  import {isDeepStrictEqual} from 'node:util';
3
+ import Is from 'strong-type';
3
4
  import {createArcaneEventSource} from '../event-manager.mjs';
4
5
 
6
+ const is = new Is(false);
5
7
  const brokers = new Map();
6
8
 
7
9
  /** Share the native library owner between Core services in this host. */
@@ -81,6 +83,26 @@ export function createImageRuntime(
81
83
  );
82
84
  }
83
85
 
86
+ function edit({model, image, prompt, strength, parameters, signal: operationSignal, onProgress} = {}) {
87
+ if (!is.number(strength) || !is.finite(strength)) {
88
+ throw new TypeError('Image editing requires an explicit finite strength.');
89
+ }
90
+ if (strength < 0 || strength > 1) {
91
+ throw new RangeError('Image editing strength must be between 0 and 1.');
92
+ }
93
+ // The accepted input must survive caller reuse while this request queues.
94
+ const input = {
95
+ ...image,
96
+ data: image?.data instanceof Uint8Array ? new Uint8Array(image.data) : image?.data
97
+ };
98
+ return operation(
99
+ 'edit',
100
+ {...selectedModel(model), image: input, prompt, strength, parameters},
101
+ operationSignal,
102
+ onProgress
103
+ );
104
+ }
105
+
84
106
  function unload({signal: operationSignal} = {}) {
85
107
  return operation('unload', {}, operationSignal);
86
108
  }
@@ -133,7 +155,7 @@ export function createImageRuntime(
133
155
  function abortRuntime() { close().catch(reportError); }
134
156
  lifetimeSignal.addEventListener('abort', abortRuntime, {once: true});
135
157
  if (lifetimeSignal.aborted) abortRuntime();
136
- return {load, generate, unload, current: broker.current, subscribe, close};
158
+ return {load, generate, edit, unload, current: broker.current, subscribe, close};
137
159
  }
138
160
 
139
161
  function createBroker(configuration, predecessor) {
@@ -1,6 +1,7 @@
1
1
  import {parentPort, workerData} from 'node:worker_threads';
2
2
  import {createImageLibrary} from './image-library.mjs';
3
3
  import {encodePNG} from './image-png.mjs';
4
+ import {decodeImage} from './image-editing.mjs';
4
5
 
5
6
  let library;
6
7
  let active;
@@ -136,6 +137,17 @@ async function execute(message, signal) {
136
137
  return state;
137
138
  }
138
139
  const parameters = message.parameters;
140
+ let image;
141
+ if (message.operation === 'edit') {
142
+ try {
143
+ image = await decodeImage({...parameters.image, signal});
144
+ } catch (error) {
145
+ // Decoding leaves the selected context unchanged. Replay its actual
146
+ // state so a broker cancellation cannot remain visibly unsettled.
147
+ publish({error: describeError(error)});
148
+ throw error;
149
+ }
150
+ }
139
151
  await load(parameters, signal);
140
152
  if (message.operation === 'load') return state;
141
153
  signal.throwIfAborted();
@@ -148,11 +160,13 @@ async function execute(message, signal) {
148
160
  prompt: parameters.prompt,
149
161
  parameters: {
150
162
  ...defaults, ...supplied,
163
+ ...(message.operation === 'edit' ? {strength: parameters.strength} : {}),
151
164
  sample_params: {
152
165
  ...defaults.sample_params, ...supplied.sample_params,
153
166
  guidance: {...defaults.sample_params?.guidance, ...supplied.sample_params?.guidance}
154
167
  }
155
168
  },
169
+ image,
156
170
  signal
157
171
  }
158
172
  );