TEMP refactor file passing

This commit is contained in:
space-nuko
2023-05-13 16:19:42 -05:00
parent 0656ae1d3a
commit 05bcce5573
14 changed files with 241 additions and 555 deletions

View File

@@ -1,7 +1,7 @@
import type { Progress, SerializedPrompt, SerializedPromptInputs, SerializedPromptInputsAll, SerializedPromptOutput, SerializedPromptOutputs } from "./components/ComfyApp";
import type TypedEmitter from "typed-emitter";
import EventEmitter from "events";
import type { GalleryOutput, GalleryOutputEntry } from "./nodes/ComfyWidgetNodes";
import type { ComfyExecutionResult, ComfyImageLocation } from "./nodes/ComfyWidgetNodes";
import type { SerializedLGraph, UUID } from "@litegraph-ts/core";
import type { SerializedLayoutState } from "./stores/layoutState";
@@ -63,7 +63,7 @@ export type ComfyPromptPNGInfo = {
}
export type ComfyBoxPromptExtraData = ComfyUIPromptExtraData & {
thumbnails?: GalleryOutputEntry[],
thumbnails?: ComfyImageLocation[],
}
export type ComfyUIPromptExtraData = {

View File

@@ -32,7 +32,7 @@ import { download, jsonToJsObject, promptToGraphVis, range, workflowToGraphVis }
import notify from "$lib/notify";
import configState from "$lib/stores/configState";
import { blankGraph } from "$lib/defaultGraph";
import type { GalleryOutput } from "$lib/nodes/ComfyWidgetNodes";
import type { ComfyExecutionResult } from "$lib/nodes/ComfyWidgetNodes";
export const COMFYBOX_SERIAL_VERSION = 1;
@@ -71,7 +71,7 @@ export type SerializedPrompt = {
output: SerializedPromptInputsAll
}
export type SerializedPromptOutputs = Record<NodeID, GalleryOutput>
export type SerializedPromptOutputs = Record<NodeID, ComfyExecutionResult>
export type Progress = {
value: number,
@@ -347,7 +347,7 @@ export default class ComfyApp {
this.lGraph.setDirtyCanvas(true, false);
});
this.api.addEventListener("executed", (promptID: PromptID, nodeID: NodeID, output: GalleryOutput) => {
this.api.addEventListener("executed", (promptID: PromptID, nodeID: NodeID, output: ComfyExecutionResult) => {
this.nodeOutputs[nodeID] = output;
const node = this.lGraph.getNodeById(nodeID) as ComfyGraphNode;
if (node?.onExecuted) {
@@ -409,7 +409,8 @@ export default class ComfyApp {
}
setColor("IMAGE", "rebeccapurple")
setColor("COMFY_IMAGE_FILE", "chartreuse")
setColor("COMFYBOX_IMAGES", "lime")
setColor("COMFYBOX_IMAGE", "green")
setColor(BuiltInSlotType.EVENT, "lightseagreen")
setColor(BuiltInSlotType.ACTION, "lightseagreen")
}

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import UploadText from "$lib/components/gradio/app/UploadText.svelte";
import type { GalleryOutputEntry } from "$lib/nodes/ComfyWidgetNodes";
import type { ComfyImageLocation } from "$lib/nodes/ComfyWidgetNodes";
import notify from "$lib/notify";
import { convertComfyOutputEntryToGradio, convertComfyOutputToComfyURL, type ComfyUploadImageAPIResponse } from "$lib/utils";
import { Block, BlockLabel } from "@gradio/atoms";
@@ -9,7 +9,7 @@
import { ModifyUpload, Upload } from "@gradio/upload";
import { createEventDispatcher, tick } from "svelte";
export let value: GalleryOutputEntry[] | null = null;
export let value: ComfyImageLocation[] | null = null;
export let imgWidth: number = 0;
export let imgHeight: number = 0;
export let imgElem: HTMLImageElement | null = null
@@ -28,9 +28,9 @@
let uploaded: boolean = false;
const dispatch = createEventDispatcher<{
change: GalleryOutputEntry[];
change: ComfyImageLocation[];
uploading: undefined;
uploaded: GalleryOutputEntry[];
uploaded: ComfyImageLocation[];
upload_error: any;
clear: undefined;
}>();
@@ -60,7 +60,7 @@
interface GradioUploadResponse {
error?: string;
files?: Array<GalleryOutputEntry>;
files?: Array<ComfyImageLocation>;
}
async function upload_files(root: string, files: Array<File>): Promise<GradioUploadResponse> {

View File

@@ -5,10 +5,10 @@ import queueState from "$lib/stores/queueState";
import { BuiltInSlotType, LiteGraph, NodeMode, type ITextWidget, type IToggleWidget, type SerializedLGraphNode, type SlotLayout, type PropertyLayout } from "@litegraph-ts/core";
import { get } from "svelte/store";
import ComfyGraphNode, { type ComfyGraphNodeProperties } from "./ComfyGraphNode";
import type { ComfyWidgetNode, GalleryOutput, GalleryOutputEntry } from "./ComfyWidgetNodes";
import type { ComfyWidgetNode, ComfyExecutionResult, ComfyImageLocation } from "./ComfyWidgetNodes";
import type { NotifyOptions } from "$lib/notify";
import type { FileData as GradioFileData } from "@gradio/upload";
import { convertComfyOutputToGradio, reuploadImageToComfyUI, type ComfyUploadImageAPIResponse } from "$lib/utils";
import { convertComfyOutputToGradio, type ComfyUploadImageAPIResponse } from "$lib/utils";
export class ComfyQueueEvents extends ComfyGraphNode {
static slotLayout: SlotLayout = {
@@ -63,7 +63,7 @@ LiteGraph.registerNodeType({
})
export interface ComfyStoreImagesActionProperties extends ComfyGraphNodeProperties {
images: GalleryOutput | null
images: ComfyExecutionResult | null
}
export class ComfyStoreImagesAction extends ComfyGraphNode {
@@ -90,7 +90,7 @@ export class ComfyStoreImagesAction extends ComfyGraphNode {
if (action !== "store" || !param || !("images" in param))
return;
this.setProperty("images", param as GalleryOutput)
this.setProperty("images", param as ComfyExecutionResult)
this.setOutputData(0, this.properties.images)
}
}
@@ -223,7 +223,7 @@ export class ComfyNotifyAction extends ComfyGraphNode {
// native notifications.
if (param != null && typeof param === "object") {
if ("images" in param) {
const output = param as GalleryOutput;
const output = param as ComfyExecutionResult;
const converted = convertComfyOutputToGradio(output);
if (converted.length > 0)
options.imageUrl = converted[0].data;
@@ -581,86 +581,6 @@ LiteGraph.registerNodeType({
type: "events/no_change"
})
export interface ComfyUploadImageActionProperties extends ComfyGraphNodeProperties {
folderType: "output" | "temp"
lastUploadedImageFile: string | null
}
export class ComfyUploadImageAction extends ComfyGraphNode {
override properties: ComfyUploadImageActionProperties = {
tags: [],
folderType: "output",
lastUploadedImageFile: null
}
static slotLayout: SlotLayout = {
inputs: [
{ name: "filename", type: "string" },
{ name: "trigger", type: BuiltInSlotType.ACTION }
],
outputs: [
{ name: "input_filename", type: "string" },
{ name: "uploaded", type: BuiltInSlotType.EVENT }
],
}
private _promise = null;
displayWidget: ITextWidget;
constructor(title?: string) {
super(title);
this.displayWidget = this.addWidget<ITextWidget>(
"text",
"File",
this.properties.lastUploadedImageFile,
"lastUploadedImageFile"
);
this.displayWidget.disabled = true;
}
override onExecute() {
this.setOutputData(0, this.properties.lastUploadedImageFile)
}
override onAction(action: any, param: any) {
if (action !== "trigger" || this._promise != null)
return;
const filename = this.getInputData(0)
if (typeof filename !== "string" || !filename) {
return;
}
const data: GalleryOutputEntry = {
filename,
subfolder: "",
type: this.properties.folderType || "output"
}
this._promise = reuploadImageToComfyUI(data, "input")
.then((entry: GalleryOutputEntry) => {
console.debug("[UploadImageAction] Succeeded", entry)
this.properties.lastUploadedImageFile = entry.filename;
this.triggerSlot(1, this.properties.lastUploadedImageFile);
this._promise = null;
})
.catch((e) => {
console.error("Error uploading:", e)
notify(`Error uploading image to ComfyUi: ${e}`, { type: "error", timeout: 10000 })
this.properties.lastUploadedImageFile = null;
this._promise = null;
})
}
}
LiteGraph.registerNodeType({
class: ComfyUploadImageAction,
title: "Comfy.UploadImageAction",
desc: "Uploads an image from the specified ComfyUI folder into its input folder",
type: "actions/store_images"
})
export interface ComfySetPromptThumbnailsActionProperties extends ComfyGraphNodeProperties {
defaultFolderType: string | null
}
@@ -679,12 +599,12 @@ export class ComfySetPromptThumbnailsAction extends ComfyGraphNode {
_value: any = null;
override getPromptThumbnails(): GalleryOutputEntry[] | null {
override getPromptThumbnails(): ComfyImageLocation[] | null {
const data = this.getInputData(0)
const folderType = this.properties.folderType || "input";
const convertString = (s: string): GalleryOutputEntry => {
const convertString = (s: string): ComfyImageLocation => {
return { filename: data, subfolder: "", type: folderType }
}
@@ -693,13 +613,13 @@ export class ComfySetPromptThumbnailsAction extends ComfyGraphNode {
}
else if (data != null && typeof data === "object") {
if ("filename" in data && "type" in data)
return [data as GalleryOutputEntry];
return [data as ComfyImageLocation];
}
else if (Array.isArray(data) && data.length > 0) {
if (typeof data[0] === "string")
return data.map(convertString)
else if (typeof data[0] === "object" && "filename" in data[0] && "type" in data[0])
return data as GalleryOutputEntry[]
return data as ComfyImageLocation[]
}
return null;
}

View File

@@ -1,7 +1,7 @@
import LGraphCanvas from "@litegraph-ts/core/src/LGraphCanvas";
import ComfyGraphNode from "./ComfyGraphNode";
import ComfyWidgets from "$lib/widgets"
import type { ComfyWidgetNode, GalleryOutput } from "./ComfyWidgetNodes";
import type { ComfyWidgetNode, ComfyExecutionResult } from "./ComfyWidgetNodes";
import { BuiltInSlotType, type SerializedLGraphNode } from "@litegraph-ts/core";
import type IComfyInputSlot from "$lib/IComfyInputSlot";
import type { ComfyInputConfig } from "$lib/IComfyInputSlot";
@@ -110,7 +110,7 @@ export class ComfyBackendNode extends ComfyGraphNode {
}
}
override onExecuted(outputData: GalleryOutput) {
override onExecuted(outputData: ComfyExecutionResult) {
console.warn("onExecuted outputs", outputData)
this.triggerSlot(0, outputData)
}

View File

@@ -3,7 +3,7 @@ import type { SerializedPrompt } from "$lib/components/ComfyApp";
import type ComfyWidget from "$lib/components/widgets/ComfyWidget";
import { LGraph, LGraphNode, LLink, LiteGraph, NodeMode, type INodeInputSlot, type SerializedLGraphNode, type Vector2, type INodeOutputSlot, LConnectionKind, type SlotType, LGraphCanvas, getStaticPropertyOnInstance, type PropertyLayout, type SlotLayout } from "@litegraph-ts/core";
import type { SvelteComponentDev } from "svelte/internal";
import type { ComfyWidgetNode, GalleryOutput, GalleryOutputEntry } from "./ComfyWidgetNodes";
import type { ComfyWidgetNode, ComfyExecutionResult, ComfyImageLocation } from "./ComfyWidgetNodes";
import type IComfyInputSlot from "$lib/IComfyInputSlot";
import uiState from "$lib/stores/uiState";
import { get } from "svelte/store";
@@ -48,14 +48,14 @@ export default class ComfyGraphNode extends LGraphNode {
* Triggered when the backend sends a finished output back with this node's ID.
* Valid for output nodes like SaveImage and PreviewImage.
*/
onExecuted?(output: GalleryOutput): void;
onExecuted?(output: ComfyExecutionResult): void;
/*
* When a prompt is queued, this will be called on the node if it can
* provide any thumbnails for use with the prompt queue. Useful for HR Fix
* or img2img workloads.
*/
getPromptThumbnails?(): GalleryOutputEntry[] | null
getPromptThumbnails?(): ComfyImageLocation[] | null
/*
* Allows you to manually specify an auto-config for certain input slot

View File

@@ -1,241 +0,0 @@
import { BuiltInSlotType, LiteGraph, type ITextWidget, type SlotLayout, clamp, type PropertyLayout, type IComboWidget, type SerializedLGraphNode } from "@litegraph-ts/core";
import ComfyGraphNode, { type ComfyGraphNodeProperties } from "./ComfyGraphNode";
import type { GalleryOutput, GalleryOutputEntry } from "./ComfyWidgetNodes";
import { reuploadImageToComfyUI, type ComfyUploadImageAPIResponse } from "$lib/utils";
export interface ComfyImageCacheNodeProperties extends ComfyGraphNodeProperties {
images: GalleryOutput | null,
index: number,
filenames: Record<number, { filename: string | null, status: ImageCacheState }>,
genNumber: number,
updateMode: "replace" | "append"
}
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 = {
tags: [],
images: null,
index: 0,
filenames: {},
genNumber: 0,
updateMode: "replace"
}
static slotLayout: SlotLayout = {
inputs: [
{ name: "images", type: "OUTPUT" },
{ name: "index", type: "number" },
{ name: "store", type: BuiltInSlotType.ACTION, options: { color_off: "rebeccapurple", color_on: "rebeccapurple" } },
{ name: "clear", type: BuiltInSlotType.ACTION }
],
outputs: [
{ name: "filename", type: "string" },
{ name: "state", type: "string" },
]
}
static propertyLayout: PropertyLayout = [
{ name: "updateMode", defaultValue: "replace", type: "enum", options: { values: ["replace", "append"] } }
]
override saveUserState = false;
private _uploadPromise: Promise<void> | null = null;
stateWidget: ITextWidget;
filenameWidget: ITextWidget;
modeWidget: IComboWidget;
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;
this.modeWidget = this.addWidget<IComboWidget>(
"combo",
"Mode",
this.properties.updateMode,
null,
{ property: "updateMode", values: ["replace", "append"] }
);
}
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
}
else if (property === "updateMode") {
this.modeWidget.value = value;
}
this.updateWidgets()
}
private updateWidgets() {
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`
}
else {
this.filenameWidget.value = `No files 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)
}
override stripUserState(o: SerializedLGraphNode) {
super.stripUserState(o);
o.properties.images = null
o.properties.index = 0
o.properties.filenames = {}
o.properties.genNumber = 0
}
private setIndex(newIndex: number, force: boolean = false) {
if (newIndex === this.properties.index && !force)
return;
if (!this.properties.images || newIndex < 0 || newIndex >= this.properties.images.images.length) {
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 promise = reuploadImageToComfyUI(data, "input")
.then((entry: GalleryOutputEntry) => {
console.debug("Gottem", entry)
if (lastGenNumber === this.properties.genNumber) {
this.properties.filenames[newIndex] = { filename: entry.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, param: any) {
if (action === "clear") {
this.setProperty("images", null)
this.setProperty("filenames", {})
this.setProperty("index", 0)
this.updateWidgets();
return
}
if (param && "images" in param) {
this.setProperty("genNumber", this.properties.genNumber + 1)
const output = param as GalleryOutput;
if (this.properties.updateMode === "append" && this.properties.images != null) {
const newImages = this.properties.images.images.concat(output.images)
this.properties.images.images = newImages
this.setProperty("images", this.properties.images)
}
else {
this.setProperty("images", param as GalleryOutput)
this.setProperty("filenames", {})
}
console.debug("[ComfyImageCacheNode] Received output!", output, this.properties.updateMode, this.properties.images)
this.setIndex(0, true)
}
this.updateWidgets();
}
}
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"
})

View File

@@ -0,0 +1,32 @@
import { LiteGraph, type SlotLayout } from "@litegraph-ts/core";
import ComfyGraphNode from "./ComfyGraphNode";
import { comfyFileToAnnotatedFilepath, isComfyBoxImageMetadata } from "$lib/utils";
export class ComfyImageToFilepath extends ComfyGraphNode {
static slotLayout: SlotLayout = {
inputs: [
{ name: "image", type: "COMFYBOX_IMAGE" },
],
outputs: [
{ name: "filepath", type: "string" },
]
}
override onExecute() {
const data = this.getInputData(0)
if (data == null || !isComfyBoxImageMetadata(data)) {
this.setOutputData(0, null)
return;
}
const path = comfyFileToAnnotatedFilepath(data.comfyUIFile);
this.setOutputData(0, path);
}
}
LiteGraph.registerNodeType({
class: ComfyImageToFilepath,
title: "Comfy.ImageToFilepath",
desc: "Converts ComfyBox image metadata to an annotated filepath like \"image.png[output]\" for use with ComfyUI.",
type: "images/file_to_filepath"
})

View File

@@ -1,10 +1,10 @@
import { LiteGraph, type ContextMenuItem, type LGraphNode, type Vector2, LConnectionKind, LLink, LGraphCanvas, type SlotType, TitleMode, type SlotLayout, LGraph, type INodeInputSlot, type ITextWidget, type INodeOutputSlot, type SerializedLGraphNode, BuiltInSlotType, type PropertyLayout, type IComboWidget, NodeMode, type INumberWidget } from "@litegraph-ts/core";
import { LiteGraph, type ContextMenuItem, type LGraphNode, type Vector2, LConnectionKind, LLink, LGraphCanvas, type SlotType, TitleMode, type SlotLayout, LGraph, type INodeInputSlot, type ITextWidget, type INodeOutputSlot, type SerializedLGraphNode, BuiltInSlotType, type PropertyLayout, type IComboWidget, NodeMode, type INumberWidget, type UUID } from "@litegraph-ts/core";
import ComfyGraphNode, { type ComfyGraphNodeProperties } from "./ComfyGraphNode";
import type { SvelteComponentDev } from "svelte/internal";
import { Watch } from "@litegraph-ts/nodes-basic";
import type IComfyInputSlot from "$lib/IComfyInputSlot";
import { writable, type Unsubscriber, type Writable, get } from "svelte/store";
import { clamp, convertComfyOutputToGradio, range, type ComfyUploadImageType } from "$lib/utils"
import { clamp, convertComfyOutputToGradio, range, type ComfyUploadImageType, isComfyBoxImageMetadata, filenameToComfyBoxMetadata, type ComfyBoxImageMetadata, isComfyExecutionResult, executionResultToImageMetadata, parseWhateverIntoImageMetadata } from "$lib/utils"
import layoutState from "$lib/stores/layoutState";
import type { FileData as GradioFileData } from "@gradio/upload";
import queueState from "$lib/stores/queueState";
@@ -18,6 +18,7 @@ import CheckboxWidget from "$lib/widgets/CheckboxWidget.svelte";
import RadioWidget from "$lib/widgets/RadioWidget.svelte";
import ImageUploadWidget from "$lib/widgets/ImageUploadWidget.svelte";
import ImageEditorWidget from "$lib/widgets/ImageEditorWidget.svelte";
import type { NodeID } from "$lib/api";
export type AutoConfigOptions = {
includeProperties?: Set<string> | null,
@@ -91,7 +92,7 @@ export abstract class ComfyWidgetNode<T = any> extends ComfyGraphNode {
// TODO these are bad, create override methods instead
// input slots
inputIndex: number = 0;
inputIndex: number | null = null;
storeActionName: string | null = "store";
// output slots
@@ -193,7 +194,7 @@ export abstract class ComfyWidgetNode<T = any> extends ComfyGraphNode {
* Logic to run if this widget can be treated as output (slider, combo, text)
*/
override onExecute(param: any, options: object) {
if (this.copyFromInputLink) {
if (this.inputIndex != null) {
if (this.inputs.length >= this.inputIndex) {
const data = this.getInputData(this.inputIndex)
if (data != null) { // TODO can "null" be a legitimate value here?
@@ -201,8 +202,10 @@ export abstract class ComfyWidgetNode<T = any> extends ComfyGraphNode {
}
}
}
if (this.outputs.length >= this.outputIndex) {
this.setOutputData(this.outputIndex, get(this.value))
if (this.outputIndex != null) {
if (this.outputs.length >= this.outputIndex) {
this.setOutputData(this.outputIndex, get(this.value))
}
}
for (const propName in this.shownOutputProperties) {
const data = this.shownOutputProperties[propName]
@@ -265,7 +268,7 @@ export abstract class ComfyWidgetNode<T = any> extends ComfyGraphNode {
}
if (options.setWidgetTitle) {
const widget = layoutState.findLayoutForNode(this.id)
const widget = layoutState.findLayoutForNode(this.id as NodeID)
if (widget && input.name !== "") {
widget.attrs.title = input.name;
}
@@ -284,7 +287,7 @@ export abstract class ComfyWidgetNode<T = any> extends ComfyGraphNode {
}
notifyPropsChanged() {
const layoutEntry = layoutState.findLayoutEntryForNode(this.id)
const layoutEntry = layoutState.findLayoutEntryForNode(this.id as NodeID)
if (layoutEntry && layoutEntry.parent) {
layoutEntry.parent.attrsChanged.set(get(layoutEntry.parent.attrsChanged) + 1)
}
@@ -596,14 +599,20 @@ LiteGraph.registerNodeType({
})
/** Raw output as received from ComfyUI's backend */
export type GalleryOutput = {
images: GalleryOutputEntry[]
export interface ComfyExecutionResult {
// Technically this response can contain arbitrary data, but "images" is the
// most frequently used as it's output by LoadImage and PreviewImage, the
// only two output nodes in base ComfyUI.
images: ComfyImageLocation[] | null,
}
/** Raw output entry as received from ComfyUI's backend */
export type GalleryOutputEntry = {
export type ComfyImageLocation = {
/* Filename with extension in the subfolder. */
filename: string,
/* Subfolder in the containing folder. */
subfolder: string,
/* Base ComfyUI folder where the image is located. */
type: ComfyUploadImageType
}
@@ -612,7 +621,7 @@ export interface ComfyGalleryProperties extends ComfyWidgetProperties {
updateMode: "replace" | "append",
}
export class ComfyGalleryNode extends ComfyWidgetNode<GradioFileData[]> {
export class ComfyGalleryNode extends ComfyWidgetNode<ComfyBoxImageMetadata[]> {
override properties: ComfyGalleryProperties = {
tags: [],
defaultValue: [],
@@ -623,14 +632,11 @@ export class ComfyGalleryNode extends ComfyWidgetNode<GradioFileData[]> {
static slotLayout: SlotLayout = {
inputs: [
{ name: "images", type: "OUTPUT" },
{ name: "store", type: BuiltInSlotType.ACTION, options: { color_off: "rebeccapurple", color_on: "rebeccapurple" } },
{ name: "clear", type: BuiltInSlotType.ACTION }
{ name: "store", type: BuiltInSlotType.ACTION, options: { color_off: "rebeccapurple", color_on: "rebeccapurple" } }
],
outputs: [
{ name: "images", type: "COMFYBOX_IMAGES" },
{ name: "selected_index", type: "number" },
{ name: "width", type: "number" },
{ name: "height", type: "number" },
{ name: "filename", type: "string" },
]
}
@@ -640,7 +646,7 @@ export class ComfyGalleryNode extends ComfyWidgetNode<GradioFileData[]> {
override svelteComponentType = GalleryWidget
override defaultValue = []
override copyFromInputLink = false;
override inputIndex = null;
override saveUserState = false;
override outputIndex = null;
override changedIndex = null;
@@ -663,53 +669,30 @@ export class ComfyGalleryNode extends ComfyWidgetNode<GradioFileData[]> {
}
}
imageSize: Vector2 = [1, 1]
override onExecute() {
const index = this.properties.index;
this.setOutputData(0, index)
this.setOutputData(1, this.imageSize[0])
this.setOutputData(2, this.imageSize[1])
let filename: string | null = null;
if (index != null) {
const entry = get(this.value)[index];
if (entry)
filename = entry.name
}
this.setOutputData(3, filename)
this.setOutputData(0, get(this.value))
this.setOutputData(1, this.properties.index)
}
override onAction(action: any, param: any, options: { action_call?: string }) {
super.onAction(action, param, options)
if (action === "clear") {
this.setValue([])
}
}
override formatValue(value: GradioFileData[] | null): string {
override formatValue(value: ComfyBoxImageMetadata[] | null): string {
return `Images: ${value?.length || 0}`
}
override parseValue(param: any): GradioFileData[] {
if (!(typeof param === "object" && "images" in param)) {
return []
}
override parseValue(param: any): ComfyBoxImageMetadata[] {
const meta = parseWhateverIntoImageMetadata(param) || [];
const data = param as GalleryOutput
console.debug("[ComfyGalleryNode] Received output!", data)
const galleryItems: GradioFileData[] = convertComfyOutputToGradio(data)
console.debug("[ComfyGalleryNode] Received output!", param)
if (this.properties.updateMode === "append") {
const currentValue = get(this.value)
return currentValue.concat(galleryItems)
return currentValue.concat(meta)
}
else {
return galleryItems;
return meta;
}
}
@@ -890,7 +873,7 @@ export interface ComfyImageUploadProperties extends ComfyWidgetProperties {
fileCount: "single" | "multiple" // gradio File component format
}
export class ComfyImageUploadNode extends ComfyWidgetNode<GalleryOutputEntry[]> {
export class ComfyImageUploadNode extends ComfyWidgetNode<ComfyBoxImageMetadata[]> {
override properties: ComfyImageUploadProperties = {
defaultValue: [],
tags: [],
@@ -902,72 +885,27 @@ export class ComfyImageUploadNode extends ComfyWidgetNode<GalleryOutputEntry[]>
{ name: "store", type: BuiltInSlotType.ACTION }
],
outputs: [
{ name: "filename", type: "string" }, // TODO support batches
{ name: "width", type: "number" },
{ name: "height", type: "number" },
{ name: "image_count", type: "number" },
{ name: "images", type: "COMFYBOX_IMAGES" }, // TODO support batches
{ name: "changed", type: BuiltInSlotType.EVENT },
]
}
override svelteComponentType = ImageUploadWidget;
override defaultValue = [];
override outputIndex = null;
override changedIndex = 4;
override outputIndex = 0;
override changedIndex = 1;
override storeActionName = "store";
override saveUserState = false;
imageSize: Vector2 = [0, 0];
constructor(name?: string) {
super(name, [])
}
override onExecute(param: any, options: object) {
super.onExecute(param, options);
const value = get(this.value)
if (value.length > 0) {
this.setOutputData(0, value[0].filename) // TODO when ComfyUI LoadImage supports loading an image batch
this.setOutputData(1, this.imageSize[0])
this.setOutputData(2, this.imageSize[1])
this.setOutputData(3, value.length)
}
else {
this.setOutputData(0, "")
this.setOutputData(1, 0)
this.setOutputData(2, 0)
this.setOutputData(3, 0)
}
override parseValue(value: any): ComfyBoxImageMetadata[] {
return parseWhateverIntoImageMetadata(value) || []
}
override parseValue(value: any): GalleryOutputEntry[] {
if (value == null)
return []
const isComfyImageSpec = (value: any): boolean => {
return value && typeof value === "object" && "filename" in value && "type" in value
}
if (typeof value === "string") {
// Single filename
return [{ filename: value, subfolder: "", type: "input" }]
}
else if (isComfyImageSpec(value)) {
// Single ComfyUI file
return [value]
}
else if (Array.isArray(value)) {
if (value.every(v => typeof v === "string"))
return value.map(filename => { return { filename, subfolder: "", type: "input" } })
else if (value.every(isComfyImageSpec))
return value
}
return []
}
override formatValue(value: GalleryOutputEntry[]): string {
override formatValue(value: ComfyImageLocation[]): string {
return `Images: ${value?.length || 0}`
}
}
@@ -979,13 +917,13 @@ LiteGraph.registerNodeType({
type: "ui/image_upload"
})
export type FileNameOrGalleryData = string | GalleryOutputEntry;
export type FileNameOrGalleryData = string | ComfyImageLocation;
export type MultiImageData = FileNameOrGalleryData[];
export interface ComfyImageEditorNodeProperties extends ComfyWidgetProperties {
}
export class ComfyImageEditorNode extends ComfyWidgetNode<GalleryOutputEntry[]> {
export class ComfyImageEditorNode extends ComfyWidgetNode<ComfyBoxImageMetadata[]> {
override properties: ComfyImageEditorNodeProperties = {
defaultValue: [],
tags: [],
@@ -996,18 +934,16 @@ export class ComfyImageEditorNode extends ComfyWidgetNode<GalleryOutputEntry[]>
{ name: "store", type: BuiltInSlotType.ACTION }
],
outputs: [
{ name: "filename", type: "string" }, // TODO support batches
{ name: "width", type: "number" },
{ name: "height", type: "number" },
{ name: "image_count", type: "number" },
{ name: "images", type: "COMFYBOX_IMAGES" }, // TODO support batches
{ name: "changed", type: BuiltInSlotType.EVENT },
]
}
override svelteComponentType = ImageEditorWidget;
override defaultValue: GalleryOutputEntry[] = [];
override defaultValue = [];
override outputIndex = null;
override changedIndex = 4;
override inputIndex = null;
override changedIndex = 1;
override storeActionName = "store";
override saveUserState = false;
@@ -1015,50 +951,8 @@ export class ComfyImageEditorNode extends ComfyWidgetNode<GalleryOutputEntry[]>
super(name, [])
}
imageSize: Vector2 = [0, 0];
override onExecute(param: any, options: object) {
super.onExecute(param, options);
const value = get(this.value)
if (value.length > 0) {
this.setOutputData(0, value[0].filename) // TODO when ComfyUI LoadImage supports loading an image batch
this.setOutputData(1, this.imageSize[0])
this.setOutputData(2, this.imageSize[1])
this.setOutputData(3, value.length)
}
else {
this.setOutputData(0, "")
this.setOutputData(1, 0)
this.setOutputData(2, 0)
this.setOutputData(3, 0)
}
}
override parseValue(value: any): GalleryOutputEntry[] {
if (value == null)
return []
const isComfyImageSpec = (value: any): boolean => {
return value && typeof value === "object" && "filename" in value && "type" in value
}
if (typeof value === "string") {
// Single filename
return [{ filename: value, subfolder: "", type: "input" }]
}
else if (isComfyImageSpec(value)) {
// Single ComfyUI file
return [value]
}
else if (Array.isArray(value)) {
if (value.every(v => typeof v === "string"))
return value.map(filename => { return { filename, subfolder: "", type: "input" } })
else if (value.every(isComfyImageSpec))
return value
}
return []
override parseValue(value: any): ComfyBoxImageMetadata[] {
return parseWhateverIntoImageMetadata(value) || [];
}
override formatValue(value: GradioFileData[]): string {

View File

@@ -1,6 +1,6 @@
import type { ComfyAPIHistoryEntry, ComfyAPIHistoryItem, ComfyAPIHistoryResponse, ComfyAPIQueueResponse, ComfyAPIStatusResponse, ComfyBoxPromptExtraData, NodeID, PromptID } from "$lib/api";
import type { Progress, SerializedPromptInputsAll, SerializedPromptOutputs } from "$lib/components/ComfyApp";
import type { GalleryOutput } from "$lib/nodes/ComfyWidgetNodes";
import type { ComfyExecutionResult } from "$lib/nodes/ComfyWidgetNodes";
import notify from "$lib/notify";
import { get, writable, type Writable } from "svelte/store";
@@ -19,7 +19,7 @@ type QueueStateOps = {
executionError: (promptID: PromptID, message: string) => void,
progressUpdated: (progress: Progress) => void
afterQueued: (promptID: PromptID, number: number, prompt: SerializedPromptInputsAll, extraData: any) => void
onExecuted: (promptID: PromptID, nodeID: NodeID, output: GalleryOutput) => void
onExecuted: (promptID: PromptID, nodeID: NodeID, output: ComfyExecutionResult) => void
}
export type QueueEntry = {
@@ -257,7 +257,7 @@ function afterQueued(promptID: PromptID, number: number, prompt: SerializedPromp
})
}
function onExecuted(promptID: PromptID, nodeID: NodeID, output: GalleryOutput) {
function onExecuted(promptID: PromptID, nodeID: NodeID, output: ComfyExecutionResult) {
console.debug("[queueState] onExecuted", promptID, nodeID, output)
store.update(s => {
const [index, entry, queue] = findEntryInPending(promptID)

View File

@@ -6,7 +6,7 @@ import { get } from "svelte/store"
import layoutState from "$lib/stores/layoutState"
import type { SvelteComponentDev } from "svelte/internal";
import type { SerializedLGraph } from "@litegraph-ts/core";
import type { FileNameOrGalleryData, GalleryOutput, GalleryOutputEntry } from "./nodes/ComfyWidgetNodes";
import type { FileNameOrGalleryData, ComfyExecutionResult, ComfyImageLocation } from "./nodes/ComfyWidgetNodes";
import type { FileData as GradioFileData } from "@gradio/upload";
export function clamp(n: number, min: number, max: number): number {
@@ -125,11 +125,11 @@ export const debounce = (callback: Function, wait = 250) => {
};
};
export function convertComfyOutputToGradio(output: GalleryOutput): GradioFileData[] {
export function convertComfyOutputToGradio(output: ComfyExecutionResult): GradioFileData[] {
return output.images.map(convertComfyOutputEntryToGradio);
}
export function convertComfyOutputEntryToGradio(r: GalleryOutputEntry): GradioFileData {
export function convertComfyOutputEntryToGradio(r: ComfyImageLocation): GradioFileData {
const url = `http://${location.hostname}:8188` // TODO make configurable
const params = new URLSearchParams(r)
const fileData: GradioFileData = {
@@ -156,7 +156,7 @@ export function convertGradioFileDataToComfyURL(image: GradioFileData, type: Com
return `${baseUrl}/view?${params}`
}
export function convertGradioFileDataToComfyOutput(fileData: GradioFileData, type: ComfyUploadImageType = "input"): GalleryOutputEntry {
export function convertGradioFileDataToComfyOutput(fileData: GradioFileData, type: ComfyUploadImageType = "input"): ComfyImageLocation {
if (!fileData.is_file)
throw "Can't convert blob data to comfy output!"
@@ -204,7 +204,7 @@ export interface ComfyUploadImageAPIResponse {
/*
* Uploads an image into ComfyUI's `input` folder.
*/
export async function uploadImageToComfyUI(blob: Blob, filename: string, type: ComfyUploadImageType, subfolder: string = "", overwrite: boolean = false): Promise<GalleryOutputEntry> {
export async function uploadImageToComfyUI(blob: Blob, filename: string, type: ComfyUploadImageType, subfolder: string = "", overwrite: boolean = false): Promise<ComfyImageLocation> {
console.debug("[utils] Uploading image to ComfyUI", filename, blob.size)
const url = `http://${location.hostname}:8188` // TODO make configurable
@@ -232,19 +232,90 @@ export async function uploadImageToComfyUI(blob: Blob, filename: string, type: C
}
/*
* Copies an *EXISTING* image in a ComfyUI image folder into a different folder,
* for use with LoadImage etc.
* Convenient type for passing around image filepaths and their metadata with
* wires. Needs to be converted to a filename for use with LoadImage.
*
* Litegraph type is COMFYBOX_IMAGE. The array type is COMFYBOX_IMAGES.
*/
export async function reuploadImageToComfyUI(data: GalleryOutputEntry, type: ComfyUploadImageType): Promise<GalleryOutputEntry> {
if (data.type === type)
return data
const url = `http://${location.hostname}:8188` // TODO make configurable
const params = new URLSearchParams(data)
console.debug("[utils] Reuploading image into to ComfyUI input folder", data)
return fetch(url + "/view?" + params)
.then((r) => r.blob())
.then((blob) => uploadImageToComfyUI(blob, data.filename, type))
export type ComfyBoxImageMetadata = {
/* For easy structural type detection */
isComfyBoxImageMetadata: true,
/* Pointer to where this image resides in ComfyUI. */
comfyUIFile: ComfyImageLocation,
/* Readable name of the image. */
name: string
/* Tags applicable to this image, like ["mask"]. */
tags: string[],
/* Image width. */
width?: number,
/* Image height. */
height?: number,
}
export function isComfyBoxImageMetadata(value: any): value is ComfyBoxImageMetadata {
return value && typeof value === "object" && (value as any).isComfyBoxImageMetadata;
}
export function isComfyExecutionResult(value: any): value is ComfyExecutionResult {
return value && typeof value === "object" && Array.isArray(value.images)
}
export function filenameToComfyBoxMetadata(filename: string, type: ComfyUploadImageType, subfolder: string = ""): ComfyBoxImageMetadata {
return {
isComfyBoxImageMetadata: true,
comfyUIFile: {
filename,
subfolder,
type
},
name: "Filename",
tags: [],
}
}
export function comfyFileToComfyBoxMetadata(comfyUIFile: ComfyImageLocation): ComfyBoxImageMetadata {
return {
isComfyBoxImageMetadata: true,
comfyUIFile,
name: "File",
tags: [],
}
}
/*
* Converts a ComfyUI file into an annotated filepath. Backend nodes like
* LoadImage support syntax like "subfolder/image.png[output]" to specify which
* image folder to load from.
*/
export function comfyFileToAnnotatedFilepath(comfyUIFile: ComfyImageLocation): string {
let path = ""
if (comfyUIFile.subfolder != "")
path = comfyUIFile.subfolder + "/";
path += `${comfyUIFile.filename}[${comfyUIFile.type}]`
return path;
}
export function executionResultToImageMetadata(result: ComfyExecutionResult): ComfyBoxImageMetadata[] {
return result.images.map(comfyFileToComfyBoxMetadata)
}
export function parseWhateverIntoImageMetadata(param: any): ComfyBoxImageMetadata[] | null {
let meta: ComfyBoxImageMetadata[] | null = null
if (isComfyBoxImageMetadata(param)) {
meta = [param];
}
if (Array.isArray(param) && !param.every(isComfyBoxImageMetadata)) {
meta = param
}
else if (isComfyExecutionResult(param)) {
meta = executionResultToImageMetadata(param)
}
return meta;
}
export function comfyBoxImageToComfyURL(image: ComfyBoxImageMetadata): string {
return convertComfyOutputToComfyURL(image.comfyUIFile)
}

View File

@@ -10,13 +10,13 @@
import type { ComfyGalleryNode } from "$lib/nodes/ComfyWidgetNodes";
import type { FileData as GradioFileData } from "@gradio/upload";
import type { SelectData as GradioSelectData } from "@gradio/utils";
import { clamp } from "$lib/utils";
import { clamp, comfyBoxImageToComfyURL, type ComfyBoxImageMetadata } from "$lib/utils";
import { f7 } from "framework7-svelte";
export let widget: WidgetLayout | null = null;
export let isMobile: boolean = false;
let node: ComfyGalleryNode | null = null;
let nodeValue: Writable<GradioFileData[]> | null = null;
let nodeValue: Writable<ComfyBoxImageMetadata[]> | null = null;
let propsChanged: Writable<number> | null = null;
let option: number | null = null;
let imageWidth: number = 1;
@@ -154,8 +154,10 @@
<div class="wrapper comfy-image-widget" style={widget.attrs.style || ""} bind:this={element}>
<Block variant="solid" padding={false}>
{#if $nodeValue && $nodeValue.length > 0}
{@const value = $nodeValue[$nodeValue.length-1]}
{@const url = comfyBoxImageToComfyURL(value)}
<StaticImage
value={$nodeValue[$nodeValue.length-1].data}
value={url}
show_label={widget.attrs.title != ""}
label={widget.attrs.title}
bind:imageWidth
@@ -167,11 +169,12 @@
</Block>
</div>
{:else}
{@const images = $nodeValue.map(comfyBoxImageToComfyURL)}
<div class="wrapper comfy-gallery-widget gradio-gallery" style={widget.attrs.style || ""} bind:this={element}>
<Block variant="solid" padding={false}>
<div class="padding">
<Gallery
bind:value={$nodeValue}
value={images}
label={widget.attrs.title}
show_label={widget.attrs.title !== ""}
{style}

View File

@@ -4,19 +4,19 @@
import { get, type Writable, writable } from "svelte/store";
import Modal from "$lib/components/Modal.svelte";
import { Button } from "@gradio/button";
import type { ComfyImageEditorNode, GalleryOutputEntry, MultiImageData } from "$lib/nodes/ComfyWidgetNodes";
import type { ComfyImageEditorNode, ComfyImageLocation, MultiImageData } from "$lib/nodes/ComfyWidgetNodes";
import { Embed as Klecks, KL, KlApp, klHistory, type KlAppOptionsEmbed } from "klecks";
import type { FileData as GradioFileData } from "@gradio/upload";
import "klecks/style/style.scss";
import ImageUpload from "$lib/components/ImageUpload.svelte";
import { uploadImageToComfyUI, type ComfyUploadImageAPIResponse, convertComfyOutputToComfyURL } from "$lib/utils";
import { uploadImageToComfyUI, type ComfyUploadImageAPIResponse, convertComfyOutputToComfyURL, type ComfyBoxImageMetadata, comfyFileToComfyBoxMetadata } from "$lib/utils";
import notify from "$lib/notify";
export let widget: WidgetLayout | null = null;
export let isMobile: boolean = false;
let node: ComfyImageEditorNode | null = null;
let nodeValue: Writable<GalleryOutputEntry[]> | null = null;
let nodeValue: Writable<ComfyBoxImageMetadata[]> | null = null;
let attrsChanged: Writable<number> | null = null;
let imgWidth: number = 0;
@@ -24,14 +24,16 @@
$: widget && setNodeValue(widget);
$: if (!(node && $nodeValue && $nodeValue.length > 0)) {
node.imageSize = [0, 0]
}
else if (imgWidth > 0 && imgHeight > 0) {
node.imageSize = [imgWidth, imgHeight]
}
else {
node.imageSize = [0, 0]
$: if ($nodeValue && $nodeValue.length > 0) {
// TODO improve
if (imgWidth > 0 && imgHeight > 0) {
$nodeValue[0].width = imgWidth
$nodeValue[0].height = imgHeight
}
else {
$nodeValue[0].width = 0
$nodeValue[0].height = 0
}
}
function setNodeValue(widget: WidgetLayout) {
@@ -125,8 +127,9 @@
const blob = kl.getPNG();
await uploadImageToComfyUI(blob, FILENAME, "input")
.then((entry: GalleryOutputEntry) => {
$nodeValue = [entry] // TODO more than one image
.then((entry: ComfyImageLocation) => {
const meta: ComfyBoxImageMetadata = comfyFileToComfyBoxMetadata(entry);
$nodeValue = [meta] // TODO more than one image
notify("Saved image to ComfyUI!", { type: "success" })
onSuccess();
})
@@ -191,7 +194,7 @@
status = "uploading"
}
function onUploaded(e: CustomEvent<GalleryOutputEntry[]>) {
function onUploaded(e: CustomEvent<ComfyImageLocation[]>) {
uploadError = null;
status = "uploaded"
$nodeValue = e.detail;
@@ -207,7 +210,7 @@
uploadError = e.detail
}
function onChange(e: CustomEvent<GalleryOutputEntry[]>) {
function onChange(e: CustomEvent<ComfyImageLocation[]>) {
// $nodeValue = e.detail;
}

View File

@@ -2,27 +2,30 @@
import ImageUpload from "$lib/components/ImageUpload.svelte"
import type { WidgetLayout } from "$lib/stores/layoutState";
import type { Writable } from "svelte/store";
import type { ComfyGalleryNode, ComfyImageUploadNode, GalleryOutputEntry, MultiImageData } from "$lib/nodes/ComfyWidgetNodes";
import type { ComfyGalleryNode, ComfyImageUploadNode, ComfyImageLocation, MultiImageData } from "$lib/nodes/ComfyWidgetNodes";
import notify from "$lib/notify";
import { comfyFileToComfyBoxMetadata, type ComfyBoxImageMetadata } from "$lib/utils";
export let widget: WidgetLayout | null = null;
export let isMobile: boolean = false;
let node: ComfyImageUploadNode | null = null;
let nodeValue: Writable<GalleryOutputEntry[]> | null = null;
let nodeValue: Writable<ComfyBoxImageMetadata[]> | null = null;
let propsChanged: Writable<number> | null = null;
let imgWidth: number = 1;
let imgHeight: number = 1;
$: widget && setNodeValue(widget);
$: if (!(node && $nodeValue && $nodeValue.length > 0)) {
node.imageSize = [0, 0]
}
else if (imgWidth > 0 && imgHeight > 0) {
node.imageSize = [imgWidth, imgHeight]
}
else {
node.imageSize = [0, 0]
$: if ($nodeValue && $nodeValue.length > 0) {
// TODO improve
if (imgWidth > 0 && imgHeight > 0) {
$nodeValue[0].width = imgWidth
$nodeValue[0].height = imgHeight
}
else {
$nodeValue[0].width = 0
$nodeValue[0].height = 0
}
}
function setNodeValue(widget: WidgetLayout) {
@@ -38,14 +41,14 @@
$nodeValue = []
}
function onChange(e: CustomEvent<GalleryOutputEntry[]>) {
function onChange(e: CustomEvent<ComfyImageLocation[]>) {
console.warn("ONCHANGE!!!", e.detail)
$nodeValue = e.detail || []
$nodeValue = (e.detail || []).map(comfyFileToComfyBoxMetadata)
}
function onUploaded(e: CustomEvent<GalleryOutputEntry[]>) {
function onUploaded(e: CustomEvent<ComfyImageLocation[]>) {
console.warn("ONUPLOADED!!!", e.detail)
$nodeValue = e.detail || []
$nodeValue = (e.detail || []).map(comfyFileToComfyBoxMetadata)
}
function onUploadError(e: CustomEvent<any>) {
@@ -54,7 +57,7 @@
$nodeValue = []
}
function onClear(e: CustomEvent<GalleryOutputEntry[]>) {
function onClear(e: CustomEvent<ComfyImageLocation[]>) {
console.warn("ONCLEAR!!!", e.detail)
$nodeValue = []
}