From ad35826c7b8dbe6c8ebdfbe50931c05cd1be8df2 Mon Sep 17 00:00:00 2001 From: space-nuko <24979496+space-nuko@users.noreply.github.com> Date: Fri, 5 May 2023 20:22:12 -0500 Subject: [PATCH] Image cache node --- litegraph | 2 +- package.json | 1 + pnpm-lock.yaml | 19 ++ src/lib/ComfyGraphCanvas.ts | 4 +- src/lib/components/ComfyApp.ts | 12 +- src/lib/components/ComfyProperties.svelte | 9 +- src/lib/nodes/ComfyActionNodes.ts | 75 ++++++-- src/lib/nodes/ComfyGraphNode.ts | 3 +- src/lib/nodes/ComfyImageCacheNode.ts | 211 ++++++++++++++++++++++ src/lib/nodes/ComfyWidgetNodes.ts | 3 +- src/lib/nodes/index.ts | 3 +- src/lib/stores/layoutState.ts | 29 ++- src/lib/widgets/ButtonWidget.svelte | 6 +- src/lib/widgets/ComboWidget.svelte | 2 +- src/lib/widgets/RangeWidget.svelte | 1 + src/lib/widgets/TextWidget.svelte | 1 + vite.config.ts | 10 +- 17 files changed, 358 insertions(+), 33 deletions(-) create mode 100644 src/lib/nodes/ComfyImageCacheNode.ts diff --git a/litegraph b/litegraph index 115bef4..4500dec 160000 --- a/litegraph +++ b/litegraph @@ -1 +1 @@ -Subproject commit 115bef46a58616395d7d3b54a1421472ac28e519 +Subproject commit 4500dec3b95f3d10f70cbd415780eb4884d2800e diff --git a/package.json b/package.json index 23ae656..9d255c3 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "@litegraph-ts/nodes-basic": "workspace:*", "@litegraph-ts/nodes-events": "workspace:*", "@litegraph-ts/nodes-math": "workspace:*", + "@litegraph-ts/nodes-strings": "workspace:*", "@litegraph-ts/tsconfig": "workspace:*", "@sveltejs/vite-plugin-svelte": "^2.1.1", "@tsconfig/svelte": "^4.0.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0fccfd3..64cd3ce 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -43,6 +43,9 @@ importers: '@litegraph-ts/nodes-math': specifier: workspace:* version: link:litegraph/packages/nodes-math + '@litegraph-ts/nodes-strings': + specifier: workspace:* + version: link:litegraph/packages/nodes-strings '@litegraph-ts/tsconfig': specifier: workspace:* version: link:litegraph/packages/tsconfig @@ -805,6 +808,22 @@ importers: specifier: ^4.2.1 version: 4.3.1 + litegraph/packages/nodes-strings: + dependencies: + '@litegraph-ts/core': + specifier: workspace:* + version: link:../core + devDependencies: + '@litegraph-ts/tsconfig': + specifier: workspace:* + version: link:../tsconfig + typescript: + specifier: ^5.0.3 + version: 5.0.3 + vite: + specifier: ^4.2.1 + version: 4.3.1 + litegraph/packages/tsconfig: {} packages: diff --git a/src/lib/ComfyGraphCanvas.ts b/src/lib/ComfyGraphCanvas.ts index cd16148..8e1db17 100644 --- a/src/lib/ComfyGraphCanvas.ts +++ b/src/lib/ComfyGraphCanvas.ts @@ -231,9 +231,9 @@ export default class ComfyGraphCanvas extends LGraphCanvas { return res; } - override onNodeSelected(node: LGraphNode) { + override onSelectionChange(nodes: Record) { const ls = get(layoutState) - ls.currentSelectionNodes = [node] + ls.currentSelectionNodes = Object.values(nodes) ls.currentSelection = [] layoutState.set(ls) } diff --git a/src/lib/components/ComfyApp.ts b/src/lib/components/ComfyApp.ts index 09f38b1..e144029 100644 --- a/src/lib/components/ComfyApp.ts +++ b/src/lib/components/ComfyApp.ts @@ -10,6 +10,8 @@ import type TypedEmitter from "typed-emitter"; import "@litegraph-ts/nodes-basic" import "@litegraph-ts/nodes-events" import "@litegraph-ts/nodes-math" +import "@litegraph-ts/nodes-strings" +import "$lib/nodes/index" import * as nodes from "$lib/nodes/index" import ComfyGraphCanvas, { type SerializedGraphCanvasState } from "$lib/ComfyGraphCanvas"; @@ -25,7 +27,7 @@ import ComfyGraph from "$lib/ComfyGraph"; import { ComfyBackendNode } from "$lib/nodes/ComfyBackendNode"; import { get } from "svelte/store"; import uiState from "$lib/stores/uiState"; -import { promptToGraphVis, toGraphVis } from "$lib/utils"; +import { promptToGraphVis } from "$lib/utils"; export const COMFYBOX_SERIAL_VERSION = 1; @@ -578,9 +580,15 @@ export default class ComfyApp { try { while (this.queueItems.length) { ({ num, batchCount } = this.queueItems.pop()); - console.log(`Queue get! ${num} ${batchCount}`); + console.debug(`Queue get! ${num} ${batchCount} ${tag}`); for (let i = 0; i < batchCount; i++) { + for (const node of this.lGraph._nodes_in_order) { + if ("beforeQueued" in node) { + (node as ComfyGraphNode).beforeQueued(); + } + } + const p = await this.graphToPrompt(tag); try { diff --git a/src/lib/components/ComfyProperties.svelte b/src/lib/components/ComfyProperties.svelte index b5aad70..0d2b9c3 100644 --- a/src/lib/components/ComfyProperties.svelte +++ b/src/lib/components/ComfyProperties.svelte @@ -41,6 +41,13 @@ targetType = "" } + function validNodeProperty(spec: AttributesSpec, node: LGraphNode): boolean { + if (spec.validNodeTypes) { + return spec.validNodeTypes.indexOf(node.type) !== -1; + } + return spec.name in node.properties + } + function updateAttribute(entry: AttributesSpec, value: any) { if (target) { const name = entry.name @@ -157,7 +164,7 @@ {/if} {:else if node} - {#if spec.location === "nodeProps" && spec.name in node.properties} + {#if spec.location === "nodeProps" && validNodeProperty(spec, node)}
{#if spec.type === "string"} { - prompt: SerializedPrompt +export interface ComfyQueueEventsProperties extends Record { + prompt: SerializedPrompt | null } -export class ComfyAfterQueuedEvent extends ComfyGraphNode { - override properties: ComfyAfterQueuedEventProperties = { +export class ComfyQueueEvents extends ComfyGraphNode { + override properties: ComfyQueueEventsProperties = { prompt: null } static slotLayout: SlotLayout = { outputs: [ + { name: "beforeQueued", type: BuiltInSlotType.EVENT }, { name: "afterQueued", type: BuiltInSlotType.EVENT }, { name: "prompt", type: "*" } ], @@ -23,17 +24,22 @@ export class ComfyAfterQueuedEvent extends ComfyGraphNode { override onPropertyChanged(property: string, value: any, prevValue?: any) { if (property === "value") { - this.setOutputData(0, this.properties.prompt) + this.setOutputData(2, this.properties.prompt) } } override onExecute() { - this.setOutputData(0, this.properties.prompt) + this.setOutputData(2, this.properties.prompt) + } + + override beforeQueued() { + this.setProperty("value", null) + this.triggerSlot(0, "bang") } override afterQueued(p: SerializedPrompt) { this.setProperty("value", p) - this.triggerSlot(0, "bang") + this.triggerSlot(1, "bang") } override onSerialize(o: SerializedLGraphNode) { @@ -43,19 +49,21 @@ export class ComfyAfterQueuedEvent extends ComfyGraphNode { } LiteGraph.registerNodeType({ - class: ComfyAfterQueuedEvent, - title: "Comfy.AfterQueuedEvent", + class: ComfyQueueEvents, + title: "Comfy.QueueEvents", desc: "Triggers a 'bang' event when a prompt is queued.", - type: "actions/after_queued" + type: "actions/queue_events" }) export interface ComfyOnExecutedEventProperties extends Record { - images: GalleryOutput | null + images: GalleryOutput | null, + filename: string | null } export class ComfyOnExecutedEvent extends ComfyGraphNode { override properties: ComfyOnExecutedEventProperties = { - images: null + images: null, + filename: null } static slotLayout: SlotLayout = { @@ -63,7 +71,7 @@ export class ComfyOnExecutedEvent extends ComfyGraphNode { { name: "images", type: "IMAGE" } ], outputs: [ - { name: "images", type: "IMAGE" }, + { name: "images", type: "OUTPUT" }, { name: "onExecuted", type: BuiltInSlotType.EVENT }, ], } @@ -76,6 +84,7 @@ export class ComfyOnExecutedEvent extends ComfyGraphNode { override receiveOutput(output: any) { if (output && "images" in output) { this.setProperty("images", output as GalleryOutput) + this.setOutputData(0, this.properties.images) this.triggerSlot(1, "bang") } } @@ -202,3 +211,43 @@ LiteGraph.registerNodeType({ desc: "Displays a message.", type: "actions/notify" }) + +export interface ComfyExecuteSubgraphActionProperties extends Record { + tag: string | null, +} + +export class ComfyExecuteSubgraphAction extends ComfyGraphNode { + override properties: ComfyExecuteSubgraphActionProperties = { + tag: null + } + + static slotLayout: SlotLayout = { + inputs: [ + { name: "execute", type: BuiltInSlotType.ACTION }, + { name: "tag", type: "string" } + ], + } + + override onExecute() { + const tag = this.getInputData(1) + if (tag) + this.setProperty("tag", tag) + } + + override onAction(action: any, param: any) { + const tag = this.getInputData(1) || this.properties.tag; + + const app = (window as any)?.app; + if (!app) + return; + + app.queuePrompt(0, 1, tag); + } +} + +LiteGraph.registerNodeType({ + class: ComfyExecuteSubgraphAction, + title: "Comfy.ExecuteSubgraphAction", + desc: "Runs a part of the graph based on a tag", + type: "actions/execute_subgraph" +}) diff --git a/src/lib/nodes/ComfyGraphNode.ts b/src/lib/nodes/ComfyGraphNode.ts index b9d1ca6..b722013 100644 --- a/src/lib/nodes/ComfyGraphNode.ts +++ b/src/lib/nodes/ComfyGraphNode.ts @@ -20,6 +20,7 @@ export type DefaultWidgetLayout = { export default class ComfyGraphNode extends LGraphNode { isBackendNode?: boolean; + beforeQueued?(): void; afterQueued?(prompt: SerializedPrompt): void; onExecuted?(output: any): void; @@ -61,7 +62,7 @@ export default class ComfyGraphNode extends LGraphNode { for (let index = 0; index < this.inputs.length; index++) { const input = this.inputs[index] const serInput = o.inputs[index] - if ("widgetNodeType" in serInput) { + if (serInput && "widgetNodeType" in serInput) { const comfyInput = input as IComfyInputSlot const ty: string = serInput.widgetNodeType as any const widgetNode = Object.values(LiteGraph.registered_node_types) diff --git a/src/lib/nodes/ComfyImageCacheNode.ts b/src/lib/nodes/ComfyImageCacheNode.ts new file mode 100644 index 0000000..9ec6933 --- /dev/null +++ b/src/lib/nodes/ComfyImageCacheNode.ts @@ -0,0 +1,211 @@ +import { BuiltInSlotType, LiteGraph, type ITextWidget, type SlotLayout, clamp } from "@litegraph-ts/core"; +import ComfyGraphNode from "./ComfyGraphNode"; +import type { GalleryOutput } from "./ComfyWidgetNodes"; + +export interface ComfyImageCacheNodeProperties extends Record { + images: GalleryOutput | null, + index: number, + filenames: Record, + genNumber: number +} + +type ImageCacheState = "none" | "uploading" | "failed" | "cached" + +/* + * A node that can act as both an input and output image node by uploading + * the output file into ComfyUI's input folder. + */ +export default class ComfyImageCacheNode extends ComfyGraphNode { + override properties: ComfyImageCacheNodeProperties = { + images: null, + index: 0, + filenames: {}, + genNumber: 0 + } + + static slotLayout: SlotLayout = { + inputs: [ + { name: "images", type: "OUTPUT" }, + { name: "index", type: "number" }, + { name: "store", type: BuiltInSlotType.ACTION }, + { name: "clear", type: BuiltInSlotType.ACTION } + ], + outputs: [ + { name: "filename", type: "string" }, + { name: "state", type: "string" }, + ] + } + + private _uploadPromise: Promise | null = null; + private _state: ImageCacheState = "none" + + stateWidget: ITextWidget; + filenameWidget: ITextWidget; + + constructor(name?: string) { + super(name) + this.stateWidget = this.addWidget( + "text", + "State", + "none" + ); + this.stateWidget.disabled = true; + + this.filenameWidget = this.addWidget( + "text", + "File", + "" + ); + this.filenameWidget.disabled = true; + } + + override onPropertyChanged(property: string, value: any, prevValue?: any) { + if (property === "images") { + if (value != null) + this.properties.index = clamp(this.properties.index, 0, value.length) + else + this.properties.index = 0 + } + + if (this.properties.filenames && this.properties.images) { + const fileCount = this.properties.images.images.length; + const cachedCount = Object.keys(this.properties.filenames).length + console.warn(cachedCount, this.properties.filenames) + this.filenameWidget.value = `${fileCount} files, ${cachedCount} cached` + } + } + + override onExecute() { + const index = this.getInputData(1) + if (typeof index === "number") + this.setIndex(index) + + const existing = this.properties.filenames[this.properties.index] + let state = "none" + if (existing) + state = existing.status + + this.stateWidget.value = state + + let filename = null + if (this.properties.index in this.properties.filenames) + filename = this.properties.filenames[this.properties.index].filename + + this.setOutputData(0, filename) + this.setOutputData(1, state) + } + + private setIndex(newIndex: number, force: boolean = false) { + console.debug("[ComfyImageCacheNode] setIndex", newIndex, force) + + if (newIndex === this.properties.index && !force) + return; + + if (!this.properties.images || newIndex < 0 || newIndex >= this.properties.images.images.length) { + console.debug("[ComfyImageCacheNode] invalid indexes", newIndex, this.properties.images) + return + } + + this.setProperty("index", newIndex) + + const data = this.properties.images.images[newIndex] + + if (data == null) { + return; + } + + this.properties.filenames ||= {} + const existing = this.properties.filenames[newIndex] + + if (existing != null && existing.status === "cached") { + return + } + + const lastGenNumber = this.properties.genNumber + + // ComfyUI's LoadImage node only operates on files in its input + // folder. Usually we're dealing with an image in either the output + // folder (SaveImage) or the temp folder (PreviewImage). So we have + // to copy the image into ComfyUI's input folder first by using + // their upload API. + + if (data.subfolder === "input") { + // Already in the correct folder for use by LoadImage + this.properties.filenames[newIndex] = { filename: data.filename, status: "cached" } + this.onPropertyChanged("filenames", this.properties.filenames) + } + else { + this.properties.filenames[newIndex] = { filename: null, status: "uploading" } + this.onPropertyChanged("filenames", this.properties.filenames) + const url = "http://localhost:8188" // TODO make configurable + const params = new URLSearchParams(data) + + const promise = fetch(url + "/view?" + params) + .then((r) => r.blob()) + .then((blob) => { + console.debug("Fetchin", url, params) + const formData = new FormData(); + formData.append("image", blob, data.filename); + return fetch( + new Request(url + "/upload/image", { + body: formData, + method: 'POST' + }) + ) + }) + .then((r) => r.json()) + .then((json) => { + console.debug("Gottem", json) + if (lastGenNumber === this.properties.genNumber) { + this.properties.filenames[newIndex] = { filename: data.filename, status: "cached" } + this.onPropertyChanged("filenames", this.properties.filenames) + } + else { + console.warn("[ComfyImageCacheNode] New generation since index switched!") + } + this._uploadPromise = null; + }) + .catch((e) => { + console.error("Error uploading:", e) + if (lastGenNumber === this.properties.genNumber) { + this.properties.filenames[newIndex] = { filename: null, status: "failed" } + this.onPropertyChanged("filenames", this.properties.filenames) + } + else { + console.warn("[ComfyImageCacheNode] New generation since index switched!") + } + }) + + if (this._uploadPromise) + this._uploadPromise.then(() => promise) + else + this._uploadPromise = promise + } + } + + override onAction(action: any) { + if (action === "clear") { + this.setProperty("images", null) + this.setProperty("filenames", {}) + this.setProperty("index", 0) + return + } + + const link = this.getInputLink(0) + + if (link.data && "images" in link.data) { + this.setProperty("genNumber", this.properties.genNumber + 1) + this.setProperty("images", link.data as GalleryOutput) + this.setProperty("filenames", {}) + console.debug("[ComfyImageCacheNode] Received output!", link.data) + this.setIndex(0, true) + } + } +} + +LiteGraph.registerNodeType({ + class: ComfyImageCacheNode, + title: "Comfy.ImageCache", + desc: "Allows reusing a previously output image by uploading it into ComfyUI's input folder.", + type: "image/cache" +}) diff --git a/src/lib/nodes/ComfyWidgetNodes.ts b/src/lib/nodes/ComfyWidgetNodes.ts index 2fead67..81e7d2d 100644 --- a/src/lib/nodes/ComfyWidgetNodes.ts +++ b/src/lib/nodes/ComfyWidgetNodes.ts @@ -119,7 +119,6 @@ export abstract class ComfyWidgetNode extends ComfyGraphNode { if (this.inputs.length >= this.inputIndex) { const data = this.getInputData(this.inputIndex) if (data) { // TODO can "null" be a legitimate value here? - console.log(data) this.setValue(data) const input = this.getInputLink(this.inputIndex) input.data = null; @@ -407,7 +406,7 @@ export class ComfyGalleryNode extends ComfyWidgetNode { static slotLayout: SlotLayout = { inputs: [ - { name: "images", type: "IMAGE" }, + { name: "images", type: "OUTPUT" }, { name: "store", type: BuiltInSlotType.ACTION } ] } diff --git a/src/lib/nodes/index.ts b/src/lib/nodes/index.ts index 087dbc5..e07776e 100644 --- a/src/lib/nodes/index.ts +++ b/src/lib/nodes/index.ts @@ -1,5 +1,6 @@ export { default as ComfyReroute } from "./ComfyReroute" export { ComfyWidgetNode, ComfySliderNode, ComfyComboNode, ComfyTextNode } from "./ComfyWidgetNodes" -export { ComfyAfterQueuedEvent, ComfyCopyAction, ComfySwapAction, ComfyNotifyAction, ComfyOnExecutedEvent } from "./ComfyActionNodes" +export { ComfyQueueEvents, ComfyCopyAction, ComfySwapAction, ComfyNotifyAction, ComfyOnExecutedEvent, ComfyExecuteSubgraphAction } from "./ComfyActionNodes" export { default as ComfyValueControl } from "./ComfyValueControl" export { default as ComfySelector } from "./ComfySelector" +export { default as ComfyImageCacheNode } from "./ComfyImageCacheNode" diff --git a/src/lib/stores/layoutState.ts b/src/lib/stores/layoutState.ts index 50acc8a..a53f2d5 100644 --- a/src/lib/stores/layoutState.ts +++ b/src/lib/stores/layoutState.ts @@ -34,6 +34,7 @@ export type Attributes = { classes: string, blockVariant?: "block" | "hidden", hidden?: boolean, + disabled?: boolean, flexGrow?: number } @@ -45,6 +46,7 @@ export type AttributesSpec = { values?: string[], hidden?: boolean, + validNodeTypes?: string[], serialize?: (arg: any) => string, deserialize?: (arg: string) => any, @@ -73,6 +75,12 @@ const ALL_ATTRIBUTES: AttributesSpecList = [ location: "widget", editable: true }, + { + name: "disabled", + type: "boolean", + location: "widget", + editable: true + }, { name: "direction", type: "enum", @@ -104,6 +112,7 @@ const ALL_ATTRIBUTES: AttributesSpecList = [ { categoryName: "behavior", specs: [ + // Node variables { name: "tags", type: "string", @@ -116,24 +125,40 @@ const ALL_ATTRIBUTES: AttributesSpecList = [ return arg.split(",").map(s => s.trim()) } }, + + // Range { name: "min", type: "number", location: "nodeProps", editable: true, + validNodeTypes: ["ui/slider"], }, { name: "max", type: "number", location: "nodeProps", - editable: true + editable: true, + validNodeTypes: ["ui/slider"], }, { name: "step", type: "number", location: "nodeProps", editable: true, + validNodeTypes: ["ui/slider"], }, + + // Button + { + name: "message", + type: "string", + location: "nodeProps", + editable: true, + validNodeTypes: ["ui/button"], + }, + + // Workflow { name: "defaultWorkflow", type: "string", @@ -531,8 +556,6 @@ function deserialize(data: SerializedLayoutState, graph: LGraph) { attrsChanged: writable(false) }; - dragItem.attrs.flexGrow = 100; - const dragEntry: DragItemEntry = { dragItem, children: [], diff --git a/src/lib/widgets/ButtonWidget.svelte b/src/lib/widgets/ButtonWidget.svelte index f2a0dcc..a0c72ed 100644 --- a/src/lib/widgets/ButtonWidget.svelte +++ b/src/lib/widgets/ButtonWidget.svelte @@ -30,7 +30,11 @@
{#if node !== null} - {/if} diff --git a/src/lib/widgets/ComboWidget.svelte b/src/lib/widgets/ComboWidget.svelte index cfc636f..3efb7c4 100644 --- a/src/lib/widgets/ComboWidget.svelte +++ b/src/lib/widgets/ComboWidget.svelte @@ -73,7 +73,7 @@