First commit

Most stuff probably still buggy
This commit is contained in:
kijai 2023-09-29 18:00:42 +03:00
commit 76d32c9822
12 changed files with 617 additions and 0 deletions

6
.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
__pycache__
/venv
.vscode
*.ckpt
*.safetensors
*.pth

7
README.md Normal file
View File

@ -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.

4
__init__.py Normal file
View File

@ -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"]

BIN
favicon-active.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

BIN
favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1006 B

110
nodes.py Normal file
View File

@ -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",
}

BIN
web/green.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

22
web/js/appearance.js Normal file
View File

@ -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;
}
}
});

41
web/js/browserstatus.js Normal file
View File

@ -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;
});
},
});

30
web/js/jsnodes.js Normal file
View File

@ -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;
}
},
});

397
web/js/setgetnodes.js Normal file
View File

@ -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";
},
});

BIN
web/red.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB