diff --git a/custom_nodes/ComfyUI-finetuners/README.md b/custom_nodes/ComfyUI-finetuners/README.md new file mode 100644 index 00000000..b899fa27 --- /dev/null +++ b/custom_nodes/ComfyUI-finetuners/README.md @@ -0,0 +1,28 @@ +# ComfyUI Finetuners + +A collection of utility nodes for ComfyUI to enhance your workflow. + +## Nodes + +### 🔄 Variables Injector +Dynamically replace placeholders (like !variable_name) in a text prompt with actual values, making it easy to reuse and modify prompts without changing their structure. + +### 📐 Auto Image Resize +Automatically resizes images based on a desired width while maintaining aspect ratio, using high-quality Lanczos scaling. + +### 🔗 Group Link +A utility node that allows you to link and toggle multiple groups of nodes simultaneously, helping you organize and control complex workflows. + +## Installation + +1. Clone this repository into your `ComfyUI/custom_nodes` directory: +```bash +cd custom_nodes +git clone https://github.com/FinetunersAI/finetunersTest.git +``` + +2. Restart ComfyUI + +## Usage + +After installation, you'll find the nodes in the node menu under the "finetuners" category. diff --git a/custom_nodes/ComfyUI-finetuners/__init__.py b/custom_nodes/ComfyUI-finetuners/__init__.py new file mode 100644 index 00000000..868917e4 --- /dev/null +++ b/custom_nodes/ComfyUI-finetuners/__init__.py @@ -0,0 +1,19 @@ +from .nodes import ( + NODE_CLASS_MAPPINGS, + NODE_DISPLAY_NAME_MAPPINGS, + AutoImageResize, + GroupLink, + VariablesInjector +) + +WEB_DIRECTORY = "./web/js" + +print("\033[34mComfyUI Finetuners: \033[92mLoaded\033[0m") + +__all__ = [ + "NODE_CLASS_MAPPINGS", + "NODE_DISPLAY_NAME_MAPPINGS", + "AutoImageResize", + "GroupLink", + "VariablesInjector" +] \ No newline at end of file diff --git a/custom_nodes/ComfyUI-finetuners/__pycache__/__init__.cpython-310.pyc b/custom_nodes/ComfyUI-finetuners/__pycache__/__init__.cpython-310.pyc new file mode 100644 index 00000000..ad8b8437 Binary files /dev/null and b/custom_nodes/ComfyUI-finetuners/__pycache__/__init__.cpython-310.pyc differ diff --git a/custom_nodes/ComfyUI-finetuners/__pycache__/nodes.cpython-310.pyc b/custom_nodes/ComfyUI-finetuners/__pycache__/nodes.cpython-310.pyc new file mode 100644 index 00000000..ac2715fc Binary files /dev/null and b/custom_nodes/ComfyUI-finetuners/__pycache__/nodes.cpython-310.pyc differ diff --git a/custom_nodes/ComfyUI-finetuners/nodes.py b/custom_nodes/ComfyUI-finetuners/nodes.py new file mode 100644 index 00000000..6f636dec --- /dev/null +++ b/custom_nodes/ComfyUI-finetuners/nodes.py @@ -0,0 +1,129 @@ +import torch +from comfy.utils import lanczos + +class AutoImageResize: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "image": ("IMAGE",), + "desired_width": ("INT", {"default": 1024, "min": 64, "max": 8192, "step": 8}), + } + } + + RETURN_TYPES = ("IMAGE", "INT", "INT") + RETURN_NAMES = ("image", "width", "height") + FUNCTION = "execute" + CATEGORY = "finetuners" + + def execute(self, image, desired_width): + # Get current dimensions + _, current_height, current_width, _ = image.shape + + # Calculate target width and scale factor + target_width = current_width + if current_width < 1024 or current_width > 1344: + target_width = desired_width + scale_factor = desired_width / current_width + else: + # No resize needed + return (image, current_width, current_height) + + # Calculate new height maintaining aspect ratio + target_height = int(current_height * scale_factor) + + # Convert to NCHW for lanczos + x = image.permute(0, 3, 1, 2) + + # Perform lanczos resize + x = lanczos(x, target_width, target_height) + + # Convert back to NHWC + x = x.permute(0, 2, 3, 1) + + return (x, target_width, target_height) + + +class GroupLink: + @classmethod + def INPUT_TYPES(s): + return {"required": {}} + + RETURN_TYPES = () + FUNCTION = "noop" + CATEGORY = "finetuners" + + def noop(self): + return {} + + +class VariablesInjector: + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "prompt": ("STRING", {"multiline": True, "height": 4, "default": "An album in !theme theme"}), # Text widget for prompt with explicit height + "var1_name": ("STRING", {"default": "theme"}), # Text widget for name + "Var1": ("STRING", {"forceInput": True}) # Connectable string input + }, + "optional": { + "var2_name": ("STRING", {"default": ""}), + "Var2": ("STRING", {"forceInput": True}), + "var3_name": ("STRING", {"default": ""}), + "Var3": ("STRING", {"forceInput": True}), + "var4_name": ("STRING", {"default": ""}), + "Var4": ("STRING", {"forceInput": True}), + "var5_name": ("STRING", {"default": ""}), + "Var5": ("STRING", {"forceInput": True}), + "var6_name": ("STRING", {"default": ""}), + "Var6": ("STRING", {"forceInput": True}), + "var7_name": ("STRING", {"default": ""}), + "Var7": ("STRING", {"forceInput": True}), + "var8_name": ("STRING", {"default": ""}), + "Var8": ("STRING", {"forceInput": True}) + } + } + + RETURN_TYPES = ("STRING",) + RETURN_NAMES = ("text",) + FUNCTION = "inject" + CATEGORY = "finetuners" + + def inject(self, **kwargs): + result = kwargs['prompt'] + pairs = [ + (kwargs.get('var1_name'), kwargs.get('Var1')), + (kwargs.get('var2_name'), kwargs.get('Var2')), + (kwargs.get('var3_name'), kwargs.get('Var3')), + (kwargs.get('var4_name'), kwargs.get('Var4')), + (kwargs.get('var5_name'), kwargs.get('Var5')), + (kwargs.get('var6_name'), kwargs.get('Var6')), + (kwargs.get('var7_name'), kwargs.get('Var7')), + (kwargs.get('var8_name'), kwargs.get('Var8')), + ] + + for name, value in pairs: + if name and value: # Only process if both name and value are present + result = result.replace(f"!{name}", value) + + return (result,) + + +# Node mappings +NODE_CLASS_MAPPINGS = { + "VariablesInjector": VariablesInjector, + "AutoImageResize": AutoImageResize, + "GroupLink": GroupLink +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "VariablesInjector": "🔄 Variables Injector", + "AutoImageResize": "📐 Auto Image Resize", + "GroupLink": "🔗 Group Link" +} + +# Informs user that nodes are loaded +print("\033[34mComfyUI Finetuners: \033[92mLoaded\033[0m") \ No newline at end of file diff --git a/custom_nodes/ComfyUI-finetuners/web/js/fast_group_link.js b/custom_nodes/ComfyUI-finetuners/web/js/fast_group_link.js new file mode 100644 index 00000000..a517cc47 --- /dev/null +++ b/custom_nodes/ComfyUI-finetuners/web/js/fast_group_link.js @@ -0,0 +1,190 @@ +import { app } from "/scripts/app.js"; + +app.registerExtension({ + name: "Comfy.Finetuners.GroupLink", + async beforeRegisterNodeDef(nodeType, nodeData, app) { + if (nodeData.name !== "GroupLink") return; + + // Store original onNodeCreated + const onNodeCreated = nodeType.prototype.onNodeCreated; + + nodeType.prototype.onNodeCreated = function() { + // Call original onNodeCreated if it exists + const r = onNodeCreated?.apply(this, arguments); + + // Initialize properties + this.properties = this.properties || {}; + this.properties["masterGroup"] = this.properties["masterGroup"] || ""; + this.properties["slaveGroup"] = this.properties["slaveGroup"] || ""; + this.properties["showNav"] = true; + this.serialize_widgets = true; + this.size = [240, 120]; // Start with expanded size + this.modeOn = LiteGraph.ALWAYS; + this.modeOff = LiteGraph.NEVER; + + // Add custom styles + this.addProperty("bgcolor", "#454545"); + this.addProperty("boxcolor", "#666"); + this.shape = LiteGraph.BOX_SHAPE; + this.round_radius = 8; + + // Remove inputs/outputs + this.removable = true; + this.removeInput(0); + this.removeOutput(0); + + // Store states + this.toggleValue = false; + this.showGroups = true; // Start expanded + + // Override the node's drawing function + this.onDrawForeground = function(ctx) { + // Update node size based on state + this.size[1] = this.showGroups ? 120 : 50; + + // Hide/show widgets + for (const w of this.widgets || []) { + if (w.name === "Master Group" || w.name === "Slave Group") { + w.hidden = !this.showGroups; + } + } + + const y = this.showGroups ? this.size[1] - 35 : 15; + + // Draw expand/collapse triangle + ctx.fillStyle = "#fff"; + ctx.beginPath(); + ctx.moveTo(20, y + 5); + ctx.lineTo(30, y + 10); + ctx.lineTo(20, y + 15); + ctx.closePath(); + ctx.fill(); + + // Draw ON/OFF text + ctx.fillStyle = "#fff"; + ctx.textAlign = "center"; + ctx.fillText(this.toggleValue ? "ON" : "OFF", this.size[0]/2, y + 12); + + // Draw toggle track + ctx.fillStyle = "#666"; + ctx.beginPath(); + ctx.roundRect(this.size[0] - 45, y + 2, 30, 16, 8); + ctx.fill(); + + // Draw toggle circle + ctx.fillStyle = this.toggleValue ? "#4CAF50" : "#f44336"; + ctx.beginPath(); + const toggleRadius = 8; + ctx.arc(this.size[0] - 23, y + 10, toggleRadius, 0, Math.PI * 2); + ctx.fill(); + }; + + // Handle mouse clicks + this.onMouseDown = function(e, local_pos) { + const y = this.showGroups ? this.size[1] - 35 : 15; + + // Handle expand/collapse triangle click + if (local_pos[1] >= y && local_pos[1] <= y + 20 && + local_pos[0] >= 15 && local_pos[0] <= 35) { + this.showGroups = !this.showGroups; + this.setDirtyCanvas(true, true); + return true; + } + + // Handle toggle click + if (local_pos[1] >= y && local_pos[1] <= y + 20 && + local_pos[0] >= this.size[0] - 45) { + this.toggleValue = !this.toggleValue; + this.updateGroupStates(); + return true; + } + }; + + // Refresh widgets on creation + setTimeout(() => this.refreshWidgets(), 100); + + return r; + }; + + nodeType.prototype.onAdded = function(graph) { + this.graph = graph; + this.refreshWidgets(); + // Initial state setup + this.updateGroupStates(); + }; + + nodeType.prototype.updateGroupStates = function() { + if (!this.graph) return; + + const masterGroup = this.graph._groups.find(g => g.title === this.properties["masterGroup"]); + const slaveGroup = this.graph._groups.find(g => g.title === this.properties["slaveGroup"]); + + if (masterGroup) { + masterGroup.recomputeInsideNodes(); + for (const node of masterGroup._nodes) { + node.mode = (this.toggleValue ? this.modeOn : this.modeOff); + } + } + + if (slaveGroup) { + slaveGroup.recomputeInsideNodes(); + for (const node of slaveGroup._nodes) { + node.mode = (this.toggleValue ? this.modeOn : this.modeOff); + } + } + + app.graph.setDirtyCanvas(true, false); + }; + + nodeType.prototype.refreshWidgets = function() { + if (!this.graph?._groups) return; + + const groups = [...this.graph._groups].sort((a, b) => (a.title || "").localeCompare(b.title || "")); + const groupTitles = groups.map(g => g.title || "Untitled"); + + // Clear existing widgets + if (!this.widgets) { + this.widgets = []; + } + this.widgets.length = 0; + + // Add master group selection + const masterWidget = this.addWidget("combo", "Master Group", this.properties["masterGroup"], (v) => { + this.properties["masterGroup"] = v; + this.updateGroupStates(); + }, { values: groupTitles }); + + // Add slave group selection + const slaveWidget = this.addWidget("combo", "Slave Group", this.properties["slaveGroup"], (v) => { + this.properties["slaveGroup"] = v; + this.updateGroupStates(); + }, { values: groupTitles }); + }; + + // Handle graph reloading + nodeType.prototype.onConfigure = function(info) { + // Restore properties + if (info.properties) { + this.properties = {...info.properties}; + } + // Restore toggle state + if (info.toggleValue !== undefined) { + this.toggleValue = info.toggleValue; + } + // Update states after loading + setTimeout(() => { + this.updateGroupStates(); + }, 100); + }; + + // Save additional state + const onSerialize = nodeType.prototype.onSerialize; + nodeType.prototype.onSerialize = function(info) { + if (onSerialize) { + onSerialize.apply(this, arguments); + } + // Save toggle state + info.toggleValue = this.toggleValue; + }; + } +}); diff --git a/custom_nodes/ComfyUI-finetuners/web/js/index.js b/custom_nodes/ComfyUI-finetuners/web/js/index.js new file mode 100644 index 00000000..aabaaee5 --- /dev/null +++ b/custom_nodes/ComfyUI-finetuners/web/js/index.js @@ -0,0 +1,3 @@ +import { app } from "/scripts/app.js"; +import "./variables_injector.js"; +import "./fast_group_link.js"; diff --git a/custom_nodes/ComfyUI-finetuners/workflow/finetuners_nodes.json b/custom_nodes/ComfyUI-finetuners/workflow/finetuners_nodes.json new file mode 100644 index 00000000..be3be9f5 --- /dev/null +++ b/custom_nodes/ComfyUI-finetuners/workflow/finetuners_nodes.json @@ -0,0 +1,715 @@ +{ + "last_node_id": 154, + "last_link_id": 204, + "nodes": [ + { + "id": 143, + "type": "LoadImage", + "pos": [ + 464, + 1448 + ], + "size": [ + 315, + 314 + ], + "flags": {}, + "order": 0, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "IMAGE", + "type": "IMAGE", + "links": [ + 198 + ], + "slot_index": 0 + }, + { + "name": "MASK", + "type": "MASK", + "links": null + } + ], + "properties": { + "Node name for S&R": "LoadImage" + }, + "widgets_values": [ + " xwinobgx__00268_.png", + "image" + ] + }, + { + "id": 142, + "type": "AutoImageResize", + "pos": [ + 847, + 1443 + ], + "size": [ + 315, + 98 + ], + "flags": {}, + "order": 7, + "mode": 0, + "inputs": [ + { + "name": "image", + "type": "IMAGE", + "link": 198 + } + ], + "outputs": [ + { + "name": "image", + "type": "IMAGE", + "links": [ + 199 + ], + "slot_index": 0 + }, + { + "name": "width", + "type": "INT", + "links": null + }, + { + "name": "height", + "type": "INT", + "links": null + } + ], + "properties": { + "Node name for S&R": "AutoImageResize" + }, + "widgets_values": [ + 1024 + ] + }, + { + "id": 144, + "type": "SaveImage", + "pos": [ + 1282, + 1448 + ], + "size": [ + 315, + 270 + ], + "flags": {}, + "order": 9, + "mode": 0, + "inputs": [ + { + "name": "images", + "type": "IMAGE", + "link": 199 + } + ], + "outputs": [], + "properties": { + "Node name for S&R": "SaveImage" + }, + "widgets_values": [ + "ComfyUI" + ] + }, + { + "id": 150, + "type": "ShowText|pysssss", + "pos": [ + 1409, + 1950 + ], + "size": [ + 315, + 76 + ], + "flags": {}, + "order": 10, + "mode": 0, + "inputs": [ + { + "name": "text", + "type": "STRING", + "link": 203, + "widget": { + "name": "text" + } + } + ], + "outputs": [ + { + "name": "STRING", + "type": "STRING", + "links": null, + "shape": 6 + } + ], + "properties": { + "Node name for S&R": "ShowText|pysssss" + }, + "widgets_values": [ + "", + "An album in Hanukah theme all in red" + ] + }, + { + "id": 145, + "type": "Note", + "pos": [ + -5, + 1407 + ], + "size": [ + 398.7071533203125, + 97.78943634033203 + ], + "flags": {}, + "order": 1, + "mode": 0, + "inputs": [], + "outputs": [], + "properties": {}, + "widgets_values": [ + "Auto resizes autmaically resizes images (uplscales image) using Lazncos to their desired width size\n" + ], + "color": "#432", + "bgcolor": "#653" + }, + { + "id": 147, + "type": "String Literal", + "pos": [ + 441, + 1945 + ], + "size": [ + 336.653564453125, + 82.74593353271484 + ], + "flags": {}, + "order": 2, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "STRING", + "type": "STRING", + "links": [ + 200 + ] + } + ], + "properties": { + "Node name for S&R": "String Literal" + }, + "widgets_values": [ + "Hanukah", + [ + false, + true + ] + ] + }, + { + "id": 152, + "type": "String Literal", + "pos": [ + 445, + 2077 + ], + "size": [ + 336.653564453125, + 82.74593353271484 + ], + "flags": {}, + "order": 3, + "mode": 0, + "inputs": [], + "outputs": [ + { + "name": "STRING", + "type": "STRING", + "links": [ + 204 + ], + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "String Literal" + }, + "widgets_values": [ + "red", + [ + false, + true + ] + ] + }, + { + "id": 146, + "type": "VariablesInjector", + "pos": [ + 961, + 1950 + ], + "size": [ + 371.72882080078125, + 458.1750183105469 + ], + "flags": {}, + "order": 8, + "mode": 0, + "inputs": [ + { + "name": "Var1", + "type": "STRING", + "link": 200, + "widget": { + "name": "Var1" + } + }, + { + "name": "Var2", + "type": "STRING", + "link": 204, + "widget": { + "name": "Var2" + }, + "shape": 7 + }, + { + "name": "Var3", + "type": "STRING", + "link": null, + "widget": { + "name": "Var3" + }, + "shape": 7 + }, + { + "name": "Var4", + "type": "STRING", + "link": null, + "widget": { + "name": "Var4" + }, + "shape": 7 + }, + { + "name": "Var5", + "type": "STRING", + "link": null, + "widget": { + "name": "Var5" + }, + "shape": 7 + }, + { + "name": "Var6", + "type": "STRING", + "link": null, + "widget": { + "name": "Var6" + }, + "shape": 7 + }, + { + "name": "Var7", + "type": "STRING", + "link": null, + "widget": { + "name": "Var7" + }, + "shape": 7 + }, + { + "name": "Var8", + "type": "STRING", + "link": null, + "widget": { + "name": "Var8" + }, + "shape": 7 + } + ], + "outputs": [ + { + "name": "text", + "type": "STRING", + "links": [ + 203 + ], + "slot_index": 0 + } + ], + "properties": { + "Node name for S&R": "VariablesInjector" + }, + "widgets_values": [ + "An album in !theme theme all in !color", + "theme", + "", + "color", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + "", + [ + false, + true + ] + ] + }, + { + "id": 153, + "type": "GroupLink", + "pos": [ + 469, + 2530 + ], + "size": [ + 210, + 120 + ], + "flags": {}, + "order": 4, + "mode": 0, + "inputs": [], + "outputs": [], + "properties": { + "Node name for S&R": "GroupLink", + "masterGroup": "Auto resize", + "slaveGroup": "var Injector", + "showNav": true, + "bgcolor": "#454545", + "boxcolor": "#666" + }, + "widgets_values": [ + "Auto resize", + "var Injector" + ], + "shape": 1, + "toggleValue": true + }, + { + "id": 151, + "type": "Note", + "pos": [ + -18, + 1921 + ], + "size": [ + 398.7071533203125, + 97.78943634033203 + ], + "flags": {}, + "order": 5, + "mode": 0, + "inputs": [], + "outputs": [], + "properties": {}, + "widgets_values": [ + "Var injector replaces words for variables, for dynamic prompting." + ], + "color": "#432", + "bgcolor": "#653" + }, + { + "id": 154, + "type": "Note", + "pos": [ + -2, + 2524 + ], + "size": [ + 398.7071533203125, + 97.78943634033203 + ], + "flags": {}, + "order": 6, + "mode": 0, + "inputs": [], + "outputs": [], + "properties": {}, + "widgets_values": [ + "Group link bindes 2 groups together for bypassing purposes" + ], + "color": "#432", + "bgcolor": "#653" + } + ], + "links": [ + [ + 82, + 12, + 0, + 56, + 4, + "VAE" + ], + [ + 127, + 12, + 0, + 19, + 1, + "VAE" + ], + [ + 128, + 10, + 0, + 20, + 0, + "MODEL" + ], + [ + 129, + 11, + 0, + 88, + 0, + "CLIP" + ], + [ + 130, + 12, + 0, + 72, + 1, + "VAE" + ], + [ + 131, + 12, + 0, + 19, + 1, + "VAE" + ], + [ + 132, + 10, + 0, + 20, + 0, + "MODEL" + ], + [ + 133, + 11, + 0, + 88, + 0, + "CLIP" + ], + [ + 134, + 12, + 0, + 72, + 1, + "VAE" + ], + [ + 182, + 12, + 0, + 19, + 1, + "VAE" + ], + [ + 183, + 11, + 0, + 88, + 0, + "CLIP" + ], + [ + 184, + 12, + 0, + 56, + 4, + "VAE" + ], + [ + 185, + 10, + 0, + 20, + 0, + "MODEL" + ], + [ + 186, + 12, + 0, + 72, + 1, + "VAE" + ], + [ + 187, + 12, + 0, + 19, + 1, + "VAE" + ], + [ + 188, + 11, + 0, + 88, + 0, + "CLIP" + ], + [ + 189, + 12, + 0, + 72, + 1, + "VAE" + ], + [ + 190, + 10, + 0, + 20, + 0, + "MODEL" + ], + [ + 198, + 143, + 0, + 142, + 0, + "IMAGE" + ], + [ + 199, + 142, + 0, + 144, + 0, + "IMAGE" + ], + [ + 200, + 147, + 0, + 146, + 0, + "STRING" + ], + [ + 203, + 146, + 0, + 150, + 0, + "STRING" + ], + [ + 204, + 152, + 0, + 146, + 1, + "STRING" + ] + ], + "groups": [ + { + "id": 8, + "title": "Auto resize", + "bounding": [ + 454, + 1369.4000244140625, + 1153, + 402.6000061035156 + ], + "color": "#3f789e", + "font_size": 24, + "flags": {} + }, + { + "id": 9, + "title": "var Injector", + "bounding": [ + 431, + 1871.4000244140625, + 1303, + 546.7750244140625 + ], + "color": "#3f789e", + "font_size": 24, + "flags": {} + } + ], + "config": {}, + "extra": { + "ds": { + "scale": 0.40909090909090967, + "offset": [ + 1948.0091156553785, + -1060.0555512182152 + ] + }, + "ue_links": [ + { + "downstream": 19, + "downstream_slot": 1, + "upstream": "12", + "upstream_slot": 0, + "controller": 13, + "type": "VAE" + }, + { + "downstream": 88, + "downstream_slot": 0, + "upstream": "11", + "upstream_slot": 0, + "controller": 13, + "type": "CLIP" + }, + { + "downstream": 72, + "downstream_slot": 1, + "upstream": "12", + "upstream_slot": 0, + "controller": 13, + "type": "VAE" + }, + { + "downstream": 20, + "downstream_slot": 0, + "upstream": "10", + "upstream_slot": 0, + "controller": 13, + "type": "MODEL" + } + ] + }, + "version": 0.4 +} \ No newline at end of file