Merge branch 'comfyanonymous:master' into custom-node-socket

This commit is contained in:
pythongosssss 2023-03-26 10:12:39 +01:00 committed by GitHub
commit a49b5659bc
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 714 additions and 89 deletions

View File

@ -18,7 +18,7 @@ class DDIMSampler(object):
def register_buffer(self, name, attr): def register_buffer(self, name, attr):
if type(attr) == torch.Tensor: if type(attr) == torch.Tensor:
if attr.device != self.device: if attr.device != self.device:
attr = attr.to(self.device) attr = attr.float().to(self.device)
setattr(self, name, attr) setattr(self, name, attr)
def make_schedule(self, ddim_num_steps, ddim_discretize="uniform", ddim_eta=0., verbose=True): def make_schedule(self, ddim_num_steps, ddim_discretize="uniform", ddim_eta=0., verbose=True):

View File

@ -4,6 +4,7 @@ NO_VRAM = 1
LOW_VRAM = 2 LOW_VRAM = 2
NORMAL_VRAM = 3 NORMAL_VRAM = 3
HIGH_VRAM = 4 HIGH_VRAM = 4
MPS = 5
accelerate_enabled = False accelerate_enabled = False
vram_state = NORMAL_VRAM vram_state = NORMAL_VRAM
@ -76,10 +77,16 @@ if set_vram_to == LOW_VRAM or set_vram_to == NO_VRAM:
total_vram_available_mb = (total_vram - 1024) // 2 total_vram_available_mb = (total_vram - 1024) // 2
total_vram_available_mb = int(max(256, total_vram_available_mb)) total_vram_available_mb = int(max(256, total_vram_available_mb))
try:
if torch.backends.mps.is_available():
vram_state = MPS
except:
pass
if "--cpu" in sys.argv: if "--cpu" in sys.argv:
vram_state = CPU vram_state = CPU
print("Set vram state to:", ["CPU", "NO VRAM", "LOW VRAM", "NORMAL VRAM", "HIGH VRAM"][vram_state]) print("Set vram state to:", ["CPU", "NO VRAM", "LOW VRAM", "NORMAL VRAM", "HIGH VRAM", "MPS"][vram_state])
current_loaded_model = None current_loaded_model = None
@ -128,6 +135,10 @@ def load_model_gpu(model):
current_loaded_model = model current_loaded_model = model
if vram_state == CPU: if vram_state == CPU:
pass pass
elif vram_state == MPS:
mps_device = torch.device("mps")
real_model.to(mps_device)
pass
elif vram_state == NORMAL_VRAM or vram_state == HIGH_VRAM: elif vram_state == NORMAL_VRAM or vram_state == HIGH_VRAM:
model_accelerated = False model_accelerated = False
real_model.cuda() real_model.cuda()
@ -155,9 +166,10 @@ def load_controlnet_gpu(models):
if m not in models: if m not in models:
m.cpu() m.cpu()
device = get_torch_device()
current_gpu_controlnets = [] current_gpu_controlnets = []
for m in models: for m in models:
current_gpu_controlnets.append(m.cuda()) current_gpu_controlnets.append(m.to(device))
def load_if_low_vram(model): def load_if_low_vram(model):
@ -173,6 +185,8 @@ def unload_if_low_vram(model):
return model return model
def get_torch_device(): def get_torch_device():
if vram_state == MPS:
return torch.device("mps")
if vram_state == CPU: if vram_state == CPU:
return torch.device("cpu") return torch.device("cpu")
else: else:
@ -195,7 +209,7 @@ def get_free_memory(dev=None, torch_free_too=False):
if dev is None: if dev is None:
dev = get_torch_device() dev = get_torch_device()
if hasattr(dev, 'type') and dev.type == 'cpu': if hasattr(dev, 'type') and (dev.type == 'cpu' or dev.type == 'mps'):
mem_free_total = psutil.virtual_memory().available mem_free_total = psutil.virtual_memory().available
mem_free_torch = mem_free_total mem_free_torch = mem_free_total
else: else:
@ -224,8 +238,12 @@ def cpu_mode():
global vram_state global vram_state
return vram_state == CPU return vram_state == CPU
def mps_mode():
global vram_state
return vram_state == MPS
def should_use_fp16(): def should_use_fp16():
if cpu_mode(): if cpu_mode() or mps_mode():
return False #TODO ? return False #TODO ?
if torch.cuda.is_bf16_supported(): if torch.cuda.is_bf16_supported():

View File

