-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Initial commit: ComfyUI Finetuners with example workflow
- Loading branch information
1 parent
0d4e29f
commit e047b53
Showing
8 changed files
with
1,084 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 added
BIN
+436 Bytes
custom_nodes/ComfyUI-finetuners/__pycache__/__init__.cpython-310.pyc
Binary file not shown.
Binary file not shown.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
190
custom_nodes/ComfyUI-finetuners/web/js/fast_group_link.js
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
}; | ||
} | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"; |
Oops, something went wrong.