@bendyline/gezel-inference 1.0.6 → 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.js +1 -0
- 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/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
|
@@ -148,10 +148,13 @@ export function imageCatalogWeightFiles(manifest) {
|
|
|
148
148
|
}
|
|
149
149
|
/** The weight files of an installed sd-cpp model, headers read from disk. */
|
|
150
150
|
export async function localSdCppWeightFiles(model) {
|
|
151
|
-
|
|
151
|
+
return localWeightFiles([
|
|
152
152
|
{ component: sdCppMainComponent(model.weightsKind), path: model.weightsPath },
|
|
153
153
|
...model.auxiliaryFiles.map((a) => ({ component: a.role, path: a.path })),
|
|
154
|
-
];
|
|
154
|
+
]);
|
|
155
|
+
}
|
|
156
|
+
/** Weight files on disk, priced from their headers where they are safetensors. */
|
|
157
|
+
export async function localWeightFiles(entries) {
|
|
155
158
|
const out = [];
|
|
156
159
|
for (const entry of entries) {
|
|
157
160
|
const format = weightFileFormat(entry.path);
|
|
@@ -123,7 +123,7 @@ export async function buildDs4Provider(opts) {
|
|
|
123
123
|
// This tier assumes DeepSeek-V4's small resident footprint. A model whose
|
|
124
124
|
// non-routed weights are much larger caps it further via the catalog's
|
|
125
125
|
// `ds4.maxLaunchCtx`; see `resolveDs4LaunchCtx` below.
|
|
126
|
-
const totalRamBytes = (await import('
|
|
126
|
+
const totalRamBytes = (await import('../../system/host-memory.js')).totalMemoryBytes();
|
|
127
127
|
const totalRamGb = totalRamBytes / 1024 ** 3;
|
|
128
128
|
const ramTieredCtx = ds4RamTieredContext(totalRamBytes);
|
|
129
129
|
const ds4ConstrainedToolNoSignalMs = (() => {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { totalmem } from 'node:os';
|
|
2
1
|
import { DS4_FULL_RESIDENCY_HEADROOM_BYTES, LLAMA_CPP_VISION_COMPUTE_BYTES, LLAMA_CPP_WEIGHTS_MULTIPLIER, ds4FitsFullResidency, } from '@bendyline/gezel';
|
|
2
|
+
import { totalMemoryBytes } from '../../system/host-memory.js';
|
|
3
3
|
const GB = 1024 ** 3;
|
|
4
4
|
export { DS4_FULL_RESIDENCY_HEADROOM_BYTES };
|
|
5
5
|
/**
|
|
@@ -43,7 +43,7 @@ export function ds4BaseResidentBytes(opts) {
|
|
|
43
43
|
export function canUseDs4FullResidency(opts = {}) {
|
|
44
44
|
return ds4FitsFullResidency({
|
|
45
45
|
modelSizeBytes: opts.modelSizeBytes,
|
|
46
|
-
totalRamBytes: opts.totalRamBytes ??
|
|
46
|
+
totalRamBytes: opts.totalRamBytes ?? totalMemoryBytes(),
|
|
47
47
|
companionBytes: opts.companionBytes,
|
|
48
48
|
platform: opts.platform ?? process.platform,
|
|
49
49
|
arch: opts.arch ?? process.arch,
|
|
@@ -82,7 +82,7 @@ export function shouldUseDs4SsdStreaming(opts = {}) {
|
|
|
82
82
|
* state + the cache cannot consume the runtime/OS reserve.
|
|
83
83
|
*/
|
|
84
84
|
export function planDs4ExpertCache(opts) {
|
|
85
|
-
const totalRamBytes = opts.totalRamBytes ??
|
|
85
|
+
const totalRamBytes = opts.totalRamBytes ?? totalMemoryBytes();
|
|
86
86
|
const catalogCacheBytes = opts.catalogCacheBytes ?? 0;
|
|
87
87
|
const fixedResidentBytes = Math.max(0, (opts.catalogResidentBytes ?? 0) - catalogCacheBytes);
|
|
88
88
|
const fallbackGb = totalRamBytes >= 120 * GB ? 64 : totalRamBytes >= 88 * GB ? 48 : 32;
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Per-model
|
|
2
|
+
* Per-model image-editing capability for the local sd-cpp engine: whether a
|
|
3
|
+
* model re-noises a source (img2img), reads sources as references beside an
|
|
4
|
+
* instruction, or cannot use one. The img2img half first.
|
|
3
5
|
*
|
|
4
6
|
* stable-diffusion.cpp's `/sdapi/v1/img2img` endpoint accepts
|
|
5
7
|
* `init_images` for any loaded model, but whether the *architecture*
|
|
@@ -37,4 +39,37 @@ export interface Img2ImgSupportVerdict {
|
|
|
37
39
|
/** Human-readable reason, set only when unsupported. */
|
|
38
40
|
reason?: string;
|
|
39
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* Models that edit by reading source pictures as references beside an
|
|
44
|
+
* instruction (sd-server's `extra_images`), rather than re-noising one. The
|
|
45
|
+
* same ladder as above: a catalog `referenceImages` declaration persisted at
|
|
46
|
+
* pull time wins, then this map, which is how installs that predate the
|
|
47
|
+
* field learn it.
|
|
48
|
+
*
|
|
49
|
+
* FLUX.2 Klein: verified on the pinned engine (native sd-cpp master-975) on
|
|
50
|
+
* an RTX Spark, 2026-10-27, recoloring, adding an object and composing two
|
|
51
|
+
* sources at 4 steps in 12 s.
|
|
52
|
+
*/
|
|
53
|
+
export declare const MODEL_REFERENCE_EDIT_SUPPORT: Map<string, {
|
|
54
|
+
maxImages: number;
|
|
55
|
+
editOnly?: boolean;
|
|
56
|
+
}>;
|
|
57
|
+
export interface ImageEditCapabilityInput extends Img2ImgSupportInput {
|
|
58
|
+
/** Explicit `referenceImages` from catalog/installed metadata. */
|
|
59
|
+
referenceImages?: {
|
|
60
|
+
max: number;
|
|
61
|
+
} | undefined;
|
|
62
|
+
/** Explicit `editOnly` from catalog/installed metadata. */
|
|
63
|
+
editOnly?: boolean | undefined;
|
|
64
|
+
}
|
|
65
|
+
export interface ImageEditCapability {
|
|
66
|
+
/** Absent when the model cannot use a source picture at all. */
|
|
67
|
+
editMode?: 'reference' | 'img2img';
|
|
68
|
+
/** Sources one edit takes: the reference limit, or 1 for img2img. */
|
|
69
|
+
maxSourceImages?: number;
|
|
70
|
+
editOnly: boolean;
|
|
71
|
+
/** Why the model cannot edit; set only when `editMode` is absent. */
|
|
72
|
+
reason?: string;
|
|
73
|
+
}
|
|
74
|
+
export declare function resolveImageEditCapability(input: ImageEditCapabilityInput): ImageEditCapability;
|
|
40
75
|
export declare function resolveImg2ImgSupport(input: Img2ImgSupportInput): Img2ImgSupportVerdict;
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Per-model
|
|
2
|
+
* Per-model image-editing capability for the local sd-cpp engine: whether a
|
|
3
|
+
* model re-noises a source (img2img), reads sources as references beside an
|
|
4
|
+
* instruction, or cannot use one. The img2img half first.
|
|
3
5
|
*
|
|
4
6
|
* stable-diffusion.cpp's `/sdapi/v1/img2img` endpoint accepts
|
|
5
7
|
* `init_images` for any loaded model, but whether the *architecture*
|
|
@@ -44,6 +46,33 @@ export const MODEL_IMG2IMG_SUPPORT = new Map([
|
|
|
44
46
|
['krea-2-turbo-q6', false],
|
|
45
47
|
['krea-2-turbo-q8', false],
|
|
46
48
|
]);
|
|
49
|
+
/**
|
|
50
|
+
* Models that edit by reading source pictures as references beside an
|
|
51
|
+
* instruction (sd-server's `extra_images`), rather than re-noising one. The
|
|
52
|
+
* same ladder as above: a catalog `referenceImages` declaration persisted at
|
|
53
|
+
* pull time wins, then this map, which is how installs that predate the
|
|
54
|
+
* field learn it.
|
|
55
|
+
*
|
|
56
|
+
* FLUX.2 Klein: verified on the pinned engine (native sd-cpp master-975) on
|
|
57
|
+
* an RTX Spark, 2026-10-27, recoloring, adding an object and composing two
|
|
58
|
+
* sources at 4 steps in 12 s.
|
|
59
|
+
*/
|
|
60
|
+
export const MODEL_REFERENCE_EDIT_SUPPORT = new Map([
|
|
61
|
+
['flux-2-klein-4b-q4', { maxImages: 3 }],
|
|
62
|
+
['qwen-image-edit-2511-q4', { maxImages: 3, editOnly: true }],
|
|
63
|
+
]);
|
|
64
|
+
export function resolveImageEditCapability(input) {
|
|
65
|
+
const assessed = input.modelId ? MODEL_REFERENCE_EDIT_SUPPORT.get(input.modelId) : undefined;
|
|
66
|
+
const editOnly = input.editOnly ?? assessed?.editOnly ?? false;
|
|
67
|
+
const maxImages = input.referenceImages?.max ?? assessed?.maxImages;
|
|
68
|
+
if (maxImages !== undefined) {
|
|
69
|
+
return { editMode: 'reference', maxSourceImages: maxImages, editOnly };
|
|
70
|
+
}
|
|
71
|
+
const img2img = resolveImg2ImgSupport(input);
|
|
72
|
+
if (img2img.supported)
|
|
73
|
+
return { editMode: 'img2img', maxSourceImages: 1, editOnly };
|
|
74
|
+
return { editOnly, ...(img2img.reason ? { reason: img2img.reason } : {}) };
|
|
75
|
+
}
|
|
47
76
|
export function resolveImg2ImgSupport(input) {
|
|
48
77
|
if (input.explicit !== undefined) {
|
|
49
78
|
return input.explicit
|
|
@@ -21,6 +21,7 @@ export interface LocalImageEngineOptions {
|
|
|
21
21
|
export declare class LocalImageEngine {
|
|
22
22
|
private readonly opts;
|
|
23
23
|
readonly pulls: ImageModelPullRegistry;
|
|
24
|
+
readonly loraPulls: ImageModelPullRegistry;
|
|
24
25
|
private readonly lifecycle;
|
|
25
26
|
constructor(opts: LocalImageEngineOptions);
|
|
26
27
|
/** Whether this host can render at all: an engine binary, a server URL, or the mock. */
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ProviderLifecycle } from '../provider-lifecycle.js';
|
|
2
2
|
import { createLocalImageProvider } from './local-factory.js';
|
|
3
|
-
import { ImageModelPullRegistry } from './pull-registry.js';
|
|
3
|
+
import { ImageModelPullRegistry, imageLoraPullResolver } from './pull-registry.js';
|
|
4
4
|
/**
|
|
5
5
|
* Hosted inference's image engine: the local sd-server provider, built on
|
|
6
6
|
* first use, plus the pulls that install its models. Reset drains in-flight
|
|
@@ -9,12 +9,17 @@ import { ImageModelPullRegistry } from './pull-registry.js';
|
|
|
9
9
|
export class LocalImageEngine {
|
|
10
10
|
opts;
|
|
11
11
|
pulls;
|
|
12
|
+
loraPulls;
|
|
12
13
|
// Only promise-returning operations can be tracked; a pull is an async
|
|
13
14
|
// iterable and is cancelled through its own registry instead.
|
|
14
|
-
lifecycle = new ProviderLifecycle(new Set(['generate', 'deleteModel']));
|
|
15
|
+
lifecycle = new ProviderLifecycle(new Set(['generate', 'deleteModel', 'deleteLora']));
|
|
15
16
|
constructor(opts) {
|
|
16
17
|
this.opts = opts;
|
|
17
18
|
this.pulls = new ImageModelPullRegistry({ imageProvider: this, catalog: opts.catalog });
|
|
19
|
+
this.loraPulls = new ImageModelPullRegistry({
|
|
20
|
+
resolve: imageLoraPullResolver(this, opts.catalog),
|
|
21
|
+
kind: 'image-lora',
|
|
22
|
+
});
|
|
18
23
|
}
|
|
19
24
|
/** Whether this host can render at all: an engine binary, a server URL, or the mock. */
|
|
20
25
|
available() {
|
|
@@ -42,6 +47,7 @@ export class LocalImageEngine {
|
|
|
42
47
|
}
|
|
43
48
|
async shutdown() {
|
|
44
49
|
this.pulls.clear();
|
|
50
|
+
this.loraPulls.clear();
|
|
45
51
|
await this.lifecycle.reset();
|
|
46
52
|
}
|
|
47
53
|
}
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* base URL (127.0.0.1:9081) and no supervisor. `health()` reports
|
|
16
16
|
* `not-configured` so the UI can say the engine is not installed.
|
|
17
17
|
*/
|
|
18
|
+
import type { MediaWeightFile } from '@bendyline/gezel';
|
|
18
19
|
import type { GpuArbiter } from '../gpu-arbiter.js';
|
|
19
20
|
import type { NativeCapacityOptions } from '../native/device-capacity.js';
|
|
20
21
|
import type { ImageProvider, InstalledImageModelInfo } from './types.js';
|
|
@@ -92,4 +93,12 @@ export interface ResolvedInstalledModel extends InstalledImageModelInfo {
|
|
|
92
93
|
* encoder runs once per image, so GPUs lose nothing. `--diffusion-fa` means
|
|
93
94
|
* `--flash-attn off,diffusion=on`, so that branch already leaves it off.
|
|
94
95
|
*/
|
|
95
|
-
export declare function buildSdServerArgs(model: ResolvedInstalledModel, port: number
|
|
96
|
+
export declare function buildSdServerArgs(model: ResolvedInstalledModel, port: number, opts?: {
|
|
97
|
+
loraDir?: string;
|
|
98
|
+
}): string[];
|
|
99
|
+
/**
|
|
100
|
+
* The LoRA files a render of `modelId` can hold at once (its fast mode and
|
|
101
|
+
* its largest style), priced like weights. Read when the engine launches, so
|
|
102
|
+
* a LoRA installed later counts from the next launch.
|
|
103
|
+
*/
|
|
104
|
+
export declare function launchedLoraWeightFiles(loraRoot: string, modelId: string): Promise<MediaWeightFile[]>;
|
|
@@ -15,18 +15,18 @@
|
|
|
15
15
|
* base URL (127.0.0.1:9081) and no supervisor. `health()` reports
|
|
16
16
|
* `not-configured` so the UI can say the engine is not installed.
|
|
17
17
|
*/
|
|
18
|
-
import { readFile } from 'node:fs/promises';
|
|
18
|
+
import { mkdir, readFile } from 'node:fs/promises';
|
|
19
19
|
import { join } from 'node:path';
|
|
20
20
|
import { imageMediaPool, sdServerLedgerClaim, sdServerRequirement, } from '../../models/image-fit.js';
|
|
21
21
|
import { findModelRoot, listOverlayModelIds, modelStorageRoots, verifyReadOnlyModelPayload, } from '../../models/storage-roots.js';
|
|
22
|
-
import { localSdCppWeightFiles } from '../../models/weight-facts.js';
|
|
22
|
+
import { localSdCppWeightFiles, localWeightFiles } from '../../models/weight-facts.js';
|
|
23
23
|
import { pressureIdleGraceMs } from '../native/capacity-broker.js';
|
|
24
24
|
import { measuredCapacityBudget } from '../native/measured-budget.js';
|
|
25
25
|
import { pickFreePort } from '../native/port.js';
|
|
26
26
|
import { NativeEngineSupervisor } from '../native/supervisor.js';
|
|
27
27
|
import { patientFetch } from '../patient-fetch.js';
|
|
28
28
|
import { MockImageProvider } from './mock.js';
|
|
29
|
-
import { MODEL_SAMPLE_STEP_DEFAULTS, StableDiffusionCppProvider, sdServerLoadProfile, } from './sd-cpp.js';
|
|
29
|
+
import { MODEL_SAMPLE_STEP_DEFAULTS, StableDiffusionCppProvider, readInstalledLoras, sdServerLoadProfile, } from './sd-cpp.js';
|
|
30
30
|
/**
|
|
31
31
|
* Nothing to generate with: the person has to download a model first. Its
|
|
32
32
|
* own type so a route can answer with the fix, not a bare 500, which reads
|
|
@@ -80,6 +80,8 @@ export async function createLocalImageProvider(opts) {
|
|
|
80
80
|
// when it changes; resolveLaunch reads it on the next start.
|
|
81
81
|
const launchState = { modelId: undefined };
|
|
82
82
|
let launchedModel;
|
|
83
|
+
const loraRoots = modelStorageRoots({ home: opts.home, engine: 'sd-cpp-loras', env });
|
|
84
|
+
const loraDir = loraRoots.writableRoot;
|
|
83
85
|
const supervisor = new NativeEngineSupervisor({
|
|
84
86
|
capacity: {
|
|
85
87
|
...sdServerCapacity(opts.home),
|
|
@@ -93,7 +95,10 @@ export async function createLocalImageProvider(opts) {
|
|
|
93
95
|
const requirement = sdServerRequirement({
|
|
94
96
|
modelId: launchedModel.id,
|
|
95
97
|
weightsKind: launchedModel.weightsKind,
|
|
96
|
-
files:
|
|
98
|
+
files: [
|
|
99
|
+
...(await localSdCppWeightFiles(launchedModel)),
|
|
100
|
+
...(await launchedLoraWeightFiles(loraDir, launchedModel.id)),
|
|
101
|
+
],
|
|
97
102
|
pool: imageMediaPool(budget.kind),
|
|
98
103
|
});
|
|
99
104
|
return sdServerLedgerClaim(requirement, budget.fastBytes);
|
|
@@ -131,9 +136,11 @@ export async function createLocalImageProvider(opts) {
|
|
|
131
136
|
if (!model)
|
|
132
137
|
throw new NoLocalImageModelError();
|
|
133
138
|
launchedModel = model;
|
|
139
|
+
// sd-server ignores a LoRA folder that does not exist yet, then lists none.
|
|
140
|
+
await mkdir(loraDir, { recursive: true });
|
|
134
141
|
return {
|
|
135
142
|
command: binary,
|
|
136
|
-
args: buildSdServerArgs(model, port),
|
|
143
|
+
args: buildSdServerArgs(model, port, { loraDir }),
|
|
137
144
|
baseUrl: `http://127.0.0.1:${port}`,
|
|
138
145
|
};
|
|
139
146
|
},
|
|
@@ -146,6 +153,7 @@ export async function createLocalImageProvider(opts) {
|
|
|
146
153
|
storageRoots,
|
|
147
154
|
supervisor,
|
|
148
155
|
launchState,
|
|
156
|
+
loraRoots,
|
|
149
157
|
fetchImpl: patientFetch(),
|
|
150
158
|
...(defaultLocalModelId ? { defaultModelId: defaultLocalModelId } : {}),
|
|
151
159
|
...(opts.arbiter ? { arbiter: opts.arbiter } : {}),
|
|
@@ -239,7 +247,7 @@ async function findFirstInstalledModel(storageRoots) {
|
|
|
239
247
|
* encoder runs once per image, so GPUs lose nothing. `--diffusion-fa` means
|
|
240
248
|
* `--flash-attn off,diffusion=on`, so that branch already leaves it off.
|
|
241
249
|
*/
|
|
242
|
-
export function buildSdServerArgs(model, port) {
|
|
250
|
+
export function buildSdServerArgs(model, port, opts = {}) {
|
|
243
251
|
const args = [];
|
|
244
252
|
if (model.weightsKind === 'diffusion-model') {
|
|
245
253
|
args.push('--diffusion-model', model.weightsPath);
|
|
@@ -265,6 +273,25 @@ export function buildSdServerArgs(model, port) {
|
|
|
265
273
|
args.push('--vae-tiling');
|
|
266
274
|
if (profile.stepCache)
|
|
267
275
|
args.push('--cache-mode', profile.stepCache);
|
|
276
|
+
if (profile.modelArgs)
|
|
277
|
+
args.push('--model-args', profile.modelArgs);
|
|
278
|
+
// At runtime for every model: a LoRA then changes per request without
|
|
279
|
+
// touching the weights. In auto mode an unquantized checkpoint (SD, SDXL)
|
|
280
|
+
// would merge it into the weights instead.
|
|
281
|
+
if (opts.loraDir)
|
|
282
|
+
args.push('--lora-model-dir', opts.loraDir, '--lora-apply-mode', 'at_runtime');
|
|
268
283
|
args.push('--listen-ip', '127.0.0.1', '--listen-port', String(port));
|
|
269
284
|
return args;
|
|
270
285
|
}
|
|
286
|
+
/**
|
|
287
|
+
* The LoRA files a render of `modelId` can hold at once (its fast mode and
|
|
288
|
+
* its largest style), priced like weights. Read when the engine launches, so
|
|
289
|
+
* a LoRA installed later counts from the next launch.
|
|
290
|
+
*/
|
|
291
|
+
export async function launchedLoraWeightFiles(loraRoot, modelId) {
|
|
292
|
+
const fitting = (await readInstalledLoras(loraRoot)).filter((l) => l.baseModels.includes(modelId));
|
|
293
|
+
const largest = (purpose) => fitting
|
|
294
|
+
.filter((l) => l.purpose === purpose)
|
|
295
|
+
.sort((a, b) => b.approxSizeBytes - a.approxSizeBytes)[0];
|
|
296
|
+
return localWeightFiles([largest('speed'), largest('style')].flatMap((l) => l ? [{ component: 'lora', path: join(loraRoot, l.id, l.filename) }] : []));
|
|
297
|
+
}
|
|
@@ -4,19 +4,26 @@
|
|
|
4
4
|
* colour is derived from a hash of the prompt so different prompts yield
|
|
5
5
|
* visually different (but still trivially small) outputs.
|
|
6
6
|
*/
|
|
7
|
-
import type { ImageEngineHealth, ImageGenerationInput, ImageGenerationOutput, ImageModelPullEvent, ImageModelPullSpec, ImageProvider, InstalledImageModelInfo } from './types.js';
|
|
7
|
+
import type { ImageEngineHealth, ImageGenerationInput, ImageGenerationOutput, ImageLoraPullSpec, ImageModelPullEvent, ImageModelPullSpec, ImageProvider, InstalledImageLoraInfo, InstalledImageModelInfo } from './types.js';
|
|
8
8
|
export declare class MockImageProvider implements ImageProvider {
|
|
9
9
|
readonly name = "mock";
|
|
10
10
|
private readonly installed;
|
|
11
|
+
private readonly loras;
|
|
11
12
|
/** Internal counter driving deterministic seed generation on "random" seeds. */
|
|
12
13
|
private nextSeed;
|
|
13
14
|
constructor(opts?: {
|
|
14
15
|
preinstalled?: InstalledImageModelInfo[];
|
|
16
|
+
loras?: InstalledImageLoraInfo[];
|
|
15
17
|
});
|
|
16
18
|
/** Number of input images received on the most recent generate(). Tests use this. */
|
|
17
19
|
lastInputImageCount: number;
|
|
20
|
+
/** The most recent generate() input. Tests use this. */
|
|
21
|
+
lastInput: ImageGenerationInput | undefined;
|
|
18
22
|
generate(input: ImageGenerationInput): Promise<ImageGenerationOutput>;
|
|
19
23
|
listInstalledModels(): Promise<InstalledImageModelInfo[]>;
|
|
24
|
+
listLoras(): Promise<InstalledImageLoraInfo[]>;
|
|
25
|
+
pullLora(id: string, spec: ImageLoraPullSpec): AsyncIterable<ImageModelPullEvent>;
|
|
26
|
+
deleteLora(id: string): Promise<void>;
|
|
20
27
|
pullModel(id: string, spec: ImageModelPullSpec, signal?: AbortSignal): AsyncIterable<ImageModelPullEvent>;
|
|
21
28
|
deleteModel(id: string): Promise<void>;
|
|
22
29
|
health(): Promise<ImageEngineHealth>;
|
|
@@ -8,22 +8,33 @@ import { deflateSync } from 'node:zlib';
|
|
|
8
8
|
export class MockImageProvider {
|
|
9
9
|
name = 'mock';
|
|
10
10
|
installed = new Map();
|
|
11
|
+
loras = new Map();
|
|
11
12
|
/** Internal counter driving deterministic seed generation on "random" seeds. */
|
|
12
13
|
nextSeed = 1;
|
|
13
14
|
constructor(opts = {}) {
|
|
14
15
|
for (const m of opts.preinstalled ?? []) {
|
|
15
16
|
this.installed.set(m.id, m);
|
|
16
17
|
}
|
|
18
|
+
for (const l of opts.loras ?? [])
|
|
19
|
+
this.loras.set(l.id, l);
|
|
17
20
|
}
|
|
18
21
|
/** Number of input images received on the most recent generate(). Tests use this. */
|
|
19
22
|
lastInputImageCount = 0;
|
|
23
|
+
/** The most recent generate() input. Tests use this. */
|
|
24
|
+
lastInput;
|
|
20
25
|
async generate(input) {
|
|
21
26
|
this.lastInputImageCount = input.inputImages?.length ?? 0;
|
|
27
|
+
this.lastInput = input;
|
|
22
28
|
const width = clamp(input.width ?? 16, 1, 512);
|
|
23
29
|
const height = clamp(input.height ?? 16, 1, 512);
|
|
24
30
|
const steps = clamp(input.steps ?? 1, 1, 100);
|
|
25
31
|
const seed = input.seed ?? this.nextSeed++;
|
|
26
32
|
const model = input.model ?? [...this.installed.keys()][0] ?? 'mock-model';
|
|
33
|
+
// Applied as the real engine applies them: installed, and fitting the model.
|
|
34
|
+
const loras = (input.loras ?? []).flatMap(({ id }) => {
|
|
35
|
+
const lora = this.loras.get(id);
|
|
36
|
+
return lora?.baseModels.includes(model) ? [{ id: lora.id, name: lora.name }] : [];
|
|
37
|
+
});
|
|
27
38
|
const rgba = solidFill(width, height, colourForPrompt(input.prompt));
|
|
28
39
|
const png = encodePng(rgba, width, height);
|
|
29
40
|
return {
|
|
@@ -35,11 +46,45 @@ export class MockImageProvider {
|
|
|
35
46
|
widthPx: width,
|
|
36
47
|
heightPx: height,
|
|
37
48
|
durationMs: 1,
|
|
49
|
+
...(loras.length > 0 ? { loras } : {}),
|
|
38
50
|
},
|
|
39
51
|
};
|
|
40
52
|
}
|
|
41
53
|
async listInstalledModels() {
|
|
42
|
-
return [...this.installed.values()]
|
|
54
|
+
return [...this.installed.values()].map((m) => {
|
|
55
|
+
const loras = [...this.loras.values()]
|
|
56
|
+
.filter((l) => l.baseModels.includes(m.id))
|
|
57
|
+
.map(({ id, name, purpose, steps }) => ({
|
|
58
|
+
id,
|
|
59
|
+
name,
|
|
60
|
+
purpose,
|
|
61
|
+
...(steps !== undefined ? { steps } : {}),
|
|
62
|
+
}));
|
|
63
|
+
return loras.length > 0 ? { ...m, loras } : m;
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
async listLoras() {
|
|
67
|
+
return [...this.loras.values()];
|
|
68
|
+
}
|
|
69
|
+
async *pullLora(id, spec) {
|
|
70
|
+
yield {
|
|
71
|
+
type: 'progress',
|
|
72
|
+
bytesWritten: spec.approxSizeBytes,
|
|
73
|
+
totalBytes: spec.approxSizeBytes,
|
|
74
|
+
};
|
|
75
|
+
this.loras.set(id, {
|
|
76
|
+
id,
|
|
77
|
+
name: spec.name,
|
|
78
|
+
purpose: spec.purpose,
|
|
79
|
+
...(spec.render?.steps !== undefined ? { steps: spec.render.steps } : {}),
|
|
80
|
+
baseModels: spec.baseModels,
|
|
81
|
+
approxSizeBytes: spec.approxSizeBytes,
|
|
82
|
+
installedAt: new Date().toISOString(),
|
|
83
|
+
});
|
|
84
|
+
yield { type: 'done', id };
|
|
85
|
+
}
|
|
86
|
+
async deleteLora(id) {
|
|
87
|
+
this.loras.delete(id);
|
|
43
88
|
}
|
|
44
89
|
async *pullModel(id, spec, signal) {
|
|
45
90
|
const total = spec.approxSizeBytes;
|
|
@@ -34,6 +34,13 @@ type Listener = (event: ImageModelPullEvent) => void;
|
|
|
34
34
|
export interface ImageProviderSource {
|
|
35
35
|
current(): Promise<Pick<ImageProvider, 'pullModel'>>;
|
|
36
36
|
}
|
|
37
|
+
/** One download the registry runs: its expected size and the event stream. */
|
|
38
|
+
export interface ImagePullJob {
|
|
39
|
+
totalBytes: number;
|
|
40
|
+
run(signal: AbortSignal): AsyncIterable<ImageModelPullEvent>;
|
|
41
|
+
}
|
|
42
|
+
/** Turns a catalog id into a pull, or `null` for an id the catalog lacks. */
|
|
43
|
+
export type ImagePullResolver = (id: string) => Promise<ImagePullJob | null>;
|
|
37
44
|
/**
|
|
38
45
|
* Owns the lifecycle of every in-flight image-model pull.
|
|
39
46
|
*
|
|
@@ -52,11 +59,18 @@ export interface ImageProviderSource {
|
|
|
52
59
|
export declare class ImageModelPullRegistry {
|
|
53
60
|
private readonly entries;
|
|
54
61
|
private readonly controllers;
|
|
55
|
-
private readonly
|
|
56
|
-
private readonly
|
|
62
|
+
private readonly resolve;
|
|
63
|
+
private readonly kind;
|
|
64
|
+
/**
|
|
65
|
+
* Image models by default; `resolve` makes it a registry for another kind
|
|
66
|
+
* of download (LoRAs), whose ids then never share keys with models.
|
|
67
|
+
*/
|
|
57
68
|
constructor(opts: {
|
|
58
69
|
imageProvider: ImageProviderSource;
|
|
59
70
|
catalog: CatalogService;
|
|
71
|
+
} | {
|
|
72
|
+
resolve: ImagePullResolver;
|
|
73
|
+
kind: string;
|
|
60
74
|
});
|
|
61
75
|
/**
|
|
62
76
|
* Start a pull for `id`. Idempotent — when a pull is already in flight
|
|
@@ -101,6 +115,14 @@ export declare class ImageModelPullRegistry {
|
|
|
101
115
|
}
|
|
102
116
|
export declare class UnknownImageModelError extends Error {
|
|
103
117
|
readonly id: string;
|
|
104
|
-
constructor(id: string);
|
|
118
|
+
constructor(id: string, kind?: string);
|
|
105
119
|
}
|
|
120
|
+
/**
|
|
121
|
+
* LoRA pulls: the catalog's `image-lora` entries, installed by whichever
|
|
122
|
+
* provider owns the engine. A provider without LoRA support fails the pull
|
|
123
|
+
* with a plain message.
|
|
124
|
+
*/
|
|
125
|
+
export declare function imageLoraPullResolver(imageProvider: {
|
|
126
|
+
current(): Promise<Pick<ImageProvider, 'pullLora'>>;
|
|
127
|
+
}, catalog: CatalogService): ImagePullResolver;
|
|
106
128
|
export {};
|
|
@@ -25,11 +25,21 @@ const FINISHED_TTL_MS = 8_000;
|
|
|
25
25
|
export class ImageModelPullRegistry {
|
|
26
26
|
entries = new Map();
|
|
27
27
|
controllers = new Map();
|
|
28
|
-
|
|
29
|
-
|
|
28
|
+
resolve;
|
|
29
|
+
kind;
|
|
30
|
+
/**
|
|
31
|
+
* Image models by default; `resolve` makes it a registry for another kind
|
|
32
|
+
* of download (LoRAs), whose ids then never share keys with models.
|
|
33
|
+
*/
|
|
30
34
|
constructor(opts) {
|
|
31
|
-
|
|
32
|
-
|
|
35
|
+
if ('resolve' in opts) {
|
|
36
|
+
this.resolve = opts.resolve;
|
|
37
|
+
this.kind = opts.kind;
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
this.resolve = imageModelPullResolver(opts.imageProvider, opts.catalog);
|
|
41
|
+
this.kind = 'image-model';
|
|
42
|
+
}
|
|
33
43
|
}
|
|
34
44
|
/**
|
|
35
45
|
* Start a pull for `id`. Idempotent — when a pull is already in flight
|
|
@@ -47,40 +57,21 @@ export class ImageModelPullRegistry {
|
|
|
47
57
|
}
|
|
48
58
|
// Resolve the catalog entry up-front so we can fail fast with a
|
|
49
59
|
// proper 404 rather than emitting a 200 + error SSE for "unknown id".
|
|
50
|
-
const
|
|
51
|
-
if (!
|
|
52
|
-
throw new UnknownImageModelError(id);
|
|
53
|
-
}
|
|
54
|
-
const manifest = item.manifest;
|
|
60
|
+
const job = await this.resolve(id);
|
|
61
|
+
if (!job)
|
|
62
|
+
throw new UnknownImageModelError(id, this.kind);
|
|
55
63
|
const snapshot = {
|
|
56
64
|
id,
|
|
57
65
|
startedAt: new Date().toISOString(),
|
|
58
66
|
bytesWritten: 0,
|
|
59
|
-
totalBytes:
|
|
60
|
-
manifest.auxiliaryFiles.reduce((n, f) => n + f.approxSizeBytes, 0),
|
|
67
|
+
totalBytes: job.totalBytes,
|
|
61
68
|
finished: false,
|
|
62
69
|
};
|
|
63
70
|
const entry = { snapshot, listeners: new Set() };
|
|
64
71
|
this.entries.set(id, entry);
|
|
65
72
|
const controller = new AbortController();
|
|
66
73
|
this.controllers.set(id, controller);
|
|
67
|
-
|
|
68
|
-
downloadUrl: manifest.downloadUrl,
|
|
69
|
-
sha256: manifest.sha256,
|
|
70
|
-
approxSizeBytes: manifest.approxSizeBytes,
|
|
71
|
-
name: manifest.name,
|
|
72
|
-
weightsKind: manifest.weightsKind,
|
|
73
|
-
...(manifest.supportsImg2Img !== undefined
|
|
74
|
-
? { supportsImg2Img: manifest.supportsImg2Img }
|
|
75
|
-
: {}),
|
|
76
|
-
auxiliaryFiles: manifest.auxiliaryFiles.map((aux) => ({
|
|
77
|
-
role: aux.role,
|
|
78
|
-
downloadUrl: aux.downloadUrl,
|
|
79
|
-
sha256: aux.sha256,
|
|
80
|
-
approxSizeBytes: aux.approxSizeBytes,
|
|
81
|
-
})),
|
|
82
|
-
};
|
|
83
|
-
void this.consume(id, spec, controller.signal);
|
|
74
|
+
void this.consume(id, job, controller.signal);
|
|
84
75
|
return { snapshot: { ...snapshot }, alreadyRunning: false };
|
|
85
76
|
}
|
|
86
77
|
list() {
|
|
@@ -148,13 +139,12 @@ export class ImageModelPullRegistry {
|
|
|
148
139
|
if (snapshot.finished && !snapshot.error)
|
|
149
140
|
listener({ type: 'done', id: snapshot.id });
|
|
150
141
|
}
|
|
151
|
-
async consume(id,
|
|
142
|
+
async consume(id, job, signal) {
|
|
152
143
|
const entry = this.entries.get(id);
|
|
153
144
|
if (!entry)
|
|
154
145
|
return;
|
|
155
146
|
try {
|
|
156
|
-
const
|
|
157
|
-
for await (const event of provider.pullModel(id, spec, signal)) {
|
|
147
|
+
for await (const event of job.run(signal)) {
|
|
158
148
|
this.applyEvent(entry, event);
|
|
159
149
|
this.broadcast(entry, event);
|
|
160
150
|
if (event.type === 'done' || event.type === 'error')
|
|
@@ -171,7 +161,8 @@ export class ImageModelPullRegistry {
|
|
|
171
161
|
finally {
|
|
172
162
|
entry.snapshot.finished = true;
|
|
173
163
|
// Drop the controller — cancel() should be a no-op once finished.
|
|
174
|
-
this.
|
|
164
|
+
if (this.entries.get(id) === entry)
|
|
165
|
+
this.controllers.delete(id);
|
|
175
166
|
// Keep the snapshot around briefly so a client mid-reconnect still
|
|
176
167
|
// sees the terminal state on `list()` / `subscribe()`. Then garbage
|
|
177
168
|
// collect.
|
|
@@ -184,6 +175,9 @@ export class ImageModelPullRegistry {
|
|
|
184
175
|
}
|
|
185
176
|
applyEvent(entry, event) {
|
|
186
177
|
const s = entry.snapshot;
|
|
178
|
+
// Publish terminal state before generator cleanup can yield to a new subscriber.
|
|
179
|
+
if (event.type === 'done' || event.type === 'error')
|
|
180
|
+
s.finished = true;
|
|
187
181
|
if (event.type === 'progress') {
|
|
188
182
|
s.bytesWritten = event.bytesWritten;
|
|
189
183
|
if (event.totalBytes !== undefined)
|
|
@@ -219,9 +213,77 @@ export class ImageModelPullRegistry {
|
|
|
219
213
|
}
|
|
220
214
|
export class UnknownImageModelError extends Error {
|
|
221
215
|
id;
|
|
222
|
-
constructor(id) {
|
|
223
|
-
super(`unknown
|
|
216
|
+
constructor(id, kind = 'image-model') {
|
|
217
|
+
super(`unknown ${kind}: ${id}`);
|
|
224
218
|
this.id = id;
|
|
225
219
|
this.name = 'UnknownImageModelError';
|
|
226
220
|
}
|
|
227
221
|
}
|
|
222
|
+
function imageModelPullResolver(imageProvider, catalog) {
|
|
223
|
+
return async (id) => {
|
|
224
|
+
const item = await catalog.get('image-model', id);
|
|
225
|
+
if (!item || item.manifest.kind !== 'image-model')
|
|
226
|
+
return null;
|
|
227
|
+
const manifest = item.manifest;
|
|
228
|
+
const spec = {
|
|
229
|
+
downloadUrl: manifest.downloadUrl,
|
|
230
|
+
sha256: manifest.sha256,
|
|
231
|
+
approxSizeBytes: manifest.approxSizeBytes,
|
|
232
|
+
name: manifest.name,
|
|
233
|
+
weightsKind: manifest.weightsKind,
|
|
234
|
+
...(manifest.supportsImg2Img !== undefined
|
|
235
|
+
? { supportsImg2Img: manifest.supportsImg2Img }
|
|
236
|
+
: {}),
|
|
237
|
+
...(manifest.referenceImages ? { referenceImages: manifest.referenceImages } : {}),
|
|
238
|
+
...(manifest.editOnly !== undefined ? { editOnly: manifest.editOnly } : {}),
|
|
239
|
+
auxiliaryFiles: manifest.auxiliaryFiles.map((aux) => ({
|
|
240
|
+
role: aux.role,
|
|
241
|
+
downloadUrl: aux.downloadUrl,
|
|
242
|
+
sha256: aux.sha256,
|
|
243
|
+
approxSizeBytes: aux.approxSizeBytes,
|
|
244
|
+
})),
|
|
245
|
+
};
|
|
246
|
+
return {
|
|
247
|
+
totalBytes: manifest.approxSizeBytes +
|
|
248
|
+
manifest.auxiliaryFiles.reduce((n, f) => n + f.approxSizeBytes, 0),
|
|
249
|
+
async *run(signal) {
|
|
250
|
+
const provider = await imageProvider.current();
|
|
251
|
+
yield* provider.pullModel(id, spec, signal);
|
|
252
|
+
},
|
|
253
|
+
};
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* LoRA pulls: the catalog's `image-lora` entries, installed by whichever
|
|
258
|
+
* provider owns the engine. A provider without LoRA support fails the pull
|
|
259
|
+
* with a plain message.
|
|
260
|
+
*/
|
|
261
|
+
export function imageLoraPullResolver(imageProvider, catalog) {
|
|
262
|
+
return async (id) => {
|
|
263
|
+
const item = await catalog.get('image-lora', id);
|
|
264
|
+
if (!item || item.manifest.kind !== 'image-lora')
|
|
265
|
+
return null;
|
|
266
|
+
const manifest = item.manifest;
|
|
267
|
+
const spec = {
|
|
268
|
+
downloadUrl: manifest.downloadUrl,
|
|
269
|
+
sha256: manifest.sha256,
|
|
270
|
+
approxSizeBytes: manifest.approxSizeBytes,
|
|
271
|
+
name: manifest.name,
|
|
272
|
+
baseModels: manifest.baseModels,
|
|
273
|
+
purpose: manifest.purpose,
|
|
274
|
+
strength: manifest.strength,
|
|
275
|
+
...(manifest.prompt ? { prompt: manifest.prompt } : {}),
|
|
276
|
+
...(manifest.render ? { render: manifest.render } : {}),
|
|
277
|
+
};
|
|
278
|
+
return {
|
|
279
|
+
totalBytes: manifest.approxSizeBytes,
|
|
280
|
+
async *run(signal) {
|
|
281
|
+
const provider = await imageProvider.current();
|
|
282
|
+
if (!provider.pullLora) {
|
|
283
|
+
throw new Error('This image engine cannot add styles or fast mode.');
|
|
284
|
+
}
|
|
285
|
+
yield* provider.pullLora(id, spec, signal);
|
|
286
|
+
},
|
|
287
|
+
};
|
|
288
|
+
};
|
|
289
|
+
}
|