@ -450,7 +450,7 @@ class KSampler:
noise_mask = None noise_mask = None
if denoise_mask is not None: if denoise_mask is not None:
noise_mask = 1.0 - denoise_mask noise_mask = 1.0 - denoise_mask
sampler = DDIMSampler(self.model) sampler = DDIMSampler(self.model, device=self.device)
sampler.make_schedule_timesteps(ddim_timesteps=timesteps, verbose=False) sampler.make_schedule_timesteps(ddim_timesteps=timesteps, verbose=False)
z_enc = sampler.stochastic_encode(latent_image, torch.tensor([len(timesteps) - 1] * noise.shape[0]).to(self.device), noise=noise, max_denoise=max_denoise) z_enc = sampler.stochastic_encode(latent_image, torch.tensor([len(timesteps) - 1] * noise.shape[0]).to(self.device), noise=noise, max_denoise=max_denoise)
samples, _ = sampler.sample_custom(ddim_timesteps=timesteps, samples, _ = sampler.sample_custom(ddim_timesteps=timesteps,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 115 KiB

After

Width:  |  Height:  |  Size: 110 KiB

View File

@ -241,8 +241,8 @@ class LoraLoader:
return {"required": { "model": ("MODEL",), return {"required": { "model": ("MODEL",),
"clip": ("CLIP", ), "clip": ("CLIP", ),
"lora_name": (folder_paths.get_filename_list("loras"), ), "lora_name": (folder_paths.get_filename_list("loras"), ),
"strength_model": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01}), "strength_model": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01}),
"strength_clip": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 10.0, "step": 0.01}), "strength_clip": ("FLOAT", {"default": 1.0, "min": -10.0, "max": 10.0, "step": 0.01}),
}} }}
RETURN_TYPES = ("MODEL", "CLIP") RETURN_TYPES = ("MODEL", "CLIP")
FUNCTION = "load_lora" FUNCTION = "load_lora"
@ -752,7 +752,7 @@ class SaveImage:
full_output_folder = os.path.join(self.output_dir, subfolder) full_output_folder = os.path.join(self.output_dir, subfolder)
if os.path.commonpath((self.output_dir, os.path.realpath(full_output_folder))) != self.output_dir: if os.path.commonpath((self.output_dir, os.path.abspath(full_output_folder))) != self.output_dir:
print("Saving image outside the output folder is not allowed.") print("Saving image outside the output folder is not allowed.")
return {} return {}
@ -908,6 +908,69 @@ class ImageInvert:
return (s,) return (s,)
class ImagePadForOutpaint:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"image": ("IMAGE",),
"left": ("INT", {"default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 64}),
"top": ("INT", {"default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 64}),
"right": ("INT", {"default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 64}),
"bottom": ("INT", {"default": 0, "min": 0, "max": MAX_RESOLUTION, "step": 64}),
"feathering": ("INT", {"default": 40, "min": 0, "max": MAX_RESOLUTION, "step": 1}),
}
}
RETURN_TYPES = ("IMAGE", "MASK")
FUNCTION = "expand_image"
CATEGORY = "image"
def expand_image(self, image, left, top, right, bottom, feathering):
d1, d2, d3, d4 = image.size()
new_image = torch.zeros(
(d1, d2 + top + bottom, d3 + left + right, d4),
dtype=torch.float32,
)
new_image[:, top:top + d2, left:left + d3, :] = image
mask = torch.ones(
(d2 + top + bottom, d3 + left + right),
dtype=torch.float32,
)
t = torch.zeros(
(d2, d3),
dtype=torch.float32
)
if feathering > 0 and feathering * 2 < d2 and feathering * 2 < d3:
for i in range(d2):
for j in range(d3):
dt = i if top != 0 else d2
db = d2 - i if bottom != 0 else d2
dl = j if left != 0 else d3
dr = d3 - j if right != 0 else d3
d = min(dt, db, dl, dr)
if d >= feathering:
continue
v = (feathering - d) / feathering
t[i, j] = v * v
mask[top:top + d2, left:left + d3] = t
return (new_image, mask)
NODE_CLASS_MAPPINGS = { NODE_CLASS_MAPPINGS = {
"KSampler": KSampler, "KSampler": KSampler,
"CheckpointLoader": CheckpointLoader, "CheckpointLoader": CheckpointLoader,
@ -926,6 +989,7 @@ NODE_CLASS_MAPPINGS = {
"LoadImageMask": LoadImageMask, "LoadImageMask": LoadImageMask,
"ImageScale": ImageScale, "ImageScale": ImageScale,
"ImageInvert": ImageInvert, "ImageInvert": ImageInvert,
"ImagePadForOutpaint": ImagePadForOutpaint,
"ConditioningCombine": ConditioningCombine, "ConditioningCombine": ConditioningCombine,
"ConditioningSetArea": ConditioningSetArea, "ConditioningSetArea": ConditioningSetArea,
"KSamplerAdvanced": KSamplerAdvanced, "KSamplerAdvanced": KSamplerAdvanced,

View File

@ -1,29 +1,13 @@
{ {
"nbformat": 4,
"nbformat_minor": 0,
"metadata": {
"colab": {
"provenance": []
},
"kernelspec": {
"name": "python3",
"display_name": "Python 3"
},
"language_info": {
"name": "python"
},
"accelerator": "GPU",
"gpuClass": "standard"
},
"cells": [ "cells": [
{ {
"cell_type": "markdown", "cell_type": "markdown",
"source": [
"Git clone the repo and install the requirements. (ignore the pip errors about protobuf)"
],
"metadata": { "metadata": {
"id": "aaaaaaaaaa" "id": "aaaaaaaaaa"
} },
"source": [
"Git clone the repo and install the requirements. (ignore the pip errors about protobuf)"
]
}, },
{ {
"cell_type": "code", "cell_type": "code",
@ -33,22 +17,55 @@
}, },
"outputs": [], "outputs": [],
"source": [ "source": [
"!git clone https://github.com/comfyanonymous/ComfyUI\n", "#@title Environment Setup\n",
"%cd ComfyUI\n", "\n",
"!pip install xformers -r requirements.txt" "from pathlib import Path\n",
"\n",
"OPTIONS = {}\n",
"\n",
"USE_GOOGLE_DRIVE = False #@param {type:\"boolean\"}\n",
"UPDATE_COMFY_UI = True #@param {type:\"boolean\"}\n",
"WORKSPACE = 'ComfyUI'\n",
"OPTIONS['USE_GOOGLE_DRIVE'] = USE_GOOGLE_DRIVE\n",
"OPTIONS['UPDATE_COMFY_UI'] = UPDATE_COMFY_UI\n",
"\n",
"if OPTIONS['USE_GOOGLE_DRIVE']:\n",
" !echo \"Mounting Google Drive...\"\n",
" %cd /\n",
" \n",
" from google.colab import drive\n",
" drive.mount('/content/drive')\n",
"\n",
" WORKSPACE = \"/content/drive/MyDrive/ComfyUI\"\n",
" %cd /content/drive/MyDrive\n",
"\n",
"![ ! -d $WORKSPACE ] && echo -= Initial setup ComfyUI =- && git clone https://github.com/comfyanonymous/ComfyUI\n",
"%cd $WORKSPACE\n",
"\n",
"if OPTIONS['UPDATE_COMFY_UI']:\n",
" !echo -= Updating ComfyUI =-\n",
" !git pull\n",
"\n",
"!echo -= Install dependencies =-\n",
"!pip -q install xformers -r requirements.txt"
] ]
}, },
{ {
"cell_type": "markdown", "cell_type": "markdown",
"source": [
"Download some models/checkpoints/vae or custom comfyui nodes (uncomment the commands for the ones you want)"
],
"metadata": { "metadata": {
"id": "cccccccccc" "id": "cccccccccc"
} },
"source": [
"Download some models/checkpoints/vae or custom comfyui nodes (uncomment the commands for the ones you want)"
]
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": null,
"metadata": {
"id": "dddddddddd"
},
"outputs": [],
"source": [ "source": [
"# Checkpoints\n", "# Checkpoints\n",
"\n", "\n",
@ -110,26 +127,26 @@
"#!wget -c https://huggingface.co/sberbank-ai/Real-ESRGAN/resolve/main/RealESRGAN_x4.pth -P ./models/upscale_models/\n", "#!wget -c https://huggingface.co/sberbank-ai/Real-ESRGAN/resolve/main/RealESRGAN_x4.pth -P ./models/upscale_models/\n",
"\n", "\n",
"\n" "\n"
], ]
"metadata": {
"id": "dddddddddd"
},
"execution_count": null,
"outputs": []
}, },
{ {
"cell_type": "markdown", "cell_type": "markdown",
"metadata": {
"id": "kkkkkkkkkkkkkk"
},
"source": [ "source": [
"### Run ComfyUI with localtunnel (Recommended Way)\n", "### Run ComfyUI with localtunnel (Recommended Way)\n",
"\n", "\n",
"\n" "\n"
], ]
"metadata": {
"id": "kkkkkkkkkkkkkk"
}
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": null,
"metadata": {
"id": "jjjjjjjjjjjjj"
},
"outputs": [],
"source": [ "source": [
"!npm install -g localtunnel\n", "!npm install -g localtunnel\n",
"\n", "\n",
@ -154,15 +171,13 @@
"threading.Thread(target=iframe_thread, daemon=True, args=(8188,)).start()\n", "threading.Thread(target=iframe_thread, daemon=True, args=(8188,)).start()\n",
"\n", "\n",
"!python main.py --dont-print-server" "!python main.py --dont-print-server"
], ]
"metadata": {
"id": "jjjjjjjjjjjjj"
},
"execution_count": null,
"outputs": []
}, },
{ {
"cell_type": "markdown", "cell_type": "markdown",
"metadata": {
"id": "gggggggggg"
},
"source": [ "source": [
"### Run ComfyUI with colab iframe (use only in case the previous way with localtunnel doesn't work)\n", "### Run ComfyUI with colab iframe (use only in case the previous way with localtunnel doesn't work)\n",
"\n", "\n",
@ -171,13 +186,15 @@
"If you want to open it in another window use the link.\n", "If you want to open it in another window use the link.\n",
"\n", "\n",
"Note that some UI features like live image previews won't work because the colab iframe blocks websockets." "Note that some UI features like live image previews won't work because the colab iframe blocks websockets."
], ]
"metadata": {
"id": "gggggggggg"
}
}, },
{ {
"cell_type": "code", "cell_type": "code",
"execution_count": null,
"metadata": {
"id": "hhhhhhhhhh"
},
"outputs": [],
"source": [ "source": [
"import threading\n", "import threading\n",
"import time\n", "import time\n",
@ -198,12 +215,23 @@
"threading.Thread(target=iframe_thread, daemon=True, args=(8188,)).start()\n", "threading.Thread(target=iframe_thread, daemon=True, args=(8188,)).start()\n",
"\n", "\n",
"!python main.py --dont-print-server" "!python main.py --dont-print-server"
]
}
], ],
"metadata": { "metadata": {
"id": "hhhhhhhhhh" "accelerator": "GPU",
"colab": {
"provenance": []
}, },
"execution_count": null, "gpuClass": "standard",
"outputs": [] "kernelspec": {
"display_name": "Python 3",
"name": "python3"
},
"language_info": {
"name": "python"
} }
] },
"nbformat": 4,
"nbformat_minor": 0
} }

View File

@ -127,7 +127,7 @@ class PromptServer():
output_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), type) output_dir = os.path.join(os.path.dirname(os.path.realpath(__file__)), type)
if "subfolder" in request.rel_url.query: if "subfolder" in request.rel_url.query:
full_output_dir = os.path.join(output_dir, request.rel_url.query["subfolder"]) full_output_dir = os.path.join(output_dir, request.rel_url.query["subfolder"])
if os.path.commonpath((os.path.realpath(full_output_dir), output_dir)) != output_dir: if os.path.commonpath((os.path.abspath(full_output_dir), output_dir)) != output_dir:
return web.Response(status=403) return web.Response(status=403)
output_dir = full_output_dir output_dir = full_output_dir

View File

@ -0,0 +1,362 @@
import { ComfyWidgets, addRandomizeWidget } from "/scripts/widgets.js";
import { app } from "/scripts/app.js";
const CONVERTED_TYPE = "converted-widget";
const VALID_TYPES = ["STRING", "combo", "number"];
function isConvertableWidget(widget, config) {
return VALID_TYPES.includes(widget.type) || VALID_TYPES.includes(config[0]);
}
function hideWidget(node, widget, suffix = "") {
widget.origType = widget.type;
widget.origComputeSize = widget.computeSize;
widget.origSerializeValue = widget.serializeValue;
widget.computeSize = () => [0, -4]; // -4 is due to the gap litegraph adds between widgets automatically
widget.type = CONVERTED_TYPE + suffix;
widget.serializeValue = () => {
// Prevent serializing the widget if we have no input linked
const { link } = node.inputs.find((i) => i.widget?.name === widget.name);
if (link == null) {
return undefined;
}
return widget.value;
};
// Hide any linked widgets, e.g. seed+randomize
if (widget.linkedWidgets) {
for (const w of widget.linkedWidgets) {
hideWidget(node, w, ":" + widget.name);
}
}
}
function showWidget(widget) {
widget.type = widget.origType;
widget.computeSize = widget.origComputeSize;
widget.serializeValue = widget.origSerializeValue;
delete widget.origType;
delete widget.origComputeSize;
delete widget.origSerializeValue;
// Hide any linked widgets, e.g. seed+randomize
if (widget.linkedWidgets) {
for (const w of widget.linkedWidgets) {
showWidget(w);
}
}
}
function convertToInput(node, widget, config) {
hideWidget(node, widget);
const { linkType } = getWidgetType(config);
// Add input and store widget config for creating on primitive node
const sz = node.size;
node.addInput(widget.name, linkType, {
widget: { name: widget.name, config },
});
// Restore original size but grow if needed
node.setSize([Math.max(sz[0], node.size[0]), Math.max(sz[1], node.size[1])]);
}
function convertToWidget(node, widget) {
showWidget(widget);
const sz = node.size;
node.removeInput(node.inputs.findIndex((i) => i.widget?.name === widget.name));
// Restore original size but grow if needed
node.setSize([Math.max(sz[0], node.size[0]), Math.max(sz[1], node.size[1])]);
}
function getWidgetType(config) {
// Special handling for COMBO so we restrict links based on the entries
let type = config[0];
let linkType = type;
if (type instanceof Array) {
type = "COMBO";
linkType = linkType.join(",");
}
return { type, linkType };
}
app.registerExtension({
name: "Comfy.WidgetInputs",
async beforeRegisterNodeDef(nodeType, nodeData, app) {
// Add menu options to conver to/from widgets
const origGetExtraMenuOptions = nodeType.prototype.getExtraMenuOptions;
nodeType.prototype.getExtraMenuOptions = function (_, options) {
const r = origGetExtraMenuOptions ? origGetExtraMenuOptions.apply(this, arguments) : undefined;
if (this.widgets) {
let toInput = [];
let toWidget = [];
for (const w of this.widgets) {
if (w.type === CONVERTED_TYPE) {
toWidget.push({
content: `Convert ${w.name} to widget`,
callback: () => convertToWidget(this, w),
});
} else {
const config = nodeData?.input?.required[w.name] || [w.type, w.options || {}];
if (isConvertableWidget(w, config)) {
toInput.push({
content: `Convert ${w.name} to input`,
callback: () => convertToInput(this, w, config),
});
}
}
}
if (toInput.length) {
options.push(...toInput, null);
}
if (toWidget.length) {
options.push(...toWidget, null);
}
}
return r;
};
// On initial configure of nodes hide all converted widgets
const origOnConfigure = nodeType.prototype.onConfigure;
nodeType.prototype.onConfigure = function () {
const r = origOnConfigure ? origOnConfigure.apply(this, arguments) : undefined;
if (this.inputs) {
for (const input of this.inputs) {
if (input.widget) {
const w = this.widgets.find((w) => w.name === input.widget.name);
if (w) {
hideWidget(this, w);
} else {
convertToWidget(this, input)
}
}
}
}
return r;
};
function isNodeAtPos(pos) {
for (const n of app.graph._nodes) {
if (n.pos[0] === pos[0] && n.pos[1] === pos[1]) {
return true;
}
}
return false;
}
// Double click a widget input to automatically attach a primitive
const origOnInputDblClick = nodeType.prototype.onInputDblClick;
const ignoreDblClick = Symbol();
nodeType.prototype.onInputDblClick = function (slot) {
const r = origOnInputDblClick ? origOnInputDblClick.apply(this, arguments) : undefined;
const input = this.inputs[slot];
if (input.widget && !input[ignoreDblClick]) {
const node = LiteGraph.createNode("PrimitiveNode");
app.graph.add(node);
// Calculate a position that wont directly overlap another node
const pos = [this.pos[0] - node.size[0] - 30, this.pos[1]];
while (isNodeAtPos(pos)) {
pos[1] += LiteGraph.NODE_TITLE_HEIGHT;
}
node.pos = pos;
node.connect(0, this, slot);
node.title = input.name;
// Prevent adding duplicates due to triple clicking
input[ignoreDblClick] = true;
setTimeout(() => {
delete input[ignoreDblClick];
}, 300);
}
return r;
};
},
registerCustomNodes() {
class PrimitiveNode {
constructor() {
this.addOutput("connect to widget input", "*");
this.serialize_widgets = true;
this.isVirtualNode = true;
}
applyToGraph() {
if (!this.outputs[0].links?.length) return;
// For each output link copy our value over the original widget value
for (const l of this.outputs[0].links) {
const linkInfo = app.graph.links[l];
const node = this.graph.getNodeById(linkInfo.target_id);
const input = node.inputs[linkInfo.target_slot];
const widgetName = input.widget.name;
if (widgetName) {
const widget = node.widgets.find((w) => w.name === widgetName);
if (widget) {
widget.value = this.widgets[0].value;
if (widget.callback) {
widget.callback(widget.value, app.canvas, node, app.canvas.graph_mouse, {});
}
}
}
}
}
onConnectionsChange(_, index, connected) {
if (connected) {
if (this.outputs[0].links?.length) {
if (!this.widgets?.length) {
this.#onFirstConnection();
}
if (!this.widgets?.length && this.outputs[0].widget) {
// On first load it often cant recreate the widget as the other node doesnt exist yet
// Manually recreate it from the output info
this.#createWidget(this.outputs[0].widget.config);
}
}
} else if (!this.outputs[0].links?.length) {
this.#onLastDisconnect();
}
}
onConnectOutput(slot, type, input, target_node, target_slot) {
// Fires before the link is made allowing us to reject it if it isn't valid
// No widget, we cant connect
if (!input.widget) return false;
if (this.outputs[slot].links?.length) {
return this.#isValidConnection(input);
}
}
#onFirstConnection() {
// First connection can fire before the graph is ready on initial load so random things can be missing
const linkId = this.outputs[0].links[0];
const link = this.graph.links[linkId];
if (!link) return;
const theirNode = this.graph.getNodeById(link.target_id);
if (!theirNode || !theirNode.inputs) return;
const input = theirNode.inputs[link.target_slot];
if (!input) return;
const widget = input.widget;
const { type, linkType } = getWidgetType(widget.config);
// Update our output to restrict to the widget type
this.outputs[0].type = linkType;
this.outputs[0].name = type;
this.outputs[0].widget = widget;
this.#createWidget(widget.config, theirNode, widget.name);
}
#createWidget(inputData, node, widgetName) {
let type = inputData[0];
if (type instanceof Array) {
type = "COMBO";
}
let widget;
if (type in ComfyWidgets) {
widget = (ComfyWidgets[type](this, "value", inputData, app) || {}).widget;
} else {
widget = this.addWidget(type, "value", null, () => {}, {});
}
if (node?.widgets && widget) {
const theirWidget = node.widgets.find((w) => w.name === widgetName);
if (theirWidget) {
widget.value = theirWidget.value;
}
}
if (widget.type === "number") {
addRandomizeWidget(this, widget, "Random after every gen");
}
// When our value changes, update other widgets to reflect our changes
// e.g. so LoadImage shows correct image
const callback = widget.callback;
const self = this;
widget.callback = function () {
const r = callback ? callback.apply(this, arguments) : undefined;
self.applyToGraph();
return r;
};
// Grow our node if required
const sz = this.computeSize();
if (this.size[0] < sz[0]) {
this.size[0] = sz[0];
}
if (this.size[1] < sz[1]) {
this.size[1] = sz[1];
}
requestAnimationFrame(() => {
if (this.onResize) {
this.onResize(this.size);
}
});
}
#isValidConnection(input) {
// Only allow connections where the configs match
const config1 = this.outputs[0].widget.config;
const config2 = input.widget.config;
if (config1[0] !== config2[0]) return false;
for (const k in config1[1]) {
if (k !== "default") {
if (config1[1][k] !== config2[1][k]) {
return false;
}
}
}
return true;
}
#onLastDisconnect() {
// We cant remove + re-add the output here as if you drag a link over the same link
// it removes, then re-adds, causing it to break
this.outputs[0].type = "*";
this.outputs[0].name = "connect to widget input";
delete this.outputs[0].widget;
if (this.widgets) {
// Allow widgets to cleanup
for (const w of this.widgets) {
if (w.onRemove) {
w.onRemove();
}
}
this.widgets.length = 0;
}
}
}
LiteGraph.registerNodeType(
"PrimitiveNode",
Object.assign(PrimitiveNode, {
title: "Primitive",
})
);
PrimitiveNode.category = "utils";
},
});

