@bendyline/gezel-inference 1.0.5 → 1.0.7
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/dist/cache/controller.d.ts +2 -0
- package/dist/cache/controller.js +38 -5
- package/dist/catalog/chat-models/index.json +1 -1
- package/dist/catalog/chat-models/lf/lfm3-1.7b-q4/manifest.json +114 -0
- package/dist/catalog/chat-models/lf/lfm3-1.7b-q4/versions/1.0.0/manifest.json +15 -0
- package/dist/catalog/chat-models/raw-index.json +1 -1
- package/dist/catalog/image-loras/index.json +1 -0
- package/dist/catalog/image-loras/kr/krea-2-ink-wash/manifest.json +31 -0
- package/dist/catalog/image-loras/kr/krea-2-ink-wash/versions/1.0.0/manifest.json +15 -0
- package/dist/catalog/image-loras/kr/krea-2-isometric-3d/manifest.json +32 -0
- package/dist/catalog/image-loras/kr/krea-2-isometric-3d/versions/1.0.0/manifest.json +15 -0
- package/dist/catalog/image-loras/kr/krea-2-kids-drawing/manifest.json +31 -0
- package/dist/catalog/image-loras/kr/krea-2-kids-drawing/versions/1.0.0/manifest.json +15 -0
- package/dist/catalog/image-loras/kr/krea-2-pixel-art/manifest.json +31 -0
- package/dist/catalog/image-loras/kr/krea-2-pixel-art/versions/1.0.0/manifest.json +15 -0
- package/dist/catalog/image-loras/kr/krea-2-soft-watercolor/manifest.json +31 -0
- package/dist/catalog/image-loras/kr/krea-2-soft-watercolor/versions/1.0.0/manifest.json +15 -0
- package/dist/catalog/image-loras/kr/krea-2-turbo-4step/manifest.json +31 -0
- package/dist/catalog/image-loras/kr/krea-2-turbo-4step/versions/1.0.0/manifest.json +15 -0
- package/dist/catalog/image-loras/mo/moss-image-gouache/manifest.json +29 -0
- package/dist/catalog/image-loras/mo/moss-image-gouache/versions/1.0.0/manifest.json +15 -0
- package/dist/catalog/image-loras/mo/moss-image-sft-4step/manifest.json +30 -0
- package/dist/catalog/image-loras/mo/moss-image-sft-4step/versions/1.0.0/manifest.json +15 -0
- package/dist/catalog/image-loras/qw/qwen-image-edit-2511-lightning/manifest.json +31 -0
- package/dist/catalog/image-loras/qw/qwen-image-edit-2511-lightning/versions/1.0.0/manifest.json +15 -0
- package/dist/catalog/image-loras/raw-index.json +1 -0
- package/dist/catalog/image-loras/sd/sdxl-3d-render/manifest.json +29 -0
- package/dist/catalog/image-loras/sd/sdxl-3d-render/versions/1.0.0/manifest.json +15 -0
- package/dist/catalog/image-loras/sd/sdxl-coloring-book/manifest.json +30 -0
- package/dist/catalog/image-loras/sd/sdxl-coloring-book/versions/1.0.0/manifest.json +15 -0
- package/dist/catalog/image-loras/sd/sdxl-papercut/manifest.json +29 -0
- package/dist/catalog/image-loras/sd/sdxl-papercut/versions/1.0.0/manifest.json +15 -0
- package/dist/catalog/image-loras/sd/sdxl-pixel-art/manifest.json +29 -0
- package/dist/catalog/image-loras/sd/sdxl-pixel-art/versions/1.0.0/manifest.json +15 -0
- package/dist/catalog/image-models/fl/flux-2-klein-4b-q4/manifest.json +3 -0
- package/dist/catalog/image-models/index.json +1 -1
- package/dist/catalog/image-models/qw/qwen-image-edit-2511-q4/manifest.json +44 -0
- package/dist/catalog/image-models/qw/qwen-image-edit-2511-q4/versions/1.0.0/manifest.json +36 -0
- package/dist/catalog/image-models/raw-index.json +1 -1
- package/dist/catalog/knowledge-catalogs/index.json +1 -1
- package/dist/catalog/knowledge-catalogs/raw-index.json +1 -1
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-arts/versions/2026.10.2/manifest.json +181 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-astronomy/versions/2026.10.2/manifest.json +181 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-biology/versions/2026.10.2/manifest.json +217 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-business/versions/2026.10.2/manifest.json +197 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-film-tv/versions/2026.10.2/manifest.json +197 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-food-drink/versions/2026.10.2/manifest.json +185 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-games/versions/2026.10.2/manifest.json +181 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-general/manifest.json +55 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-general/versions/2026.10.2/manifest.json +165 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-geography/versions/2026.10.2/manifest.json +217 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-history/versions/2026.10.2/manifest.json +217 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-language/versions/2026.10.2/manifest.json +181 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-law/versions/2026.10.2/manifest.json +185 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-literature/versions/2026.10.2/manifest.json +193 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-mathematics/versions/2026.10.2/manifest.json +189 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-medicine/versions/2026.10.2/manifest.json +185 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-military/versions/2026.10.2/manifest.json +189 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-music/versions/2026.10.2/manifest.json +197 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-people-arts/versions/2026.10.2/manifest.json +209 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-people-business/versions/2026.10.2/manifest.json +189 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-people-entertainment/versions/2026.10.2/manifest.json +201 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-people-military/versions/2026.10.2/manifest.json +185 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-people-other/versions/2026.10.2/manifest.json +165 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-people-politics/versions/2026.10.2/manifest.json +197 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-people-religion/versions/2026.10.2/manifest.json +201 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-people-science/versions/2026.10.2/manifest.json +205 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-people-sports/versions/2026.10.2/manifest.json +237 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-politics/versions/2026.10.2/manifest.json +197 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-religion-philosophy/versions/2026.10.2/manifest.json +189 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-science/versions/2026.10.2/manifest.json +185 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-sports/versions/2026.10.2/manifest.json +193 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-technology/versions/2026.10.2/manifest.json +185 -0
- package/dist/catalog/knowledge-catalogs/wi/wikipedia-transportation/versions/2026.10.2/manifest.json +189 -0
- package/dist/catalog/provenance.json +2 -1
- package/dist/engine-components.js +2 -2
- package/dist/engines/electron-native-reuse.d.ts +7 -1
- package/dist/engines/electron-native-reuse.js +12 -4
- package/dist/engines/native-file-manifest.json +546 -406
- package/dist/engines/native-manifest.d.ts +4 -4
- package/dist/engines/native-manifest.js +36 -36
- package/dist/engines/resolver.d.ts +1 -1
- package/dist/engines/resolver.js +48 -51
- package/dist/http/auth-context.d.ts +12 -0
- package/dist/http/routes/image-ensure-routes.d.ts +4 -0
- package/dist/http/routes/image-ensure-routes.js +102 -0
- package/dist/http/routes/v1-image-styles.d.ts +5 -0
- package/dist/http/routes/v1-image-styles.js +91 -0
- package/dist/http/routes/v1-images.d.ts +1 -0
- package/dist/http/routes/v1-images.js +15 -93
- package/dist/inference-service.js +2 -1
- package/dist/knowledge/worker-host.js +2 -0
- package/dist/machine-engine/inspect-runtime.js +12 -3
- package/dist/memory/embed-core.js +3 -0
- package/dist/memory/embeddings.js +2 -0
- package/dist/models/image-fit.js +2 -2
- package/dist/models/weight-facts.d.ts +5 -0
- package/dist/models/weight-facts.js +5 -2
- package/dist/providers/ds4/build-provider.js +1 -1
- package/dist/providers/ds4/residency.js +3 -3
- package/dist/providers/image/img2img-support.d.ts +36 -1
- package/dist/providers/image/img2img-support.js +30 -1
- package/dist/providers/image/local-engine.d.ts +1 -0
- package/dist/providers/image/local-engine.js +8 -2
- package/dist/providers/image/local-factory.d.ts +10 -1
- package/dist/providers/image/local-factory.js +33 -6
- package/dist/providers/image/mock.d.ts +8 -1
- package/dist/providers/image/mock.js +46 -1
- package/dist/providers/image/pull-registry.d.ts +25 -3
- package/dist/providers/image/pull-registry.js +96 -34
- package/dist/providers/image/sd-cpp.d.ts +63 -4
- package/dist/providers/image/sd-cpp.js +361 -38
- package/dist/providers/image/types.d.ts +76 -4
- package/dist/providers/image/types.js +8 -0
- package/dist/providers/llama-cpp/build-provider.js +3 -2
- package/dist/providers/llama-cpp/cache-adapter.d.ts +3 -0
- package/dist/providers/llama-cpp/cache-adapter.js +18 -4
- package/dist/providers/llama-cpp/devices.js +2 -2
- package/dist/providers/llama-cpp/engine-flags.d.ts +5 -0
- package/dist/providers/llama-cpp/engine-flags.js +5 -2
- package/dist/providers/llama-cpp/provider.js +20 -4
- package/dist/providers/mlx/provider.js +13 -4
- package/dist/providers/mlx/python/gezel_mlx_server.py +9 -2
- package/dist/providers/mlx/python/spec_decode.py +44 -5
- package/dist/providers/native/capacity-broker.d.ts +1 -1
- package/dist/providers/native/capacity-broker.js +8 -8
- package/dist/providers/native/capacity-budget.js +5 -4
- package/dist/providers/native/device-capacity-ledger.d.ts +2 -0
- package/dist/providers/native/device-capacity-ledger.js +36 -5
- package/dist/providers/native/device-capacity.js +4 -3
- package/dist/providers/native/local-engine-runtime.js +4 -4
- package/dist/providers/native/process-identity.d.ts +2 -0
- package/dist/providers/native/process-identity.js +70 -0
- package/dist/relevance/relevance-core.js +2 -0
- package/dist/relevance/relevance-model.js +2 -0
- package/dist/relevance/settings.js +2 -2
- package/dist/runtime-lock.d.ts +43 -5
- package/dist/runtime-lock.js +189 -26
- package/dist/system/host-memory.d.ts +46 -0
- package/dist/system/host-memory.js +89 -0
- package/dist/system/memory.js +5 -4
- package/dist/transformers-cache.js +2 -0
- package/dist/utils/download-common.d.ts +1 -1
- package/package.json +8 -8
|
@@ -19,13 +19,13 @@ import { createHash } from 'node:crypto';
|
|
|
19
19
|
import { createReadStream } from 'node:fs';
|
|
20
20
|
import { mkdir, readFile, readdir, rename, rm, writeFile } from 'node:fs/promises';
|
|
21
21
|
import { join } from 'node:path';
|
|
22
|
-
import { createAwakeTimeout, createLogger, imageModelDefaultSide } from '@bendyline/gezel';
|
|
23
|
-
import { resolveModelDirectory } from '../../models/model-id.js';
|
|
24
|
-
import { MODEL_HASH_READ_BUFFER_BYTES, assertModelStorePathSafe, findModelRoot, hashModelPayloadFiles, listOverlayModelIds, makeSharedModelReadable, modelExistsOnlyReadOnly, readOnlyModelError, verifyReadOnlyModelPayload, } from '../../models/storage-roots.js';
|
|
22
|
+
import { ImageLoraPromptSchema, ImageLoraPurposeSchema, ImageLoraRenderSchema, chooseImageModel, createAwakeTimeout, createLogger, imageModelDefaultSide, } from '@bendyline/gezel';
|
|
23
|
+
import { isSafeModelId, resolveModelDirectory } from '../../models/model-id.js';
|
|
24
|
+
import { MODEL_HASH_READ_BUFFER_BYTES, assertModelStorePathSafe, findModelRoot, hashModelPayloadFiles, listOverlayModelIds, makeSharedModelReadable, modelExistsOnlyReadOnly, readOnlyModelError, removeModelDir, verifyReadOnlyModelPayload, } from '../../models/storage-roots.js';
|
|
25
25
|
import { downloadWithRetry } from '../../utils/download-with-retry.js';
|
|
26
26
|
import { readImageMeta } from '../../utils/image-meta.js';
|
|
27
|
-
import {
|
|
28
|
-
import { ImageCapacityError, ImageGenerationCancelledError, } from './types.js';
|
|
27
|
+
import { resolveImageEditCapability } from './img2img-support.js';
|
|
28
|
+
import { ImageCapacityError, ImageGenerationCancelledError, ImageSourceRequiredError, } from './types.js';
|
|
29
29
|
const log = createLogger('image');
|
|
30
30
|
const DEFAULT_TIMEOUT_MS = 30 * 60 * 1000;
|
|
31
31
|
const DEFAULT_SAMPLE_STEPS = 20;
|
|
@@ -56,6 +56,10 @@ export const MODEL_SAMPLE_STEP_DEFAULTS = new Map([
|
|
|
56
56
|
// The publisher recommends 100; 50 renders the same at half the time
|
|
57
57
|
// (300 s to 148 s per 1024px image on a DGX Spark, image bench 2026-10-19).
|
|
58
58
|
['iris-3b-f32', 50],
|
|
59
|
+
// The publisher recommends 40; at 20 a recolor matched it apart from a
|
|
60
|
+
// hubcap's trim, and a restyle and a two-picture composite held up, in half
|
|
61
|
+
// the time (654 s to 334 s per 1024x768 edit on an RTX Spark N1X, 2026-10-27).
|
|
62
|
+
['qwen-image-edit-2511-q4', 20],
|
|
59
63
|
]);
|
|
60
64
|
/**
|
|
61
65
|
* Classifier-free guidance (CFG) scale per model, applied to the
|
|
@@ -83,6 +87,8 @@ export const MODEL_CFG_DEFAULTS = new Map([
|
|
|
83
87
|
['iris-3b-f32', 3],
|
|
84
88
|
// The model card's recommendation; the server's 7 over-saturates it.
|
|
85
89
|
['animagine-xl-4', 5],
|
|
90
|
+
// The model card's true CFG.
|
|
91
|
+
['qwen-image-edit-2511-q4', 4],
|
|
86
92
|
]);
|
|
87
93
|
/**
|
|
88
94
|
* Whether the engine runs this model with guidance that reads a negative
|
|
@@ -98,6 +104,7 @@ export const MODEL_SAMPLER_DEFAULTS = new Map([
|
|
|
98
104
|
['ming-image-0.1-design-bf16', 'Euler'],
|
|
99
105
|
['moss-image-sft-q4', 'Euler'],
|
|
100
106
|
['iris-3b-f32', 'Euler'],
|
|
107
|
+
['qwen-image-edit-2511-q4', 'Euler'],
|
|
101
108
|
]);
|
|
102
109
|
/**
|
|
103
110
|
* Models whose VAE shows visible tile-boundary seams under
|
|
@@ -114,6 +121,17 @@ export const VAE_TILING_INCOMPATIBLE_MODELS = new Set([
|
|
|
114
121
|
'krea-2-turbo-q4',
|
|
115
122
|
'krea-2-turbo-q6',
|
|
116
123
|
'krea-2-turbo-q8',
|
|
124
|
+
// The same Qwen-Image VAE file as Krea 2.
|
|
125
|
+
'qwen-image-edit-2511-q4',
|
|
126
|
+
]);
|
|
127
|
+
/**
|
|
128
|
+
* `--model-args` a model needs. Qwen-Image-Edit 2511 was trained with its
|
|
129
|
+
* reference latents at timestep zero; upstream warns that edits degrade
|
|
130
|
+
* badly without the flag, and the engine only infers it from a marker
|
|
131
|
+
* tensor not every quantization carries.
|
|
132
|
+
*/
|
|
133
|
+
const MODEL_ARGS = new Map([
|
|
134
|
+
['qwen-image-edit-2511-q4', 'qwen_image_zero_cond_t=true'],
|
|
117
135
|
]);
|
|
118
136
|
/**
|
|
119
137
|
* Models that render the same with EasyCache step reuse. MOSS took 205 s per
|
|
@@ -123,7 +141,11 @@ export const VAE_TILING_INCOMPATIBLE_MODELS = new Set([
|
|
|
123
141
|
*/
|
|
124
142
|
const EASYCACHE_MODELS = new Set(['moss-image-sft-q4']);
|
|
125
143
|
export function sdServerLoadProfile(modelId) {
|
|
126
|
-
const
|
|
144
|
+
const modelArgs = MODEL_ARGS.get(modelId);
|
|
145
|
+
const extras = {
|
|
146
|
+
...(EASYCACHE_MODELS.has(modelId) ? { stepCache: 'easycache' } : {}),
|
|
147
|
+
...(modelArgs ? { modelArgs } : {}),
|
|
148
|
+
};
|
|
127
149
|
if (modelId === 'iris-3b-f32') {
|
|
128
150
|
// Iris generates pixels directly: no VAE to tile. Upstream's Iris recipe
|
|
129
151
|
// enables diffusion flash attention for the 1024px sequence. The
|
|
@@ -134,7 +156,7 @@ export function sdServerLoadProfile(modelId) {
|
|
|
134
156
|
defaultSidePx: imageModelDefaultSide(modelId),
|
|
135
157
|
bf16TensorPattern: '^model\\.diffusion_model\\.',
|
|
136
158
|
diffusionFlashAttention: true,
|
|
137
|
-
...
|
|
159
|
+
...extras,
|
|
138
160
|
};
|
|
139
161
|
}
|
|
140
162
|
// Tiling avoids VAE-decode OOM on large images, but produces visible
|
|
@@ -143,7 +165,7 @@ export function sdServerLoadProfile(modelId) {
|
|
|
143
165
|
vaeDecode: VAE_TILING_INCOMPATIBLE_MODELS.has(modelId) ? 'untiled' : 'tiled',
|
|
144
166
|
defaultSidePx: imageModelDefaultSide(modelId),
|
|
145
167
|
diffusionFlashAttention: false,
|
|
146
|
-
...
|
|
168
|
+
...extras,
|
|
147
169
|
};
|
|
148
170
|
}
|
|
149
171
|
export class StableDiffusionCppProvider {
|
|
@@ -151,6 +173,7 @@ export class StableDiffusionCppProvider {
|
|
|
151
173
|
baseUrl;
|
|
152
174
|
modelsRoot;
|
|
153
175
|
storageRoots;
|
|
176
|
+
loraRoots;
|
|
154
177
|
timeoutMs;
|
|
155
178
|
fetchImpl;
|
|
156
179
|
supervisor;
|
|
@@ -167,6 +190,8 @@ export class StableDiffusionCppProvider {
|
|
|
167
190
|
writableRoot: opts.modelsRoot,
|
|
168
191
|
readOnlyRoots: [],
|
|
169
192
|
};
|
|
193
|
+
if (opts.loraRoots)
|
|
194
|
+
this.loraRoots = opts.loraRoots;
|
|
170
195
|
this.timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
171
196
|
this.fetchImpl = opts.fetchImpl ?? fetch;
|
|
172
197
|
this.supervisor = opts.supervisor;
|
|
@@ -222,14 +247,39 @@ export class StableDiffusionCppProvider {
|
|
|
222
247
|
const { signal } = input;
|
|
223
248
|
if (signal?.aborted)
|
|
224
249
|
throw new ImageGenerationCancelledError();
|
|
225
|
-
const
|
|
250
|
+
const hasSources = (input.inputImages?.length ?? 0) > 0;
|
|
251
|
+
const modelId = input.model ?? (await this.chooseModel(hasSources));
|
|
252
|
+
const installed = await this.readInstalledManifestFields(modelId);
|
|
253
|
+
const capability = resolveImageEditCapability({
|
|
254
|
+
modelId,
|
|
255
|
+
explicit: installed?.supportsImg2Img,
|
|
256
|
+
weightsKind: installed?.weightsKind,
|
|
257
|
+
referenceImages: installed?.referenceImages,
|
|
258
|
+
editOnly: installed?.editOnly,
|
|
259
|
+
});
|
|
260
|
+
// An edit-only model handed nothing to edit, named or the only kind
|
|
261
|
+
// installed, fails here rather than after a launch that loads its weights.
|
|
262
|
+
if (!hasSources) {
|
|
263
|
+
const blocker = capability.editOnly
|
|
264
|
+
? { id: modelId, name: installed?.name }
|
|
265
|
+
: modelId === undefined
|
|
266
|
+
? (await this.listInstalledModels()).find((m) => m.editOnly)
|
|
267
|
+
: undefined;
|
|
268
|
+
if (blocker)
|
|
269
|
+
throw new ImageSourceRequiredError(blocker.name ?? blocker.id ?? 'This model');
|
|
270
|
+
}
|
|
226
271
|
const defaultDimension = imageModelDefaultSide(modelId);
|
|
227
272
|
const width = input.width ?? defaultDimension;
|
|
228
273
|
const height = input.height ?? defaultDimension;
|
|
229
|
-
const
|
|
274
|
+
const resolvedLoras = await this.resolveLoras(modelId, input.loras ?? []);
|
|
275
|
+
let loras = resolvedLoras.loras;
|
|
276
|
+
// A speed LoRA's recipe is what makes its few steps valid, so it wins
|
|
277
|
+
// over a style's.
|
|
278
|
+
const render = loras.find((l) => l.purpose === 'speed')?.render ?? loras.find((l) => l.render)?.render;
|
|
279
|
+
const steps = input.steps ?? render?.steps ?? (await this.defaultSampleSteps(modelId));
|
|
230
280
|
const seed = input.seed ?? Math.floor(Math.random() * 2_147_483_647);
|
|
231
281
|
const started = Date.now();
|
|
232
|
-
log.info(`[sd-cpp] generate model=${modelId ?? 'default'} steps=${steps} explicitSteps=${input.steps !== undefined} size=${width}x${height}`);
|
|
282
|
+
log.info(`[sd-cpp] generate model=${modelId ?? 'default'} steps=${steps} explicitSteps=${input.steps !== undefined} size=${width}x${height}${loras.length > 0 ? ` loras=${loras.map((l) => l.id).join(',')}` : ''}`);
|
|
233
283
|
// Model selection: sd-server serves one model per process. When a
|
|
234
284
|
// request targets a different installed model than the one currently
|
|
235
285
|
// loaded, point resolveLaunch at it (via the shared launchState) and
|
|
@@ -290,7 +340,11 @@ export class StableDiffusionCppProvider {
|
|
|
290
340
|
// `seed`, `cfg_scale`, and `negative_prompt`, so caller overrides
|
|
291
341
|
// actually take effect. `cfg_scale` is set per model (distilled /
|
|
292
342
|
// Flux-flow models need CFG 1; others inherit the server default).
|
|
293
|
-
const cfg = modelId ? MODEL_CFG_DEFAULTS.get(modelId) : undefined;
|
|
343
|
+
const cfg = render?.cfg ?? (modelId ? MODEL_CFG_DEFAULTS.get(modelId) : undefined);
|
|
344
|
+
const settingsNotes = [...resolvedLoras.notes];
|
|
345
|
+
if (loras.length > 0) {
|
|
346
|
+
loras = await this.lorasTheEngineHas(baseUrl, loras, request.signal, settingsNotes);
|
|
347
|
+
}
|
|
294
348
|
let inputImages = input.inputImages ?? [];
|
|
295
349
|
// Capability gate: models whose architecture doesn't honor init
|
|
296
350
|
// latents on the pinned sd-server fall back to txt2img instead of
|
|
@@ -299,40 +353,56 @@ export class StableDiffusionCppProvider {
|
|
|
299
353
|
// bubble) can say so honestly.
|
|
300
354
|
let img2imgSkippedReason;
|
|
301
355
|
if (inputImages.length > 0) {
|
|
302
|
-
const
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
weightsKind: installed?.weightsKind,
|
|
307
|
-
});
|
|
308
|
-
if (!verdict.supported) {
|
|
309
|
-
img2imgSkippedReason = verdict.reason ?? 'model does not support image editing (img2img)';
|
|
356
|
+
const max = capability.maxSourceImages ?? 1;
|
|
357
|
+
if (!capability.editMode) {
|
|
358
|
+
img2imgSkippedReason =
|
|
359
|
+
capability.reason ?? 'model does not support image editing (img2img)';
|
|
310
360
|
log.info(`[sd-cpp] dropping ${inputImages.length} input image(s): ${img2imgSkippedReason}`);
|
|
311
361
|
inputImages = [];
|
|
312
362
|
}
|
|
363
|
+
else if (inputImages.length > max) {
|
|
364
|
+
log.info(`[sd-cpp] received ${inputImages.length} input images; ${modelId} takes ${max}`);
|
|
365
|
+
settingsNotes.push(`Used the first ${max} of ${inputImages.length} source pictures; ${installed?.name ?? modelId} takes ${max}.`);
|
|
366
|
+
inputImages = inputImages.slice(0, max);
|
|
367
|
+
}
|
|
313
368
|
}
|
|
369
|
+
// A LoRA's trigger words or trained instruction go around the prompt
|
|
370
|
+
// here, so neither the person nor a gezel ever writes them.
|
|
371
|
+
const prompt = [
|
|
372
|
+
...loras.map((l) => l.prompt?.prefix),
|
|
373
|
+
withoutEngineDirectives(input.prompt),
|
|
374
|
+
...loras.map((l) => l.prompt?.suffix),
|
|
375
|
+
]
|
|
376
|
+
.filter((part) => Boolean(part?.trim()))
|
|
377
|
+
.join(' ');
|
|
314
378
|
const body = {
|
|
315
|
-
prompt
|
|
316
|
-
negative_prompt: input.negativePrompt ?? '',
|
|
379
|
+
prompt,
|
|
380
|
+
negative_prompt: withoutEngineDirectives(input.negativePrompt ?? ''),
|
|
317
381
|
width,
|
|
318
382
|
height,
|
|
319
383
|
steps,
|
|
320
384
|
seed,
|
|
321
385
|
batch_size: 1,
|
|
386
|
+
// Always present: the server's `lora` field replaces any LoRA list a
|
|
387
|
+
// prompt directive slipped past the strip above.
|
|
388
|
+
lora: loras.map((l) => ({ path: `${l.id}/${l.filename}`, multiplier: l.strength })),
|
|
322
389
|
};
|
|
323
390
|
if (cfg !== undefined)
|
|
324
391
|
body.cfg_scale = cfg;
|
|
325
|
-
const sampler = modelId ? MODEL_SAMPLER_DEFAULTS.get(modelId) : undefined;
|
|
392
|
+
const sampler = render?.sampler ?? (modelId ? MODEL_SAMPLER_DEFAULTS.get(modelId) : undefined);
|
|
326
393
|
if (sampler)
|
|
327
394
|
body.sampler_name = sampler;
|
|
395
|
+
if (render?.scheduler)
|
|
396
|
+
body.scheduler = render.scheduler;
|
|
328
397
|
let endpoint = `${baseUrl}/sdapi/v1/txt2img`;
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
398
|
+
if (inputImages.length > 0 && capability.editMode === 'reference') {
|
|
399
|
+
// A reference editor reads the sources beside the instruction
|
|
400
|
+
// (`extra_images` becomes the engine's ref_images) and renders
|
|
401
|
+
// from noise; `strength` has no meaning there.
|
|
402
|
+
body.extra_images = inputImages.map((image) => Buffer.from(image.data).toString('base64'));
|
|
403
|
+
}
|
|
404
|
+
else if (inputImages.length > 0) {
|
|
405
|
+
// Img2img: one source drives `/sdapi/v1/img2img` via `init_images`.
|
|
336
406
|
const first = inputImages[0];
|
|
337
407
|
body.init_images = [Buffer.from(first.data).toString('base64')];
|
|
338
408
|
if (typeof input.strength === 'number')
|
|
@@ -395,6 +465,8 @@ export class StableDiffusionCppProvider {
|
|
|
395
465
|
heightPx: rendered?.height || height,
|
|
396
466
|
durationMs: Date.now() - started,
|
|
397
467
|
...(img2imgSkippedReason ? { img2imgSkippedReason } : {}),
|
|
468
|
+
...(settingsNotes.length > 0 ? { settingsNotes } : {}),
|
|
469
|
+
...(loras.length > 0 ? { loras: loras.map((l) => ({ id: l.id, name: l.name })) } : {}),
|
|
398
470
|
},
|
|
399
471
|
};
|
|
400
472
|
}
|
|
@@ -415,6 +487,7 @@ export class StableDiffusionCppProvider {
|
|
|
415
487
|
async listInstalledModels() {
|
|
416
488
|
const entries = await listOverlayModelIds(this.storageRoots);
|
|
417
489
|
const out = [];
|
|
490
|
+
const installedLoras = await this.listLoras();
|
|
418
491
|
for (const id of entries) {
|
|
419
492
|
const root = await findModelRoot(this.storageRoots, id);
|
|
420
493
|
if (!root)
|
|
@@ -428,6 +501,13 @@ export class StableDiffusionCppProvider {
|
|
|
428
501
|
if (!(await verifyReadOnlyModelPayload(this.storageRoots, root, id, parsed.fileSha256))) {
|
|
429
502
|
continue;
|
|
430
503
|
}
|
|
504
|
+
const capability = resolveImageEditCapability({
|
|
505
|
+
modelId: parsed.id,
|
|
506
|
+
explicit: parsed.supportsImg2Img,
|
|
507
|
+
weightsKind: parsed.weightsKind,
|
|
508
|
+
referenceImages: parseReferenceImages(parsed.referenceImages),
|
|
509
|
+
editOnly: typeof parsed.editOnly === 'boolean' ? parsed.editOnly : undefined,
|
|
510
|
+
});
|
|
431
511
|
out.push({
|
|
432
512
|
id: parsed.id,
|
|
433
513
|
name: parsed.name,
|
|
@@ -437,6 +517,11 @@ export class StableDiffusionCppProvider {
|
|
|
437
517
|
...(typeof parsed.supportsImg2Img === 'boolean'
|
|
438
518
|
? { supportsImg2Img: parsed.supportsImg2Img }
|
|
439
519
|
: {}),
|
|
520
|
+
...(capability.editMode
|
|
521
|
+
? { editMode: capability.editMode, maxSourceImages: capability.maxSourceImages }
|
|
522
|
+
: {}),
|
|
523
|
+
...(capability.editOnly ? { editOnly: true } : {}),
|
|
524
|
+
...lorasFitting(installedLoras, parsed.id),
|
|
440
525
|
});
|
|
441
526
|
}
|
|
442
527
|
catch {
|
|
@@ -446,6 +531,147 @@ export class StableDiffusionCppProvider {
|
|
|
446
531
|
out.sort((a, b) => a.name.localeCompare(b.name));
|
|
447
532
|
return out;
|
|
448
533
|
}
|
|
534
|
+
/** Installed LoRAs, from the root the engine reads; none when this engine takes none. */
|
|
535
|
+
async listLoras() {
|
|
536
|
+
if (!this.loraRoots)
|
|
537
|
+
return [];
|
|
538
|
+
const out = [];
|
|
539
|
+
for (const lora of await readInstalledLoras(this.loraRoots.writableRoot)) {
|
|
540
|
+
out.push({
|
|
541
|
+
id: lora.id,
|
|
542
|
+
name: lora.name,
|
|
543
|
+
purpose: lora.purpose,
|
|
544
|
+
...(lora.render?.steps !== undefined ? { steps: lora.render.steps } : {}),
|
|
545
|
+
baseModels: lora.baseModels,
|
|
546
|
+
approxSizeBytes: lora.approxSizeBytes,
|
|
547
|
+
installedAt: lora.installedAt,
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
return out.sort((a, b) => a.name.localeCompare(b.name));
|
|
551
|
+
}
|
|
552
|
+
/**
|
|
553
|
+
* Install one LoRA: a single safetensors file, sha256-checked like a model,
|
|
554
|
+
* beside a `manifest.json` that keeps the catalog's recipe so the engine can
|
|
555
|
+
* apply it without the catalog.
|
|
556
|
+
*/
|
|
557
|
+
async *pullLora(id, spec, signal) {
|
|
558
|
+
const fail = (error) => [
|
|
559
|
+
{ type: 'error', error },
|
|
560
|
+
{ type: 'done', id },
|
|
561
|
+
];
|
|
562
|
+
// A fixed short name, not the upstream one: sd-server lists a file it
|
|
563
|
+
// cannot open past Windows' 260-character path limit, then draws without
|
|
564
|
+
// it, so a long name in a deep home made a style silently do nothing.
|
|
565
|
+
const filename = 'lora.safetensors';
|
|
566
|
+
if (!this.loraRoots) {
|
|
567
|
+
yield* fail('This image engine does not take styles or fast mode.');
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
if (!isSafeModelId(id) || !/\.safetensors$/i.test(extractFilename(spec.downloadUrl))) {
|
|
571
|
+
yield* fail('Styles and fast mode install only as safetensors files.');
|
|
572
|
+
return;
|
|
573
|
+
}
|
|
574
|
+
const root = this.loraRoots.writableRoot;
|
|
575
|
+
const itemDir = join(root, id);
|
|
576
|
+
await mkdir(root, { recursive: true });
|
|
577
|
+
await assertModelStorePathSafe(root, itemDir);
|
|
578
|
+
await mkdir(itemDir, { recursive: true });
|
|
579
|
+
await assertModelStorePathSafe(root, itemDir);
|
|
580
|
+
const result = yield* this.downloadFile({
|
|
581
|
+
url: spec.downloadUrl,
|
|
582
|
+
destPath: join(itemDir, filename),
|
|
583
|
+
expectedSha256: spec.sha256,
|
|
584
|
+
approxSizeBytes: spec.approxSizeBytes,
|
|
585
|
+
writtenSoFar: 0,
|
|
586
|
+
totalAllBytes: spec.approxSizeBytes,
|
|
587
|
+
// The size is pinned beside the sha256: nothing larger is this file.
|
|
588
|
+
maxBytes: spec.approxSizeBytes + 1024 * 1024,
|
|
589
|
+
...(signal ? { signal } : {}),
|
|
590
|
+
});
|
|
591
|
+
if (result.kind === 'error') {
|
|
592
|
+
yield* fail(result.error);
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
await writeFile(join(itemDir, 'manifest.json'), JSON.stringify({
|
|
596
|
+
id,
|
|
597
|
+
name: spec.name,
|
|
598
|
+
filename,
|
|
599
|
+
approxSizeBytes: result.actualSize,
|
|
600
|
+
sha256: spec.sha256.toLowerCase(),
|
|
601
|
+
baseModels: spec.baseModels,
|
|
602
|
+
purpose: spec.purpose,
|
|
603
|
+
strength: spec.strength,
|
|
604
|
+
...(spec.prompt ? { prompt: spec.prompt } : {}),
|
|
605
|
+
...(spec.render ? { render: spec.render } : {}),
|
|
606
|
+
fileSha256: await hashModelPayloadFiles(itemDir),
|
|
607
|
+
installedAt: new Date().toISOString(),
|
|
608
|
+
}, null, 2), 'utf8');
|
|
609
|
+
await makeSharedModelReadable(itemDir);
|
|
610
|
+
yield { type: 'progress', bytesWritten: result.writtenAll, totalBytes: spec.approxSizeBytes };
|
|
611
|
+
yield { type: 'done', id };
|
|
612
|
+
}
|
|
613
|
+
async deleteLora(id) {
|
|
614
|
+
if (!this.loraRoots)
|
|
615
|
+
return;
|
|
616
|
+
const root = this.loraRoots.writableRoot;
|
|
617
|
+
await removeModelDir(resolveModelDirectory(root, id), root);
|
|
618
|
+
}
|
|
619
|
+
/**
|
|
620
|
+
* The LoRAs a render asked for that this engine can apply to `modelId`:
|
|
621
|
+
* installed in the root the engine reads and checked on that model. The
|
|
622
|
+
* rest are dropped with a note, never an error.
|
|
623
|
+
*/
|
|
624
|
+
async resolveLoras(modelId, requested) {
|
|
625
|
+
if (requested.length === 0)
|
|
626
|
+
return { loras: [], notes: [] };
|
|
627
|
+
if (!this.loraRoots) {
|
|
628
|
+
return { loras: [], notes: ['This image engine takes no styles or fast mode.'] };
|
|
629
|
+
}
|
|
630
|
+
const loras = [];
|
|
631
|
+
const notes = [];
|
|
632
|
+
for (const { id } of requested) {
|
|
633
|
+
if (loras.some((l) => l.id === id))
|
|
634
|
+
continue;
|
|
635
|
+
const lora = await this.readInstalledLora(id);
|
|
636
|
+
if (!lora) {
|
|
637
|
+
notes.push(`${id} is not installed here, so the picture was drawn without it.`);
|
|
638
|
+
}
|
|
639
|
+
else if (!modelId || !lora.baseModels.includes(modelId)) {
|
|
640
|
+
notes.push(`${lora.name} does not fit ${modelId ?? 'this model'}, so it was left out.`);
|
|
641
|
+
}
|
|
642
|
+
else {
|
|
643
|
+
loras.push(lora);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
return { loras, notes };
|
|
647
|
+
}
|
|
648
|
+
/**
|
|
649
|
+
* Keep the LoRAs the running engine can see. sd-server fills its LoRA list
|
|
650
|
+
* only when it is asked for it, and answers `invalid lora path` for any LoRA
|
|
651
|
+
* named before then, so the list is read before every LoRA render.
|
|
652
|
+
*/
|
|
653
|
+
async lorasTheEngineHas(baseUrl, loras, signal, notes) {
|
|
654
|
+
let listed;
|
|
655
|
+
try {
|
|
656
|
+
const res = await this.fetchImpl(`${baseUrl}/sdapi/v1/loras`, { signal });
|
|
657
|
+
const body = res.ok ? (await res.json()) : [];
|
|
658
|
+
listed = new Set((Array.isArray(body) ? body : []).flatMap((e) => typeof e.path === 'string' ? [e.path] : []));
|
|
659
|
+
}
|
|
660
|
+
catch (err) {
|
|
661
|
+
if (signal.aborted)
|
|
662
|
+
throw err;
|
|
663
|
+
listed = new Set();
|
|
664
|
+
}
|
|
665
|
+
return loras.filter((l) => {
|
|
666
|
+
if (listed.has(`${l.id}/${l.filename}`))
|
|
667
|
+
return true;
|
|
668
|
+
notes.push(`The image engine could not load ${l.name}, so it was left out.`);
|
|
669
|
+
return false;
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
async readInstalledLora(id) {
|
|
673
|
+
return this.loraRoots ? readInstalledLora(this.loraRoots.writableRoot, id) : undefined;
|
|
674
|
+
}
|
|
449
675
|
async *pullModel(id, spec, signal) {
|
|
450
676
|
const itemDir = join(this.modelsRoot, id);
|
|
451
677
|
await mkdir(this.modelsRoot, { recursive: true });
|
|
@@ -506,6 +732,8 @@ export class StableDiffusionCppProvider {
|
|
|
506
732
|
weightsFilename,
|
|
507
733
|
weightsKind: spec.weightsKind,
|
|
508
734
|
...(spec.supportsImg2Img !== undefined ? { supportsImg2Img: spec.supportsImg2Img } : {}),
|
|
735
|
+
...(spec.referenceImages ? { referenceImages: spec.referenceImages } : {}),
|
|
736
|
+
...(spec.editOnly !== undefined ? { editOnly: spec.editOnly } : {}),
|
|
509
737
|
weightsSizeBytes: weightsActualSize,
|
|
510
738
|
sha256: spec.sha256.toLowerCase(),
|
|
511
739
|
auxiliaryFiles: auxiliaryRecords,
|
|
@@ -535,6 +763,7 @@ export class StableDiffusionCppProvider {
|
|
|
535
763
|
destPath,
|
|
536
764
|
approxSizeBytes,
|
|
537
765
|
fetchImpl: this.fetchImpl,
|
|
766
|
+
...(opts.maxBytes !== undefined ? { maxBytes: opts.maxBytes } : {}),
|
|
538
767
|
...(signal ? { signal } : {}),
|
|
539
768
|
});
|
|
540
769
|
let writtenThisFile = 0;
|
|
@@ -665,16 +894,19 @@ export class StableDiffusionCppProvider {
|
|
|
665
894
|
await this.supervisor?.stop();
|
|
666
895
|
}
|
|
667
896
|
/**
|
|
668
|
-
* The
|
|
669
|
-
*
|
|
670
|
-
* {@link listInstalledModels} so the default is
|
|
897
|
+
* The model a fresh picture renders with: the configured default when it
|
|
898
|
+
* is installed and draws, else the first installed model that draws. Stable
|
|
899
|
+
* ordering matches {@link listInstalledModels} so the default is
|
|
900
|
+
* deterministic.
|
|
671
901
|
*/
|
|
672
902
|
async resolveDefaultModelId() {
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
903
|
+
return this.chooseModel(false);
|
|
904
|
+
}
|
|
905
|
+
async chooseModel(hasSources) {
|
|
906
|
+
return chooseImageModel(await this.listInstalledModels(), {
|
|
907
|
+
defaultModelId: this.defaultModelId,
|
|
908
|
+
hasSources,
|
|
909
|
+
});
|
|
678
910
|
}
|
|
679
911
|
async defaultSampleSteps(modelId) {
|
|
680
912
|
if (!modelId)
|
|
@@ -703,7 +935,9 @@ export class StableDiffusionCppProvider {
|
|
|
703
935
|
return null;
|
|
704
936
|
const raw = await readFile(join(root, modelId, 'manifest.json'), 'utf8');
|
|
705
937
|
const parsed = JSON.parse(raw);
|
|
938
|
+
const referenceImages = parseReferenceImages(parsed.referenceImages);
|
|
706
939
|
return {
|
|
940
|
+
...(typeof parsed.name === 'string' ? { name: parsed.name } : {}),
|
|
707
941
|
...(typeof parsed.recommendedSteps === 'number'
|
|
708
942
|
? { recommendedSteps: parsed.recommendedSteps }
|
|
709
943
|
: {}),
|
|
@@ -713,6 +947,8 @@ export class StableDiffusionCppProvider {
|
|
|
713
947
|
...(typeof parsed.supportsImg2Img === 'boolean'
|
|
714
948
|
? { supportsImg2Img: parsed.supportsImg2Img }
|
|
715
949
|
: {}),
|
|
950
|
+
...(referenceImages ? { referenceImages } : {}),
|
|
951
|
+
...(typeof parsed.editOnly === 'boolean' ? { editOnly: parsed.editOnly } : {}),
|
|
716
952
|
};
|
|
717
953
|
}
|
|
718
954
|
catch {
|
|
@@ -742,6 +978,93 @@ const RECENT_ENGINE_LINES = 40;
|
|
|
742
978
|
* answers the request with a bare `generate_image returned no results`.
|
|
743
979
|
*/
|
|
744
980
|
const ENGINE_MEMORY_SHORTFALL = /cannot make enough memory available|out of (?:device )?memory|ErrorOutOfDeviceMemory|failed to allocate/i;
|
|
981
|
+
/**
|
|
982
|
+
* sd-server reads `<sd_cpp_extra_args>{json}</sd_cpp_extra_args>` out of a
|
|
983
|
+
* prompt and applies the JSON as generation parameters, including LoRA and
|
|
984
|
+
* ADetailer model paths it never checks. Prompts come from gezels reading
|
|
985
|
+
* untrusted content and from other apps, so the tag never reaches the engine.
|
|
986
|
+
* Repeated until stable: removing one tag can join the pieces around it into
|
|
987
|
+
* another.
|
|
988
|
+
*/
|
|
989
|
+
export function withoutEngineDirectives(text) {
|
|
990
|
+
const tag = /<\s*\/?\s*sd_cpp_extra_args\s*>/gi;
|
|
991
|
+
let out = text;
|
|
992
|
+
let previous;
|
|
993
|
+
do {
|
|
994
|
+
previous = out;
|
|
995
|
+
out = out.replace(tag, '');
|
|
996
|
+
} while (out !== previous);
|
|
997
|
+
return out;
|
|
998
|
+
}
|
|
999
|
+
/**
|
|
1000
|
+
* Read an installed LoRA's manifest, or nothing when it is malformed. The
|
|
1001
|
+
* filename must be a bare `.safetensors` name: the engine resolves it inside
|
|
1002
|
+
* its LoRA folder, and it also loads pickle files.
|
|
1003
|
+
*/
|
|
1004
|
+
function parseInstalledLora(id, raw) {
|
|
1005
|
+
if (!raw || typeof raw !== 'object')
|
|
1006
|
+
return undefined;
|
|
1007
|
+
const m = raw;
|
|
1008
|
+
const purpose = ImageLoraPurposeSchema.safeParse(m.purpose);
|
|
1009
|
+
if (typeof m.name !== 'string' ||
|
|
1010
|
+
typeof m.filename !== 'string' ||
|
|
1011
|
+
!/^[^/\\]+\.safetensors$/i.test(m.filename) ||
|
|
1012
|
+
!purpose.success ||
|
|
1013
|
+
!Array.isArray(m.baseModels)) {
|
|
1014
|
+
return undefined;
|
|
1015
|
+
}
|
|
1016
|
+
const prompt = ImageLoraPromptSchema.safeParse(m.prompt);
|
|
1017
|
+
const render = ImageLoraRenderSchema.safeParse(m.render);
|
|
1018
|
+
return {
|
|
1019
|
+
id,
|
|
1020
|
+
name: m.name,
|
|
1021
|
+
filename: m.filename,
|
|
1022
|
+
purpose: purpose.data,
|
|
1023
|
+
strength: typeof m.strength === 'number' && m.strength > 0 ? m.strength : 1,
|
|
1024
|
+
baseModels: m.baseModels.filter((b) => typeof b === 'string'),
|
|
1025
|
+
...(m.prompt !== undefined && prompt.success ? { prompt: prompt.data } : {}),
|
|
1026
|
+
...(m.render !== undefined && render.success ? { render: render.data } : {}),
|
|
1027
|
+
approxSizeBytes: typeof m.approxSizeBytes === 'number' ? m.approxSizeBytes : 0,
|
|
1028
|
+
installedAt: typeof m.installedAt === 'string' ? m.installedAt : '',
|
|
1029
|
+
};
|
|
1030
|
+
}
|
|
1031
|
+
async function readInstalledLora(loraRoot, id) {
|
|
1032
|
+
if (!isSafeModelId(id))
|
|
1033
|
+
return undefined;
|
|
1034
|
+
try {
|
|
1035
|
+
const raw = await readFile(join(loraRoot, id, 'manifest.json'), 'utf8');
|
|
1036
|
+
return parseInstalledLora(id, JSON.parse(raw));
|
|
1037
|
+
}
|
|
1038
|
+
catch {
|
|
1039
|
+
return undefined;
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
/** Every LoRA installed under `loraRoot`, skipping malformed ones. */
|
|
1043
|
+
export async function readInstalledLoras(loraRoot) {
|
|
1044
|
+
const ids = await listOverlayModelIds({ writableRoot: loraRoot, readOnlyRoots: [] }).catch(() => []);
|
|
1045
|
+
const out = [];
|
|
1046
|
+
for (const id of ids) {
|
|
1047
|
+
const lora = await readInstalledLora(loraRoot, id);
|
|
1048
|
+
if (lora)
|
|
1049
|
+
out.push(lora);
|
|
1050
|
+
}
|
|
1051
|
+
return out;
|
|
1052
|
+
}
|
|
1053
|
+
function lorasFitting(installed, modelId) {
|
|
1054
|
+
const loras = installed
|
|
1055
|
+
.filter((l) => l.baseModels.includes(modelId))
|
|
1056
|
+
.map(({ id, name, purpose, steps }) => ({
|
|
1057
|
+
id,
|
|
1058
|
+
name,
|
|
1059
|
+
purpose,
|
|
1060
|
+
...(steps !== undefined ? { steps } : {}),
|
|
1061
|
+
}));
|
|
1062
|
+
return loras.length > 0 ? { loras } : {};
|
|
1063
|
+
}
|
|
1064
|
+
function parseReferenceImages(value) {
|
|
1065
|
+
const max = value?.max;
|
|
1066
|
+
return typeof max === 'number' && Number.isInteger(max) && max > 0 ? { max } : undefined;
|
|
1067
|
+
}
|
|
745
1068
|
function emitProgress(input, progress) {
|
|
746
1069
|
try {
|
|
747
1070
|
input.onProgress?.(progress);
|