Merge remote-tracking branch 'upstream/master'

This commit is contained in:
Silversith 2023-03-31 09:17:07 +02:00
commit b8a0fb6e83
17 changed files with 848 additions and 70 deletions

View File

@ -25,3 +25,7 @@ To update the ComfyUI code: update\update_comfyui.bat
To update ComfyUI with the python dependencies, note that you should ONLY run this if you have issues with python dependencies. To update ComfyUI with the python dependencies, note that you should ONLY run this if you have issues with python dependencies.
update\update_comfyui_and_python_dependencies.bat update\update_comfyui_and_python_dependencies.bat
TO SHARE MODELS BETWEEN COMFYUI AND ANOTHER UI:
In the ComfyUI directory you will find a file: extra_model_paths.yaml.example
Rename this file to: extra_model_paths.yaml and edit it with your favorite text editor.

View File

@ -30,6 +30,11 @@ This ui will let you design and execute advanced stable diffusion pipelines usin
Workflow examples can be found on the [Examples page](https://comfyanonymous.github.io/ComfyUI_examples/) Workflow examples can be found on the [Examples page](https://comfyanonymous.github.io/ComfyUI_examples/)
## Shortcuts
- **Ctrl + A** select all nodes
- **Ctrl + M** mute/unmute selected nodes
- **Delete** or **Backspace** delete selected nodes
# Installing # Installing
## Windows ## Windows
@ -40,6 +45,10 @@ There is a portable standalone build for Windows that should work for running on
Just download, extract and run. Make sure you put your Stable Diffusion checkpoints/models (the huge ckpt/safetensors files) in: ComfyUI\models\checkpoints Just download, extract and run. Make sure you put your Stable Diffusion checkpoints/models (the huge ckpt/safetensors files) in: ComfyUI\models\checkpoints
#### How do I share models between another UI and ComfyUI?
See the [Config file](extra_model_paths.yaml.example) to set the search paths for models. In the standalone windows build you can find this file in the ComfyUI directory. Rename this file to extra_model_paths.yaml and edit it with your favorite text editor.
## Colab Notebook ## Colab Notebook
To run it on colab or paperspace you can use my [Colab Notebook](notebooks/comfyui_colab.ipynb) here: [Link to open with google colab](https://colab.research.google.com/github/comfyanonymous/ComfyUI/blob/master/notebooks/comfyui_colab.ipynb) To run it on colab or paperspace you can use my [Colab Notebook](notebooks/comfyui_colab.ipynb) here: [Link to open with google colab](https://colab.research.google.com/github/comfyanonymous/ComfyUI/blob/master/notebooks/comfyui_colab.ipynb)
@ -64,7 +73,7 @@ AMD users can install rocm and pytorch with pip if you don't have it already ins
Nvidia users should install torch and xformers using this command: Nvidia users should install torch and xformers using this command:
```pip install torch==1.13.1 torchvision torchaudio --extra-index-url https://download.pytorch.org/whl/cu117 xformers``` ```pip install torch torchvision torchaudio --extra-index-url https://download.pytorch.org/whl/cu118 xformers```
#### Troubleshooting #### Troubleshooting
@ -97,7 +106,6 @@ With cmd.exe: ```"path_to_other_sd_gui\venv\Scripts\activate.bat"```
And then you can use that terminal to run Comfyui without installing any dependencies. Note that the venv folder might be called something else depending on the SD UI. And then you can use that terminal to run Comfyui without installing any dependencies. Note that the venv folder might be called something else depending on the SD UI.
# Running # Running
```python main.py``` ```python main.py```

View File

@ -15,6 +15,8 @@ total_vram_available_mb = -1
import sys import sys
import psutil import psutil
forced_cpu = "--cpu" in sys.argv
set_vram_to = NORMAL_VRAM set_vram_to = NORMAL_VRAM
try: try:
@ -22,7 +24,7 @@ try:
total_vram = torch.cuda.mem_get_info(torch.cuda.current_device())[1] / (1024 * 1024) total_vram = torch.cuda.mem_get_info(torch.cuda.current_device())[1] / (1024 * 1024)
total_ram = psutil.virtual_memory().total / (1024 * 1024) total_ram = psutil.virtual_memory().total / (1024 * 1024)
forced_normal_vram = "--normalvram" in sys.argv forced_normal_vram = "--normalvram" in sys.argv
if not forced_normal_vram: if not forced_normal_vram and not forced_cpu:
if total_vram <= 4096: if total_vram <= 4096:
print("Trying to enable lowvram mode because your GPU seems to have 4GB or less. If you don't want this use: --normalvram") print("Trying to enable lowvram mode because your GPU seems to have 4GB or less. If you don't want this use: --normalvram")
set_vram_to = LOW_VRAM set_vram_to = LOW_VRAM
@ -83,7 +85,7 @@ try:
except: except:
pass pass
if "--cpu" in sys.argv: if forced_cpu:
vram_state = CPU vram_state = CPU
print("Set vram state to:", ["CPU", "NO VRAM", "LOW VRAM", "NORMAL VRAM", "HIGH VRAM", "MPS"][vram_state]) print("Set vram state to:", ["CPU", "NO VRAM", "LOW VRAM", "NORMAL VRAM", "HIGH VRAM", "MPS"][vram_state])

View File

@ -221,7 +221,7 @@ class KSamplerX0Inpaint(torch.nn.Module):
def forward(self, x, sigma, uncond, cond, cond_scale, denoise_mask, cond_concat=None): def forward(self, x, sigma, uncond, cond, cond_scale, denoise_mask, cond_concat=None):
if denoise_mask is not None: if denoise_mask is not None:
latent_mask = 1. - denoise_mask latent_mask = 1. - denoise_mask
x = x * denoise_mask + (self.latent_image + self.noise * sigma) * latent_mask x = x * denoise_mask + (self.latent_image + self.noise * sigma.reshape([sigma.shape[0]] + [1] * (len(self.noise.shape) - 1))) * latent_mask
out = self.inner_model(x, sigma, cond=cond, uncond=uncond, cond_scale=cond_scale, cond_concat=cond_concat) out = self.inner_model(x, sigma, cond=cond, uncond=uncond, cond_scale=cond_scale, cond_concat=cond_concat)
if denoise_mask is not None: if denoise_mask is not None:
out *= denoise_mask out *= denoise_mask
@ -242,7 +242,10 @@ def ddim_scheduler(model, steps):
sigs = [] sigs = []
ddim_timesteps = make_ddim_timesteps(ddim_discr_method="uniform", num_ddim_timesteps=steps, num_ddpm_timesteps=model.inner_model.inner_model.num_timesteps, verbose=False) ddim_timesteps = make_ddim_timesteps(ddim_discr_method="uniform", num_ddim_timesteps=steps, num_ddpm_timesteps=model.inner_model.inner_model.num_timesteps, verbose=False)
for x in range(len(ddim_timesteps) - 1, -1, -1): for x in range(len(ddim_timesteps) - 1, -1, -1):
sigs.append(model.t_to_sigma(torch.tensor(ddim_timesteps[x]))) ts = ddim_timesteps[x]
if ts > 999:
ts = 999
sigs.append(model.t_to_sigma(torch.tensor(ts)))
sigs += [0.0] sigs += [0.0]
return torch.FloatTensor(sigs) return torch.FloatTensor(sigs)
@ -373,7 +376,7 @@ class KSampler:
def set_steps(self, steps, denoise=None): def set_steps(self, steps, denoise=None):
self.steps = steps self.steps = steps
if denoise is None: if denoise is None or denoise > 0.9999:
self.sigmas = self._calculate_sigmas(steps) self.sigmas = self._calculate_sigmas(steps)
else: else:
new_steps = int(steps/denoise) new_steps = int(steps/denoise)

View File

@ -439,9 +439,14 @@ class VAE:
model_management.unload_model() model_management.unload_model()
self.first_stage_model = self.first_stage_model.to(self.device) self.first_stage_model = self.first_stage_model.to(self.device)
try: try:
samples = samples_in.to(self.device) free_memory = model_management.get_free_memory(self.device)
pixel_samples = self.first_stage_model.decode(1. / self.scale_factor * samples) batch_number = int((free_memory * 0.7) / (2562 * samples_in.shape[2] * samples_in.shape[3] * 64))
pixel_samples = torch.clamp((pixel_samples + 1.0) / 2.0, min=0.0, max=1.0) batch_number = max(1, batch_number)
pixel_samples = torch.empty((samples_in.shape[0], 3, round(samples_in.shape[2] * 8), round(samples_in.shape[3] * 8)), device="cpu")
for x in range(0, samples_in.shape[0], batch_number):
samples = samples_in[x:x+batch_number].to(self.device)
pixel_samples[x:x+batch_number] = torch.clamp((self.first_stage_model.decode(1. / self.scale_factor * samples) + 1.0) / 2.0, min=0.0, max=1.0).cpu()
except model_management.OOM_EXCEPTION as e: except model_management.OOM_EXCEPTION as e:
print("Warning: Ran out of memory when regular VAE decoding, retrying with tiled VAE decoding.") print("Warning: Ran out of memory when regular VAE decoding, retrying with tiled VAE decoding.")
pixel_samples = self.decode_tiled_(samples_in) pixel_samples = self.decode_tiled_(samples_in)

View File

@ -10,7 +10,7 @@ import gc
import torch import torch
import nodes import nodes
def get_input_data(inputs, class_def, outputs={}, prompt={}, extra_data={}): def get_input_data(inputs, class_def, unique_id, outputs={}, prompt={}, extra_data={}):
valid_inputs = class_def.INPUT_TYPES() valid_inputs = class_def.INPUT_TYPES()
input_data_all = {} input_data_all = {}
for x in inputs: for x in inputs:
@ -18,6 +18,8 @@ def get_input_data(inputs, class_def, outputs={}, prompt={}, extra_data={}):
if isinstance(input_data, list): if isinstance(input_data, list):
input_unique_id = input_data[0] input_unique_id = input_data[0]
output_index = input_data[1] output_index = input_data[1]
if input_unique_id not in outputs:
return None
obj = outputs[input_unique_id][output_index] obj = outputs[input_unique_id][output_index]
input_data_all[x] = obj input_data_all[x] = obj
else: else:
@ -32,6 +34,8 @@ def get_input_data(inputs, class_def, outputs={}, prompt={}, extra_data={}):
if h[x] == "EXTRA_PNGINFO": if h[x] == "EXTRA_PNGINFO":
if "extra_pnginfo" in extra_data: if "extra_pnginfo" in extra_data:
input_data_all[x] = extra_data['extra_pnginfo'] input_data_all[x] = extra_data['extra_pnginfo']
if h[x] == "UNIQUE_ID":
input_data_all[x] = unique_id
return input_data_all return input_data_all
def recursive_execute(server, prompt, outputs, current_item, extra_data={}): def recursive_execute(server, prompt, outputs, current_item, extra_data={}):
@ -53,7 +57,7 @@ def recursive_execute(server, prompt, outputs, current_item, extra_data={}):
if input_unique_id not in outputs: if input_unique_id not in outputs:
executed += recursive_execute(server, prompt, outputs, input_unique_id, extra_data) executed += recursive_execute(server, prompt, outputs, input_unique_id, extra_data)
input_data_all = get_input_data(inputs, class_def, outputs, prompt, extra_data) input_data_all = get_input_data(inputs, class_def, unique_id, outputs, prompt, extra_data)
if server.client_id is not None: if server.client_id is not None:
server.last_node_id = unique_id server.last_node_id = unique_id
server.send_sync("executing", { "node": unique_id }, server.client_id) server.send_sync("executing", { "node": unique_id }, server.client_id)
@ -61,8 +65,11 @@ def recursive_execute(server, prompt, outputs, current_item, extra_data={}):
nodes.before_node_execution() nodes.before_node_execution()
outputs[unique_id] = getattr(obj, obj.FUNCTION)(**input_data_all) outputs[unique_id] = getattr(obj, obj.FUNCTION)(**input_data_all)
if "ui" in outputs[unique_id] and server.client_id is not None: if "ui" in outputs[unique_id]:
server.send_sync("executed", { "node": unique_id, "output": outputs[unique_id]["ui"] }, server.client_id) if server.client_id is not None:
server.send_sync("executed", { "node": unique_id, "output": outputs[unique_id]["ui"] }, server.client_id)
if "result" in outputs[unique_id]:
outputs[unique_id] = outputs[unique_id]["result"]
return executed + [unique_id] return executed + [unique_id]
def recursive_will_execute(prompt, outputs, current_item): def recursive_will_execute(prompt, outputs, current_item):
@ -94,9 +101,10 @@ def recursive_output_delete_if_changed(prompt, old_prompt, outputs, current_item
if unique_id in old_prompt and 'is_changed' in old_prompt[unique_id]: if unique_id in old_prompt and 'is_changed' in old_prompt[unique_id]:
is_changed_old = old_prompt[unique_id]['is_changed'] is_changed_old = old_prompt[unique_id]['is_changed']
if 'is_changed' not in prompt[unique_id]: if 'is_changed' not in prompt[unique_id]:
input_data_all = get_input_data(inputs, class_def) input_data_all = get_input_data(inputs, class_def, unique_id, outputs)
is_changed = class_def.IS_CHANGED(**input_data_all) if input_data_all is not None:
prompt[unique_id]['is_changed'] = is_changed is_changed = class_def.IS_CHANGED(**input_data_all)
prompt[unique_id]['is_changed'] = is_changed
else: else:
is_changed = prompt[unique_id]['is_changed'] is_changed = prompt[unique_id]['is_changed']
@ -278,7 +286,7 @@ def validate_prompt(prompt):
errors += [(o, reason)] errors += [(o, reason)]
if len(good_outputs) == 0: if len(good_outputs) == 0:
errors_list = "\n".join(map(lambda a: "{}".format(a[1]), errors)) errors_list = "\n".join(set(map(lambda a: "{}".format(a[1]), errors)))
return (False, "Prompt has no properly connected outputs\n {}".format(errors_list)) return (False, "Prompt has no properly connected outputs\n {}".format(errors_list))
return (True, "") return (True, "")

23
main.py
View File

@ -12,12 +12,13 @@ if os.name == "nt":
if __name__ == "__main__": if __name__ == "__main__":
if '--help' in sys.argv: if '--help' in sys.argv:
print("Valid Command line Arguments:") print("Valid Command line Arguments:")
print("\t--listen\t\t\tListen on 0.0.0.0 so the UI can be accessed from other computers.") print("\t--listen [ip]\t\t\tListen on ip or 0.0.0.0 if none given so the UI can be accessed from other computers.")
print("\t--port 8188\t\t\tSet the listen port.") print("\t--port 8188\t\t\tSet the listen port.")
print("\t--dont-upcast-attention\t\tDisable upcasting of attention \n\t\t\t\t\tcan boost speed but increase the chances of black images.\n") print("\t--dont-upcast-attention\t\tDisable upcasting of attention \n\t\t\t\t\tcan boost speed but increase the chances of black images.\n")
print("\t--use-split-cross-attention\tUse the split cross attention optimization instead of the sub-quadratic one.\n\t\t\t\t\tIgnored when xformers is used.") print("\t--use-split-cross-attention\tUse the split cross attention optimization instead of the sub-quadratic one.\n\t\t\t\t\tIgnored when xformers is used.")
print("\t--use-pytorch-cross-attention\tUse the new pytorch 2.0 cross attention function.") print("\t--use-pytorch-cross-attention\tUse the new pytorch 2.0 cross attention function.")
print("\t--disable-xformers\t\tdisables xformers") print("\t--disable-xformers\t\tdisables xformers")
print("\t--cuda-device 1\t\tSet the id of the cuda device this instance will use.")
print() print()
print("\t--highvram\t\t\tBy default models will be unloaded to CPU memory after being used.\n\t\t\t\t\tThis option keeps them in GPU memory.\n") print("\t--highvram\t\t\tBy default models will be unloaded to CPU memory after being used.\n\t\t\t\t\tThis option keeps them in GPU memory.\n")
print("\t--normalvram\t\t\tUsed to force normal vram use if lowvram gets automatically enabled.") print("\t--normalvram\t\t\tUsed to force normal vram use if lowvram gets automatically enabled.")
@ -31,6 +32,14 @@ if __name__ == "__main__":
print("disabling upcasting of attention") print("disabling upcasting of attention")
os.environ['ATTN_PRECISION'] = "fp16" os.environ['ATTN_PRECISION'] = "fp16"
try:
index = sys.argv.index('--cuda-device')
device = sys.argv[index + 1]
os.environ['CUDA_VISIBLE_DEVICES'] = device
print("Set cuda device to:", device)
except:
pass
import execution import execution
import server import server
import folder_paths import folder_paths
@ -92,11 +101,19 @@ if __name__ == "__main__":
hijack_progress(server) hijack_progress(server)
threading.Thread(target=prompt_worker, daemon=True, args=(q,server,)).start() threading.Thread(target=prompt_worker, daemon=True, args=(q,server,)).start()
if '--listen' in sys.argv: try:
address = '0.0.0.0' address = '0.0.0.0'
else: p_index = sys.argv.index('--listen')
try:
ip = sys.argv[p_index + 1]
if ip[:2] != '--':
address = ip
except:
pass
except:
address = '127.0.0.1' address = '127.0.0.1'
dont_print = False dont_print = False
if '--dont-print-server' in sys.argv: if '--dont-print-server' in sys.argv:
dont_print = True dont_print = True

View File

@ -747,6 +747,13 @@ class SaveImage:
digits = 0 digits = 0
return (digits, prefix) return (digits, prefix)
def compute_vars(input):
input = input.replace("%width%", str(images[0].shape[1]))
input = input.replace("%height%", str(images[0].shape[0]))
return input
filename_prefix = compute_vars(filename_prefix)
subfolder = os.path.dirname(os.path.normpath(filename_prefix)) subfolder = os.path.dirname(os.path.normpath(filename_prefix))
filename = os.path.basename(os.path.normpath(filename_prefix)) filename = os.path.basename(os.path.normpath(filename_prefix))
@ -1045,5 +1052,4 @@ def load_custom_nodes():
load_custom_nodes() load_custom_nodes()
load_custom_node(os.path.join(os.path.join(os.path.dirname(os.path.realpath(__file__)), "comfy_extras"), "nodes_upscale_model.py")) load_custom_node(os.path.join(os.path.join(os.path.dirname(os.path.realpath(__file__)), "comfy_extras"), "nodes_upscale_model.py"))
load_custom_node(os.path.join(os.path.join(os.path.dirname(os.path.realpath(__file__)), "comfy_extras"), "silver_custom.py"))

View File

@ -47,7 +47,7 @@
" !git pull\n", " !git pull\n",
"\n", "\n",
"!echo -= Install dependencies =-\n", "!echo -= Install dependencies =-\n",
"!pip -q install xformers -r requirements.txt" "!pip install xformers==0.0.16 -r requirements.txt --extra-index-url https://download.pytorch.org/whl/cu117"
] ]
}, },
{ {

View File

@ -3,7 +3,7 @@ torchdiffeq
torchsde torchsde
einops einops
open-clip-torch open-clip-torch
transformers transformers>=4.25.1
safetensors safetensors
pytorch_lightning pytorch_lightning
aiohttp aiohttp

View File

@ -29,6 +29,8 @@ async def cache_control(request: web.Request, handler):
class PromptServer(): class PromptServer():
def __init__(self, loop): def __init__(self, loop):
PromptServer.instance = self
mimetypes.init(); mimetypes.init();
mimetypes.types_map['.js'] = 'application/javascript; charset=utf-8' mimetypes.types_map['.js'] = 'application/javascript; charset=utf-8'
self.prompt_queue = None self.prompt_queue = None
@ -166,6 +168,7 @@ class PromptServer():
info = {} info = {}
info['input'] = obj_class.INPUT_TYPES() info['input'] = obj_class.INPUT_TYPES()
info['output'] = obj_class.RETURN_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 #TODO
info['description'] = '' info['description'] = ''
info['category'] = 'sd' info['category'] = 'sd'

View File

@ -0,0 +1,351 @@
import { app } from "/scripts/app.js";
import { $el } from "/scripts/ui.js";
import { api } from "/scripts/api.js";
// Manage color palettes
const colorPalettes = {
"palette_1": {
"id": "palette_1",
"name": "Palette 1",
"colors": {
"node_slot": {
"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
}
}
},
"palette_2": {
"id": "palette_2",
"name": "Palette 2",
"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
}
}
}
};
const id = "Comfy.ColorPalette";
const idCustomColorPalettes = "Comfy.CustomColorPalettes";
const defaultColorPaletteId = "palette_1";
const els = {}
// const ctxMenu = LiteGraph.ContextMenu;
app.registerExtension({
name: id,
init() {
const sortObjectKeys = (unordered) => {
return Object.keys(unordered).sort().reduce((obj, key) => {
obj[key] = unordered[key];
return obj;
}, {});
};
const getSlotTypes = async () => {
var types = [];
const defs = await api.getNodeDefs();
for (const nodeId in defs) {
const nodeData = defs[nodeId];
var inputs = nodeData["input"]["required"];
if (nodeData["input"]["optional"] != undefined){
inputs = Object.assign({}, nodeData["input"]["required"], nodeData["input"]["optional"])
}
for (const inputName in inputs) {
const inputData = inputs[inputName];
const type = inputData[0];
if (!Array.isArray(type)) {
types.push(type);
}
}
for (const o in nodeData["output"]) {
const output = nodeData["output"][o];
types.push(output);
}
}
return types;
};
const completeColorPalette = async (colorPalette) => {
var types = await getSlotTypes();
for (const type of types) {
if (!colorPalette.colors.node_slot[type]) {
colorPalette.colors.node_slot[type] = "";
}
}
colorPalette.colors.node_slot = sortObjectKeys(colorPalette.colors.node_slot);
return colorPalette;
};
const getColorPaletteTemplate = async () => {
let colorPalette = {
"id": "my_color_palette_unique_id",
"name": "My Color Palette",
"colors": {
"node_slot": {
}
}
};
return completeColorPalette(colorPalette);
};
const getCustomColorPalettes = () => {
return app.ui.settings.getSettingValue(idCustomColorPalettes, {});
};
const setCustomColorPalettes = (customColorPalettes) => {
return app.ui.settings.setSettingValue(idCustomColorPalettes, customColorPalettes);
};
const addCustomColorPalette = async (colorPalette) => {
if (typeof(colorPalette) !== "object") {
app.ui.dialog.show("Invalid color palette");
return;
}
if (!colorPalette.id) {
app.ui.dialog.show("Color palette missing id");
return;
}
if (!colorPalette.name) {
app.ui.dialog.show("Color palette missing name");
return;
}
if (!colorPalette.colors) {
app.ui.dialog.show("Color palette missing colors");
return;
}
if (colorPalette.colors.node_slot && typeof(colorPalette.colors.node_slot) !== "object") {
app.ui.dialog.show("Invalid color palette colors.node_slot");
return;
}
let customColorPalettes = getCustomColorPalettes();
customColorPalettes[colorPalette.id] = colorPalette;
setCustomColorPalettes(customColorPalettes);
for (const option of els.select.childNodes) {
if (option.value === "custom_" + colorPalette.id) {
els.select.removeChild(option);
}
}
els.select.append($el("option", { textContent: colorPalette.name + " (custom)", value: "custom_" + colorPalette.id, selected: true }));
setColorPalette("custom_" + colorPalette.id);
await loadColorPalette(colorPalette);
};
const deleteCustomColorPalette = async (colorPaletteId) => {
let customColorPalettes = getCustomColorPalettes();
delete customColorPalettes[colorPaletteId];
setCustomColorPalettes(customColorPalettes);
for (const option of els.select.childNodes) {
if (option.value === defaultColorPaletteId) {
option.selected = true;
}
if (option.value === "custom_" + colorPaletteId) {
els.select.removeChild(option);
}
}
setColorPalette(defaultColorPaletteId);
await loadColorPalette(getColorPalette());
};
const loadColorPalette = async (colorPalette) => {
colorPalette = await completeColorPalette(colorPalette);
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);
}
}
};
const getColorPalette = (colorPaletteId) => {
if (!colorPaletteId) {
colorPaletteId = app.ui.settings.getSettingValue(id, defaultColorPaletteId);
}
if (colorPaletteId.startsWith("custom_")) {
colorPaletteId = colorPaletteId.substr(7);
let customColorPalettes = getCustomColorPalettes();
if (customColorPalettes[colorPaletteId]) {
return customColorPalettes[colorPaletteId];
}
}
return colorPalettes[colorPaletteId];
};
const setColorPalette = (colorPaletteId) => {
app.ui.settings.setSettingValue(id, colorPaletteId);
};
const fileInput = $el("input", {
type: "file",
accept: ".json",
style: { display: "none" },
parent: document.body,
onchange: () => {
let file = fileInput.files[0];
if (file.type === "application/json" || file.name.endsWith(".json")) {
const reader = new FileReader();
reader.onload = async () => {
await addCustomColorPalette(JSON.parse(reader.result));
};
reader.readAsText(file);
}
},
});
app.ui.settings.addSetting({
id,
name: "Color Palette",
type: (name, setter, value) => {
let options = [];
for (const c in colorPalettes) {
const colorPalette = colorPalettes[c];
options.push($el("option", { textContent: colorPalette.name, value: colorPalette.id, selected: colorPalette.id === value }));
}
let customColorPalettes = getCustomColorPalettes();
for (const c in customColorPalettes) {
const colorPalette = customColorPalettes[c];
options.push($el("option", { textContent: colorPalette.name + " (custom)", value: "custom_" + colorPalette.id, selected: "custom_" + colorPalette.id === value }));
}
return $el("div", [
$el("label", { textContent: name || id }, [
els.select = $el("select", {
onchange: (e) => {
setter(e.target.value);
}
}, options)
]),
$el("input", {
type: "button",
value: "Export",
onclick: async () => {
const colorPaletteId = app.ui.settings.getSettingValue(id, defaultColorPaletteId);
const colorPalette = await completeColorPalette(getColorPalette(colorPaletteId));
const json = JSON.stringify(colorPalette, null, 2); // convert the data to a JSON string
const blob = new Blob([json], { type: "application/json" });
const url = URL.createObjectURL(blob);
const a = $el("a", {
href: url,
download: colorPaletteId + ".json",
style: { display: "none" },
parent: document.body,
});
a.click();
setTimeout(function () {
a.remove();
window.URL.revokeObjectURL(url);
}, 0);
},
}),
$el("input", {
type: "button",
value: "Import",
onclick: () => {
fileInput.click();
}
}),
$el("input", {
type: "button",
value: "Template",
onclick: async () => {
const colorPalette = await getColorPaletteTemplate();
const json = JSON.stringify(colorPalette, null, 2); // convert the data to a JSON string
const blob = new Blob([json], { type: "application/json" });
const url = URL.createObjectURL(blob);
const a = $el("a", {
href: url,
download: "color_palette.json",
style: { display: "none" },
parent: document.body,
});
a.click();
setTimeout(function () {
a.remove();
window.URL.revokeObjectURL(url);
}, 0);
}
}),
$el("input", {
type: "button",
value: "Delete",
onclick: async () => {
let colorPaletteId = app.ui.settings.getSettingValue(id, defaultColorPaletteId);
if (colorPalettes[colorPaletteId]) {
app.ui.dialog.show("You cannot delete built-in color palette");
return;
}
if (colorPaletteId.startsWith("custom_")) {
colorPaletteId = colorPaletteId.substr(7);
}
await deleteCustomColorPalette(colorPaletteId);
}
}),
]);
},
defaultValue: defaultColorPaletteId,
async onChange(value) {
if (!value) {
return;
}
if (colorPalettes[value]) {
await loadColorPalette(colorPalettes[value]);
} else if (value.startsWith("custom_")) {
value = value.substr(7);
let customColorPalettes = getCustomColorPalettes();
if (customColorPalettes[value]) {
await loadColorPalette(customColorPalettes[value]);
}
}
},
});
},
});

View File

@ -0,0 +1,100 @@
import { app } from "/scripts/app.js";
// Use widget values and dates in output filenames
app.registerExtension({
name: "Comfy.SaveImageExtraOutput",
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeData.name === "SaveImage") {
const onNodeCreated = nodeType.prototype.onNodeCreated;
// Simple date formatter
const parts = {
d: (d) => d.getDate(),
M: (d) => d.getMonth() + 1,
h: (d) => d.getHours(),
m: (d) => d.getMinutes(),
s: (d) => d.getSeconds(),
};
const format =
Object.keys(parts)
.map((k) => k + k + "?")
.join("|") + "|yyy?y?";
function formatDate(text, date) {
return text.replace(new RegExp(format, "g"), function (text) {
if (text === "yy") return (date.getFullYear() + "").substring(2);
if (text === "yyyy") return date.getFullYear();
if (text[0] in parts) {
const p = parts[text[0]](date);
return (p + "").padStart(text.length, "0");
}
return text;
});
}
// When the SaveImage node is created we want to override the serialization of the output name widget to run our S&R
nodeType.prototype.onNodeCreated = function () {
const r = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined;
const widget = this.widgets.find((w) => w.name === "filename_prefix");
widget.serializeValue = () => {
return widget.value.replace(/%([^%]+)%/g, function (match, text) {
const split = text.split(".");
if (split.length !== 2) {
// Special handling for dates
if (split[0].startsWith("date:")) {
return formatDate(split[0].substring(5), new Date());
}
if (text !== "width" && text !== "height") {
// Dont warn on standard replacements
console.warn("Invalid replacement pattern", text);
}
return match;
}
// Find node with matching S&R property name
let nodes = app.graph._nodes.filter((n) => n.properties?.["Node name for S&R"] === split[0]);
// If we cant, see if there is a node with that title
if (!nodes.length) {
nodes = app.graph._nodes.filter((n) => n.title === split[0]);
}
if (!nodes.length) {
console.warn("Unable to find node", split[0]);
return match;
}
if (nodes.length > 1) {
console.warn("Multiple nodes matched", split[0], "using first match");
}
const node = nodes[0];
const widget = node.widgets?.find((w) => w.name === split[1]);
if (!widget) {
console.warn("Unable to find widget", split[1], "on node", split[0], node);
return match;
}
return ((widget.value ?? "") + "").replaceAll(/\/|\\/g, "_");
});
};
return r;
};
} else {
// When any other node is created add a property to alias the node
const onNodeCreated = nodeType.prototype.onNodeCreated;
nodeType.prototype.onNodeCreated = function () {
const r = onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined;
if (!this.properties || !("Node name for S&R" in this.properties)) {
this.addProperty("Node name for S&R", this.title, "string");
}
return r;
};
}
},
});

