Merge remote-tracking branch 'upstream/master'

This commit is contained in:
Silversith 2023-04-11 11:24:25 +02:00
commit d7a2bdb895
18 changed files with 630 additions and 218 deletions

View File

@ -1,6 +1,6 @@
ComfyUI
=======
A powerful and modular stable diffusion GUI.
A powerful and modular stable diffusion GUI and backend.
-----------
![ComfyUI Screenshot](comfyui_screenshot.png)
@ -146,9 +146,9 @@ This will let you use: pip3.10 to install all the dependencies.
## How to increase generation speed?
Make sure you use the CheckpointLoaderSimple node to load checkpoints. It will auto pick the right settings depending on your GPU.
Make sure you use the regular loaders/Load Checkpoint node to load checkpoints. It will auto pick the right settings depending on your GPU.
You can set this command line setting to disable the upcasting to fp32 in some cross attention operations which will increase your speed. Note that this doesn't do anything when xformers is enabled and will very likely give you black images on SD2.x models.
You can set this command line setting to disable the upcasting to fp32 in some cross attention operations which will increase your speed. Note that this will very likely give you black images on SD2.x models. If you use xformers this option does not do anything.
```--dont-upcast-attention```

View File

@ -26,6 +26,6 @@ vram_group.add_argument("--cpu", action="store_true", help="To use the CPU for e
parser.add_argument("--dont-print-server", action="store_true", help="Don't print server output.")
parser.add_argument("--quick-test-for-ci", action="store_true", help="Quick test for CI.")
parser.add_argument("--windows-standalone-build", action="store_true", help="Windows standalone build.")
parser.add_argument("--windows-standalone-build", action="store_true", help="Windows standalone build: Enable convenient things that most people using the standalone windows build will probably enjoy (like auto opening the page on startup).")
args = parser.parse_args()

View File

@ -1,6 +1,7 @@
from transformers import CLIPVisionModelWithProjection, CLIPVisionConfig, CLIPImageProcessor
from .utils import load_torch_file, transformers_convert
import os
import torch
class ClipVisionModel():
def __init__(self, json_config):
@ -20,7 +21,8 @@ class ClipVisionModel():
self.model.load_state_dict(sd, strict=False)
def encode_image(self, image):
inputs = self.processor(images=[image[0]], return_tensors="pt")
img = torch.clip((255. * image[0]), 0, 255).round().int()
inputs = self.processor(images=[img], return_tensors="pt")
outputs = self.model(**inputs)
return outputs

View File

@ -45,6 +45,8 @@ try:
except:
OOM_EXCEPTION = Exception
XFORMERS_VERSION = ""
XFORMERS_ENABLED_VAE = True
if args.disable_xformers:
XFORMERS_IS_AVAILABLE = False
else:
@ -52,6 +54,17 @@ else:
import xformers
import xformers.ops
XFORMERS_IS_AVAILABLE = True
try:
XFORMERS_VERSION = xformers.version.__version__
print("xformers version:", XFORMERS_VERSION)
if XFORMERS_VERSION.startswith("0.0.18"):
print()
print("WARNING: This version of xformers has a major bug where you will get black images when generating high resolution images.")
print("Please downgrade or upgrade xformers to a different version.")
print()
XFORMERS_ENABLED_VAE = False
except:
pass
except:
XFORMERS_IS_AVAILABLE = False
@ -223,13 +236,8 @@ def xformers_enabled_vae():
enabled = xformers_enabled()
if not enabled:
return False
try:
#0.0.18 has a bug where Nan is returned when inputs are too big (1152x1920 res images and above)
if xformers.version.__version__ == "0.0.18":
return False
except:
pass
return enabled
return XFORMERS_ENABLED_VAE
def pytorch_attention_enabled():
return ENABLE_PYTORCH_ATTENTION

Binary file not shown.

Before

Width:  |  Height:  |  Size: 110 KiB

After

Width:  |  Height:  |  Size: 116 KiB

View File

@ -88,3 +88,8 @@ class Example:
NODE_CLASS_MAPPINGS = {
"Example": Example
}
# A dictionary that contains the friendly/humanly readable titles for the nodes
NODE_DISPLAY_NAME_MAPPINGS = {
"Example": "Example Node"
}

View File

@ -1106,6 +1106,54 @@ NODE_CLASS_MAPPINGS = {
"DiffusersLoader": DiffusersLoader,
}
NODE_DISPLAY_NAME_MAPPINGS = {
# Sampling
"KSampler": "KSampler",
"KSamplerAdvanced": "KSampler (Advanced)",
# Loaders
"CheckpointLoader": "Load Checkpoint (With Config)",
"CheckpointLoaderSimple": "Load Checkpoint",
"VAELoader": "Load VAE",
"LoraLoader": "Load LoRA",
"CLIPLoader": "Load CLIP",
"ControlNetLoader": "Load ControlNet Model",
"DiffControlNetLoader": "Load ControlNet Model (diff)",
"StyleModelLoader": "Load Style Model",
"CLIPVisionLoader": "Load CLIP Vision",
"UpscaleModelLoader": "Load Upscale Model",
# Conditioning
"CLIPVisionEncode": "CLIP Vision Encode",
"StyleModelApply": "Apply Style Model",
"CLIPTextEncode": "CLIP Text Encode (Prompt)",
"CLIPSetLastLayer": "CLIP Set Last Layer",
"ConditioningCombine": "Conditioning (Combine)",
"ConditioningSetArea": "Conditioning (Set Area)",
"ControlNetApply": "Apply ControlNet",
# Latent
"VAEEncodeForInpaint": "VAE Encode (for Inpainting)",
"SetLatentNoiseMask": "Set Latent Noise Mask",
"VAEDecode": "VAE Decode",
"VAEEncode": "VAE Encode",
"LatentRotate": "Rotate Latent",
"LatentFlip": "Flip Latent",
"LatentCrop": "Crop Latent",
"EmptyLatentImage": "Empty Latent Image",
"LatentUpscale": "Upscale Latent",
"LatentComposite": "Latent Composite",
# Image
"SaveImage": "Save Image",
"PreviewImage": "Preview Image",
"LoadImage": "Load Image",
"LoadImageMask": "Load Image (as Mask)",
"ImageScale": "Upscale Image",
"ImageUpscaleWithModel": "Upscale Image (using Model)",
"ImageInvert": "Invert Image",
"ImagePadForOutpaint": "Pad Image for Outpainting",
# _for_testing
"VAEDecodeTiled": "VAE Decode (Tiled)",
"VAEEncodeTiled": "VAE Encode (Tiled)",
}
def load_custom_node(module_path):
module_name = os.path.basename(module_path)
if os.path.isfile(module_path):
@ -1121,6 +1169,8 @@ def load_custom_node(module_path):
module_spec.loader.exec_module(module)
if hasattr(module, "NODE_CLASS_MAPPINGS") and getattr(module, "NODE_CLASS_MAPPINGS") is not None:
NODE_CLASS_MAPPINGS.update(module.NODE_CLASS_MAPPINGS)
if hasattr(module, "NODE_DISPLAY_NAME_MAPPINGS") and getattr(module, "NODE_DISPLAY_NAME_MAPPINGS") is not None:
NODE_DISPLAY_NAME_MAPPINGS.update(module.NODE_DISPLAY_NAME_MAPPINGS)
else:
print(f"Skip {module_path} module for custom nodes due to the lack of NODE_CLASS_MAPPINGS.")
except Exception as e:

View File

@ -47,7 +47,7 @@
" !git pull\n",
"\n",
"!echo -= Install dependencies =-\n",
"!pip install xformers -r requirements.txt --extra-index-url https://download.pytorch.org/whl/cu118"
"!pip install xformers!=0.0.18 -r requirements.txt --extra-index-url https://download.pytorch.org/whl/cu118"
]
},
{
@ -86,6 +86,11 @@
"#!wget -c https://huggingface.co/waifu-diffusion/wd-1-5-beta2/resolve/main/checkpoints/wd-1-5-beta2-fp16.safetensors -P ./models/checkpoints/\n",
"\n",
"\n",
"# unCLIP models\n",
"#!wget -c https://huggingface.co/comfyanonymous/illuminatiDiffusionV1_v11_unCLIP/resolve/main/illuminatiDiffusionV1_v11-unclip-h-fp16.safetensors -P ./models/checkpoints/\n",
"#!wget -c https://huggingface.co/comfyanonymous/wd-1.5-beta2_unCLIP/resolve/main/wd-1-5-beta2-aesthetic-unclip-h-fp16.safetensors -P ./models/checkpoints/\n",
"\n",
"\n",
"# VAE\n",
"!wget -c https://huggingface.co/stabilityai/sd-vae-ft-mse-original/resolve/main/vae-ft-mse-840000-ema-pruned.safetensors -P ./models/vae/\n",
"#!wget -c https://huggingface.co/WarriorMama777/OrangeMixs/resolve/main/VAEs/orangemix.vae.pt -P ./models/vae/\n",

View File

@ -214,7 +214,8 @@ class PromptServer():
info['input'] = obj_class.INPUT_TYPES()
info['output'] = obj_class.RETURN_TYPES
info['output_name'] = obj_class.RETURN_NAMES if hasattr(obj_class, 'RETURN_NAMES') else info['output']
info['name'] = x #TODO
info['name'] = x
info['display_name'] = nodes.NODE_DISPLAY_NAME_MAPPINGS[x] if x in nodes.NODE_DISPLAY_NAME_MAPPINGS.keys() else x
info['description'] = ''
info['category'] = 'sd'
if hasattr(obj_class, 'CATEGORY'):

View File

@ -21,28 +21,74 @@ const colorPalettes = {
"MODEL": "#B39DDB", // light lavender-purple
"STYLE_MODEL": "#C2FFAE", // light green-yellow
"VAE": "#FF6E6E", // bright red
}
}
},
"litegraph_base": {
"NODE_TITLE_COLOR": "#999",
"NODE_SELECTED_TITLE_COLOR": "#FFF",
"NODE_TEXT_SIZE": 14,
"NODE_TEXT_COLOR": "#AAA",
"NODE_SUBTEXT_SIZE": 12,
"NODE_DEFAULT_COLOR": "#333",
"NODE_DEFAULT_BGCOLOR": "#353535",
"NODE_DEFAULT_BOXCOLOR": "#666",
"NODE_DEFAULT_SHAPE": "box",
"NODE_BOX_OUTLINE_COLOR": "#FFF",
"DEFAULT_SHADOW_COLOR": "rgba(0,0,0,0.5)",
"DEFAULT_GROUP_FONT": 24,
"WIDGET_BGCOLOR": "#222",
"WIDGET_OUTLINE_COLOR": "#666",
"WIDGET_TEXT_COLOR": "#DDD",
"WIDGET_SECONDARY_TEXT_COLOR": "#999",
"LINK_COLOR": "#9A9",
"EVENT_LINK_COLOR": "#A86",
"CONNECTING_LINK_COLOR": "#AFA",
},
},
},
"palette_2": {
"id": "palette_2",
"name": "Palette 2",
"solarized": {
"id": "solarized",
"name": "Solarized",
"colors": {
"node_slot": {
"CLIP": "#556B2F", // Dark Olive Green
"CLIP_VISION": "#4B0082", // Indigo
"CLIP_VISION_OUTPUT": "#006400", // Green
"CONDITIONING": "#FF1493", // Deep Pink
"CONTROL_NET": "#8B4513", // Saddle Brown
"IMAGE": "#8B0000", // Dark Red
"LATENT": "#00008B", // Dark Blue
"MASK": "#2F4F4F", // Dark Slate Grey
"MODEL": "#FF8C00", // Dark Orange
"STYLE_MODEL": "#004A4A", // Sherpa Blue
"UPSCALE_MODEL": "#4A004A", // Tyrian Purple
"VAE": "#4F394F", // Loulou
}
}
"CLIP": "#859900", // Green
"CLIP_VISION": "#6c71c4", // Indigo
"CLIP_VISION_OUTPUT": "#859900", // Green
"CONDITIONING": "#d33682", // Magenta
"CONTROL_NET": "#cb4b16", // Orange
"IMAGE": "#dc322f", // Red
"LATENT": "#268bd2", // Blue
"MASK": "#073642", // Base02
"MODEL": "#cb4b16", // Orange
"STYLE_MODEL": "#073642", // Base02
"UPSCALE_MODEL": "#6c71c4", // Indigo
"VAE": "#586e75", // Base1
},
"litegraph_base": {
"NODE_TITLE_COLOR": "#fdf6e3",
"NODE_SELECTED_TITLE_COLOR": "#b58900",
"NODE_TEXT_SIZE": 14,
"NODE_TEXT_COLOR": "#657b83",
"NODE_SUBTEXT_SIZE": 12,
"NODE_DEFAULT_COLOR": "#586e75",
"NODE_DEFAULT_BGCOLOR": "#073642",
"NODE_DEFAULT_BOXCOLOR": "#839496",
"NODE_DEFAULT_SHAPE": "box",
"NODE_BOX_OUTLINE_COLOR": "#fdf6e3",
"DEFAULT_SHADOW_COLOR": "rgba(0,0,0,0.5)",
"DEFAULT_GROUP_FONT": 24,
"WIDGET_BGCOLOR": "#002b36",
"WIDGET_OUTLINE_COLOR": "#839496",
"WIDGET_TEXT_COLOR": "#fdf6e3",
"WIDGET_SECONDARY_TEXT_COLOR": "#93a1a1",
"LINK_COLOR": "#2aa198",
"EVENT_LINK_COLOR": "#268bd2",
"CONNECTING_LINK_COLOR": "#859900",
},
},
}
};
@ -192,8 +238,20 @@ app.registerExtension({
if (colorPalette.colors) {
if (colorPalette.colors.node_slot) {
Object.assign(app.canvas.default_connection_color_byType, colorPalette.colors.node_slot);
app.canvas.draw(true, true);
Object.assign(LGraphCanvas.link_type_colors, colorPalette.colors.node_slot);
}
if (colorPalette.colors.litegraph_base) {
// Everything updates correctly in the loop, except the Node Title and Link Color for some reason
app.canvas.node_title_color = colorPalette.colors.litegraph_base.NODE_TITLE_COLOR;
app.canvas.default_link_color = colorPalette.colors.litegraph_base.LINK_COLOR;
for (const key in colorPalette.colors.litegraph_base) {
if (colorPalette.colors.litegraph_base.hasOwnProperty(key) && LiteGraph.hasOwnProperty(key)) {
LiteGraph[key] = colorPalette.colors.litegraph_base[key];
}
}
}
app.canvas.draw(true, true);
}
};

View File

@ -0,0 +1,184 @@
import { app } from "/scripts/app.js";
import { ComfyDialog, $el } from "/scripts/ui.js";
// Adds the ability to save and add multiple nodes as a template
// To save:
// Select multiple nodes (ctrl + drag to select a region or ctrl+click individual nodes)
// Right click the canvas
// Save Node Template -> give it a name
//
// To add:
// Right click the canvas
// Node templates -> click the one to add
//
// To delete/rename:
// Right click the canvas
// Node templates -> Manage
const id = "Comfy.NodeTemplates";
class ManageTemplates extends ComfyDialog {
constructor() {
super();
this.element.classList.add("comfy-manage-templates");
this.templates = this.load();
}
createButtons() {
const btns = super.createButtons();
btns[0].textContent = "Cancel";
btns.unshift(
$el("button", {
type: "button",
textContent: "Save",
onclick: () => this.save(),
})
);
return btns;
}
load() {
const templates = localStorage.getItem(id);
if (templates) {
return JSON.parse(templates);
} else {
return [];
}
}
save() {
// Find all visible inputs and save them as our new list
const inputs = this.element.querySelectorAll("input");
const updated = [];
for (let i = 0; i < inputs.length; i++) {
const input = inputs[i];
if (input.parentElement.style.display !== "none") {
const t = this.templates[i];
t.name = input.value.trim() || input.getAttribute("data-name");
updated.push(t);
}
}
this.templates = updated;
this.store();
this.close();
}
store() {
localStorage.setItem(id, JSON.stringify(this.templates));
}
show() {
// Show list of template names + delete button
super.show(
$el(
"div",
{
style: {
display: "grid",
gridTemplateColumns: "1fr auto",
gap: "5px",
},
},
this.templates.flatMap((t) => {
let nameInput;
return [
$el(
"label",
{
textContent: "Name: ",
},
[
$el("input", {
value: t.name,
dataset: { name: t.name },
$: (el) => (nameInput = el),
}),
]
),
$el("button", {
textContent: "Delete",
style: {
fontSize: "12px",
color: "red",
fontWeight: "normal",
},
onclick: (e) => {
nameInput.value = "";
e.target.style.display = "none";
e.target.previousElementSibling.style.display = "none";
},
}),
];
})
)
);
}
}
app.registerExtension({
name: id,
setup() {
const manage = new ManageTemplates();
const clipboardAction = (cb) => {
// We use the clipboard functions but dont want to overwrite the current user clipboard
// Restore it after we've run our callback
const old = localStorage.getItem("litegrapheditor_clipboard");
cb();
localStorage.setItem("litegrapheditor_clipboard", old);
};
const orig = LGraphCanvas.prototype.getCanvasMenuOptions;
LGraphCanvas.prototype.getCanvasMenuOptions = function () {
const options = orig.apply(this, arguments);
options.push(null);
options.push({
content: `Save Selected as Template`,
disabled: !Object.keys(app.canvas.selected_nodes || {}).length,
callback: () => {
const name = prompt("Enter name");
if (!name || !name.trim()) return;
clipboardAction(() => {
app.canvas.copyToClipboard();
manage.templates.push({
name,
data: localStorage.getItem("litegrapheditor_clipboard"),
});
manage.store();
});
},
});
// Map each template to a menu item
const subItems = manage.templates.map((t) => ({
content: t.name,
callback: () => {
clipboardAction(() => {
localStorage.setItem("litegrapheditor_clipboard", t.data);
app.canvas.pasteFromClipboard();
});
},
}));
if (subItems.length) {
subItems.push(null, {
content: "Manage",
callback: () => manage.show(),
});
options.push({
content: "Node Templates",
submenu: {
options: subItems,
},
});
}
return options;
};
},
});

View File

@ -2,6 +2,7 @@
<html lang="en">
<head>
<meta charset="UTF-8">
<title>ComfyUI</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<link rel="stylesheet" type="text/css" href="lib/litegraph.css" />
<link rel="stylesheet" type="text/css" href="style.css" />

View File

@ -89,6 +89,7 @@
NO_TITLE: 1,
TRANSPARENT_TITLE: 2,
AUTOHIDE_TITLE: 3,
VERTICAL_LAYOUT: "vertical", // arrange nodes vertically
proxy: null, //used to redirect calls
node_images_path: "",
@ -125,14 +126,14 @@
registered_slot_out_types: {}, // slot types for nodeclass
slot_types_in: [], // slot types IN
slot_types_out: [], // slot types OUT
slot_types_default_in: [], // specify for each IN slot type a(/many) deafult node(s), use single string, array, or object (with node, title, parameters, ..) like for search
slot_types_default_out: [], // specify for each OUT slot type a(/many) deafult node(s), use single string, array, or object (with node, title, parameters, ..) like for search
slot_types_default_in: [], // specify for each IN slot type a(/many) default node(s), use single string, array, or object (with node, title, parameters, ..) like for search
slot_types_default_out: [], // specify for each OUT slot type a(/many) default node(s), use single string, array, or object (with node, title, parameters, ..) like for search
alt_drag_do_clone_nodes: false, // [true!] very handy, ALT click to clone and drag the new node
do_add_triggers_slots: false, // [true!] will create and connect event slots when using action/events connections, !WILL CHANGE node mode when using onTrigger (enable mode colors), onExecuted does not need this
allow_multi_output_for_events: true, // [false!] being events, it is strongly reccomended to use them sequentually, one by one
allow_multi_output_for_events: true, // [false!] being events, it is strongly reccomended to use them sequentially, one by one
middle_click_slot_add_default_node: false, //[true!] allows to create and connect a ndoe clicking with the third button (wheel)
@ -158,80 +159,67 @@
console.log("Node registered: " + type);
}
var categories = type.split("/");
var classname = base_class.name;
const classname = base_class.name;
var pos = type.lastIndexOf("/");
base_class.category = type.substr(0, pos);
const pos = type.lastIndexOf("/");
base_class.category = type.substring(0, pos);
if (!base_class.title) {
base_class.title = classname;
}
//info.name = name.substr(pos+1,name.length - pos);
//extend class
if (base_class.prototype) {
//is a class
for (var i in LGraphNode.prototype) {
if (!base_class.prototype[i]) {
base_class.prototype[i] = LGraphNode.prototype[i];
}
for (var i in LGraphNode.prototype) {
if (!base_class.prototype[i]) {
base_class.prototype[i] = LGraphNode.prototype[i];
}
}
var prev = this.registered_node_types[type];
if(prev)
console.log("replacing node type: " + type);
else
{
if( !Object.hasOwnProperty( base_class.prototype, "shape") )
Object.defineProperty(base_class.prototype, "shape", {
set: function(v) {
switch (v) {
case "default":
delete this._shape;
break;
case "box":
this._shape = LiteGraph.BOX_SHAPE;
break;
case "round":
this._shape = LiteGraph.ROUND_SHAPE;
break;
case "circle":
this._shape = LiteGraph.CIRCLE_SHAPE;
break;
case "card":
this._shape = LiteGraph.CARD_SHAPE;
break;
default:
this._shape = v;
}
},
get: function(v) {
return this._shape;
},
enumerable: true,
configurable: true
});
const prev = this.registered_node_types[type];
if(prev) {
console.log("replacing node type: " + type);
}
if( !Object.prototype.hasOwnProperty.call( base_class.prototype, "shape") ) {
Object.defineProperty(base_class.prototype, "shape", {
set: function(v) {
switch (v) {
case "default":
delete this._shape;
break;
case "box":
this._shape = LiteGraph.BOX_SHAPE;
break;
case "round":
this._shape = LiteGraph.ROUND_SHAPE;
break;
case "circle":
this._shape = LiteGraph.CIRCLE_SHAPE;
break;
case "card":
this._shape = LiteGraph.CARD_SHAPE;
break;
default:
this._shape = v;
}
},
get: function() {
return this._shape;
},
enumerable: true,
configurable: true
});
//warnings
if (base_class.prototype.onPropertyChange) {
console.warn(
"LiteGraph node class " +
type +
" has onPropertyChange method, it must be called onPropertyChanged with d at the end"
);
}
//used to know which nodes create when dragging files to the canvas
if (base_class.supported_extensions) {
for (var i in base_class.supported_extensions) {
var ext = base_class.supported_extensions[i];
if(ext && ext.constructor === String)
this.node_types_by_file_extension[ ext.toLowerCase() ] = base_class;
}
}
}
//used to know which nodes to create when dragging files to the canvas
if (base_class.supported_extensions) {
for (let i in base_class.supported_extensions) {
const ext = base_class.supported_extensions[i];
if(ext && ext.constructor === String) {
this.node_types_by_file_extension[ ext.toLowerCase() ] = base_class;
}
}
}
}
this.registered_node_types[type] = base_class;
if (base_class.constructor.name) {
@ -252,19 +240,11 @@
" has onPropertyChange method, it must be called onPropertyChanged with d at the end"
);
}
//used to know which nodes create when dragging files to the canvas
if (base_class.supported_extensions) {
for (var i=0; i < base_class.supported_extensions.length; i++) {
var ext = base_class.supported_extensions[i];
if(ext && ext.constructor === String)
this.node_types_by_file_extension[ ext.toLowerCase() ] = base_class;
}
}
// TODO one would want to know input and ouput :: this would allow trought registerNodeAndSlotType to get all the slots types
//console.debug("Registering "+type);
if (this.auto_load_slot_types) nodeTmp = new base_class(base_class.title || "tmpnode");
// TODO one would want to know input and ouput :: this would allow through registerNodeAndSlotType to get all the slots types
if (this.auto_load_slot_types) {
new base_class(base_class.title || "tmpnode");
}
},
/**
@ -1260,37 +1240,39 @@
* Positions every node in a more readable manner
* @method arrange
*/
LGraph.prototype.arrange = function(margin) {
LGraph.prototype.arrange = function (margin, layout) {
margin = margin || 100;
var nodes = this.computeExecutionOrder(false, true);
var columns = [];
for (var i = 0; i < nodes.length; ++i) {
var node = nodes[i];
var col = node._level || 1;
const nodes = this.computeExecutionOrder(false, true);
const columns = [];
for (let i = 0; i < nodes.length; ++i) {
const node = nodes[i];
const col = node._level || 1;
if (!columns[col]) {
columns[col] = [];
}
columns[col].push(node);
}
var x = margin;
let x = margin;
for (var i = 0; i < columns.length; ++i) {
var column = columns[i];
for (let i = 0; i < columns.length; ++i) {
const column = columns[i];
if (!column) {
continue;
}
var max_size = 100;
var y = margin + LiteGraph.NODE_TITLE_HEIGHT;
for (var j = 0; j < column.length; ++j) {
var node = column[j];
node.pos[0] = x;
node.pos[1] = y;
if (node.size[0] > max_size) {
max_size = node.size[0];
let max_size = 100;
let y = margin + LiteGraph.NODE_TITLE_HEIGHT;
for (let j = 0; j < column.length; ++j) {
const node = column[j];
node.pos[0] = (layout == LiteGraph.VERTICAL_LAYOUT) ? y : x;
node.pos[1] = (layout == LiteGraph.VERTICAL_LAYOUT) ? x : y;
const max_size_index = (layout == LiteGraph.VERTICAL_LAYOUT) ? 1 : 0;
if (node.size[max_size_index] > max_size) {
max_size = node.size[max_size_index];
}
y += node.size[1] + margin + LiteGraph.NODE_TITLE_HEIGHT;
const node_size_index = (layout == LiteGraph.VERTICAL_LAYOUT) ? 0 : 1;
y += node.size[node_size_index] + margin + LiteGraph.NODE_TITLE_HEIGHT;
}
x += max_size + margin;
}
@ -2468,43 +2450,34 @@
this.title = this.constructor.title;
}
if (this.onConnectionsChange) {
if (this.inputs) {
for (var i = 0; i < this.inputs.length; ++i) {
var input = this.inputs[i];
var link_info = this.graph
? this.graph.links[input.link]
: null;
this.onConnectionsChange(
LiteGraph.INPUT,
i,
true,
link_info,
input
); //link_info has been created now, so its updated
}
}
if (this.inputs) {
for (var i = 0; i < this.inputs.length; ++i) {
var input = this.inputs[i];
var link_info = this.graph ? this.graph.links[input.link] : null;
if (this.onConnectionsChange)
this.onConnectionsChange( LiteGraph.INPUT, i, true, link_info, input ); //link_info has been created now, so its updated
if (this.outputs) {
for (var i = 0; i < this.outputs.length; ++i) {
var output = this.outputs[i];
if (!output.links) {
continue;
}
for (var j = 0; j < output.links.length; ++j) {
var link_info = this.graph
? this.graph.links[output.links[j]]
: null;
this.onConnectionsChange(
LiteGraph.OUTPUT,
i,
true,
link_info,
output
); //link_info has been created now, so its updated
}
}
}
if( this.onInputAdded )
this.onInputAdded(input);
}
}
if (this.outputs) {
for (var i = 0; i < this.outputs.length; ++i) {
var output = this.outputs[i];
if (!output.links) {
continue;
}
for (var j = 0; j < output.links.length; ++j) {
var link_info = this.graph ? this.graph.links[output.links[j]] : null;
if (this.onConnectionsChange)
this.onConnectionsChange( LiteGraph.OUTPUT, i, true, link_info, output ); //link_info has been created now, so its updated
}
if( this.onOutputAdded )
this.onOutputAdded(output);
}
}
if( this.widgets )
@ -3200,6 +3173,15 @@
return;
}
if(slot == null)
{
console.error("slot must be a number");
return;
}
if(slot.constructor !== Number)
console.warn("slot must be a number, use node.trigger('name') if you want to use a string");
var output = this.outputs[slot];
if (!output) {
return;
@ -3346,26 +3328,26 @@
* @param {Object} extra_info this can be used to have special properties of an output (label, special color, position, etc)
*/
LGraphNode.prototype.addOutput = function(name, type, extra_info) {
var o = { name: name, type: type, links: null };
var output = { name: name, type: type, links: null };
if (extra_info) {
for (var i in extra_info) {
o[i] = extra_info[i];
output[i] = extra_info[i];
}
}
if (!this.outputs) {
this.outputs = [];
}
this.outputs.push(o);
this.outputs.push(output);
if (this.onOutputAdded) {
this.onOutputAdded(o);
this.onOutputAdded(output);
}
if (LiteGraph.auto_load_slot_types) LiteGraph.registerNodeAndSlotType(this,type,true);
this.setSize( this.computeSize() );
this.setDirtyCanvas(true, true);
return o;
return output;
};
/**
@ -3437,10 +3419,10 @@
*/
LGraphNode.prototype.addInput = function(name, type, extra_info) {
type = type || 0;
var o = { name: name, type: type, link: null };
var input = { name: name, type: type, link: null };
if (extra_info) {
for (var i in extra_info) {
o[i] = extra_info[i];
input[i] = extra_info[i];
}
}
@ -3448,17 +3430,17 @@
this.inputs = [];
}
this.inputs.push(o);
this.inputs.push(input);
this.setSize( this.computeSize() );
if (this.onInputAdded) {
this.onInputAdded(o);
this.onInputAdded(input);
}
LiteGraph.registerNodeAndSlotType(this,type);
this.setDirtyCanvas(true, true);
return o;
return input;
};
/**
@ -5210,6 +5192,7 @@ LGraphNode.prototype.executeAction = function(action)
this.allow_dragcanvas = true;
this.allow_dragnodes = true;
this.allow_interaction = true; //allow to control widgets, buttons, collapse, etc
this.multi_select = false; //allow selecting multi nodes without pressing extra keys
this.allow_searchbox = true;
this.allow_reconnect_links = true; //allows to change a connection with having to redo it again
this.align_to_grid = false; //snap to grid
@ -5435,7 +5418,7 @@ LGraphNode.prototype.executeAction = function(action)
};
/**
* returns the visualy active graph (in case there are more in the stack)
* returns the visually active graph (in case there are more in the stack)
* @method getCurrentGraph
* @return {LGraph} the active graph
*/
@ -6060,9 +6043,13 @@ LGraphNode.prototype.executeAction = function(action)
this.graph.beforeChange();
this.node_dragged = node;
}
if (!this.selected_nodes[node.id]) {
this.processNodeSelected(node, e);
}
this.processNodeSelected(node, e);
} else { // double-click
/**
* Don't call the function if the block is already selected.
* Otherwise, it could cause the block to be unselected while its panel is open.
*/
if (!node.is_selected) this.processNodeSelected(node, e);
}
this.dirty_canvas = true;
@ -6474,6 +6461,10 @@ LGraphNode.prototype.executeAction = function(action)
var n = this.selected_nodes[i];
n.pos[0] += delta[0] / this.ds.scale;
n.pos[1] += delta[1] / this.ds.scale;
if (!n.is_selected) this.processNodeSelected(n, e); /*
* Don't call the function if the block is already selected.
* Otherwise, it could cause the block to be unselected while dragging.
*/
}
this.dirty_canvas = true;
@ -7287,7 +7278,7 @@ LGraphNode.prototype.executeAction = function(action)
};
LGraphCanvas.prototype.processNodeSelected = function(node, e) {
this.selectNode(node, e && (e.shiftKey||e.ctrlKey));
this.selectNode(node, e && (e.shiftKey || e.ctrlKey || this.multi_select));
if (this.onNodeSelected) {
this.onNodeSelected(node);
}
@ -7323,6 +7314,7 @@ LGraphNode.prototype.executeAction = function(action)
for (var i in nodes) {
var node = nodes[i];
if (node.is_selected) {
this.deselectNode(node);
continue;
}
@ -9742,13 +9734,17 @@ LGraphNode.prototype.executeAction = function(action)
ctx.fillRect(margin, y, widget_width - margin * 2, H);
var range = w.options.max - w.options.min;
var nvalue = (w.value - w.options.min) / range;
ctx.fillStyle = active_widget == w ? "#89A" : "#678";
if(nvalue < 0.0) nvalue = 0.0;
if(nvalue > 1.0) nvalue = 1.0;
ctx.fillStyle = w.options.hasOwnProperty("slider_color") ? w.options.slider_color : (active_widget == w ? "#89A" : "#678");
ctx.fillRect(margin, y, nvalue * (widget_width - margin * 2), H);
if(show_text && !w.disabled)
ctx.strokeRect(margin, y, widget_width - margin * 2, H);
if (w.marker) {
var marker_nvalue = (w.marker - w.options.min) / range;
ctx.fillStyle = "#AA9";
if(marker_nvalue < 0.0) marker_nvalue = 0.0;
if(marker_nvalue > 1.0) marker_nvalue = 1.0;
ctx.fillStyle = w.options.hasOwnProperty("marker_color") ? w.options.marker_color : "#AA9";
ctx.fillRect( margin + marker_nvalue * (widget_width - margin * 2), y, 2, H );
}
if (show_text) {
@ -9915,6 +9911,7 @@ LGraphNode.prototype.executeAction = function(action)
case "slider":
var range = w.options.max - w.options.min;
var nvalue = Math.clamp((x - 15) / (widget_width - 30), 0, 1);
if(w.options.read_only) break;
w.value = w.options.min + (w.options.max - w.options.min) * nvalue;
if (w.callback) {
setTimeout(function() {
@ -9926,8 +9923,16 @@ LGraphNode.prototype.executeAction = function(action)
case "number":
case "combo":
var old_value = w.value;
if (event.type == LiteGraph.pointerevents_method+"move" && w.type == "number") {
w.value += event.deltaX * 0.1 * (w.options.step || 1);
var delta = x < 40 ? -1 : x > widget_width - 40 ? 1 : 0;
var allow_scroll = true;
if (delta) {
if (x > -3 && x < widget_width + 3) {
allow_scroll = false;
}
}
if (allow_scroll && event.type == LiteGraph.pointerevents_method+"move" && w.type == "number") {
if(event.deltaX)
w.value += event.deltaX * 0.1 * (w.options.step || 1);
if ( w.options.min != null && w.value < w.options.min ) {
w.value = w.options.min;
}
@ -9994,6 +9999,12 @@ LGraphNode.prototype.executeAction = function(action)
var delta = x < 40 ? -1 : x > widget_width - 40 ? 1 : 0;
if (event.click_time < 200 && delta == 0) {
this.prompt("Value",w.value,function(v) {
// check if v is a valid equation or a number
if (/^[0-9+\-*/()\s]+$/.test(v)) {
try {//solve the equation if possible
v = eval(v);
} catch (e) { }
}
this.value = Number(v);
inner_value_change(this, this.value);
}.bind(w),
@ -10022,7 +10033,6 @@ LGraphNode.prototype.executeAction = function(action)
case "text":
if (event.type == LiteGraph.pointerevents_method+"down") {
this.prompt("Value",w.value,function(v) {
this.value = v;
inner_value_change(this, v);
}.bind(w),
event,w.options ? w.options.multiline : false );
@ -10047,6 +10057,9 @@ LGraphNode.prototype.executeAction = function(action)
}//end for
function inner_value_change(widget, value) {
if(widget.type == "number"){
value = Number(value);
}
widget.value = value;
if ( widget.options && widget.options.property && node.properties[widget.options.property] !== undefined ) {
node.setProperty( widget.options.property, value );
@ -11165,7 +11178,7 @@ LGraphNode.prototype.executeAction = function(action)
LGraphCanvas.search_limit = -1;
LGraphCanvas.prototype.showSearchBox = function(event, options) {
// proposed defaults
def_options = { slot_from: null
var def_options = { slot_from: null
,node_from: null
,node_to: null
,do_type_filter: LiteGraph.search_filter_enabled // TODO check for registered_slot_[in/out]_types not empty // this will be checked for functionality enabled : filter on slot type, in and out
@ -11863,7 +11876,7 @@ LGraphNode.prototype.executeAction = function(action)
// TODO refactor, theer are different dialog, some uses createDialog, some dont
LGraphCanvas.prototype.createDialog = function(html, options) {
def_options = { checkForInput: false, closeOnLeave: true, closeOnLeave_checkModified: true };
var def_options = { checkForInput: false, closeOnLeave: true, closeOnLeave_checkModified: true };
options = Object.assign(def_options, options || {});
var dialog = document.createElement("div");
@ -11993,7 +12006,8 @@ LGraphNode.prototype.executeAction = function(action)
if (root.onClose && typeof root.onClose == "function"){
root.onClose();
}
root.parentNode.removeChild(root);
if(root.parentNode)
root.parentNode.removeChild(root);
/* XXX CHECK THIS */
if(this.parentNode){
this.parentNode.removeChild(this);
@ -12285,7 +12299,7 @@ LGraphNode.prototype.executeAction = function(action)
var ref_window = this.getCanvasWindow();
var that = this;
var graphcanvas = this;
panel = this.createPanel(node.title || "",{
var panel = this.createPanel(node.title || "",{
closable: true
,window: ref_window
,onOpen: function(){

View File

@ -1,5 +1,5 @@
import { ComfyWidgets } from "./widgets.js";
import { ComfyUI } from "./ui.js";
import { ComfyUI, $el } from "./ui.js";
import { api } from "./api.js";
import { defaultGraph } from "./defaultGraph.js";
import { getPngMetadata, importA1111 } from "./pnginfo.js";
@ -835,7 +835,7 @@ class ComfyApp {
app.#invokeExtensionsAsync("nodeCreated", this);
},
{
title: nodeData.name,
title: nodeData.display_name || nodeData.name,
comfyClass: nodeData.name,
}
);
@ -864,12 +864,62 @@ class ComfyApp {
graphData = structuredClone(defaultGraph);
}
// Patch T2IAdapterLoader to ControlNetLoader since they are the same node now
const missingNodeTypes = [];
for (let n of graphData.nodes) {
// Patch T2IAdapterLoader to ControlNetLoader since they are the same node now
if (n.type == "T2IAdapterLoader") n.type = "ControlNetLoader";
// Find missing node types
if (!(n.type in LiteGraph.registered_node_types)) {
missingNodeTypes.push(n.type);
}
}
this.graph.configure(graphData);
try {
this.graph.configure(graphData);
} catch (error) {
let errorHint = [];
// Try extracting filename to see if it was caused by an extension script
const filename = error.fileName || (error.stack || "").match(/(\/extensions\/.*\.js)/)?.[1];
const pos = (filename || "").indexOf("/extensions/");
if (pos > -1) {
errorHint.push(
$el("span", { textContent: "This may be due to the following script:" }),
$el("br"),
$el("span", {
style: {
fontWeight: "bold",
},
textContent: filename.substring(pos),
})
);
}
// Show dialog to let the user know something went wrong loading the data
this.ui.dialog.show(
$el("div", [
$el("p", { textContent: "Loading aborted due to error reloading workflow data" }),
$el("pre", {
style: { padding: "5px", backgroundColor: "rgba(255,0,0,0.2)" },
textContent: error.toString(),
}),
$el("pre", {
style: {
padding: "5px",
color: "#ccc",
fontSize: "10px",
maxHeight: "50vh",
overflow: "auto",
backgroundColor: "rgba(0,0,0,0.2)",
},
textContent: error.stack || "No stacktrace available",
}),
...errorHint,
]).outerHTML
);
return;
}
for (const node of this.graph._nodes) {
const size = node.computeSize();
@ -893,6 +943,14 @@ class ComfyApp {
this.#invokeExtensions("loadedGraphNode", node);
}
if (missingNodeTypes.length) {
this.ui.dialog.show(
`When loading the graph, the following node types were not found: <ul>${Array.from(new Set(missingNodeTypes)).map(
(t) => `<li>${t}</li>`
).join("")}</ul>Nodes that have failed to load will show as red on the graph.`
);
}
}
/**

View File

@ -13,7 +13,7 @@ export const defaultGraph = {
inputs: [{ name: "clip", type: "CLIP", link: 5 }],
outputs: [{ name: "CONDITIONING", type: "CONDITIONING", links: [6], slot_index: 0 }],
properties: {},
widgets_values: ["bad hands"],
widgets_values: ["text, watermark"],
},
{
id: 6,
@ -26,7 +26,7 @@ export const defaultGraph = {
inputs: [{ name: "clip", type: "CLIP", link: 3 }],
outputs: [{ name: "CONDITIONING", type: "CONDITIONING", links: [4], slot_index: 0 }],
properties: {},
widgets_values: ["masterpiece best quality girl"],
widgets_values: ["beautiful scenery nature glass bottle landscape, , purple galaxy bottle,"],
},
{
id: 5,
@ -56,7 +56,7 @@ export const defaultGraph = {
],
outputs: [{ name: "LATENT", type: "LATENT", links: [7], slot_index: 0 }],
properties: {},
widgets_values: [8566257, true, 20, 8, "euler", "normal", 1],
widgets_values: [156680208700286, true, 20, 8, "euler", "normal", 1],
},
{
id: 8,

View File

@ -32,8 +32,9 @@ export function getPngMetadata(file) {
}
const keyword = String.fromCharCode(...pngData.slice(offset + 8, keyword_end));
// Get the text
const text = String.fromCharCode(...pngData.slice(keyword_end + 1, offset + 8 + length));
txt_chunks[keyword] = text;
const contentArraySegment = pngData.slice(keyword_end + 1, offset + 8 + length);
const contentJson = Array.from(contentArraySegment).map(s=>String.fromCharCode(s)).join('')
txt_chunks[keyword] = contentJson;
}
offset += 12 + length;

View File

@ -8,14 +8,18 @@ export function $el(tag, propsOrChildren, children) {
if (Array.isArray(propsOrChildren)) {
element.append(...propsOrChildren);
} else {
const parent = propsOrChildren.parent;
const { parent, $: cb, dataset, style } = propsOrChildren;
delete propsOrChildren.parent;
const cb = propsOrChildren.$;
delete propsOrChildren.$;
delete propsOrChildren.dataset;
delete propsOrChildren.style;
if (propsOrChildren.style) {
Object.assign(element.style, propsOrChildren.style);
delete propsOrChildren.style;
if (style) {
Object.assign(element.style, style);
}
if (dataset) {
Object.assign(element.dataset, dataset);
}
Object.assign(element, propsOrChildren);
@ -76,7 +80,7 @@ function dragElement(dragEl, settings) {
dragEl.style.left = newPosX + "px";
dragEl.style.right = "unset";
}
dragEl.style.top = newPosY + "px";
dragEl.style.bottom = "unset";
@ -145,7 +149,7 @@ function dragElement(dragEl, settings) {
}
window.addEventListener("resize", () => {
ensureInBounds();
ensureInBounds();
});
function closeDragElement() {
@ -155,26 +159,33 @@ function dragElement(dragEl, settings) {
}
}
class ComfyDialog {
export class ComfyDialog {
constructor() {
this.element = $el("div.comfy-modal", { parent: document.body }, [
$el("div.comfy-modal-content", [
$el("p", { $: (p) => (this.textElement = p) }),
$el("button", {
type: "button",
textContent: "Close",
onclick: () => this.close(),
}),
]),
$el("div.comfy-modal-content", [$el("p", { $: (p) => (this.textElement = p) }), ...this.createButtons()]),
]);
}
createButtons() {
return [
$el("button", {
type: "button",
textContent: "Close",
onclick: () => this.close(),
}),
];
}
close() {
this.element.style.display = "none";
}
show(html) {
this.textElement.innerHTML = html;
if (typeof html === "string") {
this.textElement.innerHTML = html;
} else {
this.textElement.replaceChildren(html);
}
this.element.style.display = "flex";
}
}
@ -419,7 +430,7 @@ export class ComfyUI {
type: "boolean",
defaultValue: true,
});
const fileInput = $el("input", {
type: "file",
accept: ".json,image/png",

View File

@ -202,7 +202,8 @@ button.comfy-queue-btn {
margin: 6px 0 !important;
}
.comfy-modal.comfy-settings {
.comfy-modal.comfy-settings,
.comfy-modal.comfy-manage-templates {
text-align: center;
font-family: sans-serif;
color: #999;
@ -256,3 +257,16 @@ button.comfy-queue-btn {
color: #ddd;
border-radius: 12px 0 0 12px;
}
.litegraph .litemenu-entry.has_submenu {
position: relative;
padding-right: 20px;
}
.litemenu-entry.has_submenu::after {
content: ">";
position: absolute;
top: 0;
right: 2px;
}