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/src/local-ai/install.mjs
CHANGED
|
@@ -9,6 +9,7 @@ import {ArcaneError,ERROR_CODES,normalizeError,throwIfAborted} from '../errors.m
|
|
|
9
9
|
import {createEventQueue} from '../event-queue.mjs';
|
|
10
10
|
import {runProcess} from '../process.mjs';
|
|
11
11
|
import {extractLocalAIArchive} from './archive.mjs';
|
|
12
|
+
import {normalizeImageRuntimeRequirement} from './config.mjs';
|
|
12
13
|
|
|
13
14
|
const is=new Is(false);
|
|
14
15
|
const installations=new Map();
|
|
@@ -19,12 +20,13 @@ function selectRuntimes(runtimes){
|
|
|
19
20
|
}
|
|
20
21
|
const selected=new Map();
|
|
21
22
|
for(const item of runtimes){
|
|
22
|
-
const
|
|
23
|
-
|
|
23
|
+
const requirement=is.string(item)?{id:item}:item;
|
|
24
|
+
const runtime=requirement?.id==='stable-diffusion.cpp'?normalizeImageRuntimeRequirement(requirement):requirement;
|
|
25
|
+
if(!runtime||!['llama.cpp','ollama','nemo-speech','onnx','stable-diffusion.cpp'].includes(runtime.id)){
|
|
24
26
|
throw new ArcaneError(ERROR_CODES.targetUnavailable,`Local AI runtime installation is unavailable for ${String(runtime?.id)}.`);
|
|
25
27
|
}
|
|
26
28
|
const prior=selected.get(runtime.id);
|
|
27
|
-
if(prior&&(prior.version!==runtime.version||prior.url!==runtime.url)){
|
|
29
|
+
if(prior&&(prior.version!==runtime.version||prior.url!==runtime.url||(runtime.id==='stable-diffusion.cpp'&&prior.backend!==runtime.backend))){
|
|
28
30
|
throw new ArcaneError(ERROR_CODES.usage,`Select one ${runtime.id} runtime version and URL for this operation.`);
|
|
29
31
|
}
|
|
30
32
|
if(runtime.version!==undefined&&(!is.string(runtime.version)||!runtime.version)){
|
|
@@ -82,6 +84,9 @@ async function upstreamResponse(url,signal){
|
|
|
82
84
|
}
|
|
83
85
|
|
|
84
86
|
async function selectRelease(runtime,platform,architecture,signal){
|
|
87
|
+
if (runtime.id === 'stable-diffusion.cpp') {
|
|
88
|
+
return selectImageRelease(runtime, platform, architecture, signal);
|
|
89
|
+
}
|
|
85
90
|
if(runtime.url){
|
|
86
91
|
const name=path.posix.basename(new URL(runtime.url).pathname);
|
|
87
92
|
return {version:runtime.version??'custom',url:runtime.url,name};
|
|
@@ -109,6 +114,243 @@ async function selectRelease(runtime,platform,architecture,signal){
|
|
|
109
114
|
return {version:release.tag_name,url:asset.browser_download_url,name:asset.name};
|
|
110
115
|
}
|
|
111
116
|
|
|
117
|
+
async function imageBackendCandidate(backend, platform, signal) {
|
|
118
|
+
if (backend !== 'auto') return backend;
|
|
119
|
+
if (platform === 'darwin') return 'metal';
|
|
120
|
+
// This selects a distribution candidate. The worker probes actual devices.
|
|
121
|
+
return 'cpu';
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function imageAssetSuffix(platform, architecture, backend) {
|
|
125
|
+
if (platform === 'win32' && architecture === 'x64') {
|
|
126
|
+
if (backend === 'cpu') return '-bin-win-cpu-x64.zip';
|
|
127
|
+
}
|
|
128
|
+
if (platform === 'linux' && architecture === 'x64') {
|
|
129
|
+
if (backend === 'cpu') return '-bin-Linux-Ubuntu-24.04-x86_64.zip';
|
|
130
|
+
}
|
|
131
|
+
if (platform === 'darwin' && ['x64', 'arm64'].includes(architecture) && ['metal', 'cpu'].includes(backend)) {
|
|
132
|
+
// This upstream archive contains the universal arm64/x86_64 build.
|
|
133
|
+
return '-bin-Darwin-macOS-26.6.2-arm64.zip';
|
|
134
|
+
}
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
async function selectImageRelease(runtime, platform, architecture, signal) {
|
|
139
|
+
const backend = runtime.url ? runtime.backend : await imageBackendCandidate(runtime.backend, platform, signal);
|
|
140
|
+
if (runtime.url) {
|
|
141
|
+
return {
|
|
142
|
+
version: runtime.version,
|
|
143
|
+
backend: runtime.backend,
|
|
144
|
+
assets: [
|
|
145
|
+
{
|
|
146
|
+
backend,
|
|
147
|
+
url: runtime.url,
|
|
148
|
+
name: path.posix.basename(new URL(runtime.url).pathname)
|
|
149
|
+
}
|
|
150
|
+
],
|
|
151
|
+
sharedCPU: backend === 'auto'
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
const selection = runtime.version === 'latest' ? 'latest' : `tags/${encodeURIComponent(runtime.version)}`;
|
|
155
|
+
const response = await upstreamResponse(`https://api.github.com/repos/leejet/stable-diffusion.cpp/releases/${selection}`, signal);
|
|
156
|
+
const release = await response.json();
|
|
157
|
+
const backends = backend === 'cpu' || platform === 'darwin' ? [backend] : [backend, 'cpu'];
|
|
158
|
+
const assets = backends.map(
|
|
159
|
+
function selectImageArchive(selectedBackend) {
|
|
160
|
+
const suffix = imageAssetSuffix(platform, architecture, selectedBackend);
|
|
161
|
+
const asset = suffix && release.assets?.find(
|
|
162
|
+
function matchingImageArchive(candidate) {
|
|
163
|
+
return candidate.name.endsWith(suffix);
|
|
164
|
+
}
|
|
165
|
+
);
|
|
166
|
+
if (!asset) {
|
|
167
|
+
throw new ArcaneError(ERROR_CODES.targetUnavailable, `stable-diffusion.cpp ${release.tag_name} has no selected ${platform}/${architecture} ${selectedBackend} archive.`);
|
|
168
|
+
}
|
|
169
|
+
const selected = {backend: selectedBackend, url: asset.browser_download_url, name: asset.name};
|
|
170
|
+
return selected;
|
|
171
|
+
}
|
|
172
|
+
);
|
|
173
|
+
return {version: release.tag_name, backend: runtime.backend, assets, sharedCPU: backend === 'metal'};
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async function imageLibraryPath(root, platform, signal) {
|
|
177
|
+
const filename = platform === 'win32' ? 'stable-diffusion.dll' : platform === 'darwin' ? 'libstable-diffusion.dylib' : 'libstable-diffusion.so';
|
|
178
|
+
const directories = [root];
|
|
179
|
+
for (const directory of directories) {
|
|
180
|
+
throwIfAborted(signal);
|
|
181
|
+
const entries = await readdir(
|
|
182
|
+
directory,
|
|
183
|
+
{withFileTypes: true}
|
|
184
|
+
);
|
|
185
|
+
for (const entry of entries) {
|
|
186
|
+
const selected = path.join(directory, entry.name);
|
|
187
|
+
if (entry.name === filename && (entry.isFile() || entry.isSymbolicLink())) return selected;
|
|
188
|
+
if (entry.isDirectory()) directories.push(selected);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
throw new ArcaneError(ERROR_CODES.operationFailed, `The stable-diffusion.cpp archive does not contain ${filename}.`);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async function extractImageAsset(asset, root, signal, onEvent) {
|
|
195
|
+
const parent = path.dirname(root);
|
|
196
|
+
const archive = path.join(parent, asset.name);
|
|
197
|
+
await onEvent(
|
|
198
|
+
{
|
|
199
|
+
type: 'local-ai.install.downloading',
|
|
200
|
+
message: `Downloading ${asset.name}.`,
|
|
201
|
+
data: {id: 'stable-diffusion.cpp', asset: asset.name}
|
|
202
|
+
}
|
|
203
|
+
);
|
|
204
|
+
const response = await upstreamResponse(asset.url, signal);
|
|
205
|
+
await pipeline(
|
|
206
|
+
Readable.fromWeb(response.body),
|
|
207
|
+
createWriteStream(
|
|
208
|
+
archive,
|
|
209
|
+
{flags: 'wx'}
|
|
210
|
+
),
|
|
211
|
+
{signal}
|
|
212
|
+
);
|
|
213
|
+
await onEvent(
|
|
214
|
+
{
|
|
215
|
+
type: 'local-ai.install.extracting',
|
|
216
|
+
message: `Extracting ${asset.name}.`,
|
|
217
|
+
data: {id: 'stable-diffusion.cpp', asset: asset.name}
|
|
218
|
+
}
|
|
219
|
+
);
|
|
220
|
+
await extractLocalAIArchive(
|
|
221
|
+
{archive, directory: root, signal, onEvent}
|
|
222
|
+
);
|
|
223
|
+
await unlink(archive);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
async function installImageBinding(root, directory, platform, architecture, signal, onEvent) {
|
|
227
|
+
const prefix = path.join(root, 'binding');
|
|
228
|
+
await mkdir(
|
|
229
|
+
prefix,
|
|
230
|
+
{recursive: true}
|
|
231
|
+
);
|
|
232
|
+
await writeFile(
|
|
233
|
+
path.join(prefix, 'package.json'),
|
|
234
|
+
'{"private":true}\n',
|
|
235
|
+
{flag: 'wx', signal}
|
|
236
|
+
);
|
|
237
|
+
await onEvent(
|
|
238
|
+
{
|
|
239
|
+
type: 'local-ai.install.installing',
|
|
240
|
+
message: `Installing koffi 3.3.2 for ${platform}/${architecture}.`,
|
|
241
|
+
data: {id: 'stable-diffusion.cpp', package: 'koffi', version: '3.3.2', platform, architecture}
|
|
242
|
+
}
|
|
243
|
+
);
|
|
244
|
+
await runProcess(
|
|
245
|
+
'npm',
|
|
246
|
+
[
|
|
247
|
+
'install', '--prefix', prefix, '--global=false', '--save-exact',
|
|
248
|
+
'--omit=dev', '--include=optional', '--ignore-scripts', '--no-audit', '--no-fund',
|
|
249
|
+
'--registry=https://registry.npmjs.org/', '--cache', path.join(directory, 'npm-cache'),
|
|
250
|
+
`--os=${platform}`, `--cpu=${architecture}`, 'koffi@3.3.2'
|
|
251
|
+
],
|
|
252
|
+
{cwd: prefix, signal, onEvent}
|
|
253
|
+
);
|
|
254
|
+
const packagePath = path.join(prefix, 'package.json');
|
|
255
|
+
const bindingModulePath = createRequire(packagePath).resolve('koffi');
|
|
256
|
+
const platformPackagePath = path.join(prefix, 'node_modules', '@koromix', `koffi-${platform}-${architecture}`, 'package.json');
|
|
257
|
+
await stat(platformPackagePath);
|
|
258
|
+
return bindingModulePath;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
async function installImageDistribution(release, {root, directory, platform, architecture, signal, onEvent}) {
|
|
262
|
+
await mkdir(
|
|
263
|
+
root,
|
|
264
|
+
{recursive: true}
|
|
265
|
+
);
|
|
266
|
+
const jobs = release.assets.map(
|
|
267
|
+
async function prepareImageVariant(asset) {
|
|
268
|
+
const variantRoot = path.join(root, asset.backend);
|
|
269
|
+
await extractImageAsset(asset, variantRoot, signal, onEvent);
|
|
270
|
+
const libraryPath = await imageLibraryPath(variantRoot, platform, signal);
|
|
271
|
+
return {backend: asset.backend, root: variantRoot, libraryPath};
|
|
272
|
+
}
|
|
273
|
+
);
|
|
274
|
+
const bindingJob = installImageBinding(root, directory, platform, architecture, signal, onEvent);
|
|
275
|
+
jobs.push(bindingJob);
|
|
276
|
+
const outcomes = await Promise.allSettled(jobs);
|
|
277
|
+
const failures = outcomes.filter(
|
|
278
|
+
function imagePreparationFailed(outcome) {
|
|
279
|
+
return outcome.status === 'rejected';
|
|
280
|
+
}
|
|
281
|
+
);
|
|
282
|
+
if (failures.length === 1) throw failures[0].reason;
|
|
283
|
+
if (failures.length > 1) {
|
|
284
|
+
throw new AggregateError(
|
|
285
|
+
failures.map(
|
|
286
|
+
function imagePreparationError(outcome) {
|
|
287
|
+
return outcome.reason;
|
|
288
|
+
}
|
|
289
|
+
),
|
|
290
|
+
'Local image runtime preparation failed.'
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
const bindingModulePath = outcomes.pop().value;
|
|
294
|
+
const variants = outcomes.map(
|
|
295
|
+
function preparedImageVariant(outcome) {
|
|
296
|
+
return outcome.value;
|
|
297
|
+
}
|
|
298
|
+
);
|
|
299
|
+
if (release.sharedCPU) {
|
|
300
|
+
variants.push(
|
|
301
|
+
{...variants[0], backend: 'cpu'}
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
return {
|
|
305
|
+
version: release.version,
|
|
306
|
+
backend: release.backend,
|
|
307
|
+
libraryPath: variants[0].libraryPath,
|
|
308
|
+
bindingModulePath,
|
|
309
|
+
variants
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function imageModelRecords(models) {
|
|
314
|
+
return models.map(
|
|
315
|
+
function preparedImageModel(model) {
|
|
316
|
+
const resources = {};
|
|
317
|
+
for (const [role, resource] of Object.entries(model.resources)) {
|
|
318
|
+
const location = is.string(resource) ? resource : resource?.path;
|
|
319
|
+
resources[role] = is.string(location) ? path.resolve(location) : resource;
|
|
320
|
+
}
|
|
321
|
+
return {...model, resources};
|
|
322
|
+
}
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function bundledImagePath(location, runtimeRoot, relativeRoot) {
|
|
327
|
+
const relative = path.relative(runtimeRoot, location);
|
|
328
|
+
if (relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
|
|
329
|
+
// The model resource owner reports external-path incompatibility.
|
|
330
|
+
return location;
|
|
331
|
+
}
|
|
332
|
+
return path.posix.join(relativeRoot, relative.split(path.sep).join('/'));
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function bundledImageModelResource(resource, descriptor, model, role, runtimeRoot, relativeRoot) {
|
|
336
|
+
if (descriptor && !is.string(descriptor) && is.string(descriptor.url)) {
|
|
337
|
+
// A portable descriptor selects its original URL for post-launch asset
|
|
338
|
+
// preparation rather than carrying this host's working projection.
|
|
339
|
+
const portable = {...descriptor};
|
|
340
|
+
delete portable.path;
|
|
341
|
+
return portable;
|
|
342
|
+
}
|
|
343
|
+
if (!is.string(resource)) return resource;
|
|
344
|
+
const bundled = bundledImagePath(resource, runtimeRoot, relativeRoot);
|
|
345
|
+
if (bundled === resource) {
|
|
346
|
+
throw new ArcaneError(
|
|
347
|
+
ERROR_CODES.usage,
|
|
348
|
+
`Image model ${model.id} resource ${role} is an external native working path and is not portable in this runtime bundle. Prepare it through modelAssets after launch or include it through the explicit nativeResources workflow.`
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
return bundled;
|
|
352
|
+
}
|
|
353
|
+
|
|
112
354
|
async function installedRuntime(base,runtime,platform,architecture,signal){
|
|
113
355
|
let entries;
|
|
114
356
|
try{
|
|
@@ -117,6 +359,9 @@ async function installedRuntime(base,runtime,platform,architecture,signal){
|
|
|
117
359
|
if(error.code==='ENOENT')return null;
|
|
118
360
|
throw error;
|
|
119
361
|
}
|
|
362
|
+
const imageBackend = runtime.id === 'stable-diffusion.cpp' && !runtime.url
|
|
363
|
+
? await imageBackendCandidate(runtime.backend, platform, signal)
|
|
364
|
+
: runtime.backend;
|
|
120
365
|
for(const entry of entries){
|
|
121
366
|
throwIfAborted(signal);
|
|
122
367
|
if(!entry.isDirectory())continue;
|
|
@@ -128,9 +373,38 @@ async function installedRuntime(base,runtime,platform,architecture,signal){
|
|
|
128
373
|
throw error;
|
|
129
374
|
}
|
|
130
375
|
if(installation.requestVersion!==(runtime.version??null)||installation.requestUrl!==(runtime.url??null))continue;
|
|
376
|
+
if (runtime.id === 'stable-diffusion.cpp' && installation.requestBackend !== runtime.backend) continue;
|
|
131
377
|
const record=installation.runtime;
|
|
132
378
|
if(record?.id!==runtime.id||record.platform!==platform||record.architecture!==architecture)continue;
|
|
133
379
|
try{
|
|
380
|
+
if (runtime.id === 'stable-diffusion.cpp') {
|
|
381
|
+
const locations = [record.libraryPath, record.bindingModulePath];
|
|
382
|
+
if (!Array.isArray(record.variants) || record.variants.length === 0) continue;
|
|
383
|
+
if (record.variants[0].backend !== imageBackend) continue;
|
|
384
|
+
for (const variant of record.variants) locations.push(variant.libraryPath);
|
|
385
|
+
const missingLocation = locations.some(
|
|
386
|
+
function missingImageLocation(location) {
|
|
387
|
+
return !is.string(location);
|
|
388
|
+
}
|
|
389
|
+
);
|
|
390
|
+
if (missingLocation) continue;
|
|
391
|
+
const platformPackagePath = path.join(record.root, 'binding', 'node_modules', '@koromix', `koffi-${platform}-${architecture}`, 'package.json');
|
|
392
|
+
locations.push(platformPackagePath);
|
|
393
|
+
const entries = await Promise.all(
|
|
394
|
+
locations.map(
|
|
395
|
+
function inspectImageLocation(location) {
|
|
396
|
+
return stat(location);
|
|
397
|
+
}
|
|
398
|
+
)
|
|
399
|
+
);
|
|
400
|
+
const filesPresent = entries.every(
|
|
401
|
+
function imageFilePresent(info) {
|
|
402
|
+
return info.isFile();
|
|
403
|
+
}
|
|
404
|
+
);
|
|
405
|
+
if (filesPresent) return record;
|
|
406
|
+
continue;
|
|
407
|
+
}
|
|
134
408
|
if(runtime.id==='onnx'){
|
|
135
409
|
if(!is.string(record.root)||!is.string(record.modulePath))continue;
|
|
136
410
|
const locations=await onnxRuntimeModule(record.root,platform,architecture,signal);
|
|
@@ -242,7 +516,8 @@ async function installONNXPackage(runtime,{root,directory,platform,architecture,
|
|
|
242
516
|
}
|
|
243
517
|
|
|
244
518
|
async function installRuntime(runtime,{directory,platform,architecture,signal,onEvent}){
|
|
245
|
-
const
|
|
519
|
+
const runtimeBase=path.join(directory,runtime.id,`${platform}-${architecture}`,encodeURIComponent(runtime.version??'default'));
|
|
520
|
+
const base=runtime.id==='stable-diffusion.cpp'?path.join(runtimeBase,runtime.backend):runtimeBase;
|
|
246
521
|
await onEvent({type:'local-ai.install.starting',message:`Preparing ${runtime.id} for ${platform}/${architecture}.`,data:{id:runtime.id,platform,architecture}});
|
|
247
522
|
const installed=await installedRuntime(base,runtime,platform,architecture,signal);
|
|
248
523
|
if(installed){
|
|
@@ -257,12 +532,17 @@ async function installRuntime(runtime,{directory,platform,architecture,signal,on
|
|
|
257
532
|
await mkdir(base,{recursive:true});
|
|
258
533
|
const attempt=await mkdtemp(path.join(base,'install-'));
|
|
259
534
|
const root=path.join(attempt,'runtime');
|
|
260
|
-
const archive=release?path.join(attempt,release.name):null;
|
|
535
|
+
const archive=release&&runtime.id!=='stable-diffusion.cpp'?path.join(attempt,release.name):null;
|
|
261
536
|
let completed=false;
|
|
262
537
|
try{
|
|
263
538
|
let locations;
|
|
264
539
|
if(runtime.id==='onnx'){
|
|
265
540
|
locations=await installONNXPackage(runtime,{root,directory,platform,architecture,signal,onEvent});
|
|
541
|
+
} else if (runtime.id === 'stable-diffusion.cpp') {
|
|
542
|
+
locations = await installImageDistribution(
|
|
543
|
+
release,
|
|
544
|
+
{root, directory, platform, architecture, signal, onEvent}
|
|
545
|
+
);
|
|
266
546
|
}else{
|
|
267
547
|
await onEvent({type:'local-ai.install.downloading',message:`Downloading ${runtime.id} ${release.version}.`,data:{id:runtime.id,version:release.version,platform,architecture}});
|
|
268
548
|
const response=await upstreamResponse(release.url,signal);
|
|
@@ -276,7 +556,7 @@ async function installRuntime(runtime,{directory,platform,architecture,signal,on
|
|
|
276
556
|
const record={id:runtime.id,platform,architecture,root,...locations,...(runtime.version?{requestedVersion:runtime.version}:{})};
|
|
277
557
|
throwIfAborted(signal);
|
|
278
558
|
if(archive)await unlink(archive);
|
|
279
|
-
await writeFile(path.join(attempt,'installation.json'),`${JSON.stringify({requestVersion:runtime.version??null,requestUrl:runtime.url??null,runtime:record},null,2)}\n`,{flag:'wx',signal});
|
|
559
|
+
await writeFile(path.join(attempt,'installation.json'),`${JSON.stringify({requestVersion:runtime.version??null,requestUrl:runtime.url??null,...(runtime.id==='stable-diffusion.cpp'?{requestBackend:runtime.backend}:{}),runtime:record},null,2)}\n`,{flag:'wx',signal});
|
|
280
560
|
completed=true;
|
|
281
561
|
await onEvent({type:'local-ai.install.completed',message:`${runtime.id} ${record.version} is available.`,data:record});
|
|
282
562
|
return record;
|
|
@@ -296,7 +576,7 @@ async function installRuntime(runtime,{directory,platform,architecture,signal,on
|
|
|
296
576
|
|
|
297
577
|
function shareInstallation(runtime,options){
|
|
298
578
|
throwIfAborted(options.signal);
|
|
299
|
-
const key=JSON.stringify([options.directory,options.platform,options.architecture,runtime.id,runtime.version??null,runtime.url??null]);
|
|
579
|
+
const key=JSON.stringify([options.directory,options.platform,options.architecture,runtime.id,runtime.version??null,runtime.url??null,runtime.id==='stable-diffusion.cpp'?runtime.backend:null]);
|
|
300
580
|
let entry=installations.get(key);
|
|
301
581
|
if(entry?.controller.signal.aborted){
|
|
302
582
|
// The prior cancelled attempt owns its cleanup. A later request waits
|
|
@@ -392,7 +672,11 @@ export async function ensureLocalAIRuntimes({runtimes=[],directory,platform=proc
|
|
|
392
672
|
if(failures.length===1)throw failures[0].reason;
|
|
393
673
|
if(failures.length>1)throw new AggregateError(failures.map(function runtimeFailure(outcome){return outcome.reason;}),'Local AI runtime preparation failed.');
|
|
394
674
|
throwIfAborted(operationSignal);
|
|
395
|
-
return outcomes.map(function completedRuntime(outcome){
|
|
675
|
+
return outcomes.map(function completedRuntime(outcome,index){
|
|
676
|
+
const record=outcome.value;
|
|
677
|
+
if(record.id==='stable-diffusion.cpp')return {...record,models:imageModelRecords(selected[index].models)};
|
|
678
|
+
return record;
|
|
679
|
+
});
|
|
396
680
|
}finally{
|
|
397
681
|
clearInterval(heartbeat);
|
|
398
682
|
await events.drain();
|
|
@@ -441,12 +725,44 @@ export async function bundleLocalAIRuntimes({runtimes=[],directory,outputRoot,pl
|
|
|
441
725
|
const files=[];
|
|
442
726
|
const relative=`runtime/local-ai/${runtime.id}`;
|
|
443
727
|
const destination=path.join(root,relative);
|
|
444
|
-
await copyRuntimeTree(runtime.root,destination,root,files,signal);
|
|
445
728
|
const bundled={...runtime,root:relative};
|
|
446
|
-
for(const field of ['executable','modulePath','includeDirectory','libraryDirectory','binaryDirectory','cmakeDirectory']){
|
|
729
|
+
for(const field of ['executable','modulePath','includeDirectory','libraryDirectory','binaryDirectory','cmakeDirectory','libraryPath','bindingModulePath']){
|
|
447
730
|
if(runtime[field]===undefined)continue;
|
|
448
731
|
bundled[field]=path.posix.join(relative,path.relative(runtime.root,runtime[field]).split(path.sep).join('/'));
|
|
449
732
|
}
|
|
733
|
+
if (runtime.id === 'stable-diffusion.cpp') {
|
|
734
|
+
const requirement = selected.find(
|
|
735
|
+
function selectedImageRequirement(candidate) {
|
|
736
|
+
return candidate.id === runtime.id;
|
|
737
|
+
}
|
|
738
|
+
);
|
|
739
|
+
bundled.variants = runtime.variants.map(
|
|
740
|
+
function bundledImageVariant(variant) {
|
|
741
|
+
return {
|
|
742
|
+
...variant,
|
|
743
|
+
root: bundledImagePath(variant.root, runtime.root, relative),
|
|
744
|
+
libraryPath: bundledImagePath(variant.libraryPath, runtime.root, relative)
|
|
745
|
+
};
|
|
746
|
+
}
|
|
747
|
+
);
|
|
748
|
+
bundled.models = runtime.models.map(
|
|
749
|
+
function bundledImageModel(model, index) {
|
|
750
|
+
const resources = {};
|
|
751
|
+
for (const [role, resource] of Object.entries(model.resources)) {
|
|
752
|
+
resources[role] = bundledImageModelResource(
|
|
753
|
+
resource,
|
|
754
|
+
requirement.models[index].resources[role],
|
|
755
|
+
model,
|
|
756
|
+
role,
|
|
757
|
+
runtime.root,
|
|
758
|
+
relative
|
|
759
|
+
);
|
|
760
|
+
}
|
|
761
|
+
return {...model, resources};
|
|
762
|
+
}
|
|
763
|
+
);
|
|
764
|
+
}
|
|
765
|
+
await copyRuntimeTree(runtime.root,destination,root,files,signal);
|
|
450
766
|
return {runtime:bundled,files};
|
|
451
767
|
}));
|
|
452
768
|
const failures=outcomes.filter(function failedBundle(outcome){return outcome.status==='rejected';});
|
|
@@ -150,6 +150,9 @@ export async function copyCoreRuntime(root, files, signal, {
|
|
|
150
150
|
}
|
|
151
151
|
|
|
152
152
|
export function coreEntrySource(application, version, services, {localAI, runtimes = [], packagedWeb = false} = {}) {
|
|
153
|
+
const imageSelected = localAI?.runtimes?.some(function selectedImage(requirement) {
|
|
154
|
+
return (typeof requirement === 'string' ? requirement : requirement.id) === 'stable-diffusion.cpp';
|
|
155
|
+
}) ?? false;
|
|
153
156
|
const imports = services.map(function serviceImport(service, index) {
|
|
154
157
|
const specifier = '../app/' + service.module.split('/').map(encodeURIComponent).join('/');
|
|
155
158
|
return `import createService${index} from ${JSON.stringify(specifier)};`;
|
|
@@ -159,8 +162,20 @@ export function coreEntrySource(application, version, services, {localAI, runtim
|
|
|
159
162
|
return ` createService${index}(JSON.parse(${JSON.stringify(JSON.stringify(service.options === undefined ? {} : service.options))}), context)`;
|
|
160
163
|
});
|
|
161
164
|
if (localAI !== undefined) {
|
|
165
|
+
const chatConfiguration = {
|
|
166
|
+
...localAI,
|
|
167
|
+
runtimes: localAI.runtimes.filter(function selectedChat(requirement) {
|
|
168
|
+
return (typeof requirement === 'string' ? requirement : requirement.id) !== 'stable-diffusion.cpp';
|
|
169
|
+
})
|
|
170
|
+
};
|
|
162
171
|
imports.push("import {createLocalAIService} from 'arcane-os/core/local-ai';");
|
|
163
|
-
definitions.push(` createLocalAIService(JSON.parse(${JSON.stringify(JSON.stringify(
|
|
172
|
+
definitions.push(` createLocalAIService(JSON.parse(${JSON.stringify(JSON.stringify(chatConfiguration))}), context)`);
|
|
173
|
+
}
|
|
174
|
+
if (imageSelected) {
|
|
175
|
+
imports.push("import {createLocalImageService} from 'arcane-os/core/image';");
|
|
176
|
+
imports.push("import {createModelAssetService} from 'arcane-os/core/model-assets';");
|
|
177
|
+
definitions.push(' modelAssets');
|
|
178
|
+
definitions.push(` createLocalImageService(JSON.parse(${JSON.stringify(JSON.stringify(localAI))}), {...context, modelAssets})`);
|
|
164
179
|
}
|
|
165
180
|
if (packagedWeb) {
|
|
166
181
|
imports.push("import {createPackagedWebService} from 'arcane-os/core/packaged-web';");
|
|
@@ -185,15 +200,26 @@ export function coreEntrySource(application, version, services, {localAI, runtim
|
|
|
185
200
|
...(localAI === undefined ? [] : [
|
|
186
201
|
` runtimes: JSON.parse(${JSON.stringify(JSON.stringify(runtimes))}).map(function runtimeLocation(runtime) {`,
|
|
187
202
|
' const resolved = {...runtime};',
|
|
188
|
-
" for (const field of ['root', 'executable', 'modulePath', 'includeDirectory', 'libraryDirectory', 'binaryDirectory', 'cmakeDirectory']) {",
|
|
203
|
+
" for (const field of ['root', 'executable', 'modulePath', 'includeDirectory', 'libraryDirectory', 'binaryDirectory', 'cmakeDirectory', 'libraryPath', 'bindingModulePath']) {",
|
|
189
204
|
' if (runtime[field] !== undefined) resolved[field] = runtimePath(runtime[field]);',
|
|
190
205
|
' }',
|
|
206
|
+
' if (runtime.variants) resolved.variants = runtime.variants.map(function nativeVariant(variant) {',
|
|
207
|
+
' return {...variant, root: runtimePath(variant.root), libraryPath: runtimePath(variant.libraryPath)};',
|
|
208
|
+
' });',
|
|
209
|
+
' if (runtime.models) resolved.models = runtime.models.map(function nativeModel(model) {',
|
|
210
|
+
' const resources = {};',
|
|
211
|
+
' for (const [role, resource] of Object.entries(model.resources)) {',
|
|
212
|
+
" resources[role] = typeof resource === 'string' ? runtimePath(resource) : resource;",
|
|
213
|
+
' }',
|
|
214
|
+
' return {...model, resources};',
|
|
215
|
+
' });',
|
|
191
216
|
' return resolved;',
|
|
192
217
|
' }),'
|
|
193
218
|
]),
|
|
194
219
|
' ...await readCoreLaunchContext()',
|
|
195
220
|
'};',
|
|
196
221
|
'',
|
|
222
|
+
...(imageSelected ? ['const modelAssets = createModelAssetService({appRoot: context.appRoot});', ''] : []),
|
|
197
223
|
'const host = startCoreHost({',
|
|
198
224
|
` application: JSON.parse(${JSON.stringify(JSON.stringify(application))}),`,
|
|
199
225
|
` version: ${JSON.stringify(version)},`,
|