From 152a1c609717683c22065f75a9407644675304e2 Mon Sep 17 00:00:00 2001 From: space-nuko <24979496+space-nuko@users.noreply.github.com> Date: Sat, 13 May 2023 22:13:06 -0500 Subject: [PATCH] Imagefilethings --- klecks | 2 +- src/lib/ComfyGraphCanvas.ts | 79 +++++++------- src/lib/api.ts | 8 +- src/lib/components/ComfyApp.ts | 11 +- ...ilepath.ts => ComfyImageToFilepathNode.ts} | 15 +-- src/lib/nodes/ComfyPickImageNode.ts | 35 ++++++ src/lib/nodes/ComfyWidgetNodes.ts | 59 ++-------- src/lib/nodes/index.ts | 3 +- src/lib/stores/layoutState.ts | 27 ++--- src/lib/utils.ts | 14 ++- src/lib/widgets/ImageEditorWidget.svelte | 81 +++++++------- src/lib/widgets/ImageUploadWidget.svelte | 101 ------------------ src/main-desktop.ts | 5 +- src/main-mobile.ts | 1 + 14 files changed, 169 insertions(+), 272 deletions(-) rename src/lib/nodes/{ComfyImageToFilepath.ts => ComfyImageToFilepathNode.ts} (55%) create mode 100644 src/lib/nodes/ComfyPickImageNode.ts diff --git a/klecks b/klecks index a1cb806..a36de32 160000 --- a/klecks +++ b/klecks @@ -1 +1 @@ -Subproject commit a1cb8064a0d63414fdfb346810bd4ab90ed0918c +Subproject commit a36de3203fb970fac80f72be75a8ceee0cb01819 diff --git a/src/lib/ComfyGraphCanvas.ts b/src/lib/ComfyGraphCanvas.ts index 72ec18f..3da6311 100644 --- a/src/lib/ComfyGraphCanvas.ts +++ b/src/lib/ComfyGraphCanvas.ts @@ -1,4 +1,4 @@ -import { BuiltInSlotShape, LGraph, LGraphCanvas, LGraphNode, LiteGraph, NodeMode, type MouseEventExt, type Vector2, type Vector4, TitleMode, type ContextMenuItem, type IContextMenuItem, Subgraph } from "@litegraph-ts/core"; +import { BuiltInSlotShape, LGraph, LGraphCanvas, LGraphNode, LiteGraph, NodeMode, type MouseEventExt, type Vector2, type Vector4, TitleMode, type ContextMenuItem, type IContextMenuItem, Subgraph, LLink } from "@litegraph-ts/core"; import type ComfyApp from "./components/ComfyApp"; import queueState from "./stores/queueState"; import { get } from "svelte/store"; @@ -259,66 +259,59 @@ export default class ComfyGraphCanvas extends LGraphCanvas { const newNode = LiteGraph.createNode(node.type); - for (let index = 0; index < newNode.inputs.length; index++) { - const newInput = newNode.inputs[index]; - const oldInput = node.inputs[index] - - if (oldInput && newInput.type === oldInput.type) { - continue; - } - - let link: LLink | null = null; - - if (oldInput) { - link = node.getInputLink(index); - node.disconnectInput(index) - oldInput.type = newInput.type - oldInput.name = newInput.name - } - else { - node.addInput(newInput.name, newInput.type, newInput) - } - + const createInputReroute = (slotIndex: number, link: LLink | null): ComfyReroute => { const reroute = LiteGraph.createNode(ComfyReroute); reroute.properties.ignoreTypes = true; node.graph.add(reroute) - const inputPos = node.getConnectionPos(true, index); + const inputPos = node.getConnectionPos(true, slotIndex); reroute.pos = [inputPos[0] - 140, inputPos[1] + LiteGraph.NODE_SLOT_HEIGHT / 2]; - reroute.connect(0, node, index); - if (link != null) - node.graph.getNodeById(link.target_id).connect(link.target_slot, reroute, 0) + node.graph.getNodeById(link.origin_id).connect(link.origin_slot, reroute, 0) + return reroute } - for (let index = 0; index < newNode.outputs.length; index++) { - const newOutput = newNode.outputs[index]; - const oldOutput = node.outputs[index] + for (let index = node.inputs.length - 1; index >= 0; index--) { + let link: LLink | null = null; - if (oldOutput && newOutput.type === oldOutput.type) { - continue; - } + link = node.getInputLink(index); + node.disconnectInput(index) - let links = [] + if (link) + createInputReroute(index, link); - if (oldOutput) { - links = node.getOutputLinks(index) - node.disconnectOutput(index) - oldOutput.type = newOutput.type - oldOutput.name = newOutput.name - } - else { - node.addOutput(newOutput.name, newOutput.type, newOutput) - } + node.removeInput(index); + } + for (let index = 0; index < newNode.inputs.length; index++) { + const newInput = newNode.inputs[index] + const input = node.addInput(newInput.name, newInput.type); + } + + const createOutputReroute = (index: number, links: LLink[], connect: boolean = true): ComfyReroute => { const reroute = LiteGraph.createNode(ComfyReroute); reroute.properties.ignoreTypes = true; node.graph.add(reroute) const rerouteSize = reroute.computeSize(); const outputPos = node.getConnectionPos(false, index); reroute.pos = [outputPos[0] + rerouteSize[0] + 20, outputPos[1] + LiteGraph.NODE_SLOT_HEIGHT / 2]; - node.connect(index, reroute, 0); for (const link of links) { - reroute.connect(0, link.target_id, link.target_slot) + reroute.connect(0, node.graph.getNodeById(link.target_id), link.target_slot) } + return reroute + } + + for (let index = node.outputs.length - 1; index >= 0; index--) { + let links = node.getOutputLinks(index) + node.disconnectOutput(index) + + if (links.length > 0) + createOutputReroute(index, links); + + node.removeOutput(index); + } + + for (let index = 0; index < newNode.outputs.length; index++) { + const newOutput = newNode.outputs[index] + const output = node.addOutput(newOutput.name, newOutput.type); } } diff --git a/src/lib/api.ts b/src/lib/api.ts index 1adf2e7..2ac50e7 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -258,9 +258,13 @@ export default class ComfyAPI { }, body: postBody }) - .then(res => res.json()) + .then(async (res) => { + if (res.status != 200) { + throw await res.text() + } + return res.json() + }) .then(raw => { return { promptID: raw.prompt_id } }) - .catch(res => { throw res.text() }) .catch(error => { return { error } }) } diff --git a/src/lib/components/ComfyApp.ts b/src/lib/components/ComfyApp.ts index 707f124..de6b88c 100644 --- a/src/lib/components/ComfyApp.ts +++ b/src/lib/components/ComfyApp.ts @@ -408,9 +408,8 @@ export default class ComfyApp { setColor(type, "orange") } - setColor("IMAGE", "rebeccapurple") - setColor("COMFYBOX_IMAGES", "lime") - setColor("COMFYBOX_IMAGE", "green") + setColor("COMFYBOX_IMAGES", "rebeccapurple") + setColor("COMFYBOX_IMAGE", "fuchsia") setColor(BuiltInSlotType.EVENT, "lightseagreen") setColor(BuiltInSlotType.ACTION, "lightseagreen") } @@ -787,15 +786,15 @@ export default class ComfyApp { queueState.afterQueued(promptID, num, p.output, extraData) error = response.error; - } catch (error) { - error = error.toString(); + } catch (err) { + error = err } if (error != null) { const mes = error.response || error.toString() notify(`Error queuing prompt:\n${mes}`, { type: "error" }) console.error(promptToGraphVis(p)) - console.error("Error queuing prompt", mes, num, p) + console.error("Error queuing prompt", error, num, p) break; } diff --git a/src/lib/nodes/ComfyImageToFilepath.ts b/src/lib/nodes/ComfyImageToFilepathNode.ts similarity index 55% rename from src/lib/nodes/ComfyImageToFilepath.ts rename to src/lib/nodes/ComfyImageToFilepathNode.ts index c5a2026..5118d31 100644 --- a/src/lib/nodes/ComfyImageToFilepath.ts +++ b/src/lib/nodes/ComfyImageToFilepathNode.ts @@ -1,11 +1,11 @@ import { LiteGraph, type SlotLayout } from "@litegraph-ts/core"; import ComfyGraphNode from "./ComfyGraphNode"; -import { comfyFileToAnnotatedFilepath, isComfyBoxImageMetadata } from "$lib/utils"; +import { comfyFileToAnnotatedFilepath, isComfyBoxImageMetadata, parseWhateverIntoImageMetadata } from "$lib/utils"; -export class ComfyImageToFilepath extends ComfyGraphNode { +export default class ComfyImageToFilepathNode extends ComfyGraphNode { static slotLayout: SlotLayout = { inputs: [ - { name: "image", type: "COMFYBOX_IMAGE" }, + { name: "image", type: "COMFYBOX_IMAGES,COMFYBOX_IMAGE" }, ], outputs: [ { name: "filepath", type: "string" }, @@ -14,19 +14,20 @@ export class ComfyImageToFilepath extends ComfyGraphNode { override onExecute() { const data = this.getInputData(0) - if (data == null || !isComfyBoxImageMetadata(data)) { + const meta = parseWhateverIntoImageMetadata(data); + if (meta == null || meta.length === 0) { this.setOutputData(0, null) return; } - const path = comfyFileToAnnotatedFilepath(data.comfyUIFile); + const path = comfyFileToAnnotatedFilepath(meta[0].comfyUIFile); this.setOutputData(0, path); } } LiteGraph.registerNodeType({ - class: ComfyImageToFilepath, + class: ComfyImageToFilepathNode, 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" + type: "image/file_to_filepath" }) diff --git a/src/lib/nodes/ComfyPickImageNode.ts b/src/lib/nodes/ComfyPickImageNode.ts new file mode 100644 index 0000000..46dfb2c --- /dev/null +++ b/src/lib/nodes/ComfyPickImageNode.ts @@ -0,0 +1,35 @@ +import { LiteGraph, type SlotLayout } from "@litegraph-ts/core"; +import ComfyGraphNode from "./ComfyGraphNode"; +import { isComfyBoxImageMetadataArray } from "$lib/utils"; + +/* + * TODO: This is just a temporary workaround until litegraph can handle typed + * array arguments. + */ +export default class ComfyPickImageNode extends ComfyGraphNode { + static slotLayout: SlotLayout = { + inputs: [ + { name: "images", type: "COMFYBOX_IMAGES" }, + ], + outputs: [ + { name: "image", type: "COMFYBOX_IMAGE" }, + ] + } + + override onExecute() { + const data = this.getInputData(0) + if (data == null || !isComfyBoxImageMetadataArray(data)) { + this.setOutputData(0, null) + return; + } + + this.setOutputData(0, data[0]); + } +} + +LiteGraph.registerNodeType({ + class: ComfyPickImageNode, + title: "Comfy.PickImage", + desc: "Picks out the first image from an array of ComfyBox images.", + type: "image/pick_image" +}) diff --git a/src/lib/nodes/ComfyWidgetNodes.ts b/src/lib/nodes/ComfyWidgetNodes.ts index d28d933..0185bdc 100644 --- a/src/lib/nodes/ComfyWidgetNodes.ts +++ b/src/lib/nodes/ComfyWidgetNodes.ts @@ -170,6 +170,10 @@ export abstract class ComfyWidgetNode extends ComfyGraphNode { parseValue(value: any): T { return value as T }; + getValue(): T { + return get(this.value); + } + setValue(value: any, noChangedEvent: boolean = false) { if (noChangedEvent) this._noChangedEvent = true; @@ -869,57 +873,6 @@ LiteGraph.registerNodeType({ type: "ui/radio" }) -export interface ComfyImageUploadProperties extends ComfyWidgetProperties { - fileCount: "single" | "multiple" // gradio File component format -} - -export class ComfyImageUploadNode extends ComfyWidgetNode { - override properties: ComfyImageUploadProperties = { - defaultValue: [], - tags: [], - fileCount: "single", - } - - static slotLayout: SlotLayout = { - inputs: [ - { name: "store", type: BuiltInSlotType.ACTION } - ], - outputs: [ - { name: "images", type: "COMFYBOX_IMAGES" }, // TODO support batches - { name: "changed", type: BuiltInSlotType.EVENT }, - ] - } - - override svelteComponentType = ImageUploadWidget; - override defaultValue = []; - override outputIndex = 0; - override changedIndex = 1; - override storeActionName = "store"; - override saveUserState = false; - - constructor(name?: string) { - super(name, []) - } - - override parseValue(value: any): ComfyBoxImageMetadata[] { - return parseWhateverIntoImageMetadata(value) || [] - } - - override formatValue(value: ComfyImageLocation[]): string { - return `Images: ${value?.length || 0}` - } -} - -LiteGraph.registerNodeType({ - class: ComfyImageUploadNode, - title: "UI.ImageUpload", - desc: "Widget that lets you upload images into ComfyUI's input folder", - type: "ui/image_upload" -}) - -export type FileNameOrGalleryData = string | ComfyImageLocation; -export type MultiImageData = FileNameOrGalleryData[]; - export interface ComfyImageEditorNodeProperties extends ComfyWidgetProperties { } @@ -941,7 +894,7 @@ export class ComfyImageEditorNode extends ComfyWidgetNode, - /* - * Next ID to use for instantiating a new drag item - */ - currentId: UUID, - /* * Selected drag items. */ @@ -411,6 +407,18 @@ const ALL_ATTRIBUTES: AttributesSpecList = [ defaultValue: "" }, + // Editor + { + name: "variant", + type: "enum", + location: "widget", + editable: true, + validNodeTypes: ["ui/image_editor"], + values: ["inlineEditor", "fileUpload"], + defaultValue: "inlineEditor", + refreshPanelOnChange: true + }, + // Gallery { name: "variant", @@ -668,7 +676,6 @@ const store: Writable = writable({ root: null, allItems: {}, allItemsByNode: {}, - currentId: 0, currentSelection: [], currentSelectionNodes: [], isMenuOpen: false, @@ -703,7 +710,7 @@ function addContainer(parent: ContainerLayout | null, attrs: Partial const state = get(store); const dragItem: ContainerLayout = { type: "container", - id: `${state.currentId++}`, + id: uuidv4(), attrsChanged: writable(0), attrs: { ...defaultWidgetAttributes, @@ -726,7 +733,7 @@ function addWidget(parent: ContainerLayout, node: ComfyWidgetNode, attrs: Partia const widgetName = "Widget" const dragItem: WidgetLayout = { type: "widget", - id: `${state.currentId++}`, + id: uuidv4(), node: node, attrsChanged: writable(0), attrs: { @@ -939,7 +946,6 @@ function initDefaultLayout() { root: null, allItems: {}, allItemsByNode: {}, - currentId: 0, currentSelection: [], currentSelectionNodes: [], isMenuOpen: false, @@ -964,7 +970,6 @@ function initDefaultLayout() { export type SerializedLayoutState = { root: DragItemID | null, allItems: Record, - currentId: UUID, attrs: LayoutAttributes } @@ -1002,7 +1007,6 @@ function serialize(): SerializedLayoutState { return { root: state.root?.id, allItems, - currentId: state.currentId, attrs: state.attrs } } @@ -1055,7 +1059,6 @@ function deserialize(data: SerializedLayoutState, graph: LGraph) { root, allItems, allItemsByNode, - currentId: data.currentId, currentSelection: [], currentSelectionNodes: [], isMenuOpen: false, diff --git a/src/lib/utils.ts b/src/lib/utils.ts index a4bef48..9c0cdc1 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -256,6 +256,10 @@ export function isComfyBoxImageMetadata(value: any): value is ComfyBoxImageMetad return value && typeof value === "object" && (value as any).isComfyBoxImageMetadata; } +export function isComfyBoxImageMetadataArray(value: any): value is ComfyBoxImageMetadata[] { + return Array.isArray(value) && value.every(isComfyBoxImageMetadata); +} + export function isComfyExecutionResult(value: any): value is ComfyExecutionResult { return value && typeof value === "object" && Array.isArray(value.images) } @@ -284,7 +288,7 @@ export function comfyFileToComfyBoxMetadata(comfyUIFile: ComfyImageLocation): Co /* * Converts a ComfyUI file into an annotated filepath. Backend nodes like - * LoadImage support syntax like "subfolder/image.png[output]" to specify which + * LoadImage support syntax like "subfolder/image.png [output]" to specify which * image folder to load from. */ export function comfyFileToAnnotatedFilepath(comfyUIFile: ComfyImageLocation): string { @@ -292,7 +296,7 @@ export function comfyFileToAnnotatedFilepath(comfyUIFile: ComfyImageLocation): s if (comfyUIFile.subfolder != "") path = comfyUIFile.subfolder + "/"; - path += `${comfyUIFile.filename}[${comfyUIFile.type}]` + path += `${comfyUIFile.filename} [${comfyUIFile.type}]` return path; } @@ -306,7 +310,7 @@ export function parseWhateverIntoImageMetadata(param: any): ComfyBoxImageMetadat if (isComfyBoxImageMetadata(param)) { meta = [param]; } - if (Array.isArray(param) && !param.every(isComfyBoxImageMetadata)) { + else if (Array.isArray(param) && param.every(isComfyBoxImageMetadata)) { meta = param } else if (isComfyExecutionResult(param)) { @@ -316,6 +320,10 @@ export function parseWhateverIntoImageMetadata(param: any): ComfyBoxImageMetadat return meta; } +export function comfyBoxImageToComfyFile(image: ComfyBoxImageMetadata): ComfyImageLocation { + return image.comfyUIFile +} + export function comfyBoxImageToComfyURL(image: ComfyBoxImageMetadata): string { return convertComfyOutputToComfyURL(image.comfyUIFile) } diff --git a/src/lib/widgets/ImageEditorWidget.svelte b/src/lib/widgets/ImageEditorWidget.svelte index 0dd2e29..3ea2cf7 100644 --- a/src/lib/widgets/ImageEditorWidget.svelte +++ b/src/lib/widgets/ImageEditorWidget.svelte @@ -6,12 +6,11 @@ import { Button } from "@gradio/button"; 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, type ComfyBoxImageMetadata, comfyFileToComfyBoxMetadata } from "$lib/utils"; - import notify from "$lib/notify"; + import ImageUpload from "$lib/components/ImageUpload.svelte"; + import { uploadImageToComfyUI, type ComfyUploadImageAPIResponse, convertComfyOutputToComfyURL, type ComfyBoxImageMetadata, comfyFileToComfyBoxMetadata, comfyBoxImageToComfyURL, comfyBoxImageToComfyFile } from "$lib/utils"; + import notify from "$lib/notify"; export let widget: WidgetLayout | null = null; export let isMobile: boolean = false; @@ -44,29 +43,6 @@ } }; - const urlPattern = /^((http|https|ftp):\/\/)/; - - $: updateUrls($nodeValue); - - function updateUrls(value: MultiImageData) { - // leftUrl = "" - // rightUrl = "" - // console.warn("UPD", value) - // - // if (typeof value[0] === "string") { - // if (urlPattern.test(value[0])) - // leftUrl = value[0] - // else - // leftUrl = convertFilenameToComfyURL(value[0]) - // } - // if (typeof value[1] === "string") { - // if (urlPattern.test(value[1])) - // rightUrl = value[1] - // else - // rightUrl = convertFilenameToComfyURL(value[1]) - // } - } - let editorRoot: HTMLDivElement | null = null; let showModal = false; let kl: Klecks | null = null; @@ -190,60 +166,81 @@ let uploadError = null; function onUploading() { + console.warn("UPLOADING!!!") uploadError = null; status = "uploading" } function onUploaded(e: CustomEvent) { + console.warn("UPLOADED!!!") uploadError = null; status = "uploaded" - $nodeValue = e.detail; + $nodeValue = e.detail.map(comfyFileToComfyBoxMetadata); } function onClear() { + console.warn("CLEAR!!!") uploadError = null; status = "none" } function onUploadError(e: CustomEvent) { + console.warn("ERROR!!!") status = "error" uploadError = e.detail + notify(`Failed to upload image to ComfyUI: ${err}`, { type: "error", timeout: 10000 }) } function onChange(e: CustomEvent) { - // $nodeValue = e.detail; } - $: canEdit = status === "none" || status === "uploaded"; + let _value: ComfyImageLocation[] = [] + $: if ($nodeValue) + _value = $nodeValue.map(comfyBoxImageToComfyFile) + else + _value = [] + $: canEdit = status === "none" || status === "uploaded";
- {#if isMobile} - TODO mask editor + {#if widget.attrs.variant === "fileUpload" || isMobile} + {:else} - -
-
- -
-
-
-
- + +
+
+ +
+
+
+