View File

@ -1,8 +1,15 @@
class ComfyApi extends EventTarget { class ComfyApi extends EventTarget {
#registered = new Set();
constructor() { constructor() {
super(); super();
} }
addEventListener(type, callback, options) {
super.addEventListener(type, callback, options);
this.#registered.add(type);
}
/** /**
* Poll status for colab and other things that don't support websockets. * Poll status for colab and other things that don't support websockets.
*/ */
@ -82,7 +89,11 @@ class ComfyApi extends EventTarget {
this.dispatchEvent(new CustomEvent("executed", { detail: msg.data })); this.dispatchEvent(new CustomEvent("executed", { detail: msg.data }));
break; break;
default: default:
throw new Error("Unknown message type"); if (this.#registered.has(msg.type)) {
this.dispatchEvent(new CustomEvent(msg.type, { detail: msg.data }));
} else {
throw new Error("Unknown message type");
}
} }
} catch (error) { } catch (error) {
console.warn("Unhandled message:", event.data); console.warn("Unhandled message:", event.data);

View File

@ -371,14 +371,158 @@ class ComfyApp {
}); });
} }
/**
* Handle mouse
*
* Move group by header
*/
#addProcessMouseHandler() {
const self = this;
const origProcessMouseDown = LGraphCanvas.prototype.processMouseDown;
LGraphCanvas.prototype.processMouseDown = function(e) {
const res = origProcessMouseDown.apply(this, arguments);
this.selected_group_moving = false;
if (this.selected_group && !this.selected_group_resizing) {
var font_size =
this.selected_group.font_size || LiteGraph.DEFAULT_GROUP_FONT_SIZE;
var height = font_size * 1.4;
// Move group by header
if (LiteGraph.isInsideRectangle(e.canvasX, e.canvasY, this.selected_group.pos[0], this.selected_group.pos[1], this.selected_group.size[0], height)) {
this.selected_group_moving = true;
}
}
return res;
}
const origProcessMouseMove = LGraphCanvas.prototype.processMouseMove;
LGraphCanvas.prototype.processMouseMove = function(e) {
const orig_selected_group = this.selected_group;
if (this.selected_group && !this.selected_group_resizing && !this.selected_group_moving) {
this.selected_group = null;
}
const res = origProcessMouseMove.apply(this, arguments);
if (orig_selected_group && !this.selected_group_resizing && !this.selected_group_moving) {
this.selected_group = orig_selected_group;
}
return res;
};
}
/**
* Handle keypress
*
* Ctrl + M mute/unmute selected nodes
*/
#addProcessKeyHandler() {
const self = this;
const origProcessKey = LGraphCanvas.prototype.processKey;
LGraphCanvas.prototype.processKey = function(e) {
const res = origProcessKey.apply(this, arguments);
if (res === false) {
return res;
}
if (!this.graph) {
return;
}
var block_default = false;
if (e.target.localName == "input") {
return;
}
if (e.type == "keydown") {
// Ctrl + M mute/unmute
if (e.keyCode == 77 && e.ctrlKey) {
if (this.selected_nodes) {
for (var i in this.selected_nodes) {
if (this.selected_nodes[i].mode === 2) { // never
this.selected_nodes[i].mode = 0; // always
} else {
this.selected_nodes[i].mode = 2; // never
}
}
}
block_default = true;
}
}
this.graph.change();
if (block_default) {
e.preventDefault();
e.stopImmediatePropagation();
return false;
}
return res;
};
}
/**
* Draws group header bar
*/
#addDrawGroupsHandler() {
const self = this;
const origDrawGroups = LGraphCanvas.prototype.drawGroups;
LGraphCanvas.prototype.drawGroups = function(canvas, ctx) {
if (!this.graph) {
return;
}
var groups = this.graph._groups;
ctx.save();
ctx.globalAlpha = 0.7 * this.editor_alpha;
for (var i = 0; i < groups.length; ++i) {
var group = groups[i];
if (!LiteGraph.overlapBounding(this.visible_area, group._bounding)) {
continue;
} //out of the visible area
ctx.fillStyle = group.color || "#335";
ctx.strokeStyle = group.color || "#335";
var pos = group._pos;
var size = group._size;
ctx.globalAlpha = 0.25 * this.editor_alpha;
ctx.beginPath();
var font_size =
group.font_size || LiteGraph.DEFAULT_GROUP_FONT_SIZE;
ctx.rect(pos[0] + 0.5, pos[1] + 0.5, size[0], font_size * 1.4);
ctx.fill();
ctx.globalAlpha = this.editor_alpha;
}
ctx.restore();
const res = origDrawGroups.apply(this, arguments);
return res;
}
}
/** /**
* Draws node highlights (executing, drag drop) and progress bar * Draws node highlights (executing, drag drop) and progress bar
*/ */
#addDrawNodeHandler() { #addDrawNodeHandler() {
const orig = LGraphCanvas.prototype.drawNodeShape; const origDrawNodeShape = LGraphCanvas.prototype.drawNodeShape;
const self = this; const self = this;
LGraphCanvas.prototype.drawNodeShape = function (node, ctx, size, fgcolor, bgcolor, selected, mouse_over) { LGraphCanvas.prototype.drawNodeShape = function (node, ctx, size, fgcolor, bgcolor, selected, mouse_over) {
const res = orig.apply(this, arguments); const res = origDrawNodeShape.apply(this, arguments);
let color = null; let color = null;
if (node.id === +self.runningNodeId) { if (node.id === +self.runningNodeId) {
@ -427,6 +571,21 @@ class ComfyApp {
return res; return res;
}; };
const origDrawNode = LGraphCanvas.prototype.drawNode;
LGraphCanvas.prototype.drawNode = function (node, ctx) {
var editor_alpha = this.editor_alpha;
if (node.mode === 2) { // never
this.editor_alpha = 0.4;
}
const res = origDrawNode.apply(this, arguments);
this.editor_alpha = editor_alpha;
return res;
};
} }
/** /**
@ -458,6 +617,10 @@ class ComfyApp {
api.addEventListener("executed", ({ detail }) => { api.addEventListener("executed", ({ detail }) => {
this.nodeOutputs[detail.node] = detail.output; this.nodeOutputs[detail.node] = detail.output;
const node = this.graph.getNodeById(detail.node);
if (node?.onExecuted) {
node.onExecuted(detail.output);
}
}); });
api.init(); api.init();
@ -486,27 +649,6 @@ 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
*/ */
@ -518,12 +660,13 @@ class ComfyApp {
canvasEl.tabIndex = "1"; canvasEl.tabIndex = "1";
document.body.prepend(canvasEl); document.body.prepend(canvasEl);
this.#addProcessMouseHandler();
this.#addProcessKeyHandler();
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() {
@ -561,6 +704,7 @@ class ComfyApp {
setInterval(() => localStorage.setItem("workflow", JSON.stringify(this.graph.serialize())), 1000); setInterval(() => localStorage.setItem("workflow", JSON.stringify(this.graph.serialize())), 1000);
this.#addDrawNodeHandler(); this.#addDrawNodeHandler();
this.#addDrawGroupsHandler();
this.#addApiUpdateHandlers(); this.#addApiUpdateHandlers();
this.#addDropHandler(); this.#addDropHandler();
this.#addPasteHandler(); this.#addPasteHandler();
@ -599,23 +743,29 @@ class ComfyApp {
const inputData = inputs[inputName]; const inputData = inputs[inputName];
const type = inputData[0]; const type = inputData[0];
if (Array.isArray(type)) { if(inputData[1]?.forceInput) {
// Enums
Object.assign(config, widgets.COMBO(this, inputName, inputData, app) || {});
} else if (`${type}:${inputName}` in widgets) {
// Support custom widgets by Type:Name
Object.assign(config, widgets[`${type}:${inputName}`](this, inputName, inputData, app) || {});
} else if (type in widgets) {
// Standard type widgets
Object.assign(config, widgets[type](this, inputName, inputData, app) || {});
} else {
// Node connection inputs
this.addInput(inputName, type); this.addInput(inputName, type);
} else {
if (Array.isArray(type)) {
// Enums
Object.assign(config, widgets.COMBO(this, inputName, inputData, app) || {});
} else if (`${type}:${inputName}` in widgets) {
// Support custom widgets by Type:Name
Object.assign(config, widgets[`${type}:${inputName}`](this, inputName, inputData, app) || {});
} else if (type in widgets) {
// Standard type widgets
Object.assign(config, widgets[type](this, inputName, inputData, app) || {});
} else {
// Node connection inputs
this.addInput(inputName, type);
}
} }
} }
for (const output of nodeData["output"]) { for (const o in nodeData["output"]) {
this.addOutput(output, output); const output = nodeData["output"][o];
const outputName = nodeData["output_name"][o] || output;
this.addOutput(outputName, output);
} }
const s = this.computeSize(); const s = this.computeSize();
@ -649,6 +799,8 @@ class ComfyApp {
* @param {*} graphData A serialized graph object * @param {*} graphData A serialized graph object
*/ */
loadGraphData(graphData) { loadGraphData(graphData) {
this.clean();
if (!graphData) { if (!graphData) {
graphData = defaultGraph; graphData = defaultGraph;
} }
@ -703,6 +855,11 @@ class ComfyApp {
continue; continue;
} }
if (node.mode === 2) {
// Don't serialize muted nodes
continue;
}
const inputs = {}; const inputs = {};
const widgets = node.widgets; const widgets = node.widgets;
@ -742,6 +899,18 @@ class ComfyApp {
}; };
} }
// Remove inputs connected to removed nodes
for (const o in output) {
for (const i in output[o].inputs) {
if (Array.isArray(output[o].inputs[i])
&& output[o].inputs[i].length === 2
&& !output[output[o].inputs[i][0]]) {
delete output[o].inputs[i];
}
}
}
return { workflow, output }; return { workflow, output };
} }
@ -806,6 +975,38 @@ class ComfyApp {
} }
this.extensions.push(extension); this.extensions.push(extension);
} }
/**
* Refresh combo list on whole nodes
*/
async refreshComboInNodes() {
const defs = await api.getNodeDefs();
for(let nodeNum in this.graph._nodes) {
const node = this.graph._nodes[nodeNum];
const def = defs[node.type];
for(const widgetNum in node.widgets) {
const widget = node.widgets[widgetNum]
if(widget.type == "combo" && def["input"]["required"][widget.name] !== undefined) {
widget.options.values = def["input"]["required"][widget.name][0];
if(!widget.options.values.includes(widget.value)) {
widget.value = widget.options.values[0];
}
}
}
}
}
/**
* Clean current state
*/
clean() {
this.nodeOutputs = {};
}
} }
export const app = new ComfyApp(); export const app = new ComfyApp();

