@nhtio/adk 0.1.0-master-91c13c80 → 0.1.0-master-a4cc5f78
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/batteries/generation/local_diffusion/adapter.cjs +517 -0
- package/batteries/generation/local_diffusion/adapter.cjs.map +1 -0
- package/batteries/generation/local_diffusion/adapter.d.ts +25 -0
- package/batteries/generation/local_diffusion/adapter.mjs +515 -0
- package/batteries/generation/local_diffusion/adapter.mjs.map +1 -0
- package/batteries/generation/local_diffusion/exceptions.cjs +35 -0
- package/batteries/generation/local_diffusion/exceptions.cjs.map +1 -0
- package/batteries/generation/local_diffusion/exceptions.d.ts +31 -0
- package/batteries/generation/local_diffusion/exceptions.mjs +26 -0
- package/batteries/generation/local_diffusion/exceptions.mjs.map +1 -0
- package/batteries/generation/local_diffusion/index.d.ts +10 -0
- package/batteries/generation/local_diffusion/protocol.cjs +321 -0
- package/batteries/generation/local_diffusion/protocol.cjs.map +1 -0
- package/batteries/generation/local_diffusion/protocol.d.ts +170 -0
- package/batteries/generation/local_diffusion/protocol.mjs +313 -0
- package/batteries/generation/local_diffusion/protocol.mjs.map +1 -0
- package/batteries/generation/local_diffusion/types.cjs +2 -0
- package/batteries/generation/local_diffusion/types.d.ts +129 -0
- package/batteries/generation/local_diffusion/types.mjs +0 -0
- package/batteries/generation/local_diffusion/validation.cjs +62 -0
- package/batteries/generation/local_diffusion/validation.cjs.map +1 -0
- package/batteries/generation/local_diffusion/validation.d.ts +5 -0
- package/batteries/generation/local_diffusion/validation.mjs +59 -0
- package/batteries/generation/local_diffusion/validation.mjs.map +1 -0
- package/batteries/generation/local_diffusion.cjs +16 -0
- package/batteries/generation/local_diffusion.mjs +4 -0
- package/batteries/llm/chat_common/lifecycle.d.ts +12 -4
- package/batteries/media/forge.cjs +1 -1
- package/batteries/media/forge.mjs +1 -1
- package/batteries/media/lint.cjs +1 -1
- package/batteries/media/lint.mjs +1 -1
- package/batteries/media.cjs +1 -1
- package/batteries/media.mjs +1 -1
- package/batteries/tts/native/adapter.cjs +1 -1
- package/batteries/tts/native/adapter.mjs +1 -1
- package/batteries/tts/native.cjs +1 -1
- package/batteries/tts/native.mjs +1 -1
- package/eslint.cjs +1 -1
- package/eslint.mjs +1 -1
- package/index.cjs +1 -1
- package/index.mjs +1 -1
- package/lifecycle-C4kwYJ-N.mjs.map +1 -1
- package/lifecycle-vYQJ2rmf.js.map +1 -1
- package/mcp/adk-docs-corpus.json +1 -1
- package/package.json +331 -301
- package/server.json +2 -2
- package/skills/adk-assembly/SKILL.md +2 -2
- package/{validate-BM00UtKB.mjs → validate-Dnp_0cFt.mjs} +150 -150
- package/validate-Dnp_0cFt.mjs.map +1 -0
- package/{validate-DnFwZdPY.js → validate-VZXWZ5p4.js} +149 -149
- package/validate-VZXWZ5p4.js.map +1 -0
- package/validate-BM00UtKB.mjs.map +0 -1
- package/validate-DnFwZdPY.js.map +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -15,6 +15,12 @@ you *when* you got it, not *what changed*: a `^` range will float across battery
|
|
|
15
15
|
breaking changes, so pin an exact version if you need stability and read the entry before
|
|
16
16
|
upgrading.
|
|
17
17
|
|
|
18
|
+
## 2026-07-18
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
- **New generation engine: `local_diffusion` — a BYO-inference-subprocess image-generation engine over a stdio line protocol.** The Node-only {@link LocalDiffusionGenerationAdapter} (`@nhtio/adk/batteries/generation/local_diffusion`, subpath-only — excluded from the environment-neutral generation aggregate) drives a user-supplied inference subprocess over a stdin/stdout line protocol (modeled on DiffusionBee), so a consumer can run a local Stable-Diffusion checkpoint without the ADK bundling Python/torch. Same `generate`/`edit` → `Promise<GeneratedMediaOutput[]>` contract as the other generation engines, plus streamed per-step progress (`dnpr` → the `generating` lifecycle phase), best-effort cancellation (`AbortSignal` → advisory `__stop__`, with `reset()`/`dispose()` as the hard stop), single-flight admission, `outputDir`-contained cleanup of backend-written files, and both inline-base64 and file-path image results. Ships the protocol + a documented Python reference backend; the consumer supplies the process.
|
|
23
|
+
|
|
18
24
|
## 2026-07-13
|
|
19
25
|
|
|
20
26
|
### Added
|
|
@@ -0,0 +1,517 @@
|
|
|
1
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
2
|
+
require("../../../chunk-Cek0wNdY.js");
|
|
3
|
+
const require_tokenizable = require("../../../tokenizable-CAwoWiev.js");
|
|
4
|
+
const require_tool_call = require("../../../tool_call-B-mazneL.js");
|
|
5
|
+
require("../../../guards.cjs");
|
|
6
|
+
const require_lifecycle = require("../../../lifecycle-vYQJ2rmf.js");
|
|
7
|
+
const require_batteries_generation__shared = require("../_shared.cjs");
|
|
8
|
+
const require_batteries_generation_local_diffusion_exceptions = require("./exceptions.cjs");
|
|
9
|
+
const require_batteries_generation_local_diffusion_validation = require("./validation.cjs");
|
|
10
|
+
const require_batteries_generation_local_diffusion_protocol = require("./protocol.cjs");
|
|
11
|
+
//#region src/batteries/generation/local_diffusion/adapter.ts
|
|
12
|
+
/** Node-only local diffusion generation adapter.
|
|
13
|
+
* @module @nhtio/adk/batteries/generation/local_diffusion/adapter
|
|
14
|
+
*/
|
|
15
|
+
var BATTERY = "local_diffusion_generation";
|
|
16
|
+
var DEFAULT_MAX_DECODED_BYTES = 50 * 1024 * 1024;
|
|
17
|
+
/** The rejection error for a non-success outcome (an aborted outcome maps to the typed abort error). */
|
|
18
|
+
var outcomeError = (outcome) => outcome.kind === "aborted" ? new require_batteries_generation_local_diffusion_exceptions.E_LOCAL_DIFFUSION_ABORTED() : outcome.error;
|
|
19
|
+
var deferred = () => {
|
|
20
|
+
let resolve;
|
|
21
|
+
let reject;
|
|
22
|
+
return {
|
|
23
|
+
promise: new Promise((res, rej) => {
|
|
24
|
+
resolve = res;
|
|
25
|
+
reject = rej;
|
|
26
|
+
}),
|
|
27
|
+
resolve,
|
|
28
|
+
reject
|
|
29
|
+
};
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Whether `candidate` resolves to a location strictly inside `dir` — real containment (resolves `..`
|
|
33
|
+
* and rejects sibling-prefix escapes like `/output2`), not a raw string `startsWith`.
|
|
34
|
+
*/
|
|
35
|
+
var isContained = (dir, candidate, sep) => {
|
|
36
|
+
const base = dir.endsWith(sep) ? dir.slice(0, -sep.length) : dir;
|
|
37
|
+
return candidate === base || candidate.startsWith(base + sep);
|
|
38
|
+
};
|
|
39
|
+
/** Local diffusion adapter backed by a line-protocol subprocess. */
|
|
40
|
+
var LocalDiffusionGenerationAdapter = class LocalDiffusionGenerationAdapter {
|
|
41
|
+
#options;
|
|
42
|
+
#process;
|
|
43
|
+
#epoch = 0;
|
|
44
|
+
#state = "idle";
|
|
45
|
+
#loadPromise;
|
|
46
|
+
#loadReject;
|
|
47
|
+
#request;
|
|
48
|
+
#rid = 0;
|
|
49
|
+
#disposePromise;
|
|
50
|
+
#listeners = [];
|
|
51
|
+
/** Whether the current host exposes Node's process runtime. */
|
|
52
|
+
static isAvailable() {
|
|
53
|
+
return typeof process !== "undefined" && Boolean(process.versions?.node);
|
|
54
|
+
}
|
|
55
|
+
/** Construct and validate an adapter. */
|
|
56
|
+
constructor(options) {
|
|
57
|
+
this.#options = require_batteries_generation_local_diffusion_validation.validateOptions(options);
|
|
58
|
+
}
|
|
59
|
+
/** Whether this instance can run, honoring its injected probe. */
|
|
60
|
+
isAvailable() {
|
|
61
|
+
return (this.#options.isAvailable ?? LocalDiffusionGenerationAdapter.isAvailable)();
|
|
62
|
+
}
|
|
63
|
+
/** Start the backend and wait for its ready event. */
|
|
64
|
+
async preload() {
|
|
65
|
+
this.#assertLive();
|
|
66
|
+
if (this.#state === "ready" || this.#state === "running" || this.#state === "stopping") return;
|
|
67
|
+
if (this.#loadPromise) return this.#loadPromise;
|
|
68
|
+
const loadPromise = this.#start();
|
|
69
|
+
this.#loadPromise = loadPromise;
|
|
70
|
+
loadPromise.catch(() => void 0).finally(() => {
|
|
71
|
+
if (this.#loadPromise === loadPromise) {
|
|
72
|
+
this.#loadPromise = void 0;
|
|
73
|
+
this.#loadReject = void 0;
|
|
74
|
+
}
|
|
75
|
+
});
|
|
76
|
+
return loadPromise;
|
|
77
|
+
}
|
|
78
|
+
/** Synchronously terminate the current process and invalidate all continuations. */
|
|
79
|
+
reset() {
|
|
80
|
+
if (this.#state === "disposed") return;
|
|
81
|
+
this.#retire(new require_batteries_generation_local_diffusion_exceptions.E_LOCAL_DIFFUSION_BACKEND_ERROR(["adapter reset"]));
|
|
82
|
+
this.#state = "idle";
|
|
83
|
+
}
|
|
84
|
+
/** Gracefully stop the backend, then force terminate it. Terminal and idempotent. */
|
|
85
|
+
dispose() {
|
|
86
|
+
if (this.#disposePromise) return this.#disposePromise;
|
|
87
|
+
this.#disposePromise = this.#dispose();
|
|
88
|
+
return this.#disposePromise;
|
|
89
|
+
}
|
|
90
|
+
async #dispose() {
|
|
91
|
+
const process = this.#process;
|
|
92
|
+
this.#retire(new require_batteries_generation_local_diffusion_exceptions.E_LOCAL_DIFFUSION_DISPOSED(), false);
|
|
93
|
+
this.#state = "disposed";
|
|
94
|
+
if (process && process.stdin) {
|
|
95
|
+
try {
|
|
96
|
+
process.stdin.write(require_batteries_generation_local_diffusion_protocol.buildShutdownCommand(this.#protocol()));
|
|
97
|
+
} catch {}
|
|
98
|
+
await new Promise((resolve) => {
|
|
99
|
+
const timer = setTimeout(resolve, this.#options.disposeGraceMs ?? 5e3);
|
|
100
|
+
const finish = () => {
|
|
101
|
+
clearTimeout(timer);
|
|
102
|
+
process.off("close", finish);
|
|
103
|
+
resolve();
|
|
104
|
+
};
|
|
105
|
+
process.on("close", finish);
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
process?.kill();
|
|
109
|
+
}
|
|
110
|
+
/** Async-disposal protocol alias. */
|
|
111
|
+
async [Symbol.asyncDispose]() {
|
|
112
|
+
await this.dispose();
|
|
113
|
+
}
|
|
114
|
+
/** Generate images from text. */
|
|
115
|
+
async generate(prompt, opts) {
|
|
116
|
+
return this.#call("generate", prompt, void 0, opts);
|
|
117
|
+
}
|
|
118
|
+
/** Edit images with text guidance. */
|
|
119
|
+
async edit(inputs, prompt, opts) {
|
|
120
|
+
const normalized = await Promise.all((Array.isArray(inputs) ? inputs : [inputs]).map(require_batteries_generation__shared.toBytes));
|
|
121
|
+
return this.#call("edit", prompt, normalized, opts);
|
|
122
|
+
}
|
|
123
|
+
#assertLive() {
|
|
124
|
+
if (this.#state === "disposed" || this.#disposePromise) throw new require_batteries_generation_local_diffusion_exceptions.E_LOCAL_DIFFUSION_DISPOSED();
|
|
125
|
+
}
|
|
126
|
+
#protocol() {
|
|
127
|
+
const o = this.#options;
|
|
128
|
+
return {
|
|
129
|
+
...require_batteries_generation_local_diffusion_protocol.DEFAULT_PROTOCOL,
|
|
130
|
+
...o.protocol ?? {},
|
|
131
|
+
commandPrefix: o.commandPrefix ?? o.protocol?.commandPrefix ?? require_batteries_generation_local_diffusion_protocol.DEFAULT_PROTOCOL.commandPrefix,
|
|
132
|
+
eventPrefix: o.eventPrefix ?? o.protocol?.eventPrefix ?? require_batteries_generation_local_diffusion_protocol.DEFAULT_PROTOCOL.eventPrefix,
|
|
133
|
+
ops: {
|
|
134
|
+
...require_batteries_generation_local_diffusion_protocol.DEFAULT_PROTOCOL.ops,
|
|
135
|
+
...o.protocol?.ops,
|
|
136
|
+
...o.ops
|
|
137
|
+
},
|
|
138
|
+
control: {
|
|
139
|
+
...require_batteries_generation_local_diffusion_protocol.DEFAULT_PROTOCOL.control,
|
|
140
|
+
...o.protocol?.control,
|
|
141
|
+
...o.control
|
|
142
|
+
},
|
|
143
|
+
events: {
|
|
144
|
+
...require_batteries_generation_local_diffusion_protocol.DEFAULT_PROTOCOL.events,
|
|
145
|
+
...o.protocol?.events,
|
|
146
|
+
...o.events
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Tear down the current process + request atomically: bump the epoch (fencing every outstanding
|
|
152
|
+
* continuation), detach listeners, reject an in-flight preload, record the request's terminal
|
|
153
|
+
* outcome (which settles once its pending reads drain), and drop cached handles. Kills the child
|
|
154
|
+
* unless `kill` is false — `dispose()` defers the kill until after its graceful-shutdown wait.
|
|
155
|
+
* Leaves `#state` for the caller to set (`idle` for reset, `disposed` for dispose).
|
|
156
|
+
*/
|
|
157
|
+
#retire(error, kill = true) {
|
|
158
|
+
this.#epoch += 1;
|
|
159
|
+
this.#detach();
|
|
160
|
+
this.#loadReject?.(error);
|
|
161
|
+
this.#loadReject = void 0;
|
|
162
|
+
this.#loadPromise = void 0;
|
|
163
|
+
const request = this.#request;
|
|
164
|
+
if (request) this.#settle(request, {
|
|
165
|
+
kind: "error",
|
|
166
|
+
error
|
|
167
|
+
});
|
|
168
|
+
if (kill) this.#process?.kill();
|
|
169
|
+
this.#process = void 0;
|
|
170
|
+
}
|
|
171
|
+
async #start() {
|
|
172
|
+
this.#assertLive();
|
|
173
|
+
if (!this.isAvailable()) throw new require_batteries_generation_local_diffusion_exceptions.E_LOCAL_DIFFUSION_BACKEND_ERROR(["Node process is unavailable"]);
|
|
174
|
+
const epoch = ++this.#epoch;
|
|
175
|
+
const startupTimeoutMs = this.#options.startupTimeoutMs ?? 3e4;
|
|
176
|
+
this.#state = "starting";
|
|
177
|
+
const spawner = this.#options.spawn ?? (async (ctx) => {
|
|
178
|
+
const { spawn } = await import("node:child_process");
|
|
179
|
+
const child = spawn(ctx.command, ctx.args, { stdio: [
|
|
180
|
+
"pipe",
|
|
181
|
+
"pipe",
|
|
182
|
+
"pipe"
|
|
183
|
+
] });
|
|
184
|
+
if (!child.stdin || !child.stdout) throw new require_batteries_generation_local_diffusion_exceptions.E_LOCAL_DIFFUSION_BACKEND_ERROR(["backend stdin/stdout unavailable"]);
|
|
185
|
+
return child;
|
|
186
|
+
});
|
|
187
|
+
let process;
|
|
188
|
+
try {
|
|
189
|
+
process = await spawner({
|
|
190
|
+
command: this.#options.command,
|
|
191
|
+
args: this.#options.args ?? [],
|
|
192
|
+
model: this.#options.model
|
|
193
|
+
});
|
|
194
|
+
} catch (error) {
|
|
195
|
+
throw new require_batteries_generation_local_diffusion_exceptions.E_LOCAL_DIFFUSION_BACKEND_ERROR([require_tokenizable.isError(error) ? error.message : String(error)]);
|
|
196
|
+
}
|
|
197
|
+
if (epoch !== this.#epoch || this.#state === "disposed") {
|
|
198
|
+
process.kill();
|
|
199
|
+
throw new require_batteries_generation_local_diffusion_exceptions.E_LOCAL_DIFFUSION_BACKEND_ERROR(["startup superseded"]);
|
|
200
|
+
}
|
|
201
|
+
if (!process.stdin || !process.stdout) throw new require_batteries_generation_local_diffusion_exceptions.E_LOCAL_DIFFUSION_BACKEND_ERROR(["backend stdin/stdout unavailable"]);
|
|
202
|
+
this.#process = process;
|
|
203
|
+
const wait = deferred();
|
|
204
|
+
this.#loadReject = wait.reject;
|
|
205
|
+
const reader = require_batteries_generation_local_diffusion_protocol.createFrameReader({
|
|
206
|
+
maxLineBytes: this.#options.maxLineBytes,
|
|
207
|
+
config: this.#protocol(),
|
|
208
|
+
onFrame: (frame) => this.#onFrame(frame, epoch, wait)
|
|
209
|
+
});
|
|
210
|
+
const onData = (chunk) => {
|
|
211
|
+
if (epoch === this.#epoch) reader.push(chunk);
|
|
212
|
+
};
|
|
213
|
+
const onStreamEnd = () => {
|
|
214
|
+
if (epoch !== this.#epoch) return;
|
|
215
|
+
reader.end();
|
|
216
|
+
failure("backend stdout closed", null, null);
|
|
217
|
+
};
|
|
218
|
+
const failure = (detail, code, signal) => {
|
|
219
|
+
if (epoch !== this.#epoch) return;
|
|
220
|
+
const error = this.#state === "starting" ? new require_batteries_generation_local_diffusion_exceptions.E_LOCAL_DIFFUSION_STARTUP_TIMEOUT([startupTimeoutMs]) : new require_batteries_generation_local_diffusion_exceptions.E_LOCAL_DIFFUSION_BACKEND_ERROR([
|
|
221
|
+
detail,
|
|
222
|
+
code ?? void 0,
|
|
223
|
+
signal ?? void 0
|
|
224
|
+
]);
|
|
225
|
+
wait.reject(error);
|
|
226
|
+
this.#detach();
|
|
227
|
+
this.#loadReject = void 0;
|
|
228
|
+
this.#loadPromise = void 0;
|
|
229
|
+
const request = this.#request;
|
|
230
|
+
if (request) this.#settle(request, {
|
|
231
|
+
kind: "error",
|
|
232
|
+
error
|
|
233
|
+
});
|
|
234
|
+
this.#process?.kill();
|
|
235
|
+
this.#process = void 0;
|
|
236
|
+
this.#epoch += 1;
|
|
237
|
+
this.#state = "idle";
|
|
238
|
+
};
|
|
239
|
+
const onError = () => failure("backend error", null, null);
|
|
240
|
+
const onExit = (code, signal) => failure("backend exited", code, signal);
|
|
241
|
+
const onStdoutError = () => failure("backend stdout error", null, null);
|
|
242
|
+
process.stdout.on("data", onData);
|
|
243
|
+
process.stdout.on("end", onStreamEnd);
|
|
244
|
+
process.stdout.on("error", onStdoutError);
|
|
245
|
+
process.on("error", onError);
|
|
246
|
+
process.on("exit", onExit);
|
|
247
|
+
process.on("close", onExit);
|
|
248
|
+
this.#listeners.push({
|
|
249
|
+
target: process.stdout,
|
|
250
|
+
event: "data",
|
|
251
|
+
fn: onData
|
|
252
|
+
}, {
|
|
253
|
+
target: process.stdout,
|
|
254
|
+
event: "end",
|
|
255
|
+
fn: onStreamEnd
|
|
256
|
+
}, {
|
|
257
|
+
target: process.stdout,
|
|
258
|
+
event: "error",
|
|
259
|
+
fn: onStdoutError
|
|
260
|
+
}, {
|
|
261
|
+
target: process,
|
|
262
|
+
event: "error",
|
|
263
|
+
fn: onError
|
|
264
|
+
}, {
|
|
265
|
+
target: process,
|
|
266
|
+
event: "exit",
|
|
267
|
+
fn: onExit
|
|
268
|
+
}, {
|
|
269
|
+
target: process,
|
|
270
|
+
event: "close",
|
|
271
|
+
fn: onExit
|
|
272
|
+
});
|
|
273
|
+
await new Promise((resolve, reject) => {
|
|
274
|
+
const timer = setTimeout(() => {
|
|
275
|
+
failure("startup timed out", null, null);
|
|
276
|
+
}, startupTimeoutMs);
|
|
277
|
+
wait.promise.then(() => {
|
|
278
|
+
clearTimeout(timer);
|
|
279
|
+
resolve();
|
|
280
|
+
}, (error) => {
|
|
281
|
+
clearTimeout(timer);
|
|
282
|
+
reject(error);
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
if (epoch !== this.#epoch) throw new require_batteries_generation_local_diffusion_exceptions.E_LOCAL_DIFFUSION_BACKEND_ERROR(["startup superseded"]);
|
|
286
|
+
}
|
|
287
|
+
#onFrame(frame, epoch, load) {
|
|
288
|
+
if (epoch !== this.#epoch) return;
|
|
289
|
+
if (frame.kind === "modelLoad") {
|
|
290
|
+
require_lifecycle.emitLifecycle(this.#options, BATTERY, this.#options.model, "loading", { progress: frame.progress });
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
if (frame.kind === "ready") {
|
|
294
|
+
if (this.#state !== "starting") return;
|
|
295
|
+
this.#state = "ready";
|
|
296
|
+
require_lifecycle.emitLifecycle(this.#options, BATTERY, this.#options.model, "ready");
|
|
297
|
+
load.resolve(void 0);
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
const request = this.#request;
|
|
301
|
+
if (frame.kind === "malformed" || frame.kind === "protocolError") {
|
|
302
|
+
if (request) {
|
|
303
|
+
if (!request.outcome) request.outcome = {
|
|
304
|
+
kind: "error",
|
|
305
|
+
error: new require_batteries_generation_local_diffusion_exceptions.E_LOCAL_DIFFUSION_MALFORMED_FRAME([frame.detail])
|
|
306
|
+
};
|
|
307
|
+
this.#maybeSettle(request);
|
|
308
|
+
}
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
if (!request || !("rid" in frame) || frame.rid !== request.rid) return;
|
|
312
|
+
if (frame.kind === "progress") {
|
|
313
|
+
require_lifecycle.emitLifecycle(this.#options, BATTERY, this.#options.model, "generating", { progress: frame.progress });
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
if (frame.kind === "image") {
|
|
317
|
+
request.pending += 1;
|
|
318
|
+
this.#readImage(frame.payload).then((output) => {
|
|
319
|
+
if (this.#request === request) request.outputs.push(output);
|
|
320
|
+
}).catch((error) => {
|
|
321
|
+
if (this.#request === request && !request.outcome) request.outcome = {
|
|
322
|
+
kind: "error",
|
|
323
|
+
error
|
|
324
|
+
};
|
|
325
|
+
}).finally(() => {
|
|
326
|
+
request.pending -= 1;
|
|
327
|
+
this.#maybeSettle(request);
|
|
328
|
+
});
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
if (frame.kind === "error") {
|
|
332
|
+
if (!request.outcome) request.outcome = {
|
|
333
|
+
kind: "error",
|
|
334
|
+
error: new require_batteries_generation_local_diffusion_exceptions.E_LOCAL_DIFFUSION_BACKEND_ERROR([frame.message])
|
|
335
|
+
};
|
|
336
|
+
this.#maybeSettle(request);
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
if (frame.kind === "done") {
|
|
340
|
+
if (!request.outcome) request.outcome = { kind: "success" };
|
|
341
|
+
this.#maybeSettle(request);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
/** Settle `request` once its recorded terminal outcome is set AND all image reads have drained. */
|
|
345
|
+
#maybeSettle(request) {
|
|
346
|
+
if (request.outcome && request.pending === 0) this.#settle(request, request.outcome);
|
|
347
|
+
}
|
|
348
|
+
async #readImage(payload) {
|
|
349
|
+
const maxDecoded = this.#options.maxDecodedBytes ?? DEFAULT_MAX_DECODED_BYTES;
|
|
350
|
+
let bytes;
|
|
351
|
+
let filename;
|
|
352
|
+
if (payload.b64 !== void 0) {
|
|
353
|
+
if (!/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(payload.b64)) throw new require_batteries_generation_local_diffusion_exceptions.E_LOCAL_DIFFUSION_MALFORMED_FRAME(["invalid base64"]);
|
|
354
|
+
if (Math.floor(payload.b64.length * 3 / 4) > maxDecoded) throw new require_batteries_generation_local_diffusion_exceptions.E_LOCAL_DIFFUSION_MALFORMED_FRAME(["inline image exceeds maxDecodedBytes"]);
|
|
355
|
+
bytes = require_tool_call.decodeBase64(payload.b64);
|
|
356
|
+
} else if (payload.path !== void 0) {
|
|
357
|
+
const requestPath = payload.path;
|
|
358
|
+
const fs = this.#options.fs ?? await this.#defaultFs();
|
|
359
|
+
try {
|
|
360
|
+
bytes = await fs.readFile(requestPath);
|
|
361
|
+
} finally {
|
|
362
|
+
await this.#cleanupPath(fs, requestPath);
|
|
363
|
+
}
|
|
364
|
+
filename = requestPath.split(/[\\/]/).pop();
|
|
365
|
+
} else throw new require_batteries_generation_local_diffusion_exceptions.E_LOCAL_DIFFUSION_MALFORMED_FRAME(["image has no data"]);
|
|
366
|
+
if (bytes.length === 0 || bytes.length > maxDecoded || !/^image\/(png|jpeg|jpg|webp|gif|avif)$/i.test(payload.mimeType)) throw new require_batteries_generation_local_diffusion_exceptions.E_LOCAL_DIFFUSION_MALFORMED_FRAME(["invalid image output"]);
|
|
367
|
+
return {
|
|
368
|
+
kind: "image",
|
|
369
|
+
mimeType: payload.mimeType,
|
|
370
|
+
bytes,
|
|
371
|
+
...filename ? { filename } : {}
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* Delete a backend-written result file ONLY when its LEXICALLY-resolved path is strictly inside
|
|
376
|
+
* `outputDir` (resolves `..`, rejects sibling-prefix escapes). Containment is lexical, not
|
|
377
|
+
* realpath-based: a symlink placed inside `outputDir` that points elsewhere is NOT followed-and-checked
|
|
378
|
+
* — this cleanup trusts that the consumer's own `outputDir` does not contain adversarial symlinks.
|
|
379
|
+
* Cleanup failure is diagnostic-only and never fails the generation.
|
|
380
|
+
*/
|
|
381
|
+
async #cleanupPath(fs, filePath) {
|
|
382
|
+
const outputDir = this.#options.outputDir;
|
|
383
|
+
if (!outputDir) return;
|
|
384
|
+
try {
|
|
385
|
+
const path = await import("node:path");
|
|
386
|
+
if (!isContained(path.resolve(outputDir), path.resolve(filePath), path.sep)) return;
|
|
387
|
+
await fs.unlink(filePath);
|
|
388
|
+
} catch {}
|
|
389
|
+
}
|
|
390
|
+
async #defaultFs() {
|
|
391
|
+
const fs = await import("node:fs/promises");
|
|
392
|
+
return {
|
|
393
|
+
readFile: (path) => fs.readFile(path),
|
|
394
|
+
unlink: (path) => fs.unlink(path).then(() => void 0)
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
async #call(kind, prompt, inputs, opts) {
|
|
398
|
+
this.#assertLive();
|
|
399
|
+
if (this.#state === "running" || this.#state === "stopping") throw new require_batteries_generation_local_diffusion_exceptions.E_LOCAL_DIFFUSION_BUSY();
|
|
400
|
+
await this.preload();
|
|
401
|
+
this.#assertLive();
|
|
402
|
+
if (this.#state !== "ready") throw new require_batteries_generation_local_diffusion_exceptions.E_LOCAL_DIFFUSION_BUSY();
|
|
403
|
+
const o = this.#options;
|
|
404
|
+
const rid = this.#rid++;
|
|
405
|
+
const request = {
|
|
406
|
+
rid,
|
|
407
|
+
done: deferred(),
|
|
408
|
+
outputs: [],
|
|
409
|
+
pending: 0,
|
|
410
|
+
outcome: void 0,
|
|
411
|
+
settled: false,
|
|
412
|
+
terminated: false,
|
|
413
|
+
stopTimer: void 0,
|
|
414
|
+
requestTimer: void 0,
|
|
415
|
+
detachSignal: void 0
|
|
416
|
+
};
|
|
417
|
+
this.#request = request;
|
|
418
|
+
this.#state = "running";
|
|
419
|
+
require_lifecycle.emitLifecycle(o, BATTERY, o.model, "generating");
|
|
420
|
+
const args = {
|
|
421
|
+
prompt,
|
|
422
|
+
...kind === "edit" ? { images: (inputs ?? []).map((input) => ({
|
|
423
|
+
b64: require_tool_call.encodeBase64(input.bytes),
|
|
424
|
+
...input.mimeType ? { mimeType: input.mimeType } : {}
|
|
425
|
+
})) } : {},
|
|
426
|
+
...Object.fromEntries(Object.entries({
|
|
427
|
+
negativePrompt: opts?.negativePrompt ?? o.negativePrompt,
|
|
428
|
+
steps: opts?.steps ?? o.steps,
|
|
429
|
+
cfgScale: opts?.cfgScale ?? o.cfgScale,
|
|
430
|
+
sampler: opts?.sampler ?? o.sampler,
|
|
431
|
+
seed: opts?.seed ?? o.seed,
|
|
432
|
+
width: opts?.width ?? o.width,
|
|
433
|
+
height: opts?.height ?? o.height
|
|
434
|
+
}).filter(([, value]) => value !== void 0))
|
|
435
|
+
};
|
|
436
|
+
try {
|
|
437
|
+
this.#process?.stdin?.write(kind === "edit" ? require_batteries_generation_local_diffusion_protocol.buildEditCommand(rid, args, this.#protocol()) : require_batteries_generation_local_diffusion_protocol.buildGenerateCommand(rid, args, this.#protocol()));
|
|
438
|
+
} catch (error) {
|
|
439
|
+
this.#settle(request, {
|
|
440
|
+
kind: "error",
|
|
441
|
+
error: new require_batteries_generation_local_diffusion_exceptions.E_LOCAL_DIFFUSION_BACKEND_ERROR([require_tokenizable.isError(error) ? error.message : String(error)])
|
|
442
|
+
});
|
|
443
|
+
return request.done.promise;
|
|
444
|
+
}
|
|
445
|
+
if (opts?.signal) {
|
|
446
|
+
const signal = opts.signal;
|
|
447
|
+
const abort = () => this.#beginStop(request, { kind: "aborted" });
|
|
448
|
+
if (signal.aborted) abort();
|
|
449
|
+
else {
|
|
450
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
451
|
+
request.detachSignal = () => signal.removeEventListener("abort", abort);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
if (o.requestTimeoutMs) request.requestTimer = setTimeout(() => this.#beginStop(request, {
|
|
455
|
+
kind: "error",
|
|
456
|
+
error: new require_batteries_generation_local_diffusion_exceptions.E_LOCAL_DIFFUSION_REQUEST_TIMEOUT([o.requestTimeoutMs])
|
|
457
|
+
}), o.requestTimeoutMs);
|
|
458
|
+
return request.done.promise;
|
|
459
|
+
}
|
|
460
|
+
/**
|
|
461
|
+
* Begin a best-effort stop of `request` (abort or request-timeout): reject the caller now with the
|
|
462
|
+
* terminal `outcome`, but HOLD the single-flight slot in `stopping` (writing advisory `__stop__`)
|
|
463
|
+
* until the cancelled rid's terminal frame arrives OR `abortGraceMs` elapses → kill + respawn.
|
|
464
|
+
*/
|
|
465
|
+
#beginStop(request, outcome) {
|
|
466
|
+
if (this.#request !== request || this.#state !== "running") return;
|
|
467
|
+
this.#state = "stopping";
|
|
468
|
+
try {
|
|
469
|
+
this.#process?.stdin?.write(require_batteries_generation_local_diffusion_protocol.buildStopCommand(request.rid, this.#protocol()));
|
|
470
|
+
} catch {}
|
|
471
|
+
if (!request.outcome) request.outcome = outcome;
|
|
472
|
+
if (!request.settled) request.done.reject(outcomeError(outcome));
|
|
473
|
+
request.settled = true;
|
|
474
|
+
request.detachSignal?.();
|
|
475
|
+
request.detachSignal = void 0;
|
|
476
|
+
if (request.requestTimer) clearTimeout(request.requestTimer);
|
|
477
|
+
request.requestTimer = void 0;
|
|
478
|
+
request.stopTimer = setTimeout(() => {
|
|
479
|
+
if (this.#request === request) this.reset();
|
|
480
|
+
}, this.#options.abortGraceMs ?? 5e3);
|
|
481
|
+
}
|
|
482
|
+
/**
|
|
483
|
+
* Fully settle `request` exactly once: clear its timers, release the single-flight slot (only if it
|
|
484
|
+
* is still the current request), settle the caller's promise if not already done, and emit exactly
|
|
485
|
+
* ONE terminal lifecycle event. Idempotent — a second call (e.g. a drained pending read after an
|
|
486
|
+
* earlier retirement) is a no-op, so no duplicate lifecycle/promise settlement can occur.
|
|
487
|
+
*/
|
|
488
|
+
#settle(request, outcome) {
|
|
489
|
+
if (request.terminated) return;
|
|
490
|
+
request.terminated = true;
|
|
491
|
+
if (request.stopTimer) clearTimeout(request.stopTimer);
|
|
492
|
+
request.stopTimer = void 0;
|
|
493
|
+
if (request.requestTimer) clearTimeout(request.requestTimer);
|
|
494
|
+
request.requestTimer = void 0;
|
|
495
|
+
request.detachSignal?.();
|
|
496
|
+
request.detachSignal = void 0;
|
|
497
|
+
if (this.#request === request) {
|
|
498
|
+
this.#request = void 0;
|
|
499
|
+
if (this.#state === "running" || this.#state === "stopping") this.#state = "ready";
|
|
500
|
+
}
|
|
501
|
+
if (!request.settled) {
|
|
502
|
+
request.settled = true;
|
|
503
|
+
if (outcome.kind === "success") request.done.resolve(request.outputs);
|
|
504
|
+
else request.done.reject(outcomeError(outcome));
|
|
505
|
+
}
|
|
506
|
+
if (outcome.kind === "success") require_lifecycle.emitLifecycle(this.#options, BATTERY, this.#options.model, "complete");
|
|
507
|
+
else require_lifecycle.emitLifecycle(this.#options, BATTERY, this.#options.model, "error", { error: outcomeError(outcome) });
|
|
508
|
+
}
|
|
509
|
+
#detach() {
|
|
510
|
+
for (const listener of this.#listeners) listener.target.off(listener.event, listener.fn);
|
|
511
|
+
this.#listeners = [];
|
|
512
|
+
}
|
|
513
|
+
};
|
|
514
|
+
//#endregion
|
|
515
|
+
exports.LocalDiffusionGenerationAdapter = LocalDiffusionGenerationAdapter;
|
|
516
|
+
|
|
517
|
+
//# sourceMappingURL=adapter.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"adapter.cjs","names":["#options","#assertLive","#state","#loadPromise","#start","#loadReject","#retire","#disposePromise","#dispose","#process","#protocol","#call","#epoch","#detach","#request","#settle","#onFrame","#listeners","#maybeSettle","#readImage","#defaultFs","#cleanupPath","#rid","#beginStop"],"sources":["../../../../src/batteries/generation/local_diffusion/adapter.ts"],"sourcesContent":["/** Node-only local diffusion generation adapter.\n * @module @nhtio/adk/batteries/generation/local_diffusion/adapter\n */\nimport { toBytes } from '../_shared'\nimport { isError } from '@nhtio/adk/guards'\nimport { validateOptions } from './validation'\nimport { emitLifecycle } from '../../llm/chat_common/lifecycle'\nimport { decodeBase64, encodeBase64 } from '../../../lib/helpers/base64'\nimport {\n DEFAULT_PROTOCOL,\n buildEditCommand,\n buildGenerateCommand,\n buildShutdownCommand,\n buildStopCommand,\n createFrameReader,\n} from './protocol'\nimport {\n E_LOCAL_DIFFUSION_ABORTED,\n E_LOCAL_DIFFUSION_BACKEND_ERROR,\n E_LOCAL_DIFFUSION_BUSY,\n E_LOCAL_DIFFUSION_DISPOSED,\n E_LOCAL_DIFFUSION_MALFORMED_FRAME,\n E_LOCAL_DIFFUSION_REQUEST_TIMEOUT,\n E_LOCAL_DIFFUSION_STARTUP_TIMEOUT,\n} from './exceptions'\nimport type { GenerationImageInput } from '../_shared'\nimport type { GeneratedMediaOutput } from '../openai/types'\nimport type { ParsedFrame, ProtocolConfig } from './protocol'\nimport type {\n DiffusionBackendProcess,\n DiffusionFsLike,\n LocalDiffusionCallOptions,\n LocalDiffusionGenerationAdapterOptions,\n} from './types'\n\nconst BATTERY = 'local_diffusion_generation' as const\nconst DEFAULT_MAX_DECODED_BYTES = 50 * 1024 * 1024\n\ntype Deferred<T> = { promise: Promise<T>; resolve(value: T): void; reject(error: unknown): void }\n\n/** How a request ended, so {@link LocalDiffusionGenerationAdapter} settles it exactly once. */\ntype Outcome = { kind: 'success' } | { kind: 'error'; error: unknown } | { kind: 'aborted' }\n\n/** The rejection error for a non-success outcome (an aborted outcome maps to the typed abort error). */\nconst outcomeError = (outcome: Exclude<Outcome, { kind: 'success' }>): unknown =>\n outcome.kind === 'aborted' ? new E_LOCAL_DIFFUSION_ABORTED() : outcome.error\n\n/** Mutable state for the single in-flight request. */\ntype RequestState = {\n /** The monotonic request id fenced against backend frames. */\n rid: number\n /** The caller-facing settlement. */\n done: Deferred<GeneratedMediaOutput[]>\n /** Accumulated image outputs. */\n outputs: GeneratedMediaOutput[]\n /** Count of in-flight image reads not yet settled. */\n pending: number\n /** The recorded terminal outcome, set once; the request settles when `pending` reaches 0. */\n outcome: Outcome | undefined\n /** Whether the caller's promise has been settled (may precede full termination on abort/timeout). */\n settled: boolean\n /** Whether full terminal settlement (slot release + one lifecycle event) has already run. */\n terminated: boolean\n /** The abort/timeout grace timer holding the slot in `stopping`, if any. */\n stopTimer: ReturnType<typeof setTimeout> | undefined\n /** The request-timeout timer, if any. */\n requestTimer: ReturnType<typeof setTimeout> | undefined\n /** Detaches the caller's abort listener, if any. */\n detachSignal: (() => void) | undefined\n}\n\nconst deferred = <T>(): Deferred<T> => {\n let resolve!: (value: T) => void\n let reject!: (error: unknown) => void\n const promise = new Promise<T>((res, rej) => {\n resolve = res\n reject = rej\n })\n return { promise, resolve, reject }\n}\n\n/**\n * Whether `candidate` resolves to a location strictly inside `dir` — real containment (resolves `..`\n * and rejects sibling-prefix escapes like `/output2`), not a raw string `startsWith`.\n */\nconst isContained = (dir: string, candidate: string, sep: string): boolean => {\n const base = dir.endsWith(sep) ? dir.slice(0, -sep.length) : dir\n return candidate === base || candidate.startsWith(base + sep)\n}\n\n/** Local diffusion adapter backed by a line-protocol subprocess. */\nexport class LocalDiffusionGenerationAdapter {\n readonly #options: LocalDiffusionGenerationAdapterOptions\n #process: DiffusionBackendProcess | undefined\n #epoch = 0\n #state: 'idle' | 'starting' | 'ready' | 'running' | 'stopping' | 'disposed' = 'idle'\n #loadPromise: Promise<void> | undefined\n #loadReject: ((error: unknown) => void) | undefined\n #request: RequestState | undefined\n #rid = 0\n #disposePromise: Promise<void> | undefined\n #listeners: Array<{\n target: { off(event: string, listener: (...args: any[]) => void): void }\n event: string\n fn: (...args: any[]) => void\n }> = []\n\n /** Whether the current host exposes Node's process runtime. */\n public static isAvailable(): boolean {\n return typeof process !== 'undefined' && Boolean(process.versions?.node)\n }\n\n /** Construct and validate an adapter. */\n constructor(options: unknown) {\n this.#options = validateOptions(options)\n }\n\n /** Whether this instance can run, honoring its injected probe. */\n public isAvailable(): boolean {\n return (this.#options.isAvailable ?? LocalDiffusionGenerationAdapter.isAvailable)()\n }\n\n /** Start the backend and wait for its ready event. */\n public async preload(): Promise<void> {\n this.#assertLive()\n if (this.#state === 'ready' || this.#state === 'running' || this.#state === 'stopping') return\n if (this.#loadPromise) return this.#loadPromise\n const loadPromise = this.#start()\n this.#loadPromise = loadPromise\n loadPromise\n .catch(() => undefined)\n .finally(() => {\n // Only clear if this exact load is still the current one (a reset/dispose may have replaced it).\n if (this.#loadPromise === loadPromise) {\n this.#loadPromise = undefined\n this.#loadReject = undefined\n }\n })\n return loadPromise\n }\n\n /** Synchronously terminate the current process and invalidate all continuations. */\n public reset(): void {\n if (this.#state === 'disposed') return\n this.#retire(new E_LOCAL_DIFFUSION_BACKEND_ERROR(['adapter reset']))\n this.#state = 'idle'\n }\n\n /** Gracefully stop the backend, then force terminate it. Terminal and idempotent. */\n public dispose(): Promise<void> {\n if (this.#disposePromise) return this.#disposePromise\n this.#disposePromise = this.#dispose()\n return this.#disposePromise\n }\n\n async #dispose(): Promise<void> {\n const process = this.#process\n // Move to the terminal state SYNCHRONOUSLY so any concurrent call sees `disposed` (not `ready`)\n // and every in-flight continuation is fenced by the epoch bump inside #retire. Defer the kill:\n // the two-phase teardown sends `__shutdown__`, waits `disposeGraceMs`, then force-kills once below.\n this.#retire(new E_LOCAL_DIFFUSION_DISPOSED(), false)\n this.#state = 'disposed'\n if (process && process.stdin) {\n try {\n process.stdin.write(buildShutdownCommand(this.#protocol()))\n } catch {\n /* teardown — the process may already be gone */\n }\n await new Promise<void>((resolve) => {\n const timer = setTimeout(resolve, this.#options.disposeGraceMs ?? 5000)\n const finish = (): void => {\n clearTimeout(timer)\n process.off('close', finish)\n resolve()\n }\n process.on('close', finish)\n })\n }\n process?.kill()\n }\n\n /** Async-disposal protocol alias. */\n public async [Symbol.asyncDispose](): Promise<void> {\n await this.dispose()\n }\n\n /** Generate images from text. */\n public async generate(\n prompt: string,\n opts?: LocalDiffusionCallOptions\n ): Promise<GeneratedMediaOutput[]> {\n return this.#call('generate', prompt, undefined, opts)\n }\n\n /** Edit images with text guidance. */\n public async edit(\n inputs: GenerationImageInput | GenerationImageInput[],\n prompt: string,\n opts?: LocalDiffusionCallOptions\n ): Promise<GeneratedMediaOutput[]> {\n const normalized = await Promise.all((Array.isArray(inputs) ? inputs : [inputs]).map(toBytes))\n return this.#call('edit', prompt, normalized, opts)\n }\n\n #assertLive(): void {\n if (this.#state === 'disposed' || this.#disposePromise) throw new E_LOCAL_DIFFUSION_DISPOSED()\n }\n\n #protocol(): ProtocolConfig {\n const o = this.#options\n return {\n ...DEFAULT_PROTOCOL,\n ...(o.protocol ?? {}),\n commandPrefix: o.commandPrefix ?? o.protocol?.commandPrefix ?? DEFAULT_PROTOCOL.commandPrefix,\n eventPrefix: o.eventPrefix ?? o.protocol?.eventPrefix ?? DEFAULT_PROTOCOL.eventPrefix,\n ops: { ...DEFAULT_PROTOCOL.ops, ...o.protocol?.ops, ...o.ops },\n control: { ...DEFAULT_PROTOCOL.control, ...o.protocol?.control, ...o.control },\n events: { ...DEFAULT_PROTOCOL.events, ...o.protocol?.events, ...o.events },\n }\n }\n\n /**\n * Tear down the current process + request atomically: bump the epoch (fencing every outstanding\n * continuation), detach listeners, reject an in-flight preload, record the request's terminal\n * outcome (which settles once its pending reads drain), and drop cached handles. Kills the child\n * unless `kill` is false — `dispose()` defers the kill until after its graceful-shutdown wait.\n * Leaves `#state` for the caller to set (`idle` for reset, `disposed` for dispose).\n */\n #retire(error: unknown, kill = true): void {\n this.#epoch += 1\n this.#detach()\n this.#loadReject?.(error)\n this.#loadReject = undefined\n this.#loadPromise = undefined\n const request = this.#request\n if (request) this.#settle(request, { kind: 'error', error })\n if (kill) this.#process?.kill()\n this.#process = undefined\n }\n\n async #start(): Promise<void> {\n this.#assertLive()\n if (!this.isAvailable())\n throw new E_LOCAL_DIFFUSION_BACKEND_ERROR(['Node process is unavailable'])\n const epoch = ++this.#epoch\n const startupTimeoutMs = this.#options.startupTimeoutMs ?? 30_000\n this.#state = 'starting'\n const spawner =\n this.#options.spawn ??\n (async (ctx): Promise<DiffusionBackendProcess> => {\n const { spawn } = await import('node:child_process')\n const child = spawn(ctx.command, ctx.args, {\n stdio: ['pipe', 'pipe', 'pipe'],\n }) as unknown as DiffusionBackendProcess\n if (!child.stdin || !child.stdout)\n throw new E_LOCAL_DIFFUSION_BACKEND_ERROR(['backend stdin/stdout unavailable'])\n return child\n })\n let process: DiffusionBackendProcess\n try {\n process = await spawner({\n command: this.#options.command,\n args: this.#options.args ?? [],\n model: this.#options.model,\n })\n } catch (error) {\n throw new E_LOCAL_DIFFUSION_BACKEND_ERROR([isError(error) ? error.message : String(error)])\n }\n if (epoch !== this.#epoch || (this.#state as string) === 'disposed') {\n process.kill()\n throw new E_LOCAL_DIFFUSION_BACKEND_ERROR(['startup superseded'])\n }\n if (!process.stdin || !process.stdout)\n throw new E_LOCAL_DIFFUSION_BACKEND_ERROR(['backend stdin/stdout unavailable'])\n this.#process = process\n const wait = deferred<void>()\n this.#loadReject = wait.reject\n const reader = createFrameReader({\n maxLineBytes: this.#options.maxLineBytes,\n config: this.#protocol(),\n onFrame: (frame) => this.#onFrame(frame, epoch, wait),\n })\n const onData = (chunk: Uint8Array): void => {\n if (epoch === this.#epoch) reader.push(chunk)\n }\n const onStreamEnd = (): void => {\n if (epoch !== this.#epoch) return\n // Flush any trailing partial line, THEN treat the closed stdout as a terminal transport failure:\n // a backend that closes its stdout can produce no further frames, so a startup or in-flight\n // request would otherwise hang forever (permanent BUSY). failure() retires + settles it.\n reader.end()\n failure('backend stdout closed', null, null)\n }\n const failure = (detail: string, code: number | null, signal: NodeJS.Signals | null): void => {\n if (epoch !== this.#epoch) return\n const error =\n this.#state === 'starting'\n ? new E_LOCAL_DIFFUSION_STARTUP_TIMEOUT([startupTimeoutMs])\n : new E_LOCAL_DIFFUSION_BACKEND_ERROR([detail, code ?? undefined, signal ?? undefined])\n // Fully retire the process so no stale `#request`/`#process`/`ready` survives. Detach + kill the\n // child (a startup-timeout child may still be alive) and bump the epoch so any late frame from\n // it — including `ready` — is fenced out.\n wait.reject(error)\n this.#detach()\n this.#loadReject = undefined\n this.#loadPromise = undefined\n const request = this.#request\n if (request) this.#settle(request, { kind: 'error', error })\n this.#process?.kill()\n this.#process = undefined\n this.#epoch += 1\n this.#state = 'idle'\n }\n const onError = (): void => failure('backend error', null, null)\n const onExit = (code: number | null, signal: NodeJS.Signals | null): void =>\n failure('backend exited', code, signal)\n const onStdoutError = (): void => failure('backend stdout error', null, null)\n process.stdout.on('data', onData)\n process.stdout.on('end', onStreamEnd)\n process.stdout.on('error', onStdoutError)\n process.on('error', onError)\n process.on('exit', onExit)\n process.on('close', onExit)\n this.#listeners.push(\n { target: process.stdout, event: 'data', fn: onData },\n { target: process.stdout, event: 'end', fn: onStreamEnd },\n { target: process.stdout, event: 'error', fn: onStdoutError },\n { target: process, event: 'error', fn: onError },\n { target: process, event: 'exit', fn: onExit },\n { target: process, event: 'close', fn: onExit }\n )\n await new Promise<void>((resolve, reject) => {\n const timer = setTimeout(() => {\n // Route the timeout through failure() so the still-running child is killed + detached + fenced\n // (state → idle, epoch bumped) — a late `ready` from the timed-out child cannot revive it.\n failure('startup timed out', null, null)\n }, startupTimeoutMs)\n wait.promise.then(\n () => {\n clearTimeout(timer)\n resolve()\n },\n (error) => {\n clearTimeout(timer)\n reject(error)\n }\n )\n })\n if (epoch !== this.#epoch) throw new E_LOCAL_DIFFUSION_BACKEND_ERROR(['startup superseded'])\n }\n\n #onFrame(frame: ParsedFrame, epoch: number, load: Deferred<void>): void {\n if (epoch !== this.#epoch) return\n if (frame.kind === 'modelLoad') {\n emitLifecycle(this.#options, BATTERY, this.#options.model, 'loading', {\n progress: frame.progress,\n })\n return\n }\n if (frame.kind === 'ready') {\n // Only a pending startup transitions to ready; a duplicate/unsolicited `ready` during\n // running/stopping must not reopen the slot or overwrite the active request.\n if (this.#state !== 'starting') return\n this.#state = 'ready'\n emitLifecycle(this.#options, BATTERY, this.#options.model, 'ready')\n load.resolve(undefined)\n return\n }\n const request = this.#request\n if (frame.kind === 'malformed' || frame.kind === 'protocolError') {\n // Record the terminal outcome but hold the slot until pending image reads drain (like `done`).\n if (request) {\n if (!request.outcome)\n request.outcome = {\n kind: 'error',\n error: new E_LOCAL_DIFFUSION_MALFORMED_FRAME([frame.detail]),\n }\n this.#maybeSettle(request)\n }\n return\n }\n // Every remaining frame kind carries a rid; fence it against the current request.\n if (!request || !('rid' in frame) || frame.rid !== request.rid) return\n if (frame.kind === 'progress') {\n emitLifecycle(this.#options, BATTERY, this.#options.model, 'generating', {\n progress: frame.progress,\n })\n return\n }\n if (frame.kind === 'image') {\n request.pending += 1\n void this.#readImage(frame.payload)\n .then((output) => {\n if (this.#request === request) request.outputs.push(output)\n })\n .catch((error) => {\n // Fence against a retired/superseded request: only the current request records an outcome.\n if (this.#request === request && !request.outcome)\n request.outcome = { kind: 'error', error }\n })\n .finally(() => {\n request.pending -= 1\n this.#maybeSettle(request)\n })\n return\n }\n if (frame.kind === 'error') {\n if (!request.outcome)\n request.outcome = {\n kind: 'error',\n error: new E_LOCAL_DIFFUSION_BACKEND_ERROR([frame.message]),\n }\n this.#maybeSettle(request)\n return\n }\n if (frame.kind === 'done') {\n if (!request.outcome) request.outcome = { kind: 'success' }\n this.#maybeSettle(request)\n }\n }\n\n /** Settle `request` once its recorded terminal outcome is set AND all image reads have drained. */\n #maybeSettle(request: RequestState): void {\n if (request.outcome && request.pending === 0) this.#settle(request, request.outcome)\n }\n\n async #readImage(payload: {\n path?: string\n b64?: string\n mimeType: string\n }): Promise<GeneratedMediaOutput> {\n const maxDecoded = this.#options.maxDecodedBytes ?? DEFAULT_MAX_DECODED_BYTES\n let bytes: Uint8Array\n let filename: string | undefined\n if (payload.b64 !== undefined) {\n if (!/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(payload.b64))\n throw new E_LOCAL_DIFFUSION_MALFORMED_FRAME(['invalid base64'])\n // Bound memory BEFORE allocating: decoded length ≈ 3/4 of the base64 char count.\n const approxDecoded = Math.floor((payload.b64.length * 3) / 4)\n if (approxDecoded > maxDecoded)\n throw new E_LOCAL_DIFFUSION_MALFORMED_FRAME(['inline image exceeds maxDecodedBytes'])\n bytes = decodeBase64(payload.b64)\n } else if (payload.path !== undefined) {\n const requestPath = payload.path\n const fs = this.#options.fs ?? (await this.#defaultFs())\n try {\n bytes = await fs.readFile(requestPath)\n } finally {\n await this.#cleanupPath(fs, requestPath)\n }\n filename = requestPath.split(/[\\\\/]/).pop()\n } else throw new E_LOCAL_DIFFUSION_MALFORMED_FRAME(['image has no data'])\n if (\n bytes.length === 0 ||\n bytes.length > maxDecoded ||\n !/^image\\/(png|jpeg|jpg|webp|gif|avif)$/i.test(payload.mimeType)\n )\n throw new E_LOCAL_DIFFUSION_MALFORMED_FRAME(['invalid image output'])\n return { kind: 'image', mimeType: payload.mimeType, bytes, ...(filename ? { filename } : {}) }\n }\n\n /**\n * Delete a backend-written result file ONLY when its LEXICALLY-resolved path is strictly inside\n * `outputDir` (resolves `..`, rejects sibling-prefix escapes). Containment is lexical, not\n * realpath-based: a symlink placed inside `outputDir` that points elsewhere is NOT followed-and-checked\n * — this cleanup trusts that the consumer's own `outputDir` does not contain adversarial symlinks.\n * Cleanup failure is diagnostic-only and never fails the generation.\n */\n async #cleanupPath(fs: DiffusionFsLike, filePath: string): Promise<void> {\n const outputDir = this.#options.outputDir\n if (!outputDir) return\n try {\n const path = await import('node:path')\n const resolvedDir = path.resolve(outputDir)\n const resolvedPath = path.resolve(filePath)\n if (!isContained(resolvedDir, resolvedPath, path.sep)) return\n await fs.unlink(filePath)\n } catch {\n /* diagnostic-only cleanup — a failed unlink never fails the generation */\n }\n }\n\n async #defaultFs(): Promise<DiffusionFsLike> {\n const fs = await import('node:fs/promises')\n return {\n readFile: (path) => fs.readFile(path),\n unlink: (path) => fs.unlink(path).then(() => undefined),\n }\n }\n\n async #call(\n kind: 'generate' | 'edit',\n prompt: string,\n inputs?: Array<{ bytes: Uint8Array; mimeType?: string }>,\n opts?: LocalDiffusionCallOptions\n ): Promise<GeneratedMediaOutput[]> {\n this.#assertLive()\n if (this.#state === 'running' || this.#state === 'stopping') throw new E_LOCAL_DIFFUSION_BUSY()\n await this.preload()\n this.#assertLive()\n if (this.#state !== 'ready') throw new E_LOCAL_DIFFUSION_BUSY()\n const o = this.#options\n const rid = this.#rid++\n const request: RequestState = {\n rid,\n done: deferred<GeneratedMediaOutput[]>(),\n outputs: [],\n pending: 0,\n outcome: undefined,\n settled: false,\n terminated: false,\n stopTimer: undefined,\n requestTimer: undefined,\n detachSignal: undefined,\n }\n this.#request = request\n this.#state = 'running'\n emitLifecycle(o, BATTERY, o.model, 'generating')\n const args: Record<string, unknown> = {\n prompt,\n ...(kind === 'edit'\n ? {\n images: (inputs ?? []).map((input) => ({\n b64: encodeBase64(input.bytes),\n ...(input.mimeType ? { mimeType: input.mimeType } : {}),\n })),\n }\n : {}),\n ...Object.fromEntries(\n Object.entries({\n negativePrompt: opts?.negativePrompt ?? o.negativePrompt,\n steps: opts?.steps ?? o.steps,\n cfgScale: opts?.cfgScale ?? o.cfgScale,\n sampler: opts?.sampler ?? o.sampler,\n seed: opts?.seed ?? o.seed,\n width: opts?.width ?? o.width,\n height: opts?.height ?? o.height,\n }).filter(([, value]) => value !== undefined)\n ),\n }\n try {\n this.#process?.stdin?.write(\n kind === 'edit'\n ? buildEditCommand(rid, args, this.#protocol())\n : buildGenerateCommand(rid, args, this.#protocol())\n )\n } catch (error) {\n this.#settle(request, {\n kind: 'error',\n error: new E_LOCAL_DIFFUSION_BACKEND_ERROR([\n isError(error) ? error.message : String(error),\n ]),\n })\n return request.done.promise\n }\n if (opts?.signal) {\n const signal = opts.signal\n const abort = (): void => this.#beginStop(request, { kind: 'aborted' })\n if (signal.aborted) abort()\n else {\n signal.addEventListener('abort', abort, { once: true })\n request.detachSignal = () => signal.removeEventListener('abort', abort)\n }\n }\n if (o.requestTimeoutMs)\n request.requestTimer = setTimeout(\n () =>\n this.#beginStop(request, {\n kind: 'error',\n error: new E_LOCAL_DIFFUSION_REQUEST_TIMEOUT([o.requestTimeoutMs as number]),\n }),\n o.requestTimeoutMs\n )\n return request.done.promise\n }\n\n /**\n * Begin a best-effort stop of `request` (abort or request-timeout): reject the caller now with the\n * terminal `outcome`, but HOLD the single-flight slot in `stopping` (writing advisory `__stop__`)\n * until the cancelled rid's terminal frame arrives OR `abortGraceMs` elapses → kill + respawn.\n */\n #beginStop(request: RequestState, outcome: Exclude<Outcome, { kind: 'success' }>): void {\n if (this.#request !== request || this.#state !== 'running') return\n this.#state = 'stopping'\n try {\n this.#process?.stdin?.write(buildStopCommand(request.rid, this.#protocol()))\n } catch {\n /* advisory — the backend may ignore or never receive __stop__ */\n }\n // Record the terminal outcome + reject the caller immediately, but keep the slot held: the request\n // is not cleared until its reads drain (settle) or the grace timer fires.\n if (!request.outcome) request.outcome = outcome\n if (!request.settled) request.done.reject(outcomeError(outcome))\n request.settled = true\n request.detachSignal?.()\n request.detachSignal = undefined\n if (request.requestTimer) clearTimeout(request.requestTimer)\n request.requestTimer = undefined\n request.stopTimer = setTimeout(() => {\n if (this.#request === request) this.reset()\n }, this.#options.abortGraceMs ?? 5000)\n }\n\n /**\n * Fully settle `request` exactly once: clear its timers, release the single-flight slot (only if it\n * is still the current request), settle the caller's promise if not already done, and emit exactly\n * ONE terminal lifecycle event. Idempotent — a second call (e.g. a drained pending read after an\n * earlier retirement) is a no-op, so no duplicate lifecycle/promise settlement can occur.\n */\n #settle(request: RequestState, outcome: Outcome): void {\n if (request.terminated) return\n request.terminated = true\n if (request.stopTimer) clearTimeout(request.stopTimer)\n request.stopTimer = undefined\n if (request.requestTimer) clearTimeout(request.requestTimer)\n request.requestTimer = undefined\n request.detachSignal?.()\n request.detachSignal = undefined\n if (this.#request === request) {\n this.#request = undefined\n if (this.#state === 'running' || this.#state === 'stopping') this.#state = 'ready'\n }\n if (!request.settled) {\n request.settled = true\n if (outcome.kind === 'success') request.done.resolve(request.outputs)\n else request.done.reject(outcomeError(outcome))\n }\n // The lifecycle event mirrors the true outcome, never a spurious `complete` after a failure/abort.\n if (outcome.kind === 'success')\n emitLifecycle(this.#options, BATTERY, this.#options.model, 'complete')\n else\n emitLifecycle(this.#options, BATTERY, this.#options.model, 'error', {\n error: outcomeError(outcome),\n })\n }\n\n #detach(): void {\n for (const listener of this.#listeners) listener.target.off(listener.event, listener.fn)\n this.#listeners = []\n }\n}\n"],"mappings":";;;;;;;;;;;;;;AAmCA,IAAM,UAAU;AAChB,IAAM,4BAA4B,KAAK,OAAO;;AAQ9C,IAAM,gBAAgB,YACpB,QAAQ,SAAS,YAAY,IAAI,wDAAA,0BAA0B,IAAI,QAAQ;AA0BzE,IAAM,iBAAiC;CACrC,IAAI;CACJ,IAAI;CAKJ,OAAO;EAAE,SAAA,IAJW,SAAY,KAAK,QAAQ;GAC3C,UAAU;GACV,SAAS;EACX,CACS;EAAS;EAAS;CAAO;AACpC;;;;;AAMA,IAAM,eAAe,KAAa,WAAmB,QAAyB;CAC5E,MAAM,OAAO,IAAI,SAAS,GAAG,IAAI,IAAI,MAAM,GAAG,CAAC,IAAI,MAAM,IAAI;CAC7D,OAAO,cAAc,QAAQ,UAAU,WAAW,OAAO,GAAG;AAC9D;;AAGA,IAAa,kCAAb,MAAa,gCAAgC;CAC3C;CACA;CACA,SAAS;CACT,SAA8E;CAC9E;CACA;CACA;CACA,OAAO;CACP;CACA,aAIK,CAAC;;CAGN,OAAc,cAAuB;EACnC,OAAO,OAAO,YAAY,eAAe,QAAQ,QAAQ,UAAU,IAAI;CACzE;;CAGA,YAAY,SAAkB;EAC5B,KAAKA,WAAW,wDAAA,gBAAgB,OAAO;CACzC;;CAGA,cAA8B;EAC5B,QAAQ,KAAKA,SAAS,eAAe,gCAAgC,aAAa;CACpF;;CAGA,MAAa,UAAyB;EACpC,KAAKC,YAAY;EACjB,IAAI,KAAKC,WAAW,WAAW,KAAKA,WAAW,aAAa,KAAKA,WAAW,YAAY;EACxF,IAAI,KAAKC,cAAc,OAAO,KAAKA;EACnC,MAAM,cAAc,KAAKC,OAAO;EAChC,KAAKD,eAAe;EACpB,YACG,YAAY,KAAA,CAAS,EACrB,cAAc;GAEb,IAAI,KAAKA,iBAAiB,aAAa;IACrC,KAAKA,eAAe,KAAA;IACpB,KAAKE,cAAc,KAAA;GACrB;EACF,CAAC;EACH,OAAO;CACT;;CAGA,QAAqB;EACnB,IAAI,KAAKH,WAAW,YAAY;EAChC,KAAKI,QAAQ,IAAI,wDAAA,gCAAgC,CAAC,eAAe,CAAC,CAAC;EACnE,KAAKJ,SAAS;CAChB;;CAGA,UAAgC;EAC9B,IAAI,KAAKK,iBAAiB,OAAO,KAAKA;EACtC,KAAKA,kBAAkB,KAAKC,SAAS;EACrC,OAAO,KAAKD;CACd;CAEA,MAAMC,WAA0B;EAC9B,MAAM,UAAU,KAAKC;EAIrB,KAAKH,QAAQ,IAAI,wDAAA,2BAA2B,GAAG,KAAK;EACpD,KAAKJ,SAAS;EACd,IAAI,WAAW,QAAQ,OAAO;GAC5B,IAAI;IACF,QAAQ,MAAM,MAAM,sDAAA,qBAAqB,KAAKQ,UAAU,CAAC,CAAC;GAC5D,QAAQ,CAER;GACA,MAAM,IAAI,SAAe,YAAY;IACnC,MAAM,QAAQ,WAAW,SAAS,KAAKV,SAAS,kBAAkB,GAAI;IACtE,MAAM,eAAqB;KACzB,aAAa,KAAK;KAClB,QAAQ,IAAI,SAAS,MAAM;KAC3B,QAAQ;IACV;IACA,QAAQ,GAAG,SAAS,MAAM;GAC5B,CAAC;EACH;EACA,SAAS,KAAK;CAChB;;CAGA,OAAc,OAAO,gBAA+B;EAClD,MAAM,KAAK,QAAQ;CACrB;;CAGA,MAAa,SACX,QACA,MACiC;EACjC,OAAO,KAAKW,MAAM,YAAY,QAAQ,KAAA,GAAW,IAAI;CACvD;;CAGA,MAAa,KACX,QACA,QACA,MACiC;EACjC,MAAM,aAAa,MAAM,QAAQ,KAAK,MAAM,QAAQ,MAAM,IAAI,SAAS,CAAC,MAAM,GAAG,IAAI,qCAAA,OAAO,CAAC;EAC7F,OAAO,KAAKA,MAAM,QAAQ,QAAQ,YAAY,IAAI;CACpD;CAEA,cAAoB;EAClB,IAAI,KAAKT,WAAW,cAAc,KAAKK,iBAAiB,MAAM,IAAI,wDAAA,2BAA2B;CAC/F;CAEA,YAA4B;EAC1B,MAAM,IAAI,KAAKP;EACf,OAAO;GACL,GAAG,sDAAA;GACH,GAAI,EAAE,YAAY,CAAC;GACnB,eAAe,EAAE,iBAAiB,EAAE,UAAU,iBAAiB,sDAAA,iBAAiB;GAChF,aAAa,EAAE,eAAe,EAAE,UAAU,eAAe,sDAAA,iBAAiB;GAC1E,KAAK;IAAE,GAAG,sDAAA,iBAAiB;IAAK,GAAG,EAAE,UAAU;IAAK,GAAG,EAAE;GAAI;GAC7D,SAAS;IAAE,GAAG,sDAAA,iBAAiB;IAAS,GAAG,EAAE,UAAU;IAAS,GAAG,EAAE;GAAQ;GAC7E,QAAQ;IAAE,GAAG,sDAAA,iBAAiB;IAAQ,GAAG,EAAE,UAAU;IAAQ,GAAG,EAAE;GAAO;EAC3E;CACF;;;;;;;;CASA,QAAQ,OAAgB,OAAO,MAAY;EACzC,KAAKY,UAAU;EACf,KAAKC,QAAQ;EACb,KAAKR,cAAc,KAAK;EACxB,KAAKA,cAAc,KAAA;EACnB,KAAKF,eAAe,KAAA;EACpB,MAAM,UAAU,KAAKW;EACrB,IAAI,SAAS,KAAKC,QAAQ,SAAS;GAAE,MAAM;GAAS;EAAM,CAAC;EAC3D,IAAI,MAAM,KAAKN,UAAU,KAAK;EAC9B,KAAKA,WAAW,KAAA;CAClB;CAEA,MAAML,SAAwB;EAC5B,KAAKH,YAAY;EACjB,IAAI,CAAC,KAAK,YAAY,GACpB,MAAM,IAAI,wDAAA,gCAAgC,CAAC,6BAA6B,CAAC;EAC3E,MAAM,QAAQ,EAAE,KAAKW;EACrB,MAAM,mBAAmB,KAAKZ,SAAS,oBAAoB;EAC3D,KAAKE,SAAS;EACd,MAAM,UACJ,KAAKF,SAAS,UACb,OAAO,QAA0C;GAChD,MAAM,EAAE,UAAU,MAAM,OAAO;GAC/B,MAAM,QAAQ,MAAM,IAAI,SAAS,IAAI,MAAM,EACzC,OAAO;IAAC;IAAQ;IAAQ;GAAM,EAChC,CAAC;GACD,IAAI,CAAC,MAAM,SAAS,CAAC,MAAM,QACzB,MAAM,IAAI,wDAAA,gCAAgC,CAAC,kCAAkC,CAAC;GAChF,OAAO;EACT;EACF,IAAI;EACJ,IAAI;GACF,UAAU,MAAM,QAAQ;IACtB,SAAS,KAAKA,SAAS;IACvB,MAAM,KAAKA,SAAS,QAAQ,CAAC;IAC7B,OAAO,KAAKA,SAAS;GACvB,CAAC;EACH,SAAS,OAAO;GACd,MAAM,IAAI,wDAAA,gCAAgC,CAAC,oBAAA,QAAQ,KAAK,IAAI,MAAM,UAAU,OAAO,KAAK,CAAC,CAAC;EAC5F;EACA,IAAI,UAAU,KAAKY,UAAW,KAAKV,WAAsB,YAAY;GACnE,QAAQ,KAAK;GACb,MAAM,IAAI,wDAAA,gCAAgC,CAAC,oBAAoB,CAAC;EAClE;EACA,IAAI,CAAC,QAAQ,SAAS,CAAC,QAAQ,QAC7B,MAAM,IAAI,wDAAA,gCAAgC,CAAC,kCAAkC,CAAC;EAChF,KAAKO,WAAW;EAChB,MAAM,OAAO,SAAe;EAC5B,KAAKJ,cAAc,KAAK;EACxB,MAAM,SAAS,sDAAA,kBAAkB;GAC/B,cAAc,KAAKL,SAAS;GAC5B,QAAQ,KAAKU,UAAU;GACvB,UAAU,UAAU,KAAKM,SAAS,OAAO,OAAO,IAAI;EACtD,CAAC;EACD,MAAM,UAAU,UAA4B;GAC1C,IAAI,UAAU,KAAKJ,QAAQ,OAAO,KAAK,KAAK;EAC9C;EACA,MAAM,oBAA0B;GAC9B,IAAI,UAAU,KAAKA,QAAQ;GAI3B,OAAO,IAAI;GACX,QAAQ,yBAAyB,MAAM,IAAI;EAC7C;EACA,MAAM,WAAW,QAAgB,MAAqB,WAAwC;GAC5F,IAAI,UAAU,KAAKA,QAAQ;GAC3B,MAAM,QACJ,KAAKV,WAAW,aACZ,IAAI,wDAAA,kCAAkC,CAAC,gBAAgB,CAAC,IACxD,IAAI,wDAAA,gCAAgC;IAAC;IAAQ,QAAQ,KAAA;IAAW,UAAU,KAAA;GAAS,CAAC;GAI1F,KAAK,OAAO,KAAK;GACjB,KAAKW,QAAQ;GACb,KAAKR,cAAc,KAAA;GACnB,KAAKF,eAAe,KAAA;GACpB,MAAM,UAAU,KAAKW;GACrB,IAAI,SAAS,KAAKC,QAAQ,SAAS;IAAE,MAAM;IAAS;GAAM,CAAC;GAC3D,KAAKN,UAAU,KAAK;GACpB,KAAKA,WAAW,KAAA;GAChB,KAAKG,UAAU;GACf,KAAKV,SAAS;EAChB;EACA,MAAM,gBAAsB,QAAQ,iBAAiB,MAAM,IAAI;EAC/D,MAAM,UAAU,MAAqB,WACnC,QAAQ,kBAAkB,MAAM,MAAM;EACxC,MAAM,sBAA4B,QAAQ,wBAAwB,MAAM,IAAI;EAC5E,QAAQ,OAAO,GAAG,QAAQ,MAAM;EAChC,QAAQ,OAAO,GAAG,OAAO,WAAW;EACpC,QAAQ,OAAO,GAAG,SAAS,aAAa;EACxC,QAAQ,GAAG,SAAS,OAAO;EAC3B,QAAQ,GAAG,QAAQ,MAAM;EACzB,QAAQ,GAAG,SAAS,MAAM;EAC1B,KAAKe,WAAW,KACd;GAAE,QAAQ,QAAQ;GAAQ,OAAO;GAAQ,IAAI;EAAO,GACpD;GAAE,QAAQ,QAAQ;GAAQ,OAAO;GAAO,IAAI;EAAY,GACxD;GAAE,QAAQ,QAAQ;GAAQ,OAAO;GAAS,IAAI;EAAc,GAC5D;GAAE,QAAQ;GAAS,OAAO;GAAS,IAAI;EAAQ,GAC/C;GAAE,QAAQ;GAAS,OAAO;GAAQ,IAAI;EAAO,GAC7C;GAAE,QAAQ;GAAS,OAAO;GAAS,IAAI;EAAO,CAChD;EACA,MAAM,IAAI,SAAe,SAAS,WAAW;GAC3C,MAAM,QAAQ,iBAAiB;IAG7B,QAAQ,qBAAqB,MAAM,IAAI;GACzC,GAAG,gBAAgB;GACnB,KAAK,QAAQ,WACL;IACJ,aAAa,KAAK;IAClB,QAAQ;GACV,IACC,UAAU;IACT,aAAa,KAAK;IAClB,OAAO,KAAK;GACd,CACF;EACF,CAAC;EACD,IAAI,UAAU,KAAKL,QAAQ,MAAM,IAAI,wDAAA,gCAAgC,CAAC,oBAAoB,CAAC;CAC7F;CAEA,SAAS,OAAoB,OAAe,MAA4B;EACtE,IAAI,UAAU,KAAKA,QAAQ;EAC3B,IAAI,MAAM,SAAS,aAAa;GAC9B,kBAAA,cAAc,KAAKZ,UAAU,SAAS,KAAKA,SAAS,OAAO,WAAW,EACpE,UAAU,MAAM,SAClB,CAAC;GACD;EACF;EACA,IAAI,MAAM,SAAS,SAAS;GAG1B,IAAI,KAAKE,WAAW,YAAY;GAChC,KAAKA,SAAS;GACd,kBAAA,cAAc,KAAKF,UAAU,SAAS,KAAKA,SAAS,OAAO,OAAO;GAClE,KAAK,QAAQ,KAAA,CAAS;GACtB;EACF;EACA,MAAM,UAAU,KAAKc;EACrB,IAAI,MAAM,SAAS,eAAe,MAAM,SAAS,iBAAiB;GAEhE,IAAI,SAAS;IACX,IAAI,CAAC,QAAQ,SACX,QAAQ,UAAU;KAChB,MAAM;KACN,OAAO,IAAI,wDAAA,kCAAkC,CAAC,MAAM,MAAM,CAAC;IAC7D;IACF,KAAKI,aAAa,OAAO;GAC3B;GACA;EACF;EAEA,IAAI,CAAC,WAAW,EAAE,SAAS,UAAU,MAAM,QAAQ,QAAQ,KAAK;EAChE,IAAI,MAAM,SAAS,YAAY;GAC7B,kBAAA,cAAc,KAAKlB,UAAU,SAAS,KAAKA,SAAS,OAAO,cAAc,EACvE,UAAU,MAAM,SAClB,CAAC;GACD;EACF;EACA,IAAI,MAAM,SAAS,SAAS;GAC1B,QAAQ,WAAW;GACnB,KAAUmB,WAAW,MAAM,OAAO,EAC/B,MAAM,WAAW;IAChB,IAAI,KAAKL,aAAa,SAAS,QAAQ,QAAQ,KAAK,MAAM;GAC5D,CAAC,EACA,OAAO,UAAU;IAEhB,IAAI,KAAKA,aAAa,WAAW,CAAC,QAAQ,SACxC,QAAQ,UAAU;KAAE,MAAM;KAAS;IAAM;GAC7C,CAAC,EACA,cAAc;IACb,QAAQ,WAAW;IACnB,KAAKI,aAAa,OAAO;GAC3B,CAAC;GACH;EACF;EACA,IAAI,MAAM,SAAS,SAAS;GAC1B,IAAI,CAAC,QAAQ,SACX,QAAQ,UAAU;IAChB,MAAM;IACN,OAAO,IAAI,wDAAA,gCAAgC,CAAC,MAAM,OAAO,CAAC;GAC5D;GACF,KAAKA,aAAa,OAAO;GACzB;EACF;EACA,IAAI,MAAM,SAAS,QAAQ;GACzB,IAAI,CAAC,QAAQ,SAAS,QAAQ,UAAU,EAAE,MAAM,UAAU;GAC1D,KAAKA,aAAa,OAAO;EAC3B;CACF;;CAGA,aAAa,SAA6B;EACxC,IAAI,QAAQ,WAAW,QAAQ,YAAY,GAAG,KAAKH,QAAQ,SAAS,QAAQ,OAAO;CACrF;CAEA,MAAMI,WAAW,SAIiB;EAChC,MAAM,aAAa,KAAKnB,SAAS,mBAAmB;EACpD,IAAI;EACJ,IAAI;EACJ,IAAI,QAAQ,QAAQ,KAAA,GAAW;GAC7B,IAAI,CAAC,mEAAmE,KAAK,QAAQ,GAAG,GACtF,MAAM,IAAI,wDAAA,kCAAkC,CAAC,gBAAgB,CAAC;GAGhE,IADsB,KAAK,MAAO,QAAQ,IAAI,SAAS,IAAK,CACxD,IAAgB,YAClB,MAAM,IAAI,wDAAA,kCAAkC,CAAC,sCAAsC,CAAC;GACtF,QAAQ,kBAAA,aAAa,QAAQ,GAAG;EAClC,OAAO,IAAI,QAAQ,SAAS,KAAA,GAAW;GACrC,MAAM,cAAc,QAAQ;GAC5B,MAAM,KAAK,KAAKA,SAAS,MAAO,MAAM,KAAKoB,WAAW;GACtD,IAAI;IACF,QAAQ,MAAM,GAAG,SAAS,WAAW;GACvC,UAAU;IACR,MAAM,KAAKC,aAAa,IAAI,WAAW;GACzC;GACA,WAAW,YAAY,MAAM,OAAO,EAAE,IAAI;EAC5C,OAAO,MAAM,IAAI,wDAAA,kCAAkC,CAAC,mBAAmB,CAAC;EACxE,IACE,MAAM,WAAW,KACjB,MAAM,SAAS,cACf,CAAC,yCAAyC,KAAK,QAAQ,QAAQ,GAE/D,MAAM,IAAI,wDAAA,kCAAkC,CAAC,sBAAsB,CAAC;EACtE,OAAO;GAAE,MAAM;GAAS,UAAU,QAAQ;GAAU;GAAO,GAAI,WAAW,EAAE,SAAS,IAAI,CAAC;EAAG;CAC/F;;;;;;;;CASA,MAAMA,aAAa,IAAqB,UAAiC;EACvE,MAAM,YAAY,KAAKrB,SAAS;EAChC,IAAI,CAAC,WAAW;EAChB,IAAI;GACF,MAAM,OAAO,MAAM,OAAO;GAG1B,IAAI,CAAC,YAFe,KAAK,QAAQ,SAEhB,GADI,KAAK,QAAQ,QACJ,GAAc,KAAK,GAAG,GAAG;GACvD,MAAM,GAAG,OAAO,QAAQ;EAC1B,QAAQ,CAER;CACF;CAEA,MAAMoB,aAAuC;EAC3C,MAAM,KAAK,MAAM,OAAO;EACxB,OAAO;GACL,WAAW,SAAS,GAAG,SAAS,IAAI;GACpC,SAAS,SAAS,GAAG,OAAO,IAAI,EAAE,WAAW,KAAA,CAAS;EACxD;CACF;CAEA,MAAMT,MACJ,MACA,QACA,QACA,MACiC;EACjC,KAAKV,YAAY;EACjB,IAAI,KAAKC,WAAW,aAAa,KAAKA,WAAW,YAAY,MAAM,IAAI,wDAAA,uBAAuB;EAC9F,MAAM,KAAK,QAAQ;EACnB,KAAKD,YAAY;EACjB,IAAI,KAAKC,WAAW,SAAS,MAAM,IAAI,wDAAA,uBAAuB;EAC9D,MAAM,IAAI,KAAKF;EACf,MAAM,MAAM,KAAKsB;EACjB,MAAM,UAAwB;GAC5B;GACA,MAAM,SAAiC;GACvC,SAAS,CAAC;GACV,SAAS;GACT,SAAS,KAAA;GACT,SAAS;GACT,YAAY;GACZ,WAAW,KAAA;GACX,cAAc,KAAA;GACd,cAAc,KAAA;EAChB;EACA,KAAKR,WAAW;EAChB,KAAKZ,SAAS;EACd,kBAAA,cAAc,GAAG,SAAS,EAAE,OAAO,YAAY;EAC/C,MAAM,OAAgC;GACpC;GACA,GAAI,SAAS,SACT,EACE,SAAS,UAAU,CAAC,GAAG,KAAK,WAAW;IACrC,KAAK,kBAAA,aAAa,MAAM,KAAK;IAC7B,GAAI,MAAM,WAAW,EAAE,UAAU,MAAM,SAAS,IAAI,CAAC;GACvD,EAAE,EACJ,IACA,CAAC;GACL,GAAG,OAAO,YACR,OAAO,QAAQ;IACb,gBAAgB,MAAM,kBAAkB,EAAE;IAC1C,OAAO,MAAM,SAAS,EAAE;IACxB,UAAU,MAAM,YAAY,EAAE;IAC9B,SAAS,MAAM,WAAW,EAAE;IAC5B,MAAM,MAAM,QAAQ,EAAE;IACtB,OAAO,MAAM,SAAS,EAAE;IACxB,QAAQ,MAAM,UAAU,EAAE;GAC5B,CAAC,EAAE,QAAQ,GAAG,WAAW,UAAU,KAAA,CAAS,CAC9C;EACF;EACA,IAAI;GACF,KAAKO,UAAU,OAAO,MACpB,SAAS,SACL,sDAAA,iBAAiB,KAAK,MAAM,KAAKC,UAAU,CAAC,IAC5C,sDAAA,qBAAqB,KAAK,MAAM,KAAKA,UAAU,CAAC,CACtD;EACF,SAAS,OAAO;GACd,KAAKK,QAAQ,SAAS;IACpB,MAAM;IACN,OAAO,IAAI,wDAAA,gCAAgC,CACzC,oBAAA,QAAQ,KAAK,IAAI,MAAM,UAAU,OAAO,KAAK,CAC/C,CAAC;GACH,CAAC;GACD,OAAO,QAAQ,KAAK;EACtB;EACA,IAAI,MAAM,QAAQ;GAChB,MAAM,SAAS,KAAK;GACpB,MAAM,cAAoB,KAAKQ,WAAW,SAAS,EAAE,MAAM,UAAU,CAAC;GACtE,IAAI,OAAO,SAAS,MAAM;QACrB;IACH,OAAO,iBAAiB,SAAS,OAAO,EAAE,MAAM,KAAK,CAAC;IACtD,QAAQ,qBAAqB,OAAO,oBAAoB,SAAS,KAAK;GACxE;EACF;EACA,IAAI,EAAE,kBACJ,QAAQ,eAAe,iBAEnB,KAAKA,WAAW,SAAS;GACvB,MAAM;GACN,OAAO,IAAI,wDAAA,kCAAkC,CAAC,EAAE,gBAA0B,CAAC;EAC7E,CAAC,GACH,EAAE,gBACJ;EACF,OAAO,QAAQ,KAAK;CACtB;;;;;;CAOA,WAAW,SAAuB,SAAsD;EACtF,IAAI,KAAKT,aAAa,WAAW,KAAKZ,WAAW,WAAW;EAC5D,KAAKA,SAAS;EACd,IAAI;GACF,KAAKO,UAAU,OAAO,MAAM,sDAAA,iBAAiB,QAAQ,KAAK,KAAKC,UAAU,CAAC,CAAC;EAC7E,QAAQ,CAER;EAGA,IAAI,CAAC,QAAQ,SAAS,QAAQ,UAAU;EACxC,IAAI,CAAC,QAAQ,SAAS,QAAQ,KAAK,OAAO,aAAa,OAAO,CAAC;EAC/D,QAAQ,UAAU;EAClB,QAAQ,eAAe;EACvB,QAAQ,eAAe,KAAA;EACvB,IAAI,QAAQ,cAAc,aAAa,QAAQ,YAAY;EAC3D,QAAQ,eAAe,KAAA;EACvB,QAAQ,YAAY,iBAAiB;GACnC,IAAI,KAAKI,aAAa,SAAS,KAAK,MAAM;EAC5C,GAAG,KAAKd,SAAS,gBAAgB,GAAI;CACvC;;;;;;;CAQA,QAAQ,SAAuB,SAAwB;EACrD,IAAI,QAAQ,YAAY;EACxB,QAAQ,aAAa;EACrB,IAAI,QAAQ,WAAW,aAAa,QAAQ,SAAS;EACrD,QAAQ,YAAY,KAAA;EACpB,IAAI,QAAQ,cAAc,aAAa,QAAQ,YAAY;EAC3D,QAAQ,eAAe,KAAA;EACvB,QAAQ,eAAe;EACvB,QAAQ,eAAe,KAAA;EACvB,IAAI,KAAKc,aAAa,SAAS;GAC7B,KAAKA,WAAW,KAAA;GAChB,IAAI,KAAKZ,WAAW,aAAa,KAAKA,WAAW,YAAY,KAAKA,SAAS;EAC7E;EACA,IAAI,CAAC,QAAQ,SAAS;GACpB,QAAQ,UAAU;GAClB,IAAI,QAAQ,SAAS,WAAW,QAAQ,KAAK,QAAQ,QAAQ,OAAO;QAC/D,QAAQ,KAAK,OAAO,aAAa,OAAO,CAAC;EAChD;EAEA,IAAI,QAAQ,SAAS,WACnB,kBAAA,cAAc,KAAKF,UAAU,SAAS,KAAKA,SAAS,OAAO,UAAU;OAErE,kBAAA,cAAc,KAAKA,UAAU,SAAS,KAAKA,SAAS,OAAO,SAAS,EAClE,OAAO,aAAa,OAAO,EAC7B,CAAC;CACL;CAEA,UAAgB;EACd,KAAK,MAAM,YAAY,KAAKiB,YAAY,SAAS,OAAO,IAAI,SAAS,OAAO,SAAS,EAAE;EACvF,KAAKA,aAAa,CAAC;CACrB;AACF"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { GenerationImageInput } from "../_shared/index";
|
|
2
|
+
import type { GeneratedMediaOutput } from "../openai/types";
|
|
3
|
+
import type { LocalDiffusionCallOptions } from "./types";
|
|
4
|
+
/** Local diffusion adapter backed by a line-protocol subprocess. */
|
|
5
|
+
export declare class LocalDiffusionGenerationAdapter {
|
|
6
|
+
#private;
|
|
7
|
+
/** Whether the current host exposes Node's process runtime. */
|
|
8
|
+
static isAvailable(): boolean;
|
|
9
|
+
/** Construct and validate an adapter. */
|
|
10
|
+
constructor(options: unknown);
|
|
11
|
+
/** Whether this instance can run, honoring its injected probe. */
|
|
12
|
+
isAvailable(): boolean;
|
|
13
|
+
/** Start the backend and wait for its ready event. */
|
|
14
|
+
preload(): Promise<void>;
|
|
15
|
+
/** Synchronously terminate the current process and invalidate all continuations. */
|
|
16
|
+
reset(): void;
|
|
17
|
+
/** Gracefully stop the backend, then force terminate it. Terminal and idempotent. */
|
|
18
|
+
dispose(): Promise<void>;
|
|
19
|
+
/** Async-disposal protocol alias. */
|
|
20
|
+
[Symbol.asyncDispose](): Promise<void>;
|
|
21
|
+
/** Generate images from text. */
|
|
22
|
+
generate(prompt: string, opts?: LocalDiffusionCallOptions): Promise<GeneratedMediaOutput[]>;
|
|
23
|
+
/** Edit images with text guidance. */
|
|
24
|
+
edit(inputs: GenerationImageInput | GenerationImageInput[], prompt: string, opts?: LocalDiffusionCallOptions): Promise<GeneratedMediaOutput[]>;
|
|
25
|
+
}
|