View File

@ -1,10 +1,11 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8">
<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="lib/litegraph.css" />
<link rel="stylesheet" type="text/css" href="style.css" /> <link rel="stylesheet" type="text/css" href="style.css" />
<script type="text/javascript" src="lib/litegraph.core.js"></script> <script type="text/javascript" src="lib/litegraph.core.js"></script>
<script type="module"> <script type="module">
import { app } from "/scripts/app.js"; import { app } from "/scripts/app.js";
await app.setup(); await app.setup();

9
web/jsconfig.json Normal file
View File

@ -0,0 +1,9 @@
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"/*": ["./*"]
}
},
"include": ["."]
}

View File

@ -108,7 +108,7 @@
node_box_coloured_when_on: false, // [true!] this make the nodes box (top left circle) coloured when triggered (execute/action), visual feedback node_box_coloured_when_on: false, // [true!] this make the nodes box (top left circle) coloured when triggered (execute/action), visual feedback
node_box_coloured_by_mode: false, // [true!] nodebox based on node mode, visual feedback node_box_coloured_by_mode: false, // [true!] nodebox based on node mode, visual feedback
dialog_close_on_mouse_leave: true, // [false on mobile] better true if not touch device, TODO add an helper/listener to close if false dialog_close_on_mouse_leave: false, // [false on mobile] better true if not touch device, TODO add an helper/listener to close if false
dialog_close_on_mouse_leave_delay: 500, dialog_close_on_mouse_leave_delay: 500,
shift_click_do_break_link_from: false, // [false!] prefer false if results too easy to break links - implement with ALT or TODO custom keys shift_click_do_break_link_from: false, // [false!] prefer false if results too easy to break links - implement with ALT or TODO custom keys
@ -138,7 +138,7 @@
release_link_on_empty_shows_menu: false, //[true!] dragging a link to empty space will open a menu, add from list, search or defaults release_link_on_empty_shows_menu: false, //[true!] dragging a link to empty space will open a menu, add from list, search or defaults
pointerevents_method: "mouse", // "mouse"|"pointer" use mouse for retrocompatibility issues? (none found @ now) pointerevents_method: "pointer", // "mouse"|"pointer" use mouse for retrocompatibility issues? (none found @ now)
// TODO implement pointercancel, gotpointercapture, lostpointercapture, (pointerover, pointerout if necessary) // TODO implement pointercancel, gotpointercapture, lostpointercapture, (pointerover, pointerout if necessary)
/** /**
@ -5801,7 +5801,7 @@ LGraphNode.prototype.executeAction = function(action)
var skip_action = false; var skip_action = false;
var now = LiteGraph.getTime(); var now = LiteGraph.getTime();
var is_primary = (e.isPrimary === undefined || !e.isPrimary); var is_primary = (e.isPrimary === undefined || !e.isPrimary);
var is_double_click = (now - this.last_mouseclick < 300) && is_primary; var is_double_click = (now - this.last_mouseclick < 300);
this.mouse[0] = e.clientX; this.mouse[0] = e.clientX;
this.mouse[1] = e.clientY; this.mouse[1] = e.clientY;
this.graph_mouse[0] = e.canvasX; this.graph_mouse[0] = e.canvasX;

View File

@ -486,6 +486,27 @@ class ComfyApp {
} }
} }
/**
* Setup slot colors for types
*/
setupSlotColors() {
let colors = {
"CLIP": "#FFD500", // bright yellow
"CLIP_VISION": "#A8DADC", // light blue-gray
"CLIP_VISION_OUTPUT": "#ad7452", // rusty brown-orange
"CONDITIONING": "#FFA931", // vibrant orange-yellow
"CONTROL_NET": "#6EE7B7", // soft mint green
"IMAGE": "#64B5F6", // bright sky blue
"LATENT": "#FF9CF9", // light pink-purple
"MASK": "#81C784", // muted green
"MODEL": "#B39DDB", // light lavender-purple
"STYLE_MODEL": "#C2FFAE", // light green-yellow
"VAE": "#FF6E6E", // bright red
};
Object.assign(this.canvas.default_connection_color_byType, colors);
}
/** /**
* Set up the app on the page * Set up the app on the page
*/ */
@ -494,13 +515,15 @@ class ComfyApp {
// Create and mount the LiteGraph in the DOM // Create and mount the LiteGraph in the DOM
const canvasEl = (this.canvasEl = Object.assign(document.createElement("canvas"), { id: "graph-canvas" })); const canvasEl = (this.canvasEl = Object.assign(document.createElement("canvas"), { id: "graph-canvas" }));
canvasEl.tabIndex = "1" canvasEl.tabIndex = "1";
document.body.prepend(canvasEl); document.body.prepend(canvasEl);
this.graph = new LGraph(); this.graph = new LGraph();
const canvas = (this.canvas = new LGraphCanvas(canvasEl, this.graph)); const canvas = (this.canvas = new LGraphCanvas(canvasEl, this.graph));
this.ctx = canvasEl.getContext("2d"); this.ctx = canvasEl.getContext("2d");
this.setupSlotColors();
this.graph.start(); this.graph.start();
function resizeCanvas() { function resizeCanvas() {
@ -525,7 +548,9 @@ class ComfyApp {
this.loadGraphData(workflow); this.loadGraphData(workflow);
restored = true; restored = true;
} }
} catch (err) {} } catch (err) {
console.error("Error loading previous workflow", err);
}
// We failed to restore a workflow so load the default // We failed to restore a workflow so load the default
if (!restored) { if (!restored) {
@ -572,12 +597,8 @@ class ComfyApp {
const type = inputData[0]; const type = inputData[0];
if (Array.isArray(type)) { if (Array.isArray(type)) {
// Enums e.g. latent rotation // Enums
let defaultValue = type[0]; Object.assign(config, widgets.COMBO(this, inputName, inputData, app) || {});
if (inputData[1] && inputData[1].default) {
defaultValue = inputData[1].default;
}
this.addWidget("combo", inputName, defaultValue, () => {}, { values: type });
} else if (`${type}:${inputName}` in widgets) { } else if (`${type}:${inputName}` in widgets) {
// Support custom widgets by Type:Name // Support custom widgets by Type:Name
Object.assign(config, widgets[`${type}:${inputName}`](this, inputName, inputData, app) || {}); Object.assign(config, widgets[`${type}:${inputName}`](this, inputName, inputData, app) || {});
@ -667,11 +688,15 @@ class ComfyApp {
async graphToPrompt() { async graphToPrompt() {
const workflow = this.graph.serialize(); const workflow = this.graph.serialize();
const output = {}; const output = {};
for (const n of workflow.nodes) { // Process nodes in order of execution
const node = this.graph.getNodeById(n.id); for (const node of this.graph.computeExecutionOrder(false)) {
const n = workflow.nodes.find((n) => n.id === node.id);
if (node.isVirtualNode) { if (node.isVirtualNode) {
// Don't serialize frontend only nodes // Don't serialize frontend only nodes but let them make changes
if (node.applyToGraph) {
node.applyToGraph(workflow);
}
continue; continue;
} }
@ -695,7 +720,11 @@ class ComfyApp {
let link = node.getInputLink(i); let link = node.getInputLink(i);
while (parent && parent.isVirtualNode) { while (parent && parent.isVirtualNode) {
link = parent.getInputLink(link.origin_slot); link = parent.getInputLink(link.origin_slot);
if (link) {
parent = parent.getInputNode(link.origin_slot); parent = parent.getInputNode(link.origin_slot);
} else {
parent = null;
}
} }
if (link) { if (link) {

View File

@ -35,6 +35,54 @@ function $el(tag, propsOrChildren, children) {
return element; return element;
} }
function dragElement(dragEl) {
var posDiffX = 0,
posDiffY = 0,
posStartX = 0,
posStartY = 0,
newPosX = 0,
newPosY = 0;
if (dragEl.getElementsByClassName('drag-handle')[0]) {
// if present, the handle is where you move the DIV from:
dragEl.getElementsByClassName('drag-handle')[0].onmousedown = dragMouseDown;
} else {
// otherwise, move the DIV from anywhere inside the DIV:
dragEl.onmousedown = dragMouseDown;
}
function dragMouseDown(e) {
e = e || window.event;
e.preventDefault();
// get the mouse cursor position at startup:
posStartX = e.clientX;
posStartY = e.clientY;
document.onmouseup = closeDragElement;
// call a function whenever the cursor moves:
document.onmousemove = elementDrag;
}
function elementDrag(e) {
e = e || window.event;
e.preventDefault();
// calculate the new cursor position:
posDiffX = e.clientX - posStartX;
posDiffY = e.clientY - posStartY;
posStartX = e.clientX;
posStartY = e.clientY;
newPosX = Math.min((document.body.clientWidth - dragEl.clientWidth), Math.max(0, (dragEl.offsetLeft + posDiffX)));
newPosY = Math.min((document.body.clientHeight - dragEl.clientHeight), Math.max(0, (dragEl.offsetTop + posDiffY)));
// set the element's new position:
dragEl.style.top = newPosY + "px";
dragEl.style.left = newPosX + "px";
}
function closeDragElement() {
// stop moving when mouse button is released:
document.onmouseup = null;
document.onmousemove = null;
}
}
class ComfyDialog { class ComfyDialog {
constructor() { constructor() {
this.element = $el("div.comfy-modal", { parent: document.body }, [ this.element = $el("div.comfy-modal", { parent: document.body }, [
@ -253,6 +301,7 @@ export class ComfyUI {
this.menuContainer = $el("div.comfy-menu", { parent: document.body }, [ this.menuContainer = $el("div.comfy-menu", { parent: document.body }, [
$el("div", { style: { overflow: "hidden", position: "relative", width: "100%" } }, [ $el("div", { style: { overflow: "hidden", position: "relative", width: "100%" } }, [
$el("span.drag-handle"),
$el("span", { $: (q) => (this.queueSize = q) }), $el("span", { $: (q) => (this.queueSize = q) }),
$el("button.comfy-settings-btn", { textContent: "⚙️", onclick: () => this.settings.show() }), $el("button.comfy-settings-btn", { textContent: "⚙️", onclick: () => this.settings.show() }),
]), ]),
@ -331,6 +380,8 @@ export class ComfyUI {
$el("button", { textContent: "Load Default", onclick: () => app.loadGraphData() }), $el("button", { textContent: "Load Default", onclick: () => app.loadGraphData() }),
]); ]);
dragElement(this.menuContainer);
this.setStatus({ exec_info: { queue_remaining: "X" } }); this.setStatus({ exec_info: { queue_remaining: "X" } });
} }

View File

@ -10,9 +10,8 @@ function getNumberDefaults(inputData, defaultStep) {
return { val: defaultVal, config: { min, max, step: 10.0 * step } }; return { val: defaultVal, config: { min, max, step: 10.0 * step } };
} }
function seedWidget(node, inputName, inputData) { export function addRandomizeWidget(node, targetWidget, name, defaultValue = false) {
const seed = ComfyWidgets.INT(node, inputName, inputData); const randomize = node.addWidget("toggle", name, defaultValue, function (v) {}, {
const randomize = node.addWidget("toggle", "Random seed after every gen", true, function (v) {}, {
on: "enabled", on: "enabled",
off: "disabled", off: "disabled",
serialize: false, // Don't include this in prompt. serialize: false, // Don't include this in prompt.
@ -20,14 +19,32 @@ function seedWidget(node, inputName, inputData) {
randomize.afterQueued = () => { randomize.afterQueued = () => {
if (randomize.value) { if (randomize.value) {
seed.widget.value = Math.floor(Math.random() * 1125899906842624); const min = targetWidget.options?.min;
let max = targetWidget.options?.max;
if (min != null || max != null) {
if (max) {
// limit max to something that javascript can handle
max = Math.min(1125899906842624, max);
}
targetWidget.value = Math.floor(Math.random() * ((max ?? 9999999999) - (min ?? 0) + 1) + (min ?? 0));
} else {
targetWidget.value = Math.floor(Math.random() * 1125899906842624);
}
} }
}; };
return randomize;
}
function seedWidget(node, inputName, inputData) {
const seed = ComfyWidgets.INT(node, inputName, inputData);
const randomize = addRandomizeWidget(node, seed.widget, "Random seed after every gen", true);
seed.widget.linkedWidgets = [randomize];
return { widget: seed, randomize }; return { widget: seed, randomize };
} }
const MultilineSymbol = Symbol(); const MultilineSymbol = Symbol();
const MultilineResizeSymbol = Symbol();
function addMultilineWidget(node, name, opts, app) { function addMultilineWidget(node, name, opts, app) {
const MIN_SIZE = 50; const MIN_SIZE = 50;
@ -95,7 +112,7 @@ function addMultilineWidget(node, name, opts, app) {
// Calculate it here instead // Calculate it here instead
computeSize(node.size); computeSize(node.size);
} }
const visible = app.canvas.ds.scale > 0.5; const visible = app.canvas.ds.scale > 0.5 && this.type === "customtext";
const t = ctx.getTransform(); const t = ctx.getTransform();
const margin = 10; const margin = 10;
Object.assign(this.inputEl.style, { Object.assign(this.inputEl.style, {
@ -149,9 +166,22 @@ function addMultilineWidget(node, name, opts, app) {
} }
}; };
if (!(MultilineSymbol in node)) { widget.onRemove = () => {
node[MultilineSymbol] = true; widget.inputEl?.remove();
const onResize = node.onResize;
// Restore original size handler if we are the last
if (!--node[MultilineSymbol]) {
node.onResize = node[MultilineResizeSymbol];
delete node[MultilineSymbol];
delete node[MultilineResizeSymbol];
}
};
if (node[MultilineSymbol]) {
node[MultilineSymbol]++;
} else {
node[MultilineSymbol] = 1;
const onResize = (node[MultilineResizeSymbol] = node.onResize);
node.onResize = function (size) { node.onResize = function (size) {
computeSize(size); computeSize(size);
@ -199,6 +229,14 @@ export const ComfyWidgets = {
return { widget: node.addWidget("text", inputName, defaultVal, () => {}, {}) }; return { widget: node.addWidget("text", inputName, defaultVal, () => {}, {}) };
} }
}, },
COMBO(node, inputName, inputData) {
const type = inputData[0];
let defaultValue = type[0];
if (inputData[1] && inputData[1].default) {
defaultValue = inputData[1].default;
}
return { widget: node.addWidget("combo", inputName, defaultValue, () => {}, { values: type }) };
},
IMAGEUPLOAD(node, inputName, inputData, app) { IMAGEUPLOAD(node, inputName, inputData, app) {
const imageWidget = node.widgets.find((w) => w.name === "image"); const imageWidget = node.widgets.find((w) => w.name === "image");
let uploadWidget; let uploadWidget;

View File

@ -111,6 +111,31 @@ body {
width: 50%; width: 50%;
} }
.comfy-menu span.drag-handle {
width: 10px;
height: 20px;
display: inline-block;
overflow: hidden;
line-height: 5px;
padding: 3px 4px;
cursor: move;
vertical-align: middle;
margin-top: -.4em;
margin-left: -.2em;
font-size: 12px;
font-family: sans-serif;
letter-spacing: 2px;
color: #cccccc;
text-shadow: 1px 0 1px black;
position: absolute;
top: 0;
left: 0;
}
.comfy-menu span.drag-handle::after {
content: '.. .. ..';
}
.comfy-queue-btn { .comfy-queue-btn {
width: 100%; width: 100%;
} }