Image cache node
This commit is contained in:
@@ -5,17 +5,18 @@ import type { SerializedPrompt } from "$lib/components/ComfyApp";
|
||||
import { toast } from '@zerodevx/svelte-toast'
|
||||
import type { GalleryOutput } from "./ComfyWidgetNodes";
|
||||
|
||||
export interface ComfyAfterQueuedEventProperties extends Record<any, any> {
|
||||
prompt: SerializedPrompt
|
||||
export interface ComfyQueueEventsProperties extends Record<any, any> {
|
||||
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<any, any> {
|
||||
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<any, any> {
|
||||
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"
|
||||
})
|
||||
|
||||
@@ -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)
|
||||
|
||||
211
src/lib/nodes/ComfyImageCacheNode.ts
Normal file
211
src/lib/nodes/ComfyImageCacheNode.ts
Normal file
@@ -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<any, any> {
|
||||
images: GalleryOutput | null,
|
||||
index: number,
|
||||
filenames: Record<number, { filename: string | null, status: ImageCacheState }>,
|
||||
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<void> | null = null;
|
||||
private _state: ImageCacheState = "none"
|
||||
|
||||
stateWidget: ITextWidget;
|
||||
filenameWidget: ITextWidget;
|
||||
|
||||
constructor(name?: string) {
|
||||
super(name)
|
||||
this.stateWidget = this.addWidget<ITextWidget>(
|
||||
"text",
|
||||
"State",
|
||||
"none"
|
||||
);
|
||||
this.stateWidget.disabled = true;
|
||||
|
||||
this.filenameWidget = this.addWidget<ITextWidget>(
|
||||
"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"
|
||||
})
|
||||
@@ -119,7 +119,6 @@ export abstract class ComfyWidgetNode<T = any> 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<GradioFileData[]> {
|
||||
|
||||
static slotLayout: SlotLayout = {
|
||||
inputs: [
|
||||
{ name: "images", type: "IMAGE" },
|
||||
{ name: "images", type: "OUTPUT" },
|
||||
{ name: "store", type: BuiltInSlotType.ACTION }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user