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.
@@ -0,0 +1,985 @@
1
+ import {createRequire} from 'node:module';
2
+ import {existsSync} from 'node:fs';
3
+ import {dirname, join} from 'node:path';
4
+ import Is from 'strong-type';
5
+
6
+ const is = new Is(false);
7
+ let callbackSequence = 0;
8
+
9
+ // This adapter belongs to the process-owned image Worker. The upstream
10
+ // callbacks are library globals, so every SDK service shares that owner.
11
+ export function createImageLibrary(
12
+ {
13
+ libraryPath,
14
+ bindingModulePath,
15
+ onLog,
16
+ onProgress
17
+ }
18
+ ) {
19
+ const requireBinding = createRequire(bindingModulePath);
20
+ const koffi = requireBinding(bindingModulePath);
21
+ const library = loadLibrary(libraryPath);
22
+ const backendDirectory = dirname(libraryPath);
23
+ let backendLibrary;
24
+ let backendBaseLibrary;
25
+ let backendsLoaded = false;
26
+
27
+ let native;
28
+ let logCallback;
29
+ let progressCallback;
30
+ let nativeContext = null;
31
+ let contextMemory;
32
+ let operation;
33
+ let unloading = false;
34
+ let closing = false;
35
+ let closed = false;
36
+ let unloadPromise;
37
+ let closePromise;
38
+ const pendingCallbacks = new Set();
39
+ const callbackErrors = [];
40
+ const converters = new Map();
41
+
42
+ // Field order and C types follow stable-diffusion.cpp master-929-3f8527a:
43
+ // https://github.com/leejet/stable-diffusion.cpp/blob/3f8527a/include/stable-diffusion.h
44
+ let contextType;
45
+ let imageType;
46
+ let generationType;
47
+ try {
48
+ // The selected Windows/Linux releases ship GGML beside this library.
49
+ // macOS embeds it instead. Default GGML discovery searches beside the
50
+ // host executable and in its working directory, so dynamic releases
51
+ // need the public directory loader before enumeration or model load.
52
+ const suffix = process.platform === 'win32' ? '.dll' : process.platform === 'darwin' ? '.dylib' : '.so';
53
+ const prefix = process.platform === 'win32' ? '' : 'lib';
54
+ const backendPath = join(backendDirectory, `${prefix}ggml${suffix}`);
55
+ if (existsSync(backendPath)) {
56
+ backendLibrary = loadLibrary(backendPath);
57
+ backendBaseLibrary = loadLibrary(
58
+ join(backendDirectory, `${prefix}ggml-base${suffix}`)
59
+ );
60
+ }
61
+ const tiling = defineStruct(
62
+ {
63
+ enabled: 'bool',
64
+ temporal_tiling: 'bool',
65
+ tile_size_w: 'int',
66
+ tile_size_h: 'int',
67
+ target_overlap: 'float',
68
+ rel_size_w: 'float',
69
+ rel_size_h: 'float',
70
+ extra_tiling_args: 'cstring'
71
+ }
72
+ );
73
+ const embedding = defineStruct(
74
+ {
75
+ name: 'cstring',
76
+ path: 'cstring'
77
+ }
78
+ );
79
+ contextType = defineStruct(
80
+ {
81
+ model_path: 'cstring',
82
+ clip_l_path: 'cstring',
83
+ clip_g_path: 'cstring',
84
+ clip_vision_path: 'cstring',
85
+ t5xxl_path: 'cstring',
86
+ llm_path: 'cstring',
87
+ llm_vision_path: 'cstring',
88
+ diffusion_model_path: 'cstring',
89
+ high_noise_diffusion_model_path: 'cstring',
90
+ uncond_diffusion_model_path: 'cstring',
91
+ embeddings_connectors_path: 'cstring',
92
+ vae_path: 'cstring',
93
+ audio_vae_path: 'cstring',
94
+ audio_encoder_path: 'cstring',
95
+ taesd_path: 'cstring',
96
+ control_net_path: 'cstring',
97
+ ip_adapter_path: 'cstring',
98
+ motion_module_path: 'cstring',
99
+ embeddings: {element: embedding, count: 'embedding_count'},
100
+ embedding_count: 'uint32_t',
101
+ photo_maker_path: 'cstring',
102
+ pulid_weights_path: 'cstring',
103
+ tensor_type_rules: 'cstring',
104
+ n_threads: 'int',
105
+ wtype: {convert: 'str_to_sd_type'},
106
+ rng_type: {convert: 'str_to_rng_type'},
107
+ sampler_rng_type: {convert: 'str_to_rng_type'},
108
+ prediction: {convert: 'str_to_prediction'},
109
+ lora_apply_mode: {convert: 'str_to_lora_apply_mode'},
110
+ enable_mmap: 'bool',
111
+ flash_attn: 'bool',
112
+ diffusion_flash_attn: 'bool',
113
+ tae_preview_only: 'bool',
114
+ diffusion_conv_direct: 'bool',
115
+ vae_conv_direct: 'bool',
116
+ force_sdxl_vae_conv_scale: 'bool',
117
+ vae_format: 'int',
118
+ max_vram: 'cstring',
119
+ disable_prefetch: 'bool',
120
+ eager_load: 'bool',
121
+ backend: 'cstring',
122
+ params_backend: 'cstring',
123
+ split_mode: 'cstring',
124
+ auto_fit: 'bool',
125
+ rpc_servers: 'cstring',
126
+ model_args: 'cstring',
127
+ disable_segmented_compute: 'bool',
128
+ linear_scale: 'float',
129
+ attn_scale: 'float',
130
+ tokenizer: 'cstring',
131
+ sage_attn: 'bool',
132
+ conditioning_cache_size: 'int'
133
+ }
134
+ );
135
+ imageType = defineStruct(
136
+ {
137
+ width: 'uint32_t',
138
+ height: 'uint32_t',
139
+ channel: 'uint32_t',
140
+ data: {element: 'uint8_t', raster: true}
141
+ }
142
+ );
143
+ const preprocessing = defineStruct(
144
+ {
145
+ rules: 'cstring'
146
+ }
147
+ );
148
+ const slg = defineStruct(
149
+ {
150
+ layers: {element: 'int', count: 'layer_count'},
151
+ layer_count: 'size_t',
152
+ layer_start: 'float',
153
+ layer_end: 'float',
154
+ scale: 'float'
155
+ }
156
+ );
157
+ const guidance = defineStruct(
158
+ {
159
+ txt_cfg: 'float',
160
+ img_cfg: 'float',
161
+ distilled_guidance: 'float',
162
+ slg
163
+ }
164
+ );
165
+ const sample = defineStruct(
166
+ {
167
+ guidance,
168
+ scheduler: {convert: 'str_to_scheduler'},
169
+ sample_method: {convert: 'str_to_sample_method'},
170
+ sample_steps: 'int',
171
+ eta: 'float',
172
+ shifted_timestep: 'int',
173
+ custom_sigmas: {element: 'float', count: 'custom_sigmas_count'},
174
+ custom_sigmas_count: 'int',
175
+ flow_shift: 'float',
176
+ extra_sample_args: 'cstring'
177
+ }
178
+ );
179
+ const photoMaker = defineStruct(
180
+ {
181
+ id_images: {element: imageType, count: 'id_images_count'},
182
+ id_images_count: 'int',
183
+ id_embed_path: 'cstring',
184
+ style_strength: 'float'
185
+ }
186
+ );
187
+ const pulid = defineStruct(
188
+ {
189
+ id_embedding_path: 'cstring',
190
+ id_weight: 'float'
191
+ }
192
+ );
193
+ const cache = defineStruct(
194
+ {
195
+ mode: 'int',
196
+ reuse_threshold: 'float',
197
+ start_percent: 'float',
198
+ end_percent: 'float',
199
+ error_decay_rate: 'float',
200
+ use_relative_threshold: 'bool',
201
+ reset_error_on_compute: 'bool',
202
+ Fn_compute_blocks: 'int',
203
+ Bn_compute_blocks: 'int',
204
+ residual_diff_threshold: 'float',
205
+ max_warmup_steps: 'int',
206
+ max_cached_steps: 'int',
207
+ max_continuous_cached_steps: 'int',
208
+ taylorseer_n_derivatives: 'int',
209
+ taylorseer_skip_interval: 'int',
210
+ scm_mask: 'cstring',
211
+ scm_policy_dynamic: 'bool',
212
+ spectrum_w: 'float',
213
+ spectrum_m: 'int',
214
+ spectrum_lam: 'float',
215
+ spectrum_window_size: 'int',
216
+ spectrum_flex_window: 'float',
217
+ spectrum_warmup_steps: 'int',
218
+ spectrum_stop_percent: 'float'
219
+ }
220
+ );
221
+ const lora = defineStruct(
222
+ {
223
+ is_high_noise: 'bool',
224
+ multiplier: 'float',
225
+ path: 'cstring'
226
+ }
227
+ );
228
+ const hires = defineStruct(
229
+ {
230
+ enabled: 'bool',
231
+ upscaler: {convert: 'str_to_sd_hires_upscaler'},
232
+ model_path: 'cstring',
233
+ scale: 'float',
234
+ target_width: 'int',
235
+ target_height: 'int',
236
+ steps: 'int',
237
+ denoising_strength: 'float',
238
+ upscale_tile_size: 'int',
239
+ custom_sigmas: {element: 'float', count: 'custom_sigmas_count'},
240
+ custom_sigmas_count: 'int'
241
+ }
242
+ );
243
+ generationType = defineStruct(
244
+ {
245
+ loras: {element: lora, count: 'lora_count'},
246
+ lora_count: 'uint32_t',
247
+ prompt: 'cstring',
248
+ negative_prompt: 'cstring',
249
+ clip_skip: 'int',
250
+ init_image: imageType,
251
+ ref_images: {element: imageType, count: 'ref_images_count'},
252
+ ref_images_count: 'int',
253
+ ref_image_args: 'cstring',
254
+ mask_image: imageType,
255
+ width: 'int',
256
+ height: 'int',
257
+ sample_params: sample,
258
+ strength: 'float',
259
+ seed: 'int64_t',
260
+ batch_count: 'int',
261
+ control_image: imageType,
262
+ control_strength: 'float',
263
+ ip_adapter_image: imageType,
264
+ ip_adapter_strength: 'float',
265
+ pm_params: photoMaker,
266
+ pulid_params: pulid,
267
+ vae_tiling_params: tiling,
268
+ cache,
269
+ hires,
270
+ qwen_image_layers: 'int',
271
+ circular_x: 'bool',
272
+ circular_y: 'bool',
273
+ image_preprocess: preprocessing
274
+ }
275
+ );
276
+ const sequence = ++callbackSequence;
277
+ const logType = koffi.proto(
278
+ `ArcaneSdLog${sequence}`,
279
+ 'void',
280
+ ['int', 'str', 'void *']
281
+ );
282
+ const progressType = koffi.proto(
283
+ `ArcaneSdProgress${sequence}`,
284
+ 'void',
285
+ ['int', 'int', 'float', 'void *']
286
+ );
287
+ native = {
288
+ loadBackends: backendLibrary?.func(
289
+ 'ggml_backend_load_all_from_path', 'void',
290
+ ['void *']
291
+ ),
292
+ setBackendLog: backendBaseLibrary?.func(
293
+ 'ggml_log_set', 'void',
294
+ ['void *', 'void *']
295
+ ),
296
+ initContext: library.func(
297
+ 'sd_ctx_params_init', 'void',
298
+ ['void *']
299
+ ),
300
+ createContext: library.func(
301
+ 'new_sd_ctx', 'void *',
302
+ ['void *']
303
+ ),
304
+ freeContext: library.func(
305
+ 'free_sd_ctx', 'void',
306
+ ['void *']
307
+ ),
308
+ modelVersion: library.func(
309
+ 'sd_get_model_version_name', 'str',
310
+ ['void *']
311
+ ),
312
+ initGeneration: library.func(
313
+ 'sd_img_gen_params_init', 'void',
314
+ ['void *']
315
+ ),
316
+ generate: library.func(
317
+ 'generate_image', 'bool',
318
+ ['void *', 'void *', 'void *', 'void *']
319
+ ),
320
+ freeImages: library.func(
321
+ 'free_sd_images', 'void',
322
+ ['void *', 'int']
323
+ ),
324
+ cancel: library.func(
325
+ 'sd_cancel_generation', 'void',
326
+ ['void *', 'int']
327
+ ),
328
+ listDevices: library.func(
329
+ 'sd_list_devices', 'size_t',
330
+ ['void *', 'size_t']
331
+ ),
332
+ setLog: library.func(
333
+ 'sd_set_log_callback', 'void',
334
+ [koffi.pointer(logType), 'void *']
335
+ ),
336
+ setProgress: library.func(
337
+ 'sd_set_progress_callback', 'void',
338
+ [koffi.pointer(progressType), 'void *']
339
+ )
340
+ };
341
+ logCallback = koffi.register(
342
+ receiveLog,
343
+ koffi.pointer(logType)
344
+ );
345
+ progressCallback = koffi.register(
346
+ receiveProgress,
347
+ koffi.pointer(progressType)
348
+ );
349
+ native.setLog(logCallback, null);
350
+ native.setProgress(progressCallback, null);
351
+ } catch (error) {
352
+ if (native) {
353
+ native.setBackendLog?.(null, null);
354
+ native.setLog(null, null);
355
+ native.setProgress(null, null);
356
+ }
357
+ if (logCallback) koffi.unregister(logCallback);
358
+ if (progressCallback) koffi.unregister(progressCallback);
359
+ library.unload();
360
+ backendLibrary?.unload();
361
+ backendBaseLibrary?.unload();
362
+ throw error;
363
+ }
364
+
365
+ return {
366
+ listDevices,
367
+ load,
368
+ generate,
369
+ cancel,
370
+ unload,
371
+ close
372
+ };
373
+
374
+ function loadLibrary(path) {
375
+ try {
376
+ return koffi.load(path);
377
+ } catch (cause) {
378
+ throw imageError(
379
+ 'LOCAL_IMAGE_LIBRARY_UNAVAILABLE',
380
+ `Unable to load the local image library at ${path}.`,
381
+ cause
382
+ );
383
+ }
384
+ }
385
+
386
+ async function loadBackends() {
387
+ if (backendsLoaded) return;
388
+ if (!native.loadBackends) {
389
+ backendsLoaded = true;
390
+ return;
391
+ }
392
+ const memory = createMemory();
393
+ try {
394
+ const directory = memory.string(backendDirectory, 'backend directory');
395
+ await invoke(native.loadBackends, directory);
396
+ backendsLoaded = true;
397
+ await finishCallbacks();
398
+ } finally {
399
+ memory.release();
400
+ }
401
+ }
402
+
403
+ function defineStruct(fields) {
404
+ const nativeFields = {};
405
+ for (const [name, field] of Object.entries(fields)) {
406
+ nativeFields[name] = nativeType(field);
407
+ }
408
+ return {
409
+ fields,
410
+ type: koffi.struct(nativeFields)
411
+ };
412
+ }
413
+
414
+ function nativeType(field) {
415
+ if (field === 'cstring' || field.element) return 'void *';
416
+ if (field.convert) return 'int';
417
+ return field.type ?? field;
418
+ }
419
+
420
+ function zeroStruct(descriptor) {
421
+ const value = {};
422
+ for (const [name, field] of Object.entries(descriptor.fields)) {
423
+ value[name] = field.fields
424
+ ? zeroStruct(field)
425
+ : field === 'cstring' || field.element
426
+ ? null
427
+ : field === 'bool' ? false : 0;
428
+ }
429
+ return value;
430
+ }
431
+
432
+ function createMemory() {
433
+ const allocations = [];
434
+ const strings = [];
435
+ return {
436
+ allocate,
437
+ string,
438
+ release
439
+ };
440
+
441
+ function allocate(type, count = 1) {
442
+ const pointer = koffi.alloc(type, count);
443
+ allocations.push(pointer);
444
+ return pointer;
445
+ }
446
+
447
+ function string(value, name) {
448
+ if (value === null) return null;
449
+ if (!is.string(value)) throw new TypeError(`${name} must be a string or null.`);
450
+ if (value.includes('\0') || !value.isWellFormed()) {
451
+ throw imageError(
452
+ 'LOCAL_IMAGE_STRING_INPUT_UNSUPPORTED',
453
+ `The native UTF-8 C string for ${name} cannot preserve embedded U+0000 or unpaired UTF-16 surrogates.`
454
+ );
455
+ }
456
+ // Explicit native storage avoids transient FFI string marshalling;
457
+ // the complete owned Buffer and its C string share this lifetime.
458
+ const buffer = Buffer.from(`${value}\0`, 'utf8');
459
+ strings.push(buffer);
460
+ const pointer = allocate('uint8_t', buffer.length);
461
+ koffi.encode(pointer, 'uint8_t', buffer, buffer.length);
462
+ return pointer;
463
+ }
464
+
465
+ function release() {
466
+ const errors = [];
467
+ while (allocations.length) {
468
+ try {
469
+ koffi.free(
470
+ allocations.pop()
471
+ );
472
+ } catch (error) {
473
+ errors.push(error);
474
+ }
475
+ }
476
+ strings.length = 0;
477
+ if (errors.length) throw new AggregateError(errors, 'Local image input memory cleanup failed.');
478
+ }
479
+ }
480
+
481
+ function marshalStruct(descriptor, defaults, supplied, memory, path) {
482
+ if (!supplied || !is.object(supplied) || is.array(supplied)) {
483
+ throw new TypeError(`${path} must contain native struct fields.`);
484
+ }
485
+ const value = {...defaults};
486
+ for (const [name, input] of Object.entries(supplied)) {
487
+ const field = descriptor.fields[name];
488
+ if (field === undefined) {
489
+ throw imageError(
490
+ 'LOCAL_IMAGE_PARAMETER_UNSUPPORTED',
491
+ `${path}.${name} is absent from the selected native image ABI.`
492
+ );
493
+ }
494
+ if (input === undefined) continue;
495
+ const fieldPath = `${path}.${name}`;
496
+ if (field === 'cstring') {
497
+ value[name] = memory.string(input, fieldPath);
498
+ } else if (field.fields) {
499
+ value[name] = marshalStruct(field, defaults[name], input, memory, fieldPath);
500
+ } else if (field.convert && is.string(input)) {
501
+ let convert = converters.get(field.convert);
502
+ if (!convert) {
503
+ convert = library.func(
504
+ field.convert, 'int',
505
+ ['void *']
506
+ );
507
+ converters.set(field.convert, convert);
508
+ }
509
+ value[name] = convert(
510
+ memory.string(input, fieldPath)
511
+ );
512
+ } else if (field.element) {
513
+ const elements = input === null ? [] : input;
514
+ if (!is.array(elements) && !ArrayBuffer.isView(elements)) {
515
+ throw new TypeError(`${fieldPath} must contain the complete native array.`);
516
+ }
517
+ if (elements.length === undefined) {
518
+ throw new TypeError(`${fieldPath} requires array elements rather than a DataView.`);
519
+ }
520
+ if (field.count) {
521
+ const count = supplied[field.count];
522
+ if (count !== undefined && Number(count) !== elements.length) {
523
+ throw imageError(
524
+ 'LOCAL_IMAGE_ARRAY_INPUT_UNSUPPORTED',
525
+ `${path}.${field.count} must describe every element of ${fieldPath}.`
526
+ );
527
+ }
528
+ value[field.count] = elements.length;
529
+ }
530
+ const elementType = nativeType(field.element);
531
+ const pointer = elements.length ? memory.allocate(elementType, elements.length) : null;
532
+ if (pointer !== null) {
533
+ const encoded = field.element.fields
534
+ ? elements.map(
535
+ function marshalElement(element, index) {
536
+ return marshalStruct(
537
+ field.element,
538
+ zeroStruct(field.element),
539
+ element,
540
+ memory,
541
+ `${fieldPath}[${index}]`
542
+ );
543
+ }
544
+ )
545
+ : elements;
546
+ koffi.encode(pointer, elementType, encoded, elements.length);
547
+ }
548
+ value[name] = pointer;
549
+ } else {
550
+ value[name] = input;
551
+ }
552
+ }
553
+ for (const [name, field] of Object.entries(descriptor.fields)) {
554
+ if (field.count && supplied[name] === undefined
555
+ && supplied[field.count] !== undefined
556
+ && Number(supplied[field.count]) !== Number(defaults[field.count])) {
557
+ throw imageError(
558
+ 'LOCAL_IMAGE_ARRAY_INPUT_UNSUPPORTED',
559
+ `${path}.${field.count} requires its complete ${name} array.`
560
+ );
561
+ }
562
+ if (field.raster && supplied[name] !== undefined) {
563
+ const elements = supplied[name] === null ? [] : supplied[name];
564
+ // This is the C raster layout, not an application content cap.
565
+ if (elements.length !== value.width * value.height * value.channel) {
566
+ throw imageError(
567
+ 'LOCAL_IMAGE_RASTER_INPUT_UNSUPPORTED',
568
+ `${path}.data must contain every channel of the declared raster.`
569
+ );
570
+ }
571
+ }
572
+ }
573
+ return value;
574
+ }
575
+
576
+ function initialize(descriptor, init, supplied, memory, path) {
577
+ const pointer = memory.allocate(descriptor.type);
578
+ init(pointer);
579
+ const defaults = koffi.decode(pointer, descriptor.type);
580
+ const value = marshalStruct(descriptor, defaults, supplied, memory, path);
581
+ koffi.encode(pointer, descriptor.type, value);
582
+ return pointer;
583
+ }
584
+
585
+ function invoke(fn, ...args) {
586
+ return new Promise(
587
+ function invokeNative(resolve, reject) {
588
+ fn.async(
589
+ ...args,
590
+ function nativeCompleted(error, result) {
591
+ if (error) reject(error);
592
+ else resolve(result);
593
+ }
594
+ );
595
+ }
596
+ );
597
+ }
598
+
599
+ function receiveLog(level, text) {
600
+ reassertCancellation();
601
+ deliverCallback(
602
+ onLog,
603
+ {level, text}
604
+ );
605
+ }
606
+
607
+ function receiveProgress(step, steps, time) {
608
+ reassertCancellation();
609
+ deliverCallback(
610
+ onProgress,
611
+ {step, steps, time}
612
+ );
613
+ }
614
+
615
+ function reassertCancellation() {
616
+ // generate_image resets its atomic flag at entry. A cancellation that
617
+ // arrived while the FFI task was queued must survive that reset.
618
+ if (operation?.aborted && operation.kind === 'generate' && nativeContext !== null) {
619
+ native.cancel(nativeContext, 0);
620
+ }
621
+ }
622
+
623
+ function deliverCallback(listener, event) {
624
+ if (!is.function(listener)) return;
625
+ try {
626
+ const result = listener(event);
627
+ if (result && is.function(result.then)) {
628
+ const pending = Promise.resolve(result).catch(recordCallbackError).finally(
629
+ function callbackSettled() {
630
+ pendingCallbacks.delete(pending);
631
+ }
632
+ );
633
+ pendingCallbacks.add(pending);
634
+ }
635
+ } catch (error) {
636
+ recordCallbackError(error);
637
+ }
638
+ }
639
+
640
+ function recordCallbackError(error) {
641
+ callbackErrors.push(error);
642
+ cancel();
643
+ }
644
+
645
+ async function finishCallbacks() {
646
+ while (pendingCallbacks.size) await Promise.all(pendingCallbacks);
647
+ if (callbackErrors.length) {
648
+ const errors = callbackErrors.splice(0);
649
+ throw new AggregateError(errors, 'Local image callbacks failed.');
650
+ }
651
+ }
652
+
653
+ function begin(kind, signal) {
654
+ if (closed || closing || unloading) throw new Error('The local image library is closing or unloading.');
655
+ if (operation) throw new Error('The shared local image context already has an active operation.');
656
+ const current = {
657
+ kind,
658
+ signal,
659
+ aborted: Boolean(signal?.aborted),
660
+ done: null,
661
+ complete: null,
662
+ abort: cancel
663
+ };
664
+ current.done = new Promise(
665
+ function observeCompletion(resolve) {
666
+ current.complete = resolve;
667
+ }
668
+ );
669
+ operation = current;
670
+ signal?.addEventListener(
671
+ 'abort', current.abort,
672
+ {once: true}
673
+ );
674
+ return current;
675
+ }
676
+
677
+ function end(current) {
678
+ current.signal?.removeEventListener('abort', current.abort);
679
+ operation = undefined;
680
+ current.complete();
681
+ }
682
+
683
+ function requireActive(current) {
684
+ if (current.aborted) {
685
+ const error = imageError(
686
+ 'LOCAL_IMAGE_CANCELLED',
687
+ `Local image ${current.kind} was cancelled.`,
688
+ current.signal?.reason
689
+ );
690
+ error.name = 'AbortError';
691
+ throw error;
692
+ }
693
+ }
694
+
695
+ function cancel() {
696
+ if (!operation) return false;
697
+ operation.aborted = true;
698
+ reassertCancellation();
699
+ return true;
700
+ }
701
+
702
+ async function listDevices() {
703
+ const current = begin('device enumeration');
704
+ try {
705
+ await loadBackends();
706
+ requireActive(current);
707
+ let required = Number(
708
+ await invoke(native.listDevices, null, 0)
709
+ );
710
+ for (;;) {
711
+ // sd_list_devices reports the complete C string length without
712
+ // its terminator. Repeat only if enumeration changed mid-call.
713
+ const buffer = Buffer.alloc(required + 1);
714
+ const observed = Number(
715
+ await invoke(native.listDevices, buffer, buffer.length)
716
+ );
717
+ await finishCallbacks();
718
+ requireActive(current);
719
+ if (observed > required) {
720
+ required = observed;
721
+ continue;
722
+ }
723
+ const text = buffer.toString('utf8', 0, observed);
724
+ const devices = [];
725
+ for (const line of text.split('\n')) {
726
+ if (line === '') continue;
727
+ const separator = line.indexOf('\t');
728
+ devices.push(
729
+ {
730
+ name: separator < 0 ? line : line.substring(0, separator),
731
+ description: separator < 0 ? '' : line.substring(separator + 1)
732
+ }
733
+ );
734
+ }
735
+ return {text, devices};
736
+ }
737
+ } finally {
738
+ end(current);
739
+ }
740
+ }
741
+
742
+ async function load(
743
+ {
744
+ resources = {},
745
+ context = {},
746
+ signal
747
+ } = {}
748
+ ) {
749
+ const current = begin('load', signal);
750
+ const memory = createMemory();
751
+ let created = null;
752
+ let result;
753
+ let failure;
754
+ try {
755
+ requireActive(current);
756
+ await loadBackends();
757
+ requireActive(current);
758
+ await releaseContext();
759
+ requireActive(current);
760
+ const supplied = {...context};
761
+ if (supplied.eager_load === undefined) supplied.eager_load = true;
762
+ for (const [role, path] of Object.entries(resources)) {
763
+ const name = contextType.fields[`${role}_path`] === 'cstring' ? `${role}_path` : role;
764
+ if (contextType.fields[name] !== 'cstring') {
765
+ throw imageError('LOCAL_IMAGE_RESOURCE_UNSUPPORTED', `Unknown native model resource: ${role}.`);
766
+ }
767
+ supplied[name] = path;
768
+ }
769
+ const params = initialize(contextType, native.initContext, supplied, memory, 'context');
770
+ created = await invoke(native.createContext, params);
771
+ await finishCallbacks();
772
+ requireActive(current);
773
+ if (created === null) {
774
+ throw imageError('LOCAL_IMAGE_MODEL_LOAD_FAILED', 'The native image model context could not be loaded.');
775
+ }
776
+ result = {
777
+ modelVersion: native.modelVersion(created),
778
+ // Upstream exposes no getter for the effective auto backend.
779
+ backend: supplied.backend ?? null
780
+ };
781
+ nativeContext = created;
782
+ contextMemory = memory;
783
+ created = null;
784
+ } catch (error) {
785
+ failure = error;
786
+ } finally {
787
+ if (created !== null) {
788
+ try {
789
+ await invoke(native.freeContext, created);
790
+ await finishCallbacks();
791
+ } catch (error) {
792
+ failure = combineErrors(failure, error);
793
+ }
794
+ }
795
+ if (contextMemory !== memory) {
796
+ try {
797
+ memory.release();
798
+ } catch (error) {
799
+ failure = combineErrors(failure, error);
800
+ }
801
+ }
802
+ end(current);
803
+ }
804
+ if (failure) throw failure;
805
+ return result;
806
+ }
807
+
808
+ async function generate(
809
+ {
810
+ prompt,
811
+ parameters = {},
812
+ signal,
813
+ image
814
+ } = {}
815
+ ) {
816
+ const current = begin('generate', signal);
817
+ const memory = createMemory();
818
+ let output;
819
+ let count;
820
+ let outputsInitialized = false;
821
+ let result;
822
+ let failure;
823
+ try {
824
+ requireActive(current);
825
+ if (nativeContext === null) throw new Error('Load an image model before generating an image.');
826
+ if (!is.string(prompt)) throw new TypeError('Image generation requires the complete prompt string.');
827
+ const supplied = {...parameters, prompt};
828
+ if (image !== undefined) supplied.init_image = image;
829
+ const params = initialize(generationType, native.initGeneration, supplied, memory, 'parameters');
830
+ output = memory.allocate('void *');
831
+ koffi.encode(output, 'void *', null);
832
+ count = memory.allocate('int');
833
+ koffi.encode(count, 'int', 0);
834
+ outputsInitialized = true;
835
+ const succeeded = await invoke(native.generate, nativeContext, params, output, count);
836
+ await finishCallbacks();
837
+ requireActive(current);
838
+ if (!succeeded) {
839
+ throw imageError('LOCAL_IMAGE_GENERATION_FAILED', 'The native image generation operation failed.');
840
+ }
841
+ const pointer = koffi.decode(output, 'void *');
842
+ const imageCount = koffi.decode(count, 'int');
843
+ if (pointer === null || imageCount <= 0) {
844
+ throw imageError('LOCAL_IMAGE_GENERATION_FAILED', 'The native image operation returned no images.');
845
+ }
846
+ result = [];
847
+ for (const raster of koffi.decode(pointer, imageType.type, imageCount)) {
848
+ const elements = raster.width * raster.height * raster.channel;
849
+ if (raster.data === null || elements === 0) {
850
+ throw imageError('LOCAL_IMAGE_GENERATION_FAILED', 'The native image operation returned an empty raster.');
851
+ }
852
+ // Copy the entire raster before the upstream allocator frees it.
853
+ const data = new Uint8Array(
854
+ new Uint8Array(
855
+ koffi.view(raster.data, elements)
856
+ )
857
+ );
858
+ result.push(
859
+ {
860
+ width: raster.width,
861
+ height: raster.height,
862
+ channel: raster.channel,
863
+ data
864
+ }
865
+ );
866
+ }
867
+ } catch (error) {
868
+ failure = error;
869
+ } finally {
870
+ if (outputsInitialized) {
871
+ try {
872
+ const pointer = koffi.decode(output, 'void *');
873
+ if (pointer !== null) {
874
+ await invoke(
875
+ native.freeImages, pointer,
876
+ koffi.decode(count, 'int')
877
+ );
878
+ await finishCallbacks();
879
+ }
880
+ } catch (error) {
881
+ failure = combineErrors(failure, error);
882
+ }
883
+ }
884
+ try {
885
+ memory.release();
886
+ } catch (error) {
887
+ failure = combineErrors(failure, error);
888
+ }
889
+ end(current);
890
+ }
891
+ if (failure) throw failure;
892
+ requireActive(current);
893
+ return result;
894
+ }
895
+
896
+ async function releaseContext() {
897
+ const pointer = nativeContext;
898
+ const memory = contextMemory;
899
+ nativeContext = null;
900
+ contextMemory = undefined;
901
+ if (pointer === null) return;
902
+ let failure;
903
+ try {
904
+ await invoke(native.freeContext, pointer);
905
+ await finishCallbacks();
906
+ } catch (error) {
907
+ failure = error;
908
+ }
909
+ try {
910
+ memory.release();
911
+ } catch (error) {
912
+ failure = combineErrors(failure, error);
913
+ }
914
+ if (failure) throw failure;
915
+ }
916
+
917
+ function unload() {
918
+ if (closed) return Promise.resolve();
919
+ if (unloadPromise) return unloadPromise;
920
+ unloading = true;
921
+ unloadPromise = performUnload().finally(
922
+ function unloaded() {
923
+ unloading = false;
924
+ unloadPromise = undefined;
925
+ }
926
+ );
927
+ return unloadPromise;
928
+ }
929
+
930
+ async function performUnload() {
931
+ if (operation) {
932
+ const pending = operation.done;
933
+ cancel();
934
+ await pending;
935
+ }
936
+ await releaseContext();
937
+ await finishCallbacks();
938
+ }
939
+
940
+ function close() {
941
+ if (closePromise) return closePromise;
942
+ closing = true;
943
+ closePromise = performClose();
944
+ return closePromise;
945
+ }
946
+
947
+ async function performClose() {
948
+ let failure;
949
+ try {
950
+ await unload();
951
+ } catch (error) {
952
+ failure = error;
953
+ } finally {
954
+ // Native work and frees have joined before global callback slots
955
+ // are cleared and their registered JS functions are released.
956
+ // GGML intentionally retains dynamic backends; reset its callback
957
+ // into stable-diffusion before that shared library is unloaded.
958
+ native.setBackendLog?.(null, null);
959
+ native.setLog(null, null);
960
+ native.setProgress(null, null);
961
+ koffi.unregister(logCallback);
962
+ koffi.unregister(progressCallback);
963
+ library.unload();
964
+ backendLibrary?.unload();
965
+ backendBaseLibrary?.unload();
966
+ closed = true;
967
+ }
968
+ if (failure) throw failure;
969
+ }
970
+ }
971
+
972
+ function imageError(code, message, cause) {
973
+ const options = cause === undefined ? undefined : {cause};
974
+ const error = new Error(message, options);
975
+ error.code = code;
976
+ return error;
977
+ }
978
+
979
+ function combineErrors(first, second) {
980
+ if (!first) return second;
981
+ return new AggregateError(
982
+ [first, second],
983
+ 'Local image operation and cleanup failed.'
984
+ );
985
+ }