View File

@ -1,6 +1,6 @@
import { api } from "./api.js"; import { api } from "./api.js";
function $el(tag, propsOrChildren, children) { export function $el(tag, propsOrChildren, children) {
const split = tag.split("."); const split = tag.split(".");
const element = document.createElement(split.shift()); const element = document.createElement(split.shift());
element.classList.add(...split); element.classList.add(...split);
@ -114,6 +114,17 @@ class ComfySettingsDialog extends ComfyDialog {
this.settings = []; this.settings = [];
} }
getSettingValue(id, defaultValue) {
const settingId = "Comfy.Settings." + id;
const v = localStorage[settingId];
return v == null ? defaultValue : JSON.parse(v);
}
setSettingValue(id, value) {
const settingId = "Comfy.Settings." + id;
localStorage[settingId] = JSON.stringify(value);
}
addSetting({ id, name, type, defaultValue, onChange }) { addSetting({ id, name, type, defaultValue, onChange }) {
if (!id) { if (!id) {
throw new Error("Settings must have an ID"); throw new Error("Settings must have an ID");
@ -142,7 +153,7 @@ class ComfySettingsDialog extends ComfyDialog {
}; };
if (typeof type === "function") { if (typeof type === "function") {
return type(name, setter); return type(name, setter, value);
} }
switch (type) { switch (type) {
@ -376,7 +387,11 @@ export class ComfyUI {
}, },
}), }),
$el("button", { textContent: "Load", onclick: () => fileInput.click() }), $el("button", { textContent: "Load", onclick: () => fileInput.click() }),
$el("button", { textContent: "Clear", onclick: () => app.graph.clear() }), $el("button", { textContent: "Refresh", onclick: () => app.refreshComboInNodes() }),
$el("button", { textContent: "Clear", onclick: () => {
app.clean();
app.graph.clear();
}}),
$el("button", { textContent: "Load Default", onclick: () => app.loadGraphData() }), $el("button", { textContent: "Load Default", onclick: () => app.loadGraphData() }),
$el("button", { textContent: "Delete Images", onclick: () => api.deleteAllImages() }), $el("button", { textContent: "Delete Images", onclick: () => api.deleteAllImages() }),
]); ]);

