dsh-comfyui 0.3.0-beta.2 → 0.3.0-beta.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/README.en.md +28 -12
- package/README.md +29 -13
- package/client/client.js +373 -60
- package/client/client.js.map +1 -1
- package/lib/comfyui.d.ts +33 -1
- package/lib/comfyui.js +38 -4
- package/lib/config.d.ts +12 -0
- package/lib/config.js +5 -0
- package/lib/index.js +7 -4
- package/lib/params.d.ts +29 -1
- package/lib/params.js +133 -10
- package/lib/proxy.js +16 -3
- package/lib/routes.js +277 -44
- package/lib/skill.js +5 -5
- package/lib/store.d.ts +15 -3
- package/lib/store.js +48 -12
- package/lib/tools.d.ts +8 -13
- package/lib/tools.js +23 -2
- package/package.json +5 -1
package/lib/comfyui.d.ts
CHANGED
|
@@ -101,11 +101,31 @@ export declare class ComfyUIClient {
|
|
|
101
101
|
private endpoint;
|
|
102
102
|
private request;
|
|
103
103
|
/** Upload a file (multipart body forwarded verbatim) into ComfyUI's input directory. */
|
|
104
|
+
/** Forward a multipart/form-data upload body verbatim (the browser's own
|
|
105
|
+
* form, field "image", as ComfyUI's /upload/image expects it). */
|
|
104
106
|
uploadFile(body: Uint8Array, contentType: string): Promise<{
|
|
105
107
|
name?: string;
|
|
106
108
|
subfolder?: string;
|
|
107
109
|
type?: string;
|
|
108
110
|
}>;
|
|
111
|
+
/**
|
|
112
|
+
* Put one file into ComfyUI's input directory (images, video, audio alike).
|
|
113
|
+
*
|
|
114
|
+
* /upload/image only accepts multipart/form-data: posting the bytes as a
|
|
115
|
+
* raw body with their own content-type answers HTTP 400, which is why
|
|
116
|
+
* picking a generated file into the load area used to fail instead of
|
|
117
|
+
* copying it. The form is built here so callers can pass plain bytes.
|
|
118
|
+
* `overwrite` keeps a repeated pick from piling up "name (1).png" copies.
|
|
119
|
+
*/
|
|
120
|
+
uploadMedia(bytes: Uint8Array, filename: string, contentType: string, opts?: {
|
|
121
|
+
overwrite?: boolean;
|
|
122
|
+
subfolder?: string;
|
|
123
|
+
timeoutMs?: number;
|
|
124
|
+
}): Promise<{
|
|
125
|
+
name?: string;
|
|
126
|
+
subfolder?: string;
|
|
127
|
+
type?: string;
|
|
128
|
+
}>;
|
|
109
129
|
/** Queue one API-format workflow and return its prompt id. */
|
|
110
130
|
queuePrompt(workflow: unknown, options?: {
|
|
111
131
|
promptId?: string;
|
|
@@ -182,12 +202,24 @@ export declare class ComfyUIClient {
|
|
|
182
202
|
export declare function hasMedia(entry: ComfyUIHistoryEntry): boolean;
|
|
183
203
|
/** Compose a readable failure message from history status messages. */
|
|
184
204
|
export declare function historyErrorMessage(promptId: string, entry: ComfyUIHistoryEntry): string;
|
|
205
|
+
/**
|
|
206
|
+
* Build the same-origin proxy URL for one media file.
|
|
207
|
+
*
|
|
208
|
+
* The file is addressed by its own name/subfolder/type rather than by
|
|
209
|
+
* prompt+node+index: the latter has to be resolved through ComfyUI's
|
|
210
|
+
* /history, which lives in memory and is dropped on server restart or a
|
|
211
|
+
* "clear history" click — after which every stored asset URL 404s even though
|
|
212
|
+
* the file is still sitting in the output directory. Addressing the file
|
|
213
|
+
* directly keeps old assets viewable for as long as the file exists.
|
|
214
|
+
*/
|
|
215
|
+
export declare function mediaProxyUrl(ref: ComfyUIMediaRef, proxyBase?: string): string;
|
|
185
216
|
/**
|
|
186
217
|
* Collect media items from a completed history entry, in node/output order,
|
|
187
218
|
* capped by maxItems. The URL is the same-origin proxy route when a web
|
|
188
|
-
* server is present, otherwise
|
|
219
|
+
* server is present, otherwise the bare filename (for headless hosts).
|
|
189
220
|
*/
|
|
190
221
|
export declare function collectMedia(opts: {
|
|
222
|
+
/** The run these outputs belong to; kept for call-site clarity and logging. */
|
|
191
223
|
promptId: string;
|
|
192
224
|
entry: ComfyUIHistoryEntry;
|
|
193
225
|
maxItems: number;
|
package/lib/comfyui.js
CHANGED
|
@@ -104,6 +104,8 @@ export class ComfyUIClient {
|
|
|
104
104
|
}
|
|
105
105
|
}
|
|
106
106
|
/** Upload a file (multipart body forwarded verbatim) into ComfyUI's input directory. */
|
|
107
|
+
/** Forward a multipart/form-data upload body verbatim (the browser's own
|
|
108
|
+
* form, field "image", as ComfyUI's /upload/image expects it). */
|
|
107
109
|
async uploadFile(body, contentType) {
|
|
108
110
|
const data = await this.request('/upload/image', {
|
|
109
111
|
method: 'POST',
|
|
@@ -112,6 +114,25 @@ export class ComfyUIClient {
|
|
|
112
114
|
});
|
|
113
115
|
return data ?? {};
|
|
114
116
|
}
|
|
117
|
+
/**
|
|
118
|
+
* Put one file into ComfyUI's input directory (images, video, audio alike).
|
|
119
|
+
*
|
|
120
|
+
* /upload/image only accepts multipart/form-data: posting the bytes as a
|
|
121
|
+
* raw body with their own content-type answers HTTP 400, which is why
|
|
122
|
+
* picking a generated file into the load area used to fail instead of
|
|
123
|
+
* copying it. The form is built here so callers can pass plain bytes.
|
|
124
|
+
* `overwrite` keeps a repeated pick from piling up "name (1).png" copies.
|
|
125
|
+
*/
|
|
126
|
+
async uploadMedia(bytes, filename, contentType, opts = {}) {
|
|
127
|
+
const form = new FormData();
|
|
128
|
+
form.append('image', new Blob([bytes], { type: contentType !== '' ? contentType : 'application/octet-stream' }), filename);
|
|
129
|
+
form.append('overwrite', opts.overwrite === false ? 'false' : 'true');
|
|
130
|
+
if (opts.subfolder !== undefined && opts.subfolder !== '')
|
|
131
|
+
form.append('subfolder', opts.subfolder);
|
|
132
|
+
// Video files are large; the connect timeout is far too tight for them.
|
|
133
|
+
const data = await this.request('/upload/image', { method: 'POST', body: form }, opts.timeoutMs ?? Math.max(this.connectTimeoutMs, 120_000));
|
|
134
|
+
return data ?? {};
|
|
135
|
+
}
|
|
115
136
|
/** Queue one API-format workflow and return its prompt id. */
|
|
116
137
|
async queuePrompt(workflow, options = {}) {
|
|
117
138
|
const payload = { prompt: workflow, client_id: CLIENT_ID };
|
|
@@ -353,13 +374,27 @@ export function historyErrorMessage(promptId, entry) {
|
|
|
353
374
|
}
|
|
354
375
|
return `ComfyUI execution failed (prompt ${promptId}): ${details.join('; ') || 'unknown error'}`;
|
|
355
376
|
}
|
|
377
|
+
/**
|
|
378
|
+
* Build the same-origin proxy URL for one media file.
|
|
379
|
+
*
|
|
380
|
+
* The file is addressed by its own name/subfolder/type rather than by
|
|
381
|
+
* prompt+node+index: the latter has to be resolved through ComfyUI's
|
|
382
|
+
* /history, which lives in memory and is dropped on server restart or a
|
|
383
|
+
* "clear history" click — after which every stored asset URL 404s even though
|
|
384
|
+
* the file is still sitting in the output directory. Addressing the file
|
|
385
|
+
* directly keeps old assets viewable for as long as the file exists.
|
|
386
|
+
*/
|
|
387
|
+
export function mediaProxyUrl(ref, proxyBase) {
|
|
388
|
+
const query = new URLSearchParams({ file: ref.filename, subfolder: ref.subfolder ?? '', type: ref.type ?? 'output' });
|
|
389
|
+
return `${proxyBase ?? ''}/comfyui/media?${query.toString()}`;
|
|
390
|
+
}
|
|
356
391
|
/**
|
|
357
392
|
* Collect media items from a completed history entry, in node/output order,
|
|
358
393
|
* capped by maxItems. The URL is the same-origin proxy route when a web
|
|
359
|
-
* server is present, otherwise
|
|
394
|
+
* server is present, otherwise the bare filename (for headless hosts).
|
|
360
395
|
*/
|
|
361
396
|
export function collectMedia(opts) {
|
|
362
|
-
const {
|
|
397
|
+
const { entry, maxItems, proxyBase } = opts;
|
|
363
398
|
const items = [];
|
|
364
399
|
const AUDIO_EXT = /\.(mp3|wav|ogg|flac|m4a|aac|opus)$/i;
|
|
365
400
|
const VIDEO_EXT = /\.(mp4|webm|mov|mkv|avi)$/i;
|
|
@@ -377,13 +412,12 @@ export function collectMedia(opts) {
|
|
|
377
412
|
// Some nodes emit audio/video filenames through the image/video
|
|
378
413
|
// arrays; classify them by extension so the card renders a player.
|
|
379
414
|
const itemKind = AUDIO_EXT.test(ref.filename) ? 'audio' : VIDEO_EXT.test(ref.filename) ? 'video' : kind;
|
|
380
|
-
const query = new URLSearchParams({ prompt: promptId, node, index: String(index) });
|
|
381
415
|
items.push({
|
|
382
416
|
...ref,
|
|
383
417
|
node,
|
|
384
418
|
index,
|
|
385
419
|
kind: itemKind,
|
|
386
|
-
url: proxyBase !== undefined ?
|
|
420
|
+
url: proxyBase !== undefined ? mediaProxyUrl(ref, proxyBase) : ref.filename,
|
|
387
421
|
});
|
|
388
422
|
}
|
|
389
423
|
}
|
package/lib/config.d.ts
CHANGED
|
@@ -25,6 +25,11 @@ export declare const Config: z<Schemastery.ObjectS<{
|
|
|
25
25
|
maxAssets: z<number, number>;
|
|
26
26
|
/** External base URL for generated media (e.g. http://192.168.1.5:3080). Empty = auto-detect the browser's request host, then http://127.0.0.1:<webServerPort>. */
|
|
27
27
|
mediaHost: z<string, string>;
|
|
28
|
+
/** ComfyUI's output directory on this machine, used to delete asset files
|
|
29
|
+
* from the panel. Empty = infer it from the file paths ComfyUI reports;
|
|
30
|
+
* deletion falls back to removing the index record when neither works
|
|
31
|
+
* (e.g. a ComfyUI running on another host). */
|
|
32
|
+
outputDir: z<string, string>;
|
|
28
33
|
}>, Schemastery.ObjectT<{
|
|
29
34
|
/** ComfyUI HTTP server base URL. */
|
|
30
35
|
baseUrl: z<string, string>;
|
|
@@ -46,6 +51,11 @@ export declare const Config: z<Schemastery.ObjectS<{
|
|
|
46
51
|
maxAssets: z<number, number>;
|
|
47
52
|
/** External base URL for generated media (e.g. http://192.168.1.5:3080). Empty = auto-detect the browser's request host, then http://127.0.0.1:<webServerPort>. */
|
|
48
53
|
mediaHost: z<string, string>;
|
|
54
|
+
/** ComfyUI's output directory on this machine, used to delete asset files
|
|
55
|
+
* from the panel. Empty = infer it from the file paths ComfyUI reports;
|
|
56
|
+
* deletion falls back to removing the index record when neither works
|
|
57
|
+
* (e.g. a ComfyUI running on another host). */
|
|
58
|
+
outputDir: z<string, string>;
|
|
49
59
|
}>>;
|
|
50
60
|
export type Config = {
|
|
51
61
|
/** ComfyUI HTTP server base URL. */
|
|
@@ -68,4 +78,6 @@ export type Config = {
|
|
|
68
78
|
maxAssets: number;
|
|
69
79
|
/** External base URL for generated media; empty auto-detects the request host. */
|
|
70
80
|
mediaHost: string;
|
|
81
|
+
/** ComfyUI's output directory on this machine; empty infers it from reported file paths. */
|
|
82
|
+
outputDir: string;
|
|
71
83
|
};
|
package/lib/config.js
CHANGED
|
@@ -25,4 +25,9 @@ export const Config = z.object({
|
|
|
25
25
|
maxAssets: z.number().min(1).max(10_000).default(200),
|
|
26
26
|
/** External base URL for generated media (e.g. http://192.168.1.5:3080). Empty = auto-detect the browser's request host, then http://127.0.0.1:<webServerPort>. */
|
|
27
27
|
mediaHost: z.string().default(''),
|
|
28
|
+
/** ComfyUI's output directory on this machine, used to delete asset files
|
|
29
|
+
* from the panel. Empty = infer it from the file paths ComfyUI reports;
|
|
30
|
+
* deletion falls back to removing the index record when neither works
|
|
31
|
+
* (e.g. a ComfyUI running on another host). */
|
|
32
|
+
outputDir: z.string().default(''),
|
|
28
33
|
});
|
package/lib/index.js
CHANGED
|
@@ -77,6 +77,7 @@ export async function apply(ctx, entryConfig) {
|
|
|
77
77
|
dataDir: entryConfig.dataDir !== undefined && entryConfig.dataDir !== '' ? entryConfig.dataDir : defaultDataDir(),
|
|
78
78
|
maxAssets: entryConfig.maxAssets ?? 200,
|
|
79
79
|
mediaHost: entryConfig.mediaHost ?? '',
|
|
80
|
+
outputDir: entryConfig.outputDir ?? '',
|
|
80
81
|
};
|
|
81
82
|
const store = new ComfyUIStore(resolved.dataDir, resolved.maxAssets);
|
|
82
83
|
await store.init();
|
|
@@ -146,8 +147,9 @@ export async function apply(ctx, entryConfig) {
|
|
|
146
147
|
let prompt = workflow;
|
|
147
148
|
if (meta.parameters !== undefined && meta.parameters.length > 0) {
|
|
148
149
|
const objectInfo = await objectInfoCached(client);
|
|
149
|
-
const
|
|
150
|
-
|
|
150
|
+
const slots = await store.loadSlots();
|
|
151
|
+
const loaded = slots.filter((slot) => slot !== null);
|
|
152
|
+
prompt = applyWorkflowParameters(prompt, meta.parameters, meta.values ?? {}, objectInfo, await store.loadMediaSizes(), loaded);
|
|
151
153
|
}
|
|
152
154
|
const extraData = {};
|
|
153
155
|
if (meta.workflowId !== undefined && meta.workflowId !== null && meta.workflowName !== null) {
|
|
@@ -169,9 +171,10 @@ export async function apply(ctx, entryConfig) {
|
|
|
169
171
|
saveMediaSize: (name, size) => store.saveMediaSize(name, size),
|
|
170
172
|
lookupMediaHash: (hash) => store.lookupMediaHash(hash),
|
|
171
173
|
saveMediaHash: (hash, name) => store.saveMediaHash(hash, name),
|
|
172
|
-
|
|
173
|
-
|
|
174
|
+
loadSlots: () => store.loadSlots(),
|
|
175
|
+
saveSlots: (slots) => store.saveSlots(slots),
|
|
174
176
|
listAssets: () => store.listAssets(),
|
|
177
|
+
deleteAsset: (promptId) => store.deleteAsset(promptId),
|
|
175
178
|
sweep: async () => {
|
|
176
179
|
const client = runtime.createClient(await resolveApiKey(ctx, resolved.apiKeyEnv));
|
|
177
180
|
return tracker.sweep({ client, store, maxItems: resolved.maxMediaItems, proxyBase: runtime.proxyBase() });
|
package/lib/params.d.ts
CHANGED
|
@@ -16,6 +16,17 @@ export interface WorkflowParameter {
|
|
|
16
16
|
description?: string;
|
|
17
17
|
/** Number parameters (seeds): randomize on every run when true. */
|
|
18
18
|
random?: boolean;
|
|
19
|
+
/** Number parameters: the numeric type object_info declares for the input.
|
|
20
|
+
* 'int' is rounded on apply; 'float' takes decimals. Absent means the
|
|
21
|
+
* declaration was unavailable (no object_info, unregistered node, ambiguous
|
|
22
|
+
* union type) — decimals are then accepted as-is, since a float value is
|
|
23
|
+
* the safe superset. */
|
|
24
|
+
numberKind?: 'int' | 'float';
|
|
25
|
+
/** Declared numeric bounds and step from object_info (editor hints only —
|
|
26
|
+
* values are not clamped, ComfyUI validates them at queue time). */
|
|
27
|
+
min?: number;
|
|
28
|
+
max?: number;
|
|
29
|
+
step?: number;
|
|
19
30
|
/** Allowed values when the node input is a dropdown (object_info combo). */
|
|
20
31
|
options?: Array<string | number>;
|
|
21
32
|
/** Loader-node inputs (LoadImage/LoadVideo/LoadAudio): the value is a
|
|
@@ -38,6 +49,20 @@ export type { Workflow };
|
|
|
38
49
|
* list and whose key name is loader-shaped. Returns the upload kind.
|
|
39
50
|
*/
|
|
40
51
|
export declare function uploadKindOf(objectInfo: Record<string, unknown> | undefined, classType: string, inputKey: string): 'image' | 'video' | 'audio' | undefined;
|
|
52
|
+
/** The numeric type and bounds object_info declares for an input, if any.
|
|
53
|
+
*
|
|
54
|
+
* ComfyUI writes `["INT", {default,min,max,...}]` / `["FLOAT", {..., step,
|
|
55
|
+
* round}]`, and a few nodes use decorated or union type names (`"INT:seed"`,
|
|
56
|
+
* `"INT,FLOAT"`). A union that admits FLOAT is reported as float, because a
|
|
57
|
+
* float value is accepted wherever an int one is but not the reverse.
|
|
58
|
+
* Everything else returns undefined: the caller then treats the parameter as
|
|
59
|
+
* an unconstrained number (decimals allowed, no rounding). */
|
|
60
|
+
export declare function numberSpecOf(objectInfo: Record<string, unknown> | undefined, classType: string, inputKey: string): {
|
|
61
|
+
kind: 'int' | 'float';
|
|
62
|
+
min?: number;
|
|
63
|
+
max?: number;
|
|
64
|
+
step?: number;
|
|
65
|
+
} | undefined;
|
|
41
66
|
/** The object_info input spec for one input name of a node class, if declared. */
|
|
42
67
|
export declare function inputOptions(objectInfo: Record<string, unknown> | undefined, classType: string, inputKey: string): Array<string | number> | undefined;
|
|
43
68
|
/** Full child info (key, options, default) for one selected DynamicCombo parent value. */
|
|
@@ -60,4 +85,7 @@ export declare function analyzeWorkflowParameters(workflow: Workflow, objectInfo
|
|
|
60
85
|
export declare function applyWorkflowParameters(workflow: Workflow, parameters: WorkflowParameter[], values: Record<string, unknown>, objectInfo?: Record<string, unknown>, imageSizes?: Record<string, {
|
|
61
86
|
width: number;
|
|
62
87
|
height: number;
|
|
63
|
-
}>,
|
|
88
|
+
}>, loadArea?: Array<{
|
|
89
|
+
name: string;
|
|
90
|
+
kind: 'image' | 'video' | 'audio';
|
|
91
|
+
}>): Workflow;
|
package/lib/params.js
CHANGED
|
@@ -24,6 +24,28 @@ const TEXT_KEYS = new Set(['text', 'value', 'prompt']);
|
|
|
24
24
|
function isPrimitive(value) {
|
|
25
25
|
return typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean';
|
|
26
26
|
}
|
|
27
|
+
/** Read a boolean parameter's value, accepting the string and 0/1 spellings.
|
|
28
|
+
*
|
|
29
|
+
* A boolean default authored before the panel had a checkbox was stored as a
|
|
30
|
+
* string ("true"), and an agent may pass "false" or 0 just as readily. Writing
|
|
31
|
+
* any of those into a BOOLEAN node input would make ComfyUI reject the prompt
|
|
32
|
+
* (and "false" would read as truthy), so they are normalized here; anything
|
|
33
|
+
* that is not a recognized spelling returns undefined and the input keeps its
|
|
34
|
+
* authored value. */
|
|
35
|
+
function coerceBoolean(value) {
|
|
36
|
+
if (typeof value === 'boolean')
|
|
37
|
+
return value;
|
|
38
|
+
if (typeof value === 'number')
|
|
39
|
+
return value === 1 ? true : value === 0 ? false : undefined;
|
|
40
|
+
if (typeof value !== 'string')
|
|
41
|
+
return undefined;
|
|
42
|
+
const text = value.trim().toLowerCase();
|
|
43
|
+
if (text === 'true' || text === '1' || text === 'yes' || text === 'on')
|
|
44
|
+
return true;
|
|
45
|
+
if (text === 'false' || text === '0' || text === 'no' || text === 'off')
|
|
46
|
+
return false;
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
27
49
|
/** Parse a media-state JSON array (MiniMaxH3 loader media_state); undefined when not one. */
|
|
28
50
|
function parseMediaState(raw) {
|
|
29
51
|
try {
|
|
@@ -80,6 +102,45 @@ function displayName(workflow, nodeId, inputKey) {
|
|
|
80
102
|
return inputKey === 'value' ? '文本' : '提示词';
|
|
81
103
|
return `${node.class_type} · ${inputKey}`;
|
|
82
104
|
}
|
|
105
|
+
/** The raw object_info spec tuple for one input of a node class. */
|
|
106
|
+
function inputSpec(objectInfo, classType, inputKey) {
|
|
107
|
+
if (objectInfo === undefined)
|
|
108
|
+
return undefined;
|
|
109
|
+
const def = objectInfo[classType];
|
|
110
|
+
const spec = def?.input?.required?.[inputKey] ?? def?.input?.optional?.[inputKey];
|
|
111
|
+
return Array.isArray(spec) ? spec : undefined;
|
|
112
|
+
}
|
|
113
|
+
/** The numeric type and bounds object_info declares for an input, if any.
|
|
114
|
+
*
|
|
115
|
+
* ComfyUI writes `["INT", {default,min,max,...}]` / `["FLOAT", {..., step,
|
|
116
|
+
* round}]`, and a few nodes use decorated or union type names (`"INT:seed"`,
|
|
117
|
+
* `"INT,FLOAT"`). A union that admits FLOAT is reported as float, because a
|
|
118
|
+
* float value is accepted wherever an int one is but not the reverse.
|
|
119
|
+
* Everything else returns undefined: the caller then treats the parameter as
|
|
120
|
+
* an unconstrained number (decimals allowed, no rounding). */
|
|
121
|
+
export function numberSpecOf(objectInfo, classType, inputKey) {
|
|
122
|
+
const spec = inputSpec(objectInfo, classType, inputKey);
|
|
123
|
+
if (spec === undefined)
|
|
124
|
+
return undefined;
|
|
125
|
+
const typeName = spec[0];
|
|
126
|
+
if (typeof typeName !== 'string')
|
|
127
|
+
return undefined;
|
|
128
|
+
// "INT:seed" → INT; "INT,FLOAT" → the union members.
|
|
129
|
+
const names = typeName.split(',').map((name) => name.split(':')[0]?.trim().toUpperCase() ?? '');
|
|
130
|
+
const hasFloat = names.includes('FLOAT');
|
|
131
|
+
const hasInt = names.includes('INT');
|
|
132
|
+
if (!hasFloat && !hasInt)
|
|
133
|
+
return undefined;
|
|
134
|
+
const meta = spec[1];
|
|
135
|
+
const flags = meta !== null && typeof meta === 'object' ? meta : {};
|
|
136
|
+
const numberOf = (value) => typeof value === 'number' && Number.isFinite(value) ? value : undefined;
|
|
137
|
+
return {
|
|
138
|
+
kind: hasFloat ? 'float' : 'int',
|
|
139
|
+
min: numberOf(flags.min),
|
|
140
|
+
max: numberOf(flags.max),
|
|
141
|
+
step: numberOf(flags.step),
|
|
142
|
+
};
|
|
143
|
+
}
|
|
83
144
|
/** The object_info input spec for one input name of a node class, if declared. */
|
|
84
145
|
export function inputOptions(objectInfo, classType, inputKey) {
|
|
85
146
|
if (objectInfo === undefined)
|
|
@@ -249,6 +310,11 @@ export function analyzeWorkflowParameters(workflow, objectInfo) {
|
|
|
249
310
|
const options = input.options ?? (input.classType !== undefined
|
|
250
311
|
? inputOptions(objectInfo, input.classType, input.inputKey)
|
|
251
312
|
: undefined);
|
|
313
|
+
// Number inputs carry their declared INT/FLOAT type so the panel editor
|
|
314
|
+
// and the run path know whether decimals are allowed.
|
|
315
|
+
const numberSpec = input.type === 'number' && input.classType !== undefined
|
|
316
|
+
? numberSpecOf(objectInfo, input.classType, input.inputKey)
|
|
317
|
+
: undefined;
|
|
252
318
|
params.push({
|
|
253
319
|
id: randomUUID(),
|
|
254
320
|
name: uniqueName(input.name),
|
|
@@ -258,6 +324,10 @@ export function analyzeWorkflowParameters(workflow, objectInfo) {
|
|
|
258
324
|
inputKey: input.inputKey,
|
|
259
325
|
default: input.value,
|
|
260
326
|
random: input.random,
|
|
327
|
+
numberKind: numberSpec?.kind,
|
|
328
|
+
min: numberSpec?.min,
|
|
329
|
+
max: numberSpec?.max,
|
|
330
|
+
step: numberSpec?.step,
|
|
261
331
|
options,
|
|
262
332
|
upload: input.upload,
|
|
263
333
|
subfolder: input.subfolder,
|
|
@@ -365,16 +435,39 @@ export function analyzeWorkflowParameters(workflow, objectInfo) {
|
|
|
365
435
|
* workflow. Unknown parameters are ignored; omitted ones fall back to the
|
|
366
436
|
* parameter default. The input workflow is not mutated.
|
|
367
437
|
*/
|
|
368
|
-
export function applyWorkflowParameters(workflow, parameters, values, objectInfo, imageSizes,
|
|
438
|
+
export function applyWorkflowParameters(workflow, parameters, values, objectInfo, imageSizes, loadArea) {
|
|
369
439
|
const copy = structuredClone(workflow);
|
|
370
440
|
let effectiveValues = values;
|
|
371
|
-
// The load
|
|
372
|
-
//
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
441
|
+
// The load area supplies the default source media: each loader parameter
|
|
442
|
+
// left unset takes the next load-area slot of its own kind, in slot order
|
|
443
|
+
// (slot 1 → first image parameter, slot 2 → second, and so on). A workflow
|
|
444
|
+
// with a single image parameter therefore behaves exactly as it did when
|
|
445
|
+
// the load area held one file, and an explicit value always wins.
|
|
446
|
+
if (loadArea !== undefined && loadArea.length > 0) {
|
|
447
|
+
const queues = { image: [], video: [], audio: [] };
|
|
448
|
+
for (const slot of loadArea) {
|
|
449
|
+
if (typeof slot?.name === 'string' && slot.name !== '')
|
|
450
|
+
queues[slot.kind].push(slot.name);
|
|
451
|
+
}
|
|
452
|
+
const filled = { ...effectiveValues };
|
|
453
|
+
let used = false;
|
|
454
|
+
for (const param of parameters) {
|
|
455
|
+
const kind = param.upload;
|
|
456
|
+
if (kind !== 'image' && kind !== 'video' && kind !== 'audio')
|
|
457
|
+
continue;
|
|
458
|
+
if (Object.prototype.hasOwnProperty.call(effectiveValues, param.name))
|
|
459
|
+
continue;
|
|
460
|
+
// An audio parameter may also take a video slot: ComfyUI's LoadAudio
|
|
461
|
+
// accepts a video container and pulls its audio track, and the load
|
|
462
|
+
// area classifies an .mp4 as video.
|
|
463
|
+
const next = queues[kind].shift() ?? (kind === 'audio' ? queues.video.shift() : undefined);
|
|
464
|
+
if (next === undefined)
|
|
465
|
+
continue;
|
|
466
|
+
filled[param.name] = next;
|
|
467
|
+
used = true;
|
|
377
468
|
}
|
|
469
|
+
if (used)
|
|
470
|
+
effectiveValues = filled;
|
|
378
471
|
}
|
|
379
472
|
// Auto-match the output size to the source image: when the effective source
|
|
380
473
|
// image (explicit or load-area default) has a recorded pixel size but
|
|
@@ -421,8 +514,9 @@ export function applyWorkflowParameters(workflow, parameters, values, objectInfo
|
|
|
421
514
|
const node = copy[param.nodeId];
|
|
422
515
|
if (node === undefined)
|
|
423
516
|
continue;
|
|
517
|
+
const explicit = Object.prototype.hasOwnProperty.call(effectiveValues, param.name);
|
|
424
518
|
let value;
|
|
425
|
-
if (
|
|
519
|
+
if (explicit) {
|
|
426
520
|
value = effectiveValues[param.name];
|
|
427
521
|
}
|
|
428
522
|
else if (param.random === true && param.type === 'number') {
|
|
@@ -431,6 +525,31 @@ export function applyWorkflowParameters(workflow, parameters, values, objectInfo
|
|
|
431
525
|
else {
|
|
432
526
|
value = param.default;
|
|
433
527
|
}
|
|
528
|
+
// A loader parameter with an empty default and nothing to fill it (no
|
|
529
|
+
// explicit value, no load-area slot of its kind) leaves the workflow's
|
|
530
|
+
// authored file name alone. Writing the empty default instead would clear
|
|
531
|
+
// the node's file input and the run would fail on a missing image.
|
|
532
|
+
// 'media' slots are exempt: there an empty value *means* "remove this
|
|
533
|
+
// reference slot", and they are merged back separately below.
|
|
534
|
+
if (!explicit && param.upload !== undefined && param.upload !== 'media' && value === '')
|
|
535
|
+
continue;
|
|
536
|
+
// A parameter exposed on a *linked* input (its authored value is a
|
|
537
|
+
// [nodeId, slot] reference, not a widget value) carries an empty default,
|
|
538
|
+
// because a link cannot be represented as one. Writing that default would
|
|
539
|
+
// sever the link on every run, so an omitted value leaves the link alone —
|
|
540
|
+
// only an explicit value replaces it.
|
|
541
|
+
if (!explicit && param.default === '' && !isPrimitive(node.inputs[param.inputKey]) && node.inputs[param.inputKey] !== undefined)
|
|
542
|
+
continue;
|
|
543
|
+
// INT inputs reject decimals at queue time; round rather than fail on a
|
|
544
|
+
// value like 20.5. The stored kind wins, but a workflow saved before the
|
|
545
|
+
// kind was recorded still gets it resolved from object_info here, so old
|
|
546
|
+
// libraries behave the same as freshly analyzed ones. An input whose type
|
|
547
|
+
// stays unknown is left exactly as the caller passed it.
|
|
548
|
+
if (param.type === 'number' && typeof value === 'number' && !Number.isInteger(value)) {
|
|
549
|
+
const kind = param.numberKind ?? numberSpecOf(objectInfo, node.class_type, param.inputKey)?.kind;
|
|
550
|
+
if (kind === 'int')
|
|
551
|
+
value = Math.round(value);
|
|
552
|
+
}
|
|
434
553
|
const isComboChild = comboChildKeys.has(`${param.nodeId}:${param.inputKey}`);
|
|
435
554
|
// Upload parameters accept any server-side filename (uploaded files, or
|
|
436
555
|
// ComfyUI's "[output]"-annotated paths); options are only a reference list.
|
|
@@ -441,8 +560,12 @@ export function applyWorkflowParameters(workflow, parameters, values, objectInfo
|
|
|
441
560
|
continue;
|
|
442
561
|
if (param.type === 'number' && typeof value !== 'number')
|
|
443
562
|
continue;
|
|
444
|
-
if (param.type === 'boolean' && typeof value !== 'boolean')
|
|
445
|
-
|
|
563
|
+
if (param.type === 'boolean' && typeof value !== 'boolean') {
|
|
564
|
+
const coerced = coerceBoolean(value);
|
|
565
|
+
if (coerced === undefined)
|
|
566
|
+
continue;
|
|
567
|
+
value = coerced;
|
|
568
|
+
}
|
|
446
569
|
if (param.upload === 'media')
|
|
447
570
|
continue; // merged back into the JSON array below
|
|
448
571
|
node.inputs[param.inputKey] = value;
|
package/lib/proxy.js
CHANGED
|
@@ -63,11 +63,24 @@ export function mountComfyUIProxy(ctx, runtime) {
|
|
|
63
63
|
nodeOutput?.videos,
|
|
64
64
|
nodeOutput?.gifs,
|
|
65
65
|
];
|
|
66
|
-
|
|
66
|
+
let ref = collections
|
|
67
67
|
.flatMap((items) => items ?? [])[index];
|
|
68
68
|
if (ref === undefined) {
|
|
69
|
-
|
|
70
|
-
|
|
69
|
+
// ComfyUI's /history is in-memory: a server restart or a "clear
|
|
70
|
+
// history" click drops the entry and this lookup fails even though
|
|
71
|
+
// the file is still on disk. The plugin's own asset index keeps the
|
|
72
|
+
// file reference for every run it submitted, so fall back to it —
|
|
73
|
+
// this is what keeps older chat cards (whose URLs were minted before
|
|
74
|
+
// the proxy addressed files directly) from going blank.
|
|
75
|
+
const archived = await runtime.listAssets();
|
|
76
|
+
const item = archived
|
|
77
|
+
.find((asset) => asset.promptId === prompt)
|
|
78
|
+
?.media.find((entry) => entry.node === node && entry.index === index);
|
|
79
|
+
if (item === undefined) {
|
|
80
|
+
sendJson(response, 404, { error: `no media item ${node}[${index}] for prompt ${prompt} (history evicted and not in the asset index)` });
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
ref = { filename: item.filename, subfolder: item.subfolder, type: item.type };
|
|
71
84
|
}
|
|
72
85
|
const { bytes, contentType } = await client.fetchView(ref);
|
|
73
86
|
response.writeHead(200, {
|