@bendyline/gezel-inference 1.0.3 → 1.0.4
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/catalog/chat-models/index.json +1 -1
- package/dist/catalog/chat-models/na/nanbeige4.2-3b-q4/manifest.json +1 -1
- package/dist/catalog/chat-models/ne/nemotron3.5-bolt-e4b-q4/manifest.json +142 -0
- package/dist/catalog/chat-models/ne/nemotron3.5-bolt-e4b-q4/versions/1.0.0/manifest.json +14 -0
- package/dist/catalog/chat-models/ne/nemotron3.5-bolt-e4b-q4/versions/1.0.1/manifest.json +14 -0
- package/dist/catalog/chat-models/raw-index.json +1 -1
- package/dist/catalog/image-models/an/animagine-xl-4/manifest.json +40 -0
- package/dist/catalog/image-models/an/animagine-xl-4/versions/4.0.0/manifest.json +10 -0
- package/dist/catalog/image-models/an/animagine-xl-4/versions/4.0.1/manifest.json +16 -0
- package/dist/catalog/image-models/fl/flux-2-klein-4b-q4/manifest.json +44 -0
- package/dist/catalog/image-models/fl/flux-2-klein-4b-q4/versions/1.0.0/manifest.json +23 -0
- package/dist/catalog/image-models/fl/flux-2-klein-4b-q4/versions/1.0.1/manifest.json +29 -0
- package/dist/catalog/image-models/index.json +1 -0
- package/dist/catalog/image-models/ir/iris-3b-f32/manifest.json +41 -0
- package/dist/catalog/image-models/ir/iris-3b-f32/versions/1.0.0/manifest.json +19 -0
- package/dist/catalog/image-models/ir/iris-3b-f32/versions/1.0.1/manifest.json +25 -0
- package/dist/catalog/image-models/ir/iris-3b-f32/versions/1.0.2/manifest.json +25 -0
- package/dist/catalog/image-models/kr/krea-2-turbo-q4/manifest.json +42 -0
- package/dist/catalog/image-models/kr/krea-2-turbo-q4/versions/1.0.0/manifest.json +23 -0
- package/dist/catalog/image-models/kr/krea-2-turbo-q4/versions/1.0.1/manifest.json +29 -0
- package/dist/catalog/image-models/kr/krea-2-turbo-q6/manifest.json +42 -0
- package/dist/catalog/image-models/kr/krea-2-turbo-q6/versions/1.0.0/manifest.json +23 -0
- package/dist/catalog/image-models/kr/krea-2-turbo-q6/versions/1.0.1/manifest.json +29 -0
- package/dist/catalog/image-models/kr/krea-2-turbo-q8/manifest.json +42 -0
- package/dist/catalog/image-models/kr/krea-2-turbo-q8/versions/1.0.0/manifest.json +23 -0
- package/dist/catalog/image-models/kr/krea-2-turbo-q8/versions/1.0.1/manifest.json +29 -0
- package/dist/catalog/image-models/mi/ming-image-0.1-design-bf16/manifest.json +41 -0
- package/dist/catalog/image-models/mi/ming-image-0.1-design-bf16/versions/1.0.0/manifest.json +31 -0
- package/dist/catalog/image-models/mi/ming-image-0.1-design-bf16/versions/1.0.1/manifest.json +50 -0
- package/dist/catalog/image-models/mi/ming-image-0.1-design-bf16/versions/1.0.2/manifest.json +50 -0
- package/dist/catalog/image-models/mo/moss-image-sft-q4/manifest.json +40 -0
- package/dist/catalog/image-models/mo/moss-image-sft-q4/versions/1.0.0/manifest.json +37 -0
- package/dist/catalog/image-models/mo/moss-image-sft-q4/versions/1.0.1/manifest.json +37 -0
- package/dist/catalog/image-models/raw-index.json +1 -0
- package/dist/catalog/image-models/sd/sd-1.5/manifest.json +40 -0
- package/dist/catalog/image-models/sd/sd-1.5/versions/1.0.0/manifest.json +10 -0
- package/dist/catalog/image-models/sd/sd-1.5/versions/1.0.1/manifest.json +16 -0
- package/dist/catalog/image-models/sd/sd-turbo/manifest.json +40 -0
- package/dist/catalog/image-models/sd/sd-turbo/versions/1.0.0/manifest.json +10 -0
- package/dist/catalog/image-models/sd/sd-turbo/versions/1.0.1/manifest.json +16 -0
- package/dist/catalog/image-models/sd/sdxl-base-1.0/manifest.json +39 -0
- package/dist/catalog/image-models/sd/sdxl-base-1.0/versions/1.0.0/manifest.json +10 -0
- package/dist/catalog/image-models/sd/sdxl-base-1.0/versions/1.0.1/manifest.json +16 -0
- package/dist/catalog/image-models/sd/sdxl-lightning-4step/manifest.json +40 -0
- package/dist/catalog/image-models/sd/sdxl-lightning-4step/versions/1.0.0/manifest.json +10 -0
- package/dist/catalog/image-models/sd/sdxl-lightning-4step/versions/1.0.1/manifest.json +16 -0
- package/dist/catalog/image-models/sd/sdxl-turbo/manifest.json +41 -0
- package/dist/catalog/image-models/sd/sdxl-turbo/versions/1.0.0/manifest.json +10 -0
- package/dist/catalog/image-models/sd/sdxl-turbo/versions/1.0.1/manifest.json +16 -0
- package/dist/catalog/provenance.json +2 -1
- package/dist/engines/native-file-manifest.json +129 -119
- package/dist/engines/native-manifest.d.ts +4 -4
- package/dist/engines/native-manifest.js +36 -36
- package/dist/engines/resolver.js +9 -0
- package/dist/http/openai-compat/engine-failure.js +7 -2
- package/dist/http/routes/v1-images.d.ts +44 -0
- package/dist/http/routes/v1-images.js +370 -0
- package/dist/inference-service.d.ts +3 -0
- package/dist/inference-service.js +24 -2
- package/dist/kokoro-lexicon/manifest.json +6 -3
- package/dist/models/image-fit.d.ts +83 -0
- package/dist/models/image-fit.js +163 -0
- package/dist/models/weight-facts.d.ts +36 -0
- package/dist/models/weight-facts.js +171 -0
- package/dist/providers/ds4/build-provider.js +28 -21
- package/dist/providers/ds4/dspark.d.ts +23 -5
- package/dist/providers/ds4/dspark.js +36 -8
- package/dist/providers/image/img2img-support.d.ts +40 -0
- package/dist/providers/image/img2img-support.js +74 -0
- package/dist/providers/image/local-engine.d.ts +31 -0
- package/dist/providers/image/local-engine.js +47 -0
- package/dist/providers/image/local-factory.d.ts +95 -0
- package/dist/providers/image/local-factory.js +270 -0
- package/dist/providers/image/mock.d.ts +23 -0
- package/dist/providers/image/mock.js +151 -0
- package/dist/providers/image/pull-registry.d.ts +106 -0
- package/dist/providers/image/pull-registry.js +227 -0
- package/dist/providers/image/sd-cpp.d.ts +200 -0
- package/dist/providers/image/sd-cpp.js +845 -0
- package/dist/providers/image/types.d.ts +235 -0
- package/dist/providers/image/types.js +20 -0
- package/dist/providers/llama-cpp/backend-health.d.ts +7 -0
- package/dist/providers/llama-cpp/backend-health.js +41 -1
- package/dist/providers/mlx/provider.js +13 -3
- package/dist/providers/native/supervisor.d.ts +8 -0
- package/dist/providers/native/supervisor.js +52 -4
- package/dist/providers/project-macro-loop-bail.js +5 -3
- package/dist/providers/provider-lifecycle.d.ts +21 -0
- package/dist/providers/provider-lifecycle.js +83 -0
- package/dist/providers/retirement-gate.d.ts +20 -0
- package/dist/providers/retirement-gate.js +55 -0
- package/dist/system/memory.js +6 -0
- package/dist/utils/image-meta.d.ts +12 -0
- package/dist/utils/image-meta.js +77 -0
- package/package.json +6 -6
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MockImageProvider — deterministic image generation for tests and for the
|
|
3
|
+
* `GEZEL_MOCK_PROVIDER=1` flow. Produces a tiny, valid PNG whose pixel
|
|
4
|
+
* colour is derived from a hash of the prompt so different prompts yield
|
|
5
|
+
* visually different (but still trivially small) outputs.
|
|
6
|
+
*/
|
|
7
|
+
import { deflateSync } from 'node:zlib';
|
|
8
|
+
export class MockImageProvider {
|
|
9
|
+
name = 'mock';
|
|
10
|
+
installed = new Map();
|
|
11
|
+
/** Internal counter driving deterministic seed generation on "random" seeds. */
|
|
12
|
+
nextSeed = 1;
|
|
13
|
+
constructor(opts = {}) {
|
|
14
|
+
for (const m of opts.preinstalled ?? []) {
|
|
15
|
+
this.installed.set(m.id, m);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
/** Number of input images received on the most recent generate(). Tests use this. */
|
|
19
|
+
lastInputImageCount = 0;
|
|
20
|
+
async generate(input) {
|
|
21
|
+
this.lastInputImageCount = input.inputImages?.length ?? 0;
|
|
22
|
+
const width = clamp(input.width ?? 16, 1, 512);
|
|
23
|
+
const height = clamp(input.height ?? 16, 1, 512);
|
|
24
|
+
const steps = clamp(input.steps ?? 1, 1, 100);
|
|
25
|
+
const seed = input.seed ?? this.nextSeed++;
|
|
26
|
+
const model = input.model ?? [...this.installed.keys()][0] ?? 'mock-model';
|
|
27
|
+
const rgba = solidFill(width, height, colourForPrompt(input.prompt));
|
|
28
|
+
const png = encodePng(rgba, width, height);
|
|
29
|
+
return {
|
|
30
|
+
png,
|
|
31
|
+
meta: {
|
|
32
|
+
model,
|
|
33
|
+
seed,
|
|
34
|
+
steps,
|
|
35
|
+
widthPx: width,
|
|
36
|
+
heightPx: height,
|
|
37
|
+
durationMs: 1,
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
async listInstalledModels() {
|
|
42
|
+
return [...this.installed.values()];
|
|
43
|
+
}
|
|
44
|
+
async *pullModel(id, spec, signal) {
|
|
45
|
+
const total = spec.approxSizeBytes;
|
|
46
|
+
for (const step of [0.25, 0.5, 0.75, 1]) {
|
|
47
|
+
if (signal?.aborted) {
|
|
48
|
+
yield { type: 'error', error: 'download aborted' };
|
|
49
|
+
yield { type: 'done', id };
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
// Tiny tick so tests can observe a mid-pull state between steps.
|
|
53
|
+
await new Promise((resolve) => {
|
|
54
|
+
const t = setTimeout(resolve, 5);
|
|
55
|
+
t.unref?.();
|
|
56
|
+
});
|
|
57
|
+
yield { type: 'progress', bytesWritten: Math.round(total * step), totalBytes: total };
|
|
58
|
+
}
|
|
59
|
+
this.installed.set(id, {
|
|
60
|
+
id,
|
|
61
|
+
name: spec.name,
|
|
62
|
+
approxSizeBytes: total,
|
|
63
|
+
installedAt: new Date().toISOString(),
|
|
64
|
+
});
|
|
65
|
+
yield { type: 'done', id };
|
|
66
|
+
}
|
|
67
|
+
async deleteModel(id) {
|
|
68
|
+
this.installed.delete(id);
|
|
69
|
+
}
|
|
70
|
+
async health() {
|
|
71
|
+
return { status: 'ok', baseUrl: 'mock://image' };
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
function clamp(n, min, max) {
|
|
75
|
+
if (n < min)
|
|
76
|
+
return min;
|
|
77
|
+
if (n > max)
|
|
78
|
+
return max;
|
|
79
|
+
return n;
|
|
80
|
+
}
|
|
81
|
+
/** Hash the prompt to an `[r, g, b]` tuple so different prompts paint different pixels. */
|
|
82
|
+
function colourForPrompt(prompt) {
|
|
83
|
+
let h = 2166136261;
|
|
84
|
+
for (let i = 0; i < prompt.length; i++) {
|
|
85
|
+
h ^= prompt.charCodeAt(i);
|
|
86
|
+
h = Math.imul(h, 16777619);
|
|
87
|
+
}
|
|
88
|
+
return [h & 0xff, (h >>> 8) & 0xff, (h >>> 16) & 0xff];
|
|
89
|
+
}
|
|
90
|
+
function solidFill(width, height, rgb) {
|
|
91
|
+
const rgba = new Uint8Array(width * height * 4);
|
|
92
|
+
for (let i = 0; i < width * height; i++) {
|
|
93
|
+
rgba[i * 4] = rgb[0];
|
|
94
|
+
rgba[i * 4 + 1] = rgb[1];
|
|
95
|
+
rgba[i * 4 + 2] = rgb[2];
|
|
96
|
+
rgba[i * 4 + 3] = 0xff;
|
|
97
|
+
}
|
|
98
|
+
return rgba;
|
|
99
|
+
}
|
|
100
|
+
// ── Minimal PNG encoder (RGBA, no filter) ──────────────────────────
|
|
101
|
+
const CRC_TABLE = (() => {
|
|
102
|
+
const table = new Uint32Array(256);
|
|
103
|
+
for (let n = 0; n < 256; n++) {
|
|
104
|
+
let c = n;
|
|
105
|
+
for (let k = 0; k < 8; k++)
|
|
106
|
+
c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
|
|
107
|
+
table[n] = c >>> 0;
|
|
108
|
+
}
|
|
109
|
+
return table;
|
|
110
|
+
})();
|
|
111
|
+
function crc32(bytes) {
|
|
112
|
+
let crc = 0xffffffff;
|
|
113
|
+
for (let i = 0; i < bytes.length; i++)
|
|
114
|
+
crc = CRC_TABLE[(crc ^ bytes[i]) & 0xff] ^ (crc >>> 8);
|
|
115
|
+
return (crc ^ 0xffffffff) >>> 0;
|
|
116
|
+
}
|
|
117
|
+
function chunk(type, data) {
|
|
118
|
+
const typeBytes = Buffer.from(type, 'ascii');
|
|
119
|
+
const buf = Buffer.alloc(8 + data.length + 4);
|
|
120
|
+
buf.writeUInt32BE(data.length, 0);
|
|
121
|
+
typeBytes.copy(buf, 4);
|
|
122
|
+
Buffer.from(data).copy(buf, 8);
|
|
123
|
+
const crcInput = new Uint8Array(typeBytes.length + data.length);
|
|
124
|
+
crcInput.set(typeBytes, 0);
|
|
125
|
+
crcInput.set(data, typeBytes.length);
|
|
126
|
+
buf.writeUInt32BE(crc32(crcInput), 8 + data.length);
|
|
127
|
+
return buf;
|
|
128
|
+
}
|
|
129
|
+
function encodePng(rgba, width, height) {
|
|
130
|
+
const sig = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
|
|
131
|
+
const ihdr = Buffer.alloc(13);
|
|
132
|
+
ihdr.writeUInt32BE(width, 0);
|
|
133
|
+
ihdr.writeUInt32BE(height, 4);
|
|
134
|
+
ihdr[8] = 8;
|
|
135
|
+
ihdr[9] = 6;
|
|
136
|
+
ihdr[10] = 0;
|
|
137
|
+
ihdr[11] = 0;
|
|
138
|
+
ihdr[12] = 0;
|
|
139
|
+
const raw = new Uint8Array(height * (1 + width * 4));
|
|
140
|
+
for (let y = 0; y < height; y++) {
|
|
141
|
+
raw[y * (1 + width * 4)] = 0;
|
|
142
|
+
raw.set(rgba.subarray(y * width * 4, (y + 1) * width * 4), y * (1 + width * 4) + 1);
|
|
143
|
+
}
|
|
144
|
+
const idat = deflateSync(raw);
|
|
145
|
+
return Buffer.concat([
|
|
146
|
+
sig,
|
|
147
|
+
chunk('IHDR', ihdr),
|
|
148
|
+
chunk('IDAT', idat),
|
|
149
|
+
chunk('IEND', new Uint8Array(0)),
|
|
150
|
+
]);
|
|
151
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { type ImageModelPullEvent } from '@bendyline/gezel';
|
|
2
|
+
import type { CatalogService } from '@bendyline/gezel-catalog';
|
|
3
|
+
import type { ImageProvider } from './types.js';
|
|
4
|
+
/**
|
|
5
|
+
* Snapshot of one in-flight (or recently-finished) image-model pull.
|
|
6
|
+
* Exposed to the UI via `GET /api/image-gen/pulls` so a client that
|
|
7
|
+
* mounted after the pull started can still render an accurate
|
|
8
|
+
* progress bar without subscribing to the live SSE.
|
|
9
|
+
*/
|
|
10
|
+
export interface ActiveImagePull {
|
|
11
|
+
id: string;
|
|
12
|
+
startedAt: string;
|
|
13
|
+
bytesWritten: number;
|
|
14
|
+
/** Best-known total. Zero until the first `progress` event arrives. */
|
|
15
|
+
totalBytes: number;
|
|
16
|
+
/** Present when the shared downloader is between retry attempts. */
|
|
17
|
+
retrying?: {
|
|
18
|
+
attempt: number;
|
|
19
|
+
maxAttempts: number;
|
|
20
|
+
delayMs: number;
|
|
21
|
+
reason: string;
|
|
22
|
+
};
|
|
23
|
+
/** Set once a terminal event has been observed (done or error). */
|
|
24
|
+
finished: boolean;
|
|
25
|
+
/** Set when the pull errored — kept on the snapshot so reconnecting clients see the failure. */
|
|
26
|
+
error?: string;
|
|
27
|
+
}
|
|
28
|
+
type Listener = (event: ImageModelPullEvent) => void;
|
|
29
|
+
/**
|
|
30
|
+
* Where pulls get their provider: the service's routing manager, or the
|
|
31
|
+
* hosted inference's local provider. Read on every pull so a provider reset
|
|
32
|
+
* (an engine binary swap) is picked up.
|
|
33
|
+
*/
|
|
34
|
+
export interface ImageProviderSource {
|
|
35
|
+
current(): Promise<Pick<ImageProvider, 'pullModel'>>;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Owns the lifecycle of every in-flight image-model pull.
|
|
39
|
+
*
|
|
40
|
+
* Pulls used to be tied 1:1 to the HTTP request that started them — when
|
|
41
|
+
* the client disconnected (navigated away from Settings → Image
|
|
42
|
+
* generation), the SSE loop exited, the async generator closed, and the
|
|
43
|
+
* download was aborted by the .partial-leaves-resumable code path. A
|
|
44
|
+
* returning client had no record the pull ever existed.
|
|
45
|
+
*
|
|
46
|
+
* The registry decouples those two: the pull runs in the background owned
|
|
47
|
+
* by this object, and HTTP requests are just consumers that subscribe to
|
|
48
|
+
* the live event stream + the cached snapshot. Disconnect detaches the
|
|
49
|
+
* consumer; the pull keeps going. Cancel is now an explicit `DELETE
|
|
50
|
+
* /pulls/:id` rather than a side-effect of closing the fetch.
|
|
51
|
+
*/
|
|
52
|
+
export declare class ImageModelPullRegistry {
|
|
53
|
+
private readonly entries;
|
|
54
|
+
private readonly controllers;
|
|
55
|
+
private readonly imageProvider;
|
|
56
|
+
private readonly catalog;
|
|
57
|
+
constructor(opts: {
|
|
58
|
+
imageProvider: ImageProviderSource;
|
|
59
|
+
catalog: CatalogService;
|
|
60
|
+
});
|
|
61
|
+
/**
|
|
62
|
+
* Start a pull for `id`. Idempotent — when a pull is already in flight
|
|
63
|
+
* for this id, returns the existing snapshot and does not kick off a
|
|
64
|
+
* second download. Throws if the catalog id is unknown.
|
|
65
|
+
*
|
|
66
|
+
* The pull itself runs in a detached `void` task; this method returns
|
|
67
|
+
* synchronously-claimed state so callers can immediately subscribe
|
|
68
|
+
* without racing the first event.
|
|
69
|
+
*/
|
|
70
|
+
start(id: string): Promise<{
|
|
71
|
+
snapshot: ActiveImagePull;
|
|
72
|
+
alreadyRunning: boolean;
|
|
73
|
+
}>;
|
|
74
|
+
list(): ActiveImagePull[];
|
|
75
|
+
get(id: string): ActiveImagePull | null;
|
|
76
|
+
/**
|
|
77
|
+
* Subscribe to live events for an in-flight pull. On subscribe, the
|
|
78
|
+
* registry immediately delivers a synthetic event reflecting the last
|
|
79
|
+
* known snapshot — without it, a late subscriber to a paused (between
|
|
80
|
+
* retry attempts) or just-started pull would see nothing until the
|
|
81
|
+
* next real event, leaving the UI's progress bar blank.
|
|
82
|
+
*
|
|
83
|
+
* Returns `null` when no entry exists for `id` so the caller can
|
|
84
|
+
* 404. Returns an unsubscribe function otherwise; calling it removes
|
|
85
|
+
* the listener but does NOT cancel the pull — disconnect ≠ cancel.
|
|
86
|
+
*/
|
|
87
|
+
subscribe(id: string, listener: Listener): (() => void) | null;
|
|
88
|
+
/**
|
|
89
|
+
* Cancel an in-flight pull. The shared downloader observes the abort,
|
|
90
|
+
* leaves the `.partial` in place for resume, and surfaces an `error`
|
|
91
|
+
* event whose snapshot stays in the registry for `FINISHED_TTL_MS`.
|
|
92
|
+
* Returns true when a live entry was aborted.
|
|
93
|
+
*/
|
|
94
|
+
cancel(id: string): boolean;
|
|
95
|
+
/** Test-only: drop everything so a fresh test starts clean. */
|
|
96
|
+
clear(): void;
|
|
97
|
+
private replaySnapshot;
|
|
98
|
+
private consume;
|
|
99
|
+
private applyEvent;
|
|
100
|
+
private broadcast;
|
|
101
|
+
}
|
|
102
|
+
export declare class UnknownImageModelError extends Error {
|
|
103
|
+
readonly id: string;
|
|
104
|
+
constructor(id: string);
|
|
105
|
+
}
|
|
106
|
+
export {};
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import { createLogger } from '@bendyline/gezel';
|
|
2
|
+
const log = createLogger('image-pulls');
|
|
3
|
+
/**
|
|
4
|
+
* How long a finished pull (done OR error) stays in the registry after
|
|
5
|
+
* the terminal event. Long enough that a client polling once per second
|
|
6
|
+
* sees the final state on reconnect; short enough that the catalog UI's
|
|
7
|
+
* "Installed" row gets to take over.
|
|
8
|
+
*/
|
|
9
|
+
const FINISHED_TTL_MS = 8_000;
|
|
10
|
+
/**
|
|
11
|
+
* Owns the lifecycle of every in-flight image-model pull.
|
|
12
|
+
*
|
|
13
|
+
* Pulls used to be tied 1:1 to the HTTP request that started them — when
|
|
14
|
+
* the client disconnected (navigated away from Settings → Image
|
|
15
|
+
* generation), the SSE loop exited, the async generator closed, and the
|
|
16
|
+
* download was aborted by the .partial-leaves-resumable code path. A
|
|
17
|
+
* returning client had no record the pull ever existed.
|
|
18
|
+
*
|
|
19
|
+
* The registry decouples those two: the pull runs in the background owned
|
|
20
|
+
* by this object, and HTTP requests are just consumers that subscribe to
|
|
21
|
+
* the live event stream + the cached snapshot. Disconnect detaches the
|
|
22
|
+
* consumer; the pull keeps going. Cancel is now an explicit `DELETE
|
|
23
|
+
* /pulls/:id` rather than a side-effect of closing the fetch.
|
|
24
|
+
*/
|
|
25
|
+
export class ImageModelPullRegistry {
|
|
26
|
+
entries = new Map();
|
|
27
|
+
controllers = new Map();
|
|
28
|
+
imageProvider;
|
|
29
|
+
catalog;
|
|
30
|
+
constructor(opts) {
|
|
31
|
+
this.imageProvider = opts.imageProvider;
|
|
32
|
+
this.catalog = opts.catalog;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Start a pull for `id`. Idempotent — when a pull is already in flight
|
|
36
|
+
* for this id, returns the existing snapshot and does not kick off a
|
|
37
|
+
* second download. Throws if the catalog id is unknown.
|
|
38
|
+
*
|
|
39
|
+
* The pull itself runs in a detached `void` task; this method returns
|
|
40
|
+
* synchronously-claimed state so callers can immediately subscribe
|
|
41
|
+
* without racing the first event.
|
|
42
|
+
*/
|
|
43
|
+
async start(id) {
|
|
44
|
+
const existing = this.entries.get(id);
|
|
45
|
+
if (existing && !existing.snapshot.finished) {
|
|
46
|
+
return { snapshot: { ...existing.snapshot }, alreadyRunning: true };
|
|
47
|
+
}
|
|
48
|
+
// Resolve the catalog entry up-front so we can fail fast with a
|
|
49
|
+
// proper 404 rather than emitting a 200 + error SSE for "unknown id".
|
|
50
|
+
const item = await this.catalog.get('image-model', id);
|
|
51
|
+
if (!item || item.manifest.kind !== 'image-model') {
|
|
52
|
+
throw new UnknownImageModelError(id);
|
|
53
|
+
}
|
|
54
|
+
const manifest = item.manifest;
|
|
55
|
+
const snapshot = {
|
|
56
|
+
id,
|
|
57
|
+
startedAt: new Date().toISOString(),
|
|
58
|
+
bytesWritten: 0,
|
|
59
|
+
totalBytes: manifest.approxSizeBytes +
|
|
60
|
+
manifest.auxiliaryFiles.reduce((n, f) => n + f.approxSizeBytes, 0),
|
|
61
|
+
finished: false,
|
|
62
|
+
};
|
|
63
|
+
const entry = { snapshot, listeners: new Set() };
|
|
64
|
+
this.entries.set(id, entry);
|
|
65
|
+
const controller = new AbortController();
|
|
66
|
+
this.controllers.set(id, controller);
|
|
67
|
+
const spec = {
|
|
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);
|
|
84
|
+
return { snapshot: { ...snapshot }, alreadyRunning: false };
|
|
85
|
+
}
|
|
86
|
+
list() {
|
|
87
|
+
return Array.from(this.entries.values()).map((e) => ({ ...e.snapshot }));
|
|
88
|
+
}
|
|
89
|
+
get(id) {
|
|
90
|
+
const entry = this.entries.get(id);
|
|
91
|
+
return entry ? { ...entry.snapshot } : null;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Subscribe to live events for an in-flight pull. On subscribe, the
|
|
95
|
+
* registry immediately delivers a synthetic event reflecting the last
|
|
96
|
+
* known snapshot — without it, a late subscriber to a paused (between
|
|
97
|
+
* retry attempts) or just-started pull would see nothing until the
|
|
98
|
+
* next real event, leaving the UI's progress bar blank.
|
|
99
|
+
*
|
|
100
|
+
* Returns `null` when no entry exists for `id` so the caller can
|
|
101
|
+
* 404. Returns an unsubscribe function otherwise; calling it removes
|
|
102
|
+
* the listener but does NOT cancel the pull — disconnect ≠ cancel.
|
|
103
|
+
*/
|
|
104
|
+
subscribe(id, listener) {
|
|
105
|
+
const entry = this.entries.get(id);
|
|
106
|
+
if (!entry)
|
|
107
|
+
return null;
|
|
108
|
+
entry.listeners.add(listener);
|
|
109
|
+
// Replay the current state so the UI renders immediately.
|
|
110
|
+
this.replaySnapshot(entry.snapshot, listener);
|
|
111
|
+
return () => {
|
|
112
|
+
entry.listeners.delete(listener);
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Cancel an in-flight pull. The shared downloader observes the abort,
|
|
117
|
+
* leaves the `.partial` in place for resume, and surfaces an `error`
|
|
118
|
+
* event whose snapshot stays in the registry for `FINISHED_TTL_MS`.
|
|
119
|
+
* Returns true when a live entry was aborted.
|
|
120
|
+
*/
|
|
121
|
+
cancel(id) {
|
|
122
|
+
const controller = this.controllers.get(id);
|
|
123
|
+
const entry = this.entries.get(id);
|
|
124
|
+
if (!controller || !entry || entry.snapshot.finished)
|
|
125
|
+
return false;
|
|
126
|
+
controller.abort();
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
/** Test-only: drop everything so a fresh test starts clean. */
|
|
130
|
+
clear() {
|
|
131
|
+
for (const controller of this.controllers.values())
|
|
132
|
+
controller.abort();
|
|
133
|
+
this.controllers.clear();
|
|
134
|
+
this.entries.clear();
|
|
135
|
+
}
|
|
136
|
+
replaySnapshot(snapshot, listener) {
|
|
137
|
+
// The order matters: deliver any in-progress retry banner LAST so it
|
|
138
|
+
// overrides the bare progress bar in the UI's state machine.
|
|
139
|
+
listener({
|
|
140
|
+
type: 'progress',
|
|
141
|
+
bytesWritten: snapshot.bytesWritten,
|
|
142
|
+
...(snapshot.totalBytes > 0 ? { totalBytes: snapshot.totalBytes } : {}),
|
|
143
|
+
});
|
|
144
|
+
if (snapshot.retrying)
|
|
145
|
+
listener({ type: 'retrying', ...snapshot.retrying });
|
|
146
|
+
if (snapshot.error)
|
|
147
|
+
listener({ type: 'error', error: snapshot.error });
|
|
148
|
+
if (snapshot.finished && !snapshot.error)
|
|
149
|
+
listener({ type: 'done', id: snapshot.id });
|
|
150
|
+
}
|
|
151
|
+
async consume(id, spec, signal) {
|
|
152
|
+
const entry = this.entries.get(id);
|
|
153
|
+
if (!entry)
|
|
154
|
+
return;
|
|
155
|
+
try {
|
|
156
|
+
const provider = await this.imageProvider.current();
|
|
157
|
+
for await (const event of provider.pullModel(id, spec, signal)) {
|
|
158
|
+
this.applyEvent(entry, event);
|
|
159
|
+
this.broadcast(entry, event);
|
|
160
|
+
if (event.type === 'done' || event.type === 'error')
|
|
161
|
+
break;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
catch (err) {
|
|
165
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
166
|
+
const errorEvent = { type: 'error', error: message };
|
|
167
|
+
this.applyEvent(entry, errorEvent);
|
|
168
|
+
this.broadcast(entry, errorEvent);
|
|
169
|
+
log.warn(`[pull-registry] ${id} crashed: ${message}`);
|
|
170
|
+
}
|
|
171
|
+
finally {
|
|
172
|
+
entry.snapshot.finished = true;
|
|
173
|
+
// Drop the controller — cancel() should be a no-op once finished.
|
|
174
|
+
this.controllers.delete(id);
|
|
175
|
+
// Keep the snapshot around briefly so a client mid-reconnect still
|
|
176
|
+
// sees the terminal state on `list()` / `subscribe()`. Then garbage
|
|
177
|
+
// collect.
|
|
178
|
+
const timer = setTimeout(() => {
|
|
179
|
+
if (this.entries.get(id) === entry)
|
|
180
|
+
this.entries.delete(id);
|
|
181
|
+
}, FINISHED_TTL_MS);
|
|
182
|
+
timer.unref?.();
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
applyEvent(entry, event) {
|
|
186
|
+
const s = entry.snapshot;
|
|
187
|
+
if (event.type === 'progress') {
|
|
188
|
+
s.bytesWritten = event.bytesWritten;
|
|
189
|
+
if (event.totalBytes !== undefined)
|
|
190
|
+
s.totalBytes = event.totalBytes;
|
|
191
|
+
delete s.retrying;
|
|
192
|
+
}
|
|
193
|
+
else if (event.type === 'retrying') {
|
|
194
|
+
s.retrying = {
|
|
195
|
+
attempt: event.attempt,
|
|
196
|
+
maxAttempts: event.maxAttempts,
|
|
197
|
+
delayMs: event.delayMs,
|
|
198
|
+
reason: event.reason,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
else if (event.type === 'error') {
|
|
202
|
+
s.error = event.error;
|
|
203
|
+
delete s.retrying;
|
|
204
|
+
}
|
|
205
|
+
else if (event.type === 'done') {
|
|
206
|
+
delete s.retrying;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
broadcast(entry, event) {
|
|
210
|
+
for (const listener of entry.listeners) {
|
|
211
|
+
try {
|
|
212
|
+
listener(event);
|
|
213
|
+
}
|
|
214
|
+
catch (err) {
|
|
215
|
+
log.warn(`[pull-registry] listener threw: ${err instanceof Error ? err.message : err}`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
export class UnknownImageModelError extends Error {
|
|
221
|
+
id;
|
|
222
|
+
constructor(id) {
|
|
223
|
+
super(`unknown image-model: ${id}`);
|
|
224
|
+
this.id = id;
|
|
225
|
+
this.name = 'UnknownImageModelError';
|
|
226
|
+
}
|
|
227
|
+
}
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* StableDiffusionCppProvider — talks to a local `sd-server` binary over
|
|
3
|
+
* HTTP. The server speaks an OpenAI-compatible `/v1/images/generations`
|
|
4
|
+
* shape with a few extensions (seed, sample_steps, negative_prompt,
|
|
5
|
+
* cfg_scale) supported by upstream sd.cpp.
|
|
6
|
+
*
|
|
7
|
+
* Lifecycle of the server itself is not the provider's concern — an
|
|
8
|
+
* Electron-side supervisor (Phase 3) spawns / idle-stops the binary and
|
|
9
|
+
* the provider simply hits whichever URL is configured. If the server
|
|
10
|
+
* isn't reachable, {@link generate} surfaces a clear error so the UI
|
|
11
|
+
* can prompt the user to enable image generation in Settings.
|
|
12
|
+
*
|
|
13
|
+
* Model management is filesystem-only: weights live under
|
|
14
|
+
* `<modelsRoot>/<id>/weights.*` alongside a resolved copy of the
|
|
15
|
+
* catalog manifest (so local state is self-describing even if the
|
|
16
|
+
* catalog entry later disappears).
|
|
17
|
+
*/
|
|
18
|
+
import { type ModelStorageRoots } from '../../models/storage-roots.js';
|
|
19
|
+
import type { GpuArbiter } from '../gpu-arbiter.js';
|
|
20
|
+
import type { NativeEngineSupervisor } from '../native/supervisor.js';
|
|
21
|
+
import { type ImageEngineHealth, type ImageGenerationInput, type ImageGenerationOutput, type ImageGenerationProgress, type ImageModelPullEvent, type ImageModelPullSpec, type ImageProvider, type InstalledImageModelInfo } from './types.js';
|
|
22
|
+
export interface StableDiffusionCppProviderOptions {
|
|
23
|
+
/** Base URL of the running sd-server (no trailing slash). */
|
|
24
|
+
baseUrl: string;
|
|
25
|
+
/** Absolute path to `~/.gezel/engines/sd-cpp/models`. */
|
|
26
|
+
modelsRoot: string;
|
|
27
|
+
/** Optional read-only machine-store overlay. */
|
|
28
|
+
storageRoots?: ModelStorageRoots;
|
|
29
|
+
/** Awake-time budget for one render request, in ms. Defaults to 30 minutes. */
|
|
30
|
+
timeoutMs?: number;
|
|
31
|
+
/** Injected for tests. Defaults to global fetch. */
|
|
32
|
+
fetchImpl?: typeof fetch;
|
|
33
|
+
/**
|
|
34
|
+
* Optional lifecycle manager. When supplied, the provider calls
|
|
35
|
+
* `ensureRunning()` before each generate and uses the returned
|
|
36
|
+
* baseUrl (overriding `baseUrl`) — so lazy-start / idle-stop /
|
|
37
|
+
* health-watch all work transparently. Omit for "user runs
|
|
38
|
+
* sd-server themselves" deployments.
|
|
39
|
+
*/
|
|
40
|
+
supervisor?: NativeEngineSupervisor;
|
|
41
|
+
/**
|
|
42
|
+
* Whether the factory had a real engine wiring (an explicit URL or a
|
|
43
|
+
* spawnable binary) at construction. The default-loopback fallback
|
|
44
|
+
* sets this to false so `health()` can return `not-configured`
|
|
45
|
+
* instead of `unreachable` — distinct UI state, distinct guidance.
|
|
46
|
+
*/
|
|
47
|
+
configured?: boolean;
|
|
48
|
+
/**
|
|
49
|
+
* Cross-engine GPU coordinator. When supplied alongside a
|
|
50
|
+
* supervisor, the provider acquires the `'image'` slot before each
|
|
51
|
+
* generate (which evicts the local LLM in `swap` mode) and
|
|
52
|
+
* registers its supervisor's `stop()` as the `'image'` evictor so
|
|
53
|
+
* the LLM can evict us back symmetrically.
|
|
54
|
+
*/
|
|
55
|
+
arbiter?: GpuArbiter;
|
|
56
|
+
/**
|
|
57
|
+
* Shared, mutable holder the supervised path uses to tell the
|
|
58
|
+
* factory's `resolveLaunch` which installed model to spawn sd-server
|
|
59
|
+
* with. sd-server serves one model per process, so when a request
|
|
60
|
+
* targets a different installed model than the one loaded, the
|
|
61
|
+
* provider updates `modelId` here and stops the running server — the
|
|
62
|
+
* next `ensureRunning()` relaunches with the requested model instead
|
|
63
|
+
* of always serving whichever model sorts first.
|
|
64
|
+
*/
|
|
65
|
+
launchState?: {
|
|
66
|
+
modelId: string | undefined;
|
|
67
|
+
};
|
|
68
|
+
/**
|
|
69
|
+
* The user's configured default model (`config.defaultImageModel['sd-cpp']`,
|
|
70
|
+
* Settings -> Image generation). Read on every generate that names no model
|
|
71
|
+
* of its own. A configured id that is no longer installed falls through to
|
|
72
|
+
* the first installed model, which is what every install did before this
|
|
73
|
+
* setting existed.
|
|
74
|
+
*/
|
|
75
|
+
defaultModelId?: string;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Model-specific step counts, from fast distilled models to the 50 of MOSS
|
|
79
|
+
* and Iris. Shared with `buildSdServerArgs`, which sets
|
|
80
|
+
* the same server launch default for endpoints that ignore request steps.
|
|
81
|
+
*/
|
|
82
|
+
export declare const MODEL_SAMPLE_STEP_DEFAULTS: Map<string, number>;
|
|
83
|
+
/**
|
|
84
|
+
* Classifier-free guidance (CFG) scale per model, applied to the
|
|
85
|
+
* `/sdapi/v1/txt2img` request. Distilled / Flux-flow models are trained
|
|
86
|
+
* to run at CFG 1 — their guidance is baked in (turbo/lightning) or
|
|
87
|
+
* carried by the separate Flux distilled-guidance value. Applying
|
|
88
|
+
* sd-server's higher default CFG to them blows out color and contrast
|
|
89
|
+
* (the oversaturated, "burned" look Krea 2 Turbo had before this).
|
|
90
|
+
* Models absent from the map (traditional SD 1.x / SDXL checkpoints)
|
|
91
|
+
* omit `cfg_scale` and inherit the server default, preserving their
|
|
92
|
+
* established behavior.
|
|
93
|
+
*/
|
|
94
|
+
export declare const MODEL_CFG_DEFAULTS: Map<string, number>;
|
|
95
|
+
/**
|
|
96
|
+
* Whether the engine runs this model with guidance that reads a negative
|
|
97
|
+
* prompt. At CFG 1 the unconditional pass that carries it is skipped.
|
|
98
|
+
* Models absent from the map run at the server's default of 7.
|
|
99
|
+
*/
|
|
100
|
+
export declare function sdCppHonorsNegativePrompt(modelId: string): boolean;
|
|
101
|
+
/** Pin samplers validated with the native engine for these models. */
|
|
102
|
+
export declare const MODEL_SAMPLER_DEFAULTS: Map<string, string>;
|
|
103
|
+
/**
|
|
104
|
+
* Models whose VAE shows visible tile-boundary seams under
|
|
105
|
+
* stable-diffusion.cpp's `--vae-tiling` (Krea 2's Qwen-Image VAE is the
|
|
106
|
+
* known case — rainbow column streaks at ≤512px). `--vae-tiling` is a
|
|
107
|
+
* launch-time, size-agnostic flag: sd-server serves every request size
|
|
108
|
+
* from one process, so we can't gate it per-request on image size as
|
|
109
|
+
* we'd ideally like — we gate it per-model instead and skip it here.
|
|
110
|
+
* The trade-off is that very large renders of these models could hit a
|
|
111
|
+
* VAE allocation cap on memory-constrained GPUs; acceptable given these
|
|
112
|
+
* are high-VRAM models and seam-free output matters more.
|
|
113
|
+
*/
|
|
114
|
+
export declare const VAE_TILING_INCOMPATIBLE_MODELS: Set<string>;
|
|
115
|
+
/**
|
|
116
|
+
* The launch choices that change what sd-server holds in memory for one
|
|
117
|
+
* model. Read by the launch argv and by memory pricing, so the price is for
|
|
118
|
+
* the process that actually starts.
|
|
119
|
+
*/
|
|
120
|
+
export interface SdServerLoadProfile {
|
|
121
|
+
vaeDecode: 'tiled' | 'untiled' | 'none';
|
|
122
|
+
/** The default render size, which working memory follows more than the weights do. */
|
|
123
|
+
defaultSidePx: number;
|
|
124
|
+
/** A `--tensor-type-rules` pattern whose tensors load as bfloat16. */
|
|
125
|
+
bf16TensorPattern?: string;
|
|
126
|
+
diffusionFlashAttention: boolean;
|
|
127
|
+
/**
|
|
128
|
+
* Reuse a diffusion transformer's output across steps whose input barely
|
|
129
|
+
* changed (`--cache-mode easycache`). Only for a model whose renders it was
|
|
130
|
+
* measured not to change.
|
|
131
|
+
*/
|
|
132
|
+
stepCache?: 'easycache';
|
|
133
|
+
}
|
|
134
|
+
export declare function sdServerLoadProfile(modelId: string): SdServerLoadProfile;
|
|
135
|
+
export declare class StableDiffusionCppProvider implements ImageProvider {
|
|
136
|
+
readonly name = "sd-cpp";
|
|
137
|
+
private readonly baseUrl;
|
|
138
|
+
private readonly modelsRoot;
|
|
139
|
+
private readonly storageRoots;
|
|
140
|
+
private readonly timeoutMs;
|
|
141
|
+
private readonly fetchImpl;
|
|
142
|
+
private readonly supervisor?;
|
|
143
|
+
private readonly configured;
|
|
144
|
+
private readonly arbiter?;
|
|
145
|
+
private readonly launchState?;
|
|
146
|
+
private readonly defaultModelId?;
|
|
147
|
+
private renderTail;
|
|
148
|
+
private pendingRenders;
|
|
149
|
+
constructor(opts: StableDiffusionCppProviderOptions);
|
|
150
|
+
generate(input: ImageGenerationInput): Promise<ImageGenerationOutput>;
|
|
151
|
+
private generateInner;
|
|
152
|
+
listInstalledModels(): Promise<InstalledImageModelInfo[]>;
|
|
153
|
+
pullModel(id: string, spec: ImageModelPullSpec, signal?: AbortSignal): AsyncIterable<ImageModelPullEvent>;
|
|
154
|
+
/**
|
|
155
|
+
* Stream a single URL to disk with sha256 verification, reporting cumulative
|
|
156
|
+
* progress against `totalAllBytes` so multi-file pulls share one bar.
|
|
157
|
+
*
|
|
158
|
+
* Delegates the network plumbing to `downloadWithRetry` — that handles
|
|
159
|
+
* Range-based resume, exponential backoff, chunk-stall timeouts, and
|
|
160
|
+
* friendly error messages. This wrapper only owns the
|
|
161
|
+
* provider-specific concerns: sha256 verification, the
|
|
162
|
+
* `writtenSoFar`-aware progress translation, and the atomic rename
|
|
163
|
+
* after verify.
|
|
164
|
+
*/
|
|
165
|
+
private downloadFile;
|
|
166
|
+
deleteModel(id: string): Promise<void>;
|
|
167
|
+
health(): Promise<ImageEngineHealth>;
|
|
168
|
+
shutdown(): Promise<void>;
|
|
169
|
+
/**
|
|
170
|
+
* The configured default when it is installed, else the first installed
|
|
171
|
+
* model id, else undefined. Stable ordering matches
|
|
172
|
+
* {@link listInstalledModels} so the default is deterministic.
|
|
173
|
+
*/
|
|
174
|
+
resolveDefaultModelId(): Promise<string | undefined>;
|
|
175
|
+
private defaultSampleSteps;
|
|
176
|
+
/**
|
|
177
|
+
* Read the installed `manifest.json` fields that steer per-model
|
|
178
|
+
* behavior. Returns null for unknown ids and unreadable/legacy
|
|
179
|
+
* metadata — callers fall back to their per-field defaults. (Older
|
|
180
|
+
* installed metadata did not persist catalog generation defaults or
|
|
181
|
+
* the img2img capability.)
|
|
182
|
+
*/
|
|
183
|
+
private readInstalledManifestFields;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Parse one line of sd-server's weight-load bar, which it prints inside the
|
|
187
|
+
* first request after a launch because it loads weights lazily:
|
|
188
|
+
*
|
|
189
|
+
* |######### | 49/298 - 435.57MB/s
|
|
190
|
+
*/
|
|
191
|
+
export declare function parseWeightLoadProgress(line: string): ImageGenerationProgress | null;
|
|
192
|
+
/**
|
|
193
|
+
* Classify sd-server's progress bars into phases. Weight loads print a
|
|
194
|
+
* bytes-per-second bar. The first iteration bar is sampling; once it
|
|
195
|
+
* completes, the next is the tiled VAE decode, which reports tiles rather
|
|
196
|
+
* than steps. The step count the engine prints is used rather than the one
|
|
197
|
+
* requested, because some launch defaults override a request's steps.
|
|
198
|
+
*/
|
|
199
|
+
export declare function progressTracker(emit: (progress: ImageGenerationProgress) => void): (line: string) => void;
|
|
200
|
+
export declare function parseSamplingProgress(line: string): ImageGenerationProgress | null;
|