arcane-os 0.60.0 → 0.62.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +40 -0
- package/README.md +1 -1
- package/browser-runtime/ai/core-image.mjs +392 -0
- package/docs/reference/local-image-editing.md +102 -16
- package/docs/reference/local-image-generation.md +267 -0
- package/docs/reference/model-assets.md +14 -11
- package/docs/reference/sdk-api.md +32 -0
- package/package.json +6 -1
- package/schemas/arcane-app.schema.json +2 -2
- package/schemas/arcane-package.schema.json +2 -2
- package/src/core/services/image.mjs +605 -0
- package/src/core/services/local-ai.mjs +8 -3
- package/src/import-map.mjs +2 -0
- package/src/local-ai/config.mjs +41 -1
- package/src/local-ai/dev.mjs +46 -9
- package/src/local-ai/image-library.mjs +985 -0
- package/src/local-ai/image-models/sd14.mjs +15 -0
- package/src/local-ai/image-png.mjs +74 -0
- package/src/local-ai/image-runtime.mjs +358 -0
- package/src/local-ai/image-worker.mjs +232 -0
- package/src/local-ai/install.mjs +326 -10
- package/src/native/portable-layout.mjs +28 -2
|
@@ -0,0 +1,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
|
+
}
|