commit 76d32c98229677b357795248f7337bef2d62851c Author: kijai Date: Fri Sep 29 18:00:42 2023 +0300 First commit Most stuff probably still buggy diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9a11d94 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +__pycache__ +/venv +.vscode +*.ckpt +*.safetensors +*.pth \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..f44d9ab --- /dev/null +++ b/README.md @@ -0,0 +1,7 @@ +# KJNodes for ComfyUI + +Various quality of life -nodes for ComfyUI, mostly just visual stuff to improve usability. + +# Installation + +1. Clone this repo into `custom_nodes` folder. \ No newline at end of file diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..5109219 --- /dev/null +++ b/__init__.py @@ -0,0 +1,4 @@ +from .nodes import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS + +WEB_DIRECTORY = "./web" +__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS", "WEB_DIRECTORY"] \ No newline at end of file diff --git a/favicon-active.ico b/favicon-active.ico new file mode 100644 index 0000000..64045ab Binary files /dev/null and b/favicon-active.ico differ diff --git a/favicon.ico b/favicon.ico new file mode 100644 index 0000000..08df248 Binary files /dev/null and b/favicon.ico differ diff --git a/nodes.py b/nodes.py new file mode 100644 index 0000000..37059dd --- /dev/null +++ b/nodes.py @@ -0,0 +1,110 @@ +import nodes + +class INTConstant: + @classmethod + def INPUT_TYPES(s): + return {"required": { + "value": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}), + }, + } + + RETURN_TYPES = ("INT",) + RETURN_NAMES = ("value",) + FUNCTION = "get_value" + + CATEGORY = "KJNodes" + + def get_value(self, value): + return (value,) + +class ConditioningMultiCombine: + @classmethod + def INPUT_TYPES(s): + return {"required": { + "conditioning_1": ("CONDITIONING", ), + "conditioning_2": ("CONDITIONING", ), + }} + + RETURN_TYPES = ("CONDITIONING",) + FUNCTION = "combine" + CATEGORY = "KJNodes" + + def combine(self, combine, **kwargs): + cond_combine_node = nodes.ConditioningCombine() + cond = kwargs["c1"] + for c in range(1, combine): + new_cond = kwargs[f"c{c + 1}"] + cond = cond_combine_node.combine(new_cond, cond)[0] + return (cond,) + +class ConditioningSetMaskAndCombine: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "positive_1": ("CONDITIONING", ), + "negative_1": ("CONDITIONING", ), + "positive_2": ("CONDITIONING", ), + "negative_2": ("CONDITIONING", ), + "mask_1": ("MASK", ), + "mask_2": ("MASK", ), + "strength": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01}), + "set_cond_area": (["default", "mask bounds"],), + } + } + + RETURN_TYPES = ("CONDITIONING","CONDITIONING",) + RETURN_NAMES = ("combined_positive", "combined_negative",) + FUNCTION = "append" + CATEGORY = "KJNodes" + + def append(self, positive_1, negative_1, positive_2, negative_2, mask_1, mask_2, set_cond_area, strength): + c = [] + c2 = [] + set_area_to_bounds = False + if set_cond_area != "default": + set_area_to_bounds = True + if len(mask_1.shape) < 3: + mask_1 = mask_1.unsqueeze(0) + if len(mask_2.shape) < 3: + mask_2 = mask_2.unsqueeze(0) + for t in positive_1: + n = [t[0], t[1].copy()] + _, h, w = mask_1.shape + n[1]['mask'] = mask_1 + n[1]['set_area_to_bounds'] = set_area_to_bounds + n[1]['mask_strength'] = strength + c.append(n) + for t in positive_2: + n = [t[0], t[1].copy()] + _, h, w = mask_2.shape + n[1]['mask'] = mask_2 + n[1]['set_area_to_bounds'] = set_area_to_bounds + n[1]['mask_strength'] = strength + c.append(n) + for t in negative_1: + n = [t[0], t[1].copy()] + _, h, w = mask_1.shape + n[1]['mask'] = mask_1 + n[1]['set_area_to_bounds'] = set_area_to_bounds + n[1]['mask_strength'] = strength + c2.append(n) + for t in negative_2: + n = [t[0], t[1].copy()] + _, h, w = mask_2.shape + n[1]['mask'] = mask_2 + n[1]['set_area_to_bounds'] = set_area_to_bounds + n[1]['mask_strength'] = strength + c2.append(n) + return (c, c2) + +NODE_CLASS_MAPPINGS = { + "INTConstant": INTConstant, + "ConditioningMultiCombine": ConditioningMultiCombine, + "ConditioningSetMaskAndCombine": ConditioningSetMaskAndCombine, +} +NODE_DISPLAY_NAME_MAPPINGS = { + "INTConstant": "INT Constant", + "ConditioningMultiCombine": "Conditioning Multi Combine", + "ConditioningSetMaskAndCombine": "ConditioningSetMaskAndCombine", +} \ No newline at end of file diff --git a/web/green.png b/web/green.png new file mode 100644 index 0000000..900964e Binary files /dev/null and b/web/green.png differ diff --git a/web/js/appearance.js b/web/js/appearance.js new file mode 100644 index 0000000..305c58a --- /dev/null +++ b/web/js/appearance.js @@ -0,0 +1,22 @@ +import { app } from "../../../scripts/app.js"; + + +app.registerExtension({ + name: "KJNodes.appearance", + nodeCreated(node) { + const title = node.getTitle(); + switch (title) { + case "INT Constant": + node.setSize([200, 58]); + node.color = LGraphCanvas.node_colors.green.color; + node.bgcolor = LGraphCanvas.node_colors.green.bgcolor; + break; + case "ConditioningMultiCombine": + + node.color = LGraphCanvas.node_colors.brown.color; + node.bgcolor = LGraphCanvas.node_colors.brown.bgcolor; + break; + + } + } +}); diff --git a/web/js/browserstatus.js b/web/js/browserstatus.js new file mode 100644 index 0000000..c796b6f --- /dev/null +++ b/web/js/browserstatus.js @@ -0,0 +1,41 @@ +import { api } from "../../../scripts/api.js"; +import { app } from "../../../scripts/app.js"; + +app.registerExtension({ + name: "KJNodes.browserstatus", + setup() { + api.addEventListener("status", ({ detail }) => { + let title = "ComfyUI"; + let favicon = "green"; + let queueRemaining = detail && detail.exec_info.queue_remaining; + + if (queueRemaining) { + favicon = "red"; + + title = `00% - ${queueRemaining} | ${title}`; + } + let link = document.querySelector("link[rel~='icon']"); + if (!link) { + link = document.createElement("link"); + link.rel = "icon"; + document.head.appendChild(link); + } + link.href = new URL(`../${favicon}.png`, import.meta.url); + document.title = title; + + }); +//add progress to the title + api.addEventListener("progress", ({ detail }) => { + const { value, max } = detail; + const progress = Math.floor((value / max) * 100); + let title = document.title; + + if (!isNaN(progress) && progress >= 0 && progress <= 100) { + const paddedProgress = String(progress).padStart(2, '0'); + title = `${paddedProgress}% ${title.replace(/^\d+%\s/, '')}`; + } + + document.title = title; + }); + }, +}); \ No newline at end of file diff --git a/web/js/jsnodes.js b/web/js/jsnodes.js new file mode 100644 index 0000000..bd8998b --- /dev/null +++ b/web/js/jsnodes.js @@ -0,0 +1,30 @@ +import { app } from "../../../scripts/app.js"; + +app.registerExtension({ + name: "KJNodes.ConditioningMultiCombine", + async beforeRegisterNodeDef(nodeType, nodeData, app) { + switch (nodeData.name) { + case "ConditioningMultiCombine": + nodeType.prototype.onNodeCreated = function () { + this.inputs_offset = nodeData.name.includes("selective")?1:0 + this.cond_type = "CONDITIONING" + this.addWidget("button", "Add", null, () => { + if (!this.inputs) { + this.inputs = []; + } + if (this.inputs.length < 20) { + const newInputName = `conditioning_${this.inputs.length + 1}`; + this.addInput(newInputName, this.cond_type); + } + }); + this.addWidget("button", "Remove", null, () => { + if (this.inputs.length > 2) { + const lastInputIndex = this.inputs.length - 1; + this.removeInput(lastInputIndex); + } + }); + } + break; + } + }, +}); \ No newline at end of file diff --git a/web/js/setgetnodes.js b/web/js/setgetnodes.js new file mode 100644 index 0000000..e8165f9 --- /dev/null +++ b/web/js/setgetnodes.js @@ -0,0 +1,397 @@ +import { app } from "../../../scripts/app.js"; +import { ComfyWidgets } from '../../../scripts/widgets.js'; +//based on diffus3's SetGet: https://github.com/diffus3/ComfyUI-extensions + +// Nodes that allow you to tunnel connections for cleaner graphs +app.registerExtension({ + name: "SetNode", + registerCustomNodes() { + class SetNode { + defaultVisibility = true; + serialize_widgets = true; + constructor() { + if (!this.properties) { + this.properties = { + "previousName": "" + }; + } + this.properties.showOutputText = SetNode.defaultVisibility; + + const node = this; + + + this.addWidget( + "text", + "Constant", + '', + (s, t, u, v, x) => { + node.validateName(node.graph); + this.update(); + this.properties.previousName = this.widgets[0].value; + }, + {} + ) + + this.addInput("*", "*"); + this.addOutput("*", '*'); + + + this.onConnectionsChange = function( + slotType, //1 = input, 2 = output + slot, + isChangeConnect, + link_info, + output + ) { + //On Disconnect + if (slotType == 1 && !isChangeConnect) { + this.inputs[slot].type = '*'; + this.inputs[slot].name = '*'; + this.title = "Set" + } + if (slotType == 2 && !isChangeConnect) { + this.outputs[slot].type = '*'; + this.outputs[slot].name = '*'; + } + //On Connect + if (link_info && node.graph && slotType == 1 && isChangeConnect) { + + const fromNode = node.graph._nodes.find((otherNode) => otherNode.id == link_info.origin_id); + const type = fromNode.outputs[link_info.origin_slot].type; + this.title = "Set_" + type; + if (this.widgets[0].value == ''){ + this.widgets[0].value = type + } + + this.validateName(node.graph); + this.inputs[0].type = type; + this.inputs[0].name = type; + + switch (type) { + case "MODEL": + this.color = LGraphCanvas.node_colors.blue.color; + this.bgcolor = LGraphCanvas.node_colors.blue.bgcolor; + break; + case "LATENT": + this.color = LGraphCanvas.node_colors.purple.color; + this.bgcolor = LGraphCanvas.node_colors.purple.bgcolor; + break; + case "VAE": + this.color = LGraphCanvas.node_colors.red.color; + this.bgcolor = LGraphCanvas.node_colors.red.bgcolor; + break; + case "CONDITIONING": + this.color = LGraphCanvas.node_colors.brown.color; + this.bgcolor = LGraphCanvas.node_colors.brown.bgcolor; + break; + case "IMAGE": + this.color = LGraphCanvas.node_colors.pale_blue.color; + this.bgcolor = LGraphCanvas.node_colors.pale_blue.bgcolor; + break; + case "CLIP": + this.color = LGraphCanvas.node_colors.yellow.color; + this.bgcolor = LGraphCanvas.node_colors.yellow.bgcolor; + break; + case "INT": + this.color = LGraphCanvas.node_colors.green.color; + this.bgcolor = LGraphCanvas.node_colors.green.bgcolor; + break; + } + } + if (link_info && node.graph && slotType == 2 && isChangeConnect) { + const fromNode = node.graph._nodes.find((otherNode) => otherNode.id == link_info.origin_id); + const type = fromNode.inputs[link_info.origin_slot].type; + + this.outputs[0].type = type; + this.outputs[0].name = type; + } + + + //Update either way + this.update(); + } + + this.validateName = function(graph) { + let widgetValue = node.widgets[0].value; + + if (widgetValue != '') { + let tries = 0; + let collisions = []; + + do { + collisions = graph._nodes.filter((otherNode) => { + if (otherNode == this) { + return false; + } + if (otherNode.type == 'SetNode' && otherNode.widgets[0].value === widgetValue) { + return true; + } + return false; + }) + if (collisions.length > 0) { + widgetValue = node.widgets[0].value + "_" + tries; + } + tries++; + } while (collisions.length > 0) + node.widgets[0].value = widgetValue; + this.update(); + } + } + + this.clone = function () { + console.log("CLONE"); + const cloned = SetNode.prototype.clone.apply(this); + cloned.inputs[0].name = '*'; + cloned.inputs[0].type = '*'; + cloned.value = ''; + cloned.properties.previousName = ''; + cloned.size = cloned.computeSize(); + return cloned; + }; + + this.onAdded = function(graph) { + this.validateName(graph); + } + + + this.update = function() { + if (node.graph) { + this.findGetters(node.graph).forEach((getter) => { + getter.setType(this.inputs[0].type); + }); + if (this.widgets[0].value) { + this.findGetters(node.graph, true).forEach((getter) => { + getter.setName(this.widgets[0].value) + }); + } + + const allGetters = node.graph._nodes.filter((otherNode) => otherNode.type == "GetNode"); + allGetters.forEach((otherNode) => { + if (otherNode.setComboValues) { + otherNode.setComboValues(); + } + }) + } + } + + + this.findGetters = function(graph, checkForPreviousName) { + const name = checkForPreviousName ? this.properties.previousName : this.widgets[0].value; + return graph._nodes.filter((otherNode) => { + if (otherNode.type == 'GetNode' && otherNode.widgets[0].value === name && name != '') { + return true; + } + return false; + }) + } + + + // This node is purely frontend and does not impact the resulting prompt so should not be serialized + this.isVirtualNode = true; + } + + onRemoved() { + console.log("onRemove"); + console.log(this); + console.log(this.flags); + const allGetters = this.graph._nodes.filter((otherNode) => otherNode.type == "GetNode"); + allGetters.forEach((otherNode) => { + if (otherNode.setComboValues) { + otherNode.setComboValues([this]); + } + }) + } + } + + + LiteGraph.registerNodeType( + "SetNode", + Object.assign(SetNode, { + title: "Set", + }) + ); + + SetNode.category = "KJNodes"; + }, +}); + + +app.registerExtension({ + name: "GetNode", + registerCustomNodes() { + class GetNode { + + defaultVisibility = true; + serialize_widgets = true; + + constructor() { + if (!this.properties) { + this.properties = {}; + } + this.properties.showOutputText = GetNode.defaultVisibility; + + const node = this; + this.addWidget( + "combo", + "Constant", + "", + (e) => { + this.onRename(); + }, + { + values: () => { + const setterNodes = graph._nodes.filter((otherNode) => otherNode.type == 'SetNode'); + return setterNodes.map((otherNode) => otherNode.widgets[0].value).sort(); + } + } + ) + + + this.addOutput("*", '*'); + + + this.onConnectionsChange = function( + slotType, //0 = output, 1 = input + slot, //self-explanatory + isChangeConnect, + link_info, + output + ) { + this.validateLinks(); + } + + + this.setName = function(name) { + console.log("renaming getter: "); + console.log(node.widgets[0].value + " -> " + name); + node.widgets[0].value = name; + node.onRename(); + node.serialize(); + + } + + + this.onRename = function() { + console.log("onRename"); + + const setter = this.findSetter(node.graph); + if (setter) { + let linkType = (setter.inputs[0].type); + + this.setType(linkType); + this.title = "Get_" + setter.inputs[0].type; + + switch (linkType) { + case "MODEL": + this.color = LGraphCanvas.node_colors.blue.color; + this.bgcolor = LGraphCanvas.node_colors.blue.bgcolor; + break; + case "LATENT": + this.color = LGraphCanvas.node_colors.purple.color; + this.bgcolor = LGraphCanvas.node_colors.purple.bgcolor; + break; + case "VAE": + this.color = LGraphCanvas.node_colors.red.color; + this.bgcolor = LGraphCanvas.node_colors.red.bgcolor; + break; + case "CONDITIONING": + this.color = LGraphCanvas.node_colors.brown.color; + this.bgcolor = LGraphCanvas.node_colors.brown.bgcolor; + break; + case "IMAGE": + this.color = LGraphCanvas.node_colors.pale_blue.color; + this.bgcolor = LGraphCanvas.node_colors.pale_blue.bgcolor; + break; + case "CLIP": + this.color = LGraphCanvas.node_colors.yellow.color; + this.bgcolor = LGraphCanvas.node_colors.yellow.bgcolor; + break; + case "INT": + this.color = LGraphCanvas.node_colors.green.color; + this.bgcolor = LGraphCanvas.node_colors.green.bgcolor; + break; + } + + } else { + this.setType('*'); + } + } + + this.clone = function () { + const cloned = GetNode.prototype.clone.apply(this); + cloned.size = cloned.computeSize(); + return cloned; + }; + + this.validateLinks = function() { + console.log("validating links"); + if (this.outputs[0].type != '*' && this.outputs[0].links) { + console.log("in"); + this.outputs[0].links.forEach((linkId) => { + const link = node.graph.links[linkId]; + if (link && link.type != this.outputs[0].type && link.type != '*') { + console.log("removing link"); + node.graph.removeLink(linkId) + } + }) + } + } + + this.setType = function(type) { + this.outputs[0].name = type; + this.outputs[0].type = type; + this.validateLinks(); + } + + this.findSetter = function(graph) { + const name = this.widgets[0].value; + return graph._nodes.find((otherNode) => { + if (otherNode.type == 'SetNode' && otherNode.widgets[0].value === name && name != '') { + return true; + } + return false; + }) + } + + // This node is purely frontend and does not impact the resulting prompt so should not be serialized + this.isVirtualNode = true; + } + + + getInputLink(slot) { + const setter = this.findSetter(this.graph); + + if (setter) { + const slot_info = setter.inputs[slot]; + console.log("slot info"); + console.log(slot_info); + console.log(this.graph.links); + const link = this.graph.links[ slot_info.link ]; + console.log("link:"); + console.log(link); + return link; + } else { + console.log(this.widgets[0]); + console.log(this.widgets[0].value); + throw new Error("No SetNode found for " + this.widgets[0].value + "(" + this.type + ")"); + } + + } + onAdded(graph) { + //this.setComboValues(); + //this.validateName(graph); + } + + } + + + LiteGraph.registerNodeType( + "GetNode", + Object.assign(GetNode, { + title: "Get", + }) + ); + + GetNode.category = "KJNodes"; + }, +}); diff --git a/web/red.png b/web/red.png new file mode 100644 index 0000000..4352c11 Binary files /dev/null and b/web/red.png differ