Skip to content

Commit

Permalink
Initial commit: ComfyUI Finetuners with example workflow
Browse files Browse the repository at this point in the history
  • Loading branch information
FinetunersAI committed Jan 4, 2025
1 parent 0d4e29f commit e047b53
Show file tree
Hide file tree
Showing 8 changed files with 1,084 additions and 0 deletions.
28 changes: 28 additions & 0 deletions custom_nodes/ComfyUI-finetuners/README.md
Original file line number Diff line number Diff line change
@@ -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.
19 changes: 19 additions & 0 deletions custom_nodes/ComfyUI-finetuners/__init__.py
Original file line number Diff line number Diff line change
@@ -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"
]
Binary file not shown.
Binary file not shown.
129 changes: 129 additions & 0 deletions custom_nodes/ComfyUI-finetuners/nodes.py
Original file line number Diff line number Diff line change
@@ -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")
190 changes: 190 additions & 0 deletions custom_nodes/ComfyUI-finetuners/web/js/fast_group_link.js
Original file line number Diff line number Diff line change
@@ -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;
};
}
});
3 changes: 3 additions & 0 deletions custom_nodes/ComfyUI-finetuners/web/js/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { app } from "/scripts/app.js";
import "./variables_injector.js";
import "./fast_group_link.js";
Loading

0 comments on commit e047b53

Please sign in to comment.