View File

@ -64,6 +64,12 @@ body {
margin-bottom: 20px; /* Add some margin between the text and the close button*/ margin-bottom: 20px; /* Add some margin between the text and the close button*/
} }
.comfy-modal select,
.comfy-modal input[type=button],
.comfy-modal input[type=checkbox] {
margin: 3px 3px 3px 4px;
}
.comfy-modal button { .comfy-modal button {
cursor: pointer; cursor: pointer;
color: #aaaaaa; color: #aaaaaa;
@ -95,6 +101,12 @@ body {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
color: #999;
background-color: #353535;
font-family: sans-serif;
padding: 10px;
border-radius: 0 8px 8px 8px;
box-shadow: 3px 3px 8px rgba(0, 0, 0, 0.4);
} }
.comfy-menu button { .comfy-menu button {
@ -109,6 +121,22 @@ body {
.comfy-menu-btns button { .comfy-menu-btns button {
font-size: 10px; font-size: 10px;
width: 50%; width: 50%;
color: #999 !important;
}
.comfy-menu > button {
width: 100%;
}
.comfy-menu > button,
.comfy-menu-btns button,
.comfy-menu .comfy-list button {
color: #ddd;
background-color: #222;
border-radius: 8px;
border-color: #4e4e4e;
border-style: solid;
margin-top: 2px;
} }
.comfy-menu span.drag-handle { .comfy-menu span.drag-handle {
@ -141,14 +169,18 @@ body {
} }
.comfy-list { .comfy-list {
background-color: rgb(225, 225, 225); color: #999;
background-color: #333;
margin-bottom: 10px; margin-bottom: 10px;
border-color: #4e4e4e;
border-style: solid;
} }
.comfy-list-items { .comfy-list-items {
overflow-y: scroll; overflow-y: scroll;
max-height: 100px; max-height: 100px;
background-color: #d0d0d0; min-height: 25px;
background-color: #222;
padding: 5px; padding: 5px;
} }
@ -175,6 +207,7 @@ body {
} }
button.comfy-settings-btn { button.comfy-settings-btn {
background-color: rgba(0, 0, 0, 0);
font-size: 12px; font-size: 12px;
padding: 0; padding: 0;
position: absolute; position: absolute;
@ -182,6 +215,10 @@ button.comfy-settings-btn {
border: none; border: none;
} }
button.comfy-queue-btn {
margin: 6px 0 !important;
}
.comfy-modal.comfy-settings { .comfy-modal.comfy-settings {
background-color: var(--bg-color); background-color: var(--bg-color);
color: var(--fg-color); color: var(--fg-color);
@ -190,6 +227,13 @@ button.comfy-settings-btn {
@media only screen and (max-height: 850px) { @media only screen and (max-height: 850px) {
.comfy-menu { .comfy-menu {
margin-top: -70px; top: 0 !important;
bottom: 0 !important;
left: auto !important;
right: 0 !important;
border-radius: 0px;
}
.comfy-menu span.drag-handle {
visibility:hidden
} }
} }