From 8d0a142321f3a59303af04aa3c8ee1e9be4c19e1 Mon Sep 17 00:00:00 2001 From: pythongosssss <125205205+pythongosssss@users.noreply.github.com> Date: Fri, 24 Mar 2023 11:39:09 +0000 Subject: [PATCH 01/31] adds simple access to server from custom nodes --- server.py | 2 ++ web/scripts/api.js | 13 ++++++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/server.py b/server.py index 73429acca..2593e8086 100644 --- a/server.py +++ b/server.py @@ -29,6 +29,8 @@ async def cache_control(request: web.Request, handler): class PromptServer(): def __init__(self, loop): + PromptServer.instance = self + mimetypes.init(); mimetypes.types_map['.js'] = 'application/javascript; charset=utf-8' self.prompt_queue = None diff --git a/web/scripts/api.js b/web/scripts/api.js index b90b1c656..2b90c2abc 100644 --- a/web/scripts/api.js +++ b/web/scripts/api.js @@ -1,8 +1,15 @@ class ComfyApi extends EventTarget { + #registered = new Set(); + constructor() { 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. */ @@ -82,7 +89,11 @@ class ComfyApi extends EventTarget { this.dispatchEvent(new CustomEvent("executed", { detail: msg.data })); break; 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) { console.warn("Unhandled message:", event.data); From 8a35c12e8c2dcac25ab1dcaba5bd832c278dcce4 Mon Sep 17 00:00:00 2001 From: m957ymj75urz Date: Sun, 26 Mar 2023 13:10:20 +0200 Subject: [PATCH 02/31] compute %width% and %height% in filepath when saving --- nodes.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/nodes.py b/nodes.py index a981abb8e..bec2bf9a7 100644 --- a/nodes.py +++ b/nodes.py @@ -746,6 +746,13 @@ class SaveImage: except: digits = 0 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)) filename = os.path.basename(os.path.normpath(filename_prefix)) From 48efadeccf9793ea86df89c173c34279ebe07f1f Mon Sep 17 00:00:00 2001 From: comfyanonymous Date: Sun, 26 Mar 2023 15:16:52 -0400 Subject: [PATCH 03/31] Style. --- nodes.py | 4 ++-- server.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/nodes.py b/nodes.py index bec2bf9a7..f24bc4a5e 100644 --- a/nodes.py +++ b/nodes.py @@ -746,12 +746,12 @@ class SaveImage: except: digits = 0 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)) diff --git a/server.py b/server.py index 1a370317f..e71289cdf 100644 --- a/server.py +++ b/server.py @@ -30,7 +30,7 @@ async def cache_control(request: web.Request, handler): class PromptServer(): def __init__(self, loop): PromptServer.instance = self - + mimetypes.init(); mimetypes.types_map['.js'] = 'application/javascript; charset=utf-8' self.prompt_queue = None From 967bfce0f3b06bcb7e562b9296960d320e978898 Mon Sep 17 00:00:00 2001 From: Jairo Correa Date: Sun, 26 Mar 2023 21:40:36 -0300 Subject: [PATCH 04/31] Optional RETURN_NAMES to set the output name --- server.py | 1 + web/scripts/app.js | 6 ++++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/server.py b/server.py index e71289cdf..80fb2dc72 100644 --- a/server.py +++ b/server.py @@ -152,6 +152,7 @@ class PromptServer(): info = {} 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['description'] = '' info['category'] = 'sd' diff --git a/web/scripts/app.js b/web/scripts/app.js index a743ef63b..e9c869249 100644 --- a/web/scripts/app.js +++ b/web/scripts/app.js @@ -611,8 +611,10 @@ class ComfyApp { } } - for (const output of nodeData["output"]) { - this.addOutput(output, output); + for (const o in nodeData["output"]) { + const output = nodeData["output"][o]; + const outputName = nodeData["output_name"][o] || output; + this.addOutput(outputName, output); } const s = this.computeSize(); From 63525ee83ce5adaea6831b1e43df721d47c2e709 Mon Sep 17 00:00:00 2001 From: Jairo Correa Date: Sun, 26 Mar 2023 22:53:49 -0300 Subject: [PATCH 05/31] Move group by header --- web/scripts/app.js | 93 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) diff --git a/web/scripts/app.js b/web/scripts/app.js index a743ef63b..c5f90b116 100644 --- a/web/scripts/app.js +++ b/web/scripts/app.js @@ -371,6 +371,96 @@ 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; + }; + } + + /** + * 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 */ @@ -518,6 +608,8 @@ class ComfyApp { canvasEl.tabIndex = "1"; document.body.prepend(canvasEl); + this.#addProcessMouseHandler(); + this.graph = new LGraph(); const canvas = (this.canvas = new LGraphCanvas(canvasEl, this.graph)); this.ctx = canvasEl.getContext("2d"); @@ -561,6 +653,7 @@ class ComfyApp { setInterval(() => localStorage.setItem("workflow", JSON.stringify(this.graph.serialize())), 1000); this.#addDrawNodeHandler(); + this.#addDrawGroupsHandler(); this.#addApiUpdateHandlers(); this.#addDropHandler(); this.#addPasteHandler(); From cf0098d5398cc330ffa8af9a0472ad1b8610515a Mon Sep 17 00:00:00 2001 From: Francesco Yoshi Gobbo Date: Mon, 27 Mar 2023 04:51:18 +0200 Subject: [PATCH 06/31] no lowvram state if cpu only --- comfy/model_management.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/comfy/model_management.py b/comfy/model_management.py index 0d5702b91..d9498e29f 100644 --- a/comfy/model_management.py +++ b/comfy/model_management.py @@ -22,7 +22,8 @@ try: total_vram = torch.cuda.mem_get_info(torch.cuda.current_device())[1] / (1024 * 1024) total_ram = psutil.virtual_memory().total / (1024 * 1024) forced_normal_vram = "--normalvram" in sys.argv - if not forced_normal_vram: + forced_cpu = "--cpu" in sys.argv + if not forced_normal_vram and not forced_cpu: 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") set_vram_to = LOW_VRAM From f55755f0d24255a1207a803dc30efe9e4466bfe1 Mon Sep 17 00:00:00 2001 From: Francesco Yoshi Gobbo Date: Mon, 27 Mar 2023 06:48:09 +0200 Subject: [PATCH 07/31] code cleanup --- comfy/model_management.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/comfy/model_management.py b/comfy/model_management.py index d9498e29f..4aa47ff16 100644 --- a/comfy/model_management.py +++ b/comfy/model_management.py @@ -15,6 +15,8 @@ total_vram_available_mb = -1 import sys import psutil +forced_cpu = "--cpu" in sys.argv + set_vram_to = NORMAL_VRAM try: @@ -22,7 +24,6 @@ try: total_vram = torch.cuda.mem_get_info(torch.cuda.current_device())[1] / (1024 * 1024) total_ram = psutil.virtual_memory().total / (1024 * 1024) forced_normal_vram = "--normalvram" in sys.argv - forced_cpu = "--cpu" in sys.argv if not forced_normal_vram and not forced_cpu: 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") @@ -84,7 +85,7 @@ try: except: pass -if "--cpu" in sys.argv: +if forced_cpu: vram_state = CPU print("Set vram state to:", ["CPU", "NO VRAM", "LOW VRAM", "NORMAL VRAM", "HIGH VRAM", "MPS"][vram_state]) From bb1503ed63eb49b43a4c1a5abd4df0872cac5d8a Mon Sep 17 00:00:00 2001 From: Silversith Date: Mon, 27 Mar 2023 07:16:22 +0200 Subject: [PATCH 08/31] Bugfix/include optional node inputs (#271) * Minor changes and extra nodes * Added Preview Image for Nodes * Add a delete images button on main floating menu * Add a confirmation dialog * Remove DeleteAll, Remove Custom KSampler, Remove Image List * Remove Image and Custom Json Load * Remove Custom Nodes * remove patch.diff --- nodes.py | 2 +- web/scripts/app.js | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/nodes.py b/nodes.py index f24bc4a5e..6fb7f0175 100644 --- a/nodes.py +++ b/nodes.py @@ -1052,4 +1052,4 @@ def 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")) \ No newline at end of file diff --git a/web/scripts/app.js b/web/scripts/app.js index a9d3485ca..89eb71122 100644 --- a/web/scripts/app.js +++ b/web/scripts/app.js @@ -683,7 +683,10 @@ class ComfyApp { const nodeData = defs[nodeId]; const node = Object.assign( function ComfyNode() { - const inputs = nodeData["input"]["required"]; + var inputs = nodeData["input"]["required"]; + if (nodeData["input"]["optional"] != undefined){ + inputs = Object.assign({}, nodeData["input"]["required"], nodeData["input"]["optional"]) + } const config = { minWidth: 1, minHeight: 1 }; for (const inputName in inputs) { const inputData = inputs[inputName]; From 3444ffff3b892456f94c13365ab498e6971f018d Mon Sep 17 00:00:00 2001 From: comfyanonymous Date: Mon, 27 Mar 2023 01:56:22 -0400 Subject: [PATCH 09/31] Fix IS_CHANGED not working on nodes with an input from another node. --- execution.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/execution.py b/execution.py index 757e0d9f9..aafc86976 100644 --- a/execution.py +++ b/execution.py @@ -18,6 +18,8 @@ def get_input_data(inputs, class_def, outputs={}, prompt={}, extra_data={}): if isinstance(input_data, list): input_unique_id = input_data[0] output_index = input_data[1] + if input_unique_id not in outputs: + return None obj = outputs[input_unique_id][output_index] input_data_all[x] = obj else: @@ -94,9 +96,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]: is_changed_old = old_prompt[unique_id]['is_changed'] if 'is_changed' not in prompt[unique_id]: - input_data_all = get_input_data(inputs, class_def) - is_changed = class_def.IS_CHANGED(**input_data_all) - prompt[unique_id]['is_changed'] = is_changed + input_data_all = get_input_data(inputs, class_def, outputs) + if input_data_all is not None: + is_changed = class_def.IS_CHANGED(**input_data_all) + prompt[unique_id]['is_changed'] = is_changed else: is_changed = prompt[unique_id]['is_changed'] From bb1223d83fe7fba6bbe80dea22ff8f1044b709d9 Mon Sep 17 00:00:00 2001 From: comfyanonymous Date: Mon, 27 Mar 2023 02:16:58 -0400 Subject: [PATCH 10/31] Fix errors appearing more than once. --- execution.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/execution.py b/execution.py index aafc86976..3ca551db6 100644 --- a/execution.py +++ b/execution.py @@ -281,7 +281,7 @@ def validate_prompt(prompt): errors += [(o, reason)] 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 (True, "") From d3a375c8fbd443b674eb4a1027c200cb34a59359 Mon Sep 17 00:00:00 2001 From: ltdrdata <128333288+ltdrdata@users.noreply.github.com> Date: Tue, 28 Mar 2023 02:27:09 +0900 Subject: [PATCH 11/31] Add support for file list refresh feature in node (#192) * Added file reload feature to widgets. * Modify feature name 'reload' to 'refresh' and fixed ignoring button name. * refresh widget bugfix * crash patch for "widget" input by type mismatch * compensate offset on showimage * adding widget caused misaligned offset * patch refresh feature for general method * clean up patch and following upstream * make more clean code for refresh feature * move refresh button position * robust patch for refresh feature * patch for refreh feature * avoid specify REFRESH_LIST for each node * prevent updating selected value unless removed item * update all combo list for 'required input' in node --------- Co-authored-by: Lt.Dr.Data --- web/scripts/app.js | 25 +++++++++++++++++++++++++ web/scripts/ui.js | 1 + 2 files changed, 26 insertions(+) diff --git a/web/scripts/app.js b/web/scripts/app.js index 89eb71122..43d7f7b59 100644 --- a/web/scripts/app.js +++ b/web/scripts/app.js @@ -901,6 +901,31 @@ class ComfyApp { } 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]; + } + } + } + } + } } export const app = new ComfyApp(); diff --git a/web/scripts/ui.js b/web/scripts/ui.js index 94f3c528a..2c3bf87c7 100644 --- a/web/scripts/ui.js +++ b/web/scripts/ui.js @@ -376,6 +376,7 @@ export class ComfyUI { }, }), $el("button", { textContent: "Load", onclick: () => fileInput.click() }), + $el("button", { textContent: "Refresh", onclick: () => app.refreshComboInNodes() }), $el("button", { textContent: "Clear", onclick: () => app.graph.clear() }), $el("button", { textContent: "Load Default", onclick: () => app.loadGraphData() }), ]); From 4b9e11053c70925069f5bbe3adbd5c0e27e70f97 Mon Sep 17 00:00:00 2001 From: Jairo Correa Date: Mon, 27 Mar 2023 23:36:53 -0300 Subject: [PATCH 12/31] Color palette setting --- web/extensions/core/colorPalette.js | 351 ++++++++++++++++++++++++++++ web/scripts/app.js | 23 -- web/scripts/ui.js | 15 +- web/style.css | 6 + 4 files changed, 370 insertions(+), 25 deletions(-) create mode 100644 web/extensions/core/colorPalette.js diff --git a/web/extensions/core/colorPalette.js b/web/extensions/core/colorPalette.js new file mode 100644 index 000000000..e54bc2a38 --- /dev/null +++ b/web/extensions/core/colorPalette.js @@ -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]); + } + } + }, + }); + }, +}); diff --git a/web/scripts/app.js b/web/scripts/app.js index 43d7f7b59..609cc4cf7 100644 --- a/web/scripts/app.js +++ b/web/scripts/app.js @@ -576,27 +576,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 */ @@ -614,8 +593,6 @@ class ComfyApp { const canvas = (this.canvas = new LGraphCanvas(canvasEl, this.graph)); this.ctx = canvasEl.getContext("2d"); - this.setupSlotColors(); - this.graph.start(); function resizeCanvas() { diff --git a/web/scripts/ui.js b/web/scripts/ui.js index 2c3bf87c7..c79caaa9b 100644 --- a/web/scripts/ui.js +++ b/web/scripts/ui.js @@ -1,6 +1,6 @@ import { api } from "./api.js"; -function $el(tag, propsOrChildren, children) { +export function $el(tag, propsOrChildren, children) { const split = tag.split("."); const element = document.createElement(split.shift()); element.classList.add(...split); @@ -114,6 +114,17 @@ class ComfySettingsDialog extends ComfyDialog { 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 }) { if (!id) { throw new Error("Settings must have an ID"); @@ -142,7 +153,7 @@ class ComfySettingsDialog extends ComfyDialog { }; if (typeof type === "function") { - return type(name, setter); + return type(name, setter, value); } switch (type) { diff --git a/web/style.css b/web/style.css index d3168044f..7c3d7efa5 100644 --- a/web/style.css +++ b/web/style.css @@ -64,6 +64,12 @@ body { 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 { cursor: pointer; color: #aaaaaa; From 31dd6c0531367c40537e1fc866db31ac842cd5f4 Mon Sep 17 00:00:00 2001 From: comfyanonymous Date: Tue, 28 Mar 2023 01:42:34 -0400 Subject: [PATCH 13/31] Add way to specify listen ip with --listen. --- main.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/main.py b/main.py index 26bad1b8b..d82d1d6ed 100644 --- a/main.py +++ b/main.py @@ -12,7 +12,7 @@ if os.name == "nt": if __name__ == "__main__": if '--help' in sys.argv: 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--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.") @@ -92,11 +92,19 @@ if __name__ == "__main__": hijack_progress(server) threading.Thread(target=prompt_worker, daemon=True, args=(q,server,)).start() - if '--listen' in sys.argv: + try: 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' + dont_print = False if '--dont-print-server' in sys.argv: dont_print = True From 1e0f2b232bd0b1a5dbd5c16f08e86a5d421a91aa Mon Sep 17 00:00:00 2001 From: Davemane42 Date: Tue, 28 Mar 2023 02:52:12 -0400 Subject: [PATCH 14/31] add unique_id to nodes hidden inputs @classmethod def INPUT_TYPES(cls): return { "hidden": {"unique_id": "UNIQUE_ID"}, } --- execution.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/execution.py b/execution.py index 3ca551db6..2b26a0f78 100644 --- a/execution.py +++ b/execution.py @@ -10,7 +10,7 @@ import gc import torch 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() input_data_all = {} for x in inputs: @@ -34,6 +34,8 @@ def get_input_data(inputs, class_def, outputs={}, prompt={}, extra_data={}): if h[x] == "EXTRA_PNGINFO": if "extra_pnginfo" in extra_data: input_data_all[x] = extra_data['extra_pnginfo'] + if h[x] == "UNIQUE_ID": + input_data_all[x] = unique_id return input_data_all def recursive_execute(server, prompt, outputs, current_item, extra_data={}): @@ -55,7 +57,7 @@ def recursive_execute(server, prompt, outputs, current_item, extra_data={}): if input_unique_id not in outputs: 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: server.last_node_id = unique_id server.send_sync("executing", { "node": unique_id }, server.client_id) @@ -96,7 +98,7 @@ 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]: is_changed_old = old_prompt[unique_id]['is_changed'] if 'is_changed' not in prompt[unique_id]: - input_data_all = get_input_data(inputs, class_def, outputs) + input_data_all = get_input_data(inputs, class_def, unique_id, outputs) if input_data_all is not None: is_changed = class_def.IS_CHANGED(**input_data_all) prompt[unique_id]['is_changed'] = is_changed From 393084877c71dc7dc9549b0f3694b02060151917 Mon Sep 17 00:00:00 2001 From: Farid Safi Date: Tue, 28 Mar 2023 19:45:17 +0200 Subject: [PATCH 15/31] clean state when loading another workflow --- web/scripts/app.js | 7 +++++++ web/scripts/ui.js | 11 +++++++++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/web/scripts/app.js b/web/scripts/app.js index 609cc4cf7..ddb829ab4 100644 --- a/web/scripts/app.js +++ b/web/scripts/app.js @@ -903,6 +903,13 @@ class ComfyApp { } } } + + /** + * Clean current state + */ + clean() { + this.nodeOutputs = {}; + } } export const app = new ComfyApp(); diff --git a/web/scripts/ui.js b/web/scripts/ui.js index c79caaa9b..7e73c1080 100644 --- a/web/scripts/ui.js +++ b/web/scripts/ui.js @@ -306,6 +306,7 @@ export class ComfyUI { style: { display: "none" }, parent: document.body, onchange: () => { + app.clean(); app.handleFile(fileInput.files[0]); }, }); @@ -388,8 +389,14 @@ export class ComfyUI { }), $el("button", { textContent: "Load", onclick: () => fileInput.click() }), $el("button", { textContent: "Refresh", onclick: () => app.refreshComboInNodes() }), - $el("button", { textContent: "Clear", onclick: () => app.graph.clear() }), - $el("button", { textContent: "Load Default", onclick: () => app.loadGraphData() }), + $el("button", { textContent: "Clear", onclick: () => { + app.clean(); + app.graph.clear(); + }}), + $el("button", { textContent: "Load Default", onclick: () => { + app.clean(); + app.loadGraphData(); + }}), ]); dragElement(this.menuContainer); From 40a377775e7e383c09418297637a0cc261ead96d Mon Sep 17 00:00:00 2001 From: Farid Safi Date: Tue, 28 Mar 2023 20:22:49 +0200 Subject: [PATCH 16/31] move clean to handleFile and loadGraphData functions --- web/scripts/app.js | 2 ++ web/scripts/ui.js | 6 +----- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/web/scripts/app.js b/web/scripts/app.js index ddb829ab4..cd7fb5d1b 100644 --- a/web/scripts/app.js +++ b/web/scripts/app.js @@ -721,6 +721,8 @@ class ComfyApp { * @param {*} graphData A serialized graph object */ loadGraphData(graphData) { + this.clean(); + if (!graphData) { graphData = defaultGraph; } diff --git a/web/scripts/ui.js b/web/scripts/ui.js index 7e73c1080..2aabd29e7 100644 --- a/web/scripts/ui.js +++ b/web/scripts/ui.js @@ -306,7 +306,6 @@ export class ComfyUI { style: { display: "none" }, parent: document.body, onchange: () => { - app.clean(); app.handleFile(fileInput.files[0]); }, }); @@ -393,10 +392,7 @@ export class ComfyUI { app.clean(); app.graph.clear(); }}), - $el("button", { textContent: "Load Default", onclick: () => { - app.clean(); - app.loadGraphData(); - }}), + $el("button", { textContent: "Load Default", onclick: () => app.loadGraphData() }), ]); dragElement(this.menuContainer); From 0d65cb17b77ff80f76a6fe8181860c1694158645 Mon Sep 17 00:00:00 2001 From: comfyanonymous Date: Tue, 28 Mar 2023 16:29:35 -0400 Subject: [PATCH 17/31] Fix ddim_uniform crashing with 37 steps. --- comfy/samplers.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/comfy/samplers.py b/comfy/samplers.py index 17201d9dc..4f61a8462 100644 --- a/comfy/samplers.py +++ b/comfy/samplers.py @@ -242,7 +242,10 @@ def ddim_scheduler(model, steps): 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) 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] return torch.FloatTensor(sigs) @@ -373,7 +376,7 @@ class KSampler: def set_steps(self, steps, denoise=None): self.steps = steps - if denoise is None: + if denoise is None or denoise > 0.9999: self.sigmas = self._calculate_sigmas(steps) else: new_steps = int(steps/denoise) From 3ed814b01fcad1cf95bfd3fde4c18d0ae8bc9b13 Mon Sep 17 00:00:00 2001 From: comfyanonymous Date: Tue, 28 Mar 2023 23:58:27 -0400 Subject: [PATCH 18/31] Fix colab. --- notebooks/comfyui_colab.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/notebooks/comfyui_colab.ipynb b/notebooks/comfyui_colab.ipynb index 5108ec830..276579c99 100644 --- a/notebooks/comfyui_colab.ipynb +++ b/notebooks/comfyui_colab.ipynb @@ -47,7 +47,7 @@ " !git pull\n", "\n", "!echo -= Install dependencies =-\n", - "!pip -q install xformers -r requirements.txt" + "!pip -q install xformers==0.0.16 -r requirements.txt" ] }, { From b2554bc4dd69c3f5ad965edbc5585c4ff4948458 Mon Sep 17 00:00:00 2001 From: comfyanonymous Date: Wed, 29 Mar 2023 02:24:37 -0400 Subject: [PATCH 19/31] Split VAE decode batches depending on free memory. --- comfy/sd.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/comfy/sd.py b/comfy/sd.py index d767d8671..2e1ae8409 100644 --- a/comfy/sd.py +++ b/comfy/sd.py @@ -439,9 +439,14 @@ class VAE: model_management.unload_model() self.first_stage_model = self.first_stage_model.to(self.device) try: - samples = samples_in.to(self.device) - pixel_samples = self.first_stage_model.decode(1. / self.scale_factor * samples) - pixel_samples = torch.clamp((pixel_samples + 1.0) / 2.0, min=0.0, max=1.0) + free_memory = model_management.get_free_memory(self.device) + batch_number = int((free_memory * 0.7) / (2562 * samples_in.shape[2] * samples_in.shape[3] * 64)) + 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: print("Warning: Ran out of memory when regular VAE decoding, retrying with tiled VAE decoding.") pixel_samples = self.decode_tiled_(samples_in) From d5bf2314b3ee319585a1b2dc6a75cca04c8474f8 Mon Sep 17 00:00:00 2001 From: Jairo Correa Date: Sun, 26 Mar 2023 01:45:40 -0300 Subject: [PATCH 20/31] Mute nodes and shortcuts in README --- README.md | 7 ++++ web/scripts/app.js | 91 +++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 96 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 4ff031331..8eab7f483 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,13 @@ 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/) +## Shortcuts +- **Ctrl + C** copy selected nodes +- **Ctrl + V** paste copied nodes +- **Ctrl + A** select all nodes +- **Ctrl + M** mute/unmute selected nodes +- **Delete** or **Backspace** delete selected nodes + # Installing ## Windows diff --git a/web/scripts/app.js b/web/scripts/app.js index cd7fb5d1b..3b1f5450a 100644 --- a/web/scripts/app.js +++ b/web/scripts/app.js @@ -417,6 +417,59 @@ class ComfyApp { }; } + /** + * 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 */ @@ -465,10 +518,11 @@ class ComfyApp { * Draws node highlights (executing, drag drop) and progress bar */ #addDrawNodeHandler() { - const orig = LGraphCanvas.prototype.drawNodeShape; + const origDrawNodeShape = LGraphCanvas.prototype.drawNodeShape; const self = this; + 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; if (node.id === +self.runningNodeId) { @@ -517,6 +571,21 @@ class ComfyApp { 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; + }; } /** @@ -588,6 +657,7 @@ class ComfyApp { document.body.prepend(canvasEl); this.#addProcessMouseHandler(); + this.#addProcessKeyHandler(); this.graph = new LGraph(); const canvas = (this.canvas = new LGraphCanvas(canvasEl, this.graph)); @@ -777,6 +847,11 @@ class ComfyApp { continue; } + if (node.mode === 2) { + // Don't serialize muted nodes + continue; + } + const inputs = {}; const widgets = node.widgets; @@ -816,6 +891,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 }; } From 0f92d41ac708ced9c94725185996a0081b194f89 Mon Sep 17 00:00:00 2001 From: City <125218114+city96@users.noreply.github.com> Date: Wed, 29 Mar 2023 15:52:38 +0200 Subject: [PATCH 21/31] Match comfy-menu style to litegraph --- web/style.css | 35 +++++++++++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/web/style.css b/web/style.css index 7c3d7efa5..943fd6c30 100644 --- a/web/style.css +++ b/web/style.css @@ -101,6 +101,12 @@ body { display: flex; flex-direction: column; 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 { @@ -115,6 +121,22 @@ body { .comfy-menu-btns button { font-size: 10px; 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 { @@ -147,14 +169,18 @@ body { } .comfy-list { - background-color: rgb(225, 225, 225); + color: #999; + background-color: #333; margin-bottom: 10px; + border-color: #4e4e4e; + border-style: solid; } .comfy-list-items { overflow-y: scroll; max-height: 100px; - background-color: #d0d0d0; + min-height: 25px; + background-color: #222; padding: 5px; } @@ -181,6 +207,7 @@ body { } button.comfy-settings-btn { + background-color: rgba(0, 0, 0, 0); font-size: 12px; padding: 0; position: absolute; @@ -188,6 +215,10 @@ button.comfy-settings-btn { border: none; } +button.comfy-queue-btn { + margin: 6px 0 !important; +} + .comfy-modal.comfy-settings { background-color: var(--bg-color); color: var(--fg-color); From fcee62976682997b7fff4cad39f3b734ce3a4849 Mon Sep 17 00:00:00 2001 From: comfyanonymous Date: Wed, 29 Mar 2023 12:56:12 -0400 Subject: [PATCH 22/31] Notebook fix. --- notebooks/comfyui_colab.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/notebooks/comfyui_colab.ipynb b/notebooks/comfyui_colab.ipynb index 276579c99..a86ccc753 100644 --- a/notebooks/comfyui_colab.ipynb +++ b/notebooks/comfyui_colab.ipynb @@ -47,7 +47,7 @@ " !git pull\n", "\n", "!echo -= Install dependencies =-\n", - "!pip -q install xformers==0.0.16 -r requirements.txt" + "!pip install xformers==0.0.16 -r requirements.txt --extra-index-url https://download.pytorch.org/whl/cu117" ] }, { From 7a7c4e7f734c1f534c4b6138a738c63a15db12d2 Mon Sep 17 00:00:00 2001 From: comfyanonymous Date: Wed, 29 Mar 2023 13:13:03 -0400 Subject: [PATCH 23/31] Not compatible with transformers below this version. --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index bc8b3c558..3b4040a29 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,7 +3,7 @@ torchdiffeq torchsde einops open-clip-torch -transformers +transformers>=4.25.1 safetensors pytorch_lightning aiohttp From b90991d2c367df6b1b7c08121b7a952444cc73e5 Mon Sep 17 00:00:00 2001 From: comfyanonymous Date: Wed, 29 Mar 2023 13:41:10 -0400 Subject: [PATCH 24/31] Readme update. --- README.md | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/README.md b/README.md index 8eab7f483..4a1d76c67 100644 --- a/README.md +++ b/README.md @@ -31,8 +31,6 @@ 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/) ## Shortcuts -- **Ctrl + C** copy selected nodes -- **Ctrl + V** paste copied nodes - **Ctrl + A** select all nodes - **Ctrl + M** mute/unmute selected nodes - **Delete** or **Backspace** delete selected nodes @@ -71,7 +69,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: -```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 From 6f72c4c6ff01ee6d19670eb032548d9c4803ed01 Mon Sep 17 00:00:00 2001 From: pythongosssss <125205205+pythongosssss@users.noreply.github.com> Date: Wed, 29 Mar 2023 18:53:24 +0100 Subject: [PATCH 25/31] Allows nodes to return ui data and output data Fire executed event on node when message received --- execution.py | 7 +++++-- web/scripts/app.js | 30 +++++++++++++++++++----------- 2 files changed, 24 insertions(+), 13 deletions(-) diff --git a/execution.py b/execution.py index 2b26a0f78..79c9a3ac0 100644 --- a/execution.py +++ b/execution.py @@ -65,8 +65,11 @@ def recursive_execute(server, prompt, outputs, current_item, extra_data={}): nodes.before_node_execution() outputs[unique_id] = getattr(obj, obj.FUNCTION)(**input_data_all) - if "ui" in outputs[unique_id] and server.client_id is not None: - server.send_sync("executed", { "node": unique_id, "output": outputs[unique_id]["ui"] }, server.client_id) + if "ui" in outputs[unique_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] def recursive_will_execute(prompt, outputs, current_item): diff --git a/web/scripts/app.js b/web/scripts/app.js index 3b1f5450a..63fc22dad 100644 --- a/web/scripts/app.js +++ b/web/scripts/app.js @@ -617,6 +617,10 @@ class ComfyApp { api.addEventListener("executed", ({ detail }) => { this.nodeOutputs[detail.node] = detail.output; + const node = this.graph.getNodeById(detail.node); + if (node.onExecuted) { + node.onExecuted(detail.output); + } }); api.init(); @@ -739,18 +743,22 @@ class ComfyApp { const inputData = inputs[inputName]; const type = inputData[0]; - 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 + if(inputData[1]?.forceInput) { 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); + } } } From 00c1ec498fcee273c09991b56eee4c1bdcd18c85 Mon Sep 17 00:00:00 2001 From: pythongosssss <125205205+pythongosssss@users.noreply.github.com> Date: Wed, 29 Mar 2023 19:03:38 +0100 Subject: [PATCH 26/31] Fix crash if node is removed mid run --- web/scripts/app.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/scripts/app.js b/web/scripts/app.js index 63fc22dad..b29981091 100644 --- a/web/scripts/app.js +++ b/web/scripts/app.js @@ -618,7 +618,7 @@ class ComfyApp { api.addEventListener("executed", ({ detail }) => { this.nodeOutputs[detail.node] = detail.output; const node = this.graph.getNodeById(detail.node); - if (node.onExecuted) { + if (node?.onExecuted) { node.onExecuted(detail.output); } }); From 80e014a69e3948c0919258b6fcf220b9a56f3cae Mon Sep 17 00:00:00 2001 From: pythongosssss <125205205+pythongosssss@users.noreply.github.com> Date: Wed, 29 Mar 2023 22:09:11 +0100 Subject: [PATCH 27/31] Extra formatting values on SaveImage --- web/extensions/core/saveImageExtraOutput.js | 100 ++++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 web/extensions/core/saveImageExtraOutput.js diff --git a/web/extensions/core/saveImageExtraOutput.js b/web/extensions/core/saveImageExtraOutput.js new file mode 100644 index 000000000..ce97b5491 --- /dev/null +++ b/web/extensions/core/saveImageExtraOutput.js @@ -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; + }; + } + }, +}); From 8a730ed20e51d1bc979fb2d86648b93476261542 Mon Sep 17 00:00:00 2001 From: City <125218114+city96@users.noreply.github.com> Date: Thu, 30 Mar 2023 01:43:31 +0200 Subject: [PATCH 28/31] Turn comfy-menu into a sidebar on small screens --- web/style.css | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/web/style.css b/web/style.css index 943fd6c30..9162bbba9 100644 --- a/web/style.css +++ b/web/style.css @@ -227,6 +227,13 @@ button.comfy-queue-btn { @media only screen and (max-height: 850px) { .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 } } From 5218e5d596ce4f097f41d2f0d304e53f00aff423 Mon Sep 17 00:00:00 2001 From: comfyanonymous Date: Wed, 29 Mar 2023 23:28:21 -0400 Subject: [PATCH 29/31] Command line option to set CUDA device. --- main.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/main.py b/main.py index d82d1d6ed..c9809137a 100644 --- a/main.py +++ b/main.py @@ -18,6 +18,7 @@ if __name__ == "__main__": 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--disable-xformers\t\tdisables xformers") + print("\t--cuda-device 1\t\tSet the id of the cuda device this instance will use.") 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--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") 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 server import folder_paths From afd65d3819b8b694a4d2fb9f6e3b103161fd08b2 Mon Sep 17 00:00:00 2001 From: comfyanonymous Date: Thu, 30 Mar 2023 03:50:12 -0400 Subject: [PATCH 30/31] Fix noise mask not working with > 1 batch size on ksamplers. --- comfy/samplers.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/comfy/samplers.py b/comfy/samplers.py index 4f61a8462..66218f887 100644 --- a/comfy/samplers.py +++ b/comfy/samplers.py @@ -221,7 +221,7 @@ class KSamplerX0Inpaint(torch.nn.Module): def forward(self, x, sigma, uncond, cond, cond_scale, denoise_mask, cond_concat=None): if denoise_mask is not None: 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) if denoise_mask is not None: out *= denoise_mask From 9a27030519c6e1e2024df50cdc547f6a05d714bc Mon Sep 17 00:00:00 2001 From: comfyanonymous Date: Thu, 30 Mar 2023 04:15:28 -0400 Subject: [PATCH 31/31] Make it clear how to set the search path for models. --- .ci/windows_base_files/README_VERY_IMPORTANT.txt | 4 ++++ README.md | 5 ++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/.ci/windows_base_files/README_VERY_IMPORTANT.txt b/.ci/windows_base_files/README_VERY_IMPORTANT.txt index c19085320..0216658de 100755 --- a/.ci/windows_base_files/README_VERY_IMPORTANT.txt +++ b/.ci/windows_base_files/README_VERY_IMPORTANT.txt @@ -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. 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. diff --git a/README.md b/README.md index 4a1d76c67..84e0061ff 100644 --- a/README.md +++ b/README.md @@ -45,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 +#### 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 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) @@ -102,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. - # Running ```python main.py```