From c4f2b6861a50733cf77fc24f8e289da95f34b1fe Mon Sep 17 00:00:00 2001 From: Chris Date: Thu, 24 Apr 2025 19:49:28 +1000 Subject: [PATCH 1/2] fix for simultaneous runs --- image_filter.py | 41 +++++++++++++++++++----------- js/image_filter.js | 25 ++++++++++++++++++- js/popup.js | 62 ++++++++++++++++++++++++++++++++++------------ 3 files changed, 96 insertions(+), 32 deletions(-) diff --git a/image_filter.py b/image_filter.py index 26ebeda..8bf3a2f 100644 --- a/image_filter.py +++ b/image_filter.py @@ -2,7 +2,7 @@ from server import PromptServer from aiohttp import web from nodes import PreviewImage, LoadImage from comfy.model_management import InterruptProcessingException, throw_exception_if_processing_interrupted -import time, os +import time, os, random import torch REQUEST_RESHOW = "-1" @@ -32,27 +32,30 @@ async def cg_image_filter_message(request): print(f"Ignoring response {response} as current response is {Message.data}") return web.json_response({}) -def wait(secs, uid): +def wait(secs, uid, unique): Message.setdata(None, "start of wait") end_time = time.monotonic() + secs while(time.monotonic() < end_time and Message.data is None): throw_exception_if_processing_interrupted() - PromptServer.instance.send_sync("cg-image-filter-images", {"tick": int(end_time - time.monotonic()), "uid": uid}) + PromptServer.instance.send_sync("cg-image-filter-images", {"tick": int(end_time - time.monotonic()), "uid": uid, "unique":unique}) time.sleep(0.2) response = Message.data Message.setdata(None, "read response") if response is None: - PromptServer.instance.send_sync("cg-image-filter-images", {"timeout": True, "uid": uid}) + PromptServer.instance.send_sync("cg-image-filter-images", {"timeout": True, "uid": uid, "unique":unique}) return response -def send_with_resend(payload, timeout, uid): +def send_with_resend(payload, timeout, uid, unique): response = WAITING_FOR_RESPONSE + payload['unique'] = unique while response in SPECIALS: PromptServer.instance.send_sync("cg-image-filter-images", payload) - response = wait(timeout, uid) + response = wait(timeout, uid, unique) if response == CANCEL: raise InterruptProcessingException() + if Message.unique != unique: + print("Mismatched uniques...") return response def mask_to_image(mask:torch.Tensor): @@ -60,10 +63,17 @@ def mask_to_image(mask:torch.Tensor): class Message: data:str = None + unique:str = None @classmethod def setdata(cls, v, comment): #print(f"Message.data set to {v} (${comment})") - cls.data = v + if v: + chop = v.split('!unique!') + cls.data = chop[0] + cls.unique = chop[1] if len(chop)>1 else None + else: + cls.data = None + cls.unique = None HIDDEN = { "prompt": "PROMPT", @@ -87,6 +97,7 @@ class ImageFilter(PreviewImage): "images" : ("IMAGE", ), "timeout": ("INT", {"default": 600, "tooltip": "Timeout in seconds."}), "ontimeout": (["send none", "send all", "send first", "send last"], {}), + "node_identifier": ("INT", {"default":0, "max":99999999}), }, "optional": { "latents" : ("LATENT", {"tooltip": "Optional - if provided, will be output"}), @@ -104,7 +115,7 @@ class ImageFilter(PreviewImage): def IS_CHANGED(cls, pick_list, **kwargs): return pick_list or float("NaN") - def func(self, images, timeout, ontimeout, uid, tip="", extra1="", extra2="", extra3="", latents=None, masks=None, pick_list:str="", **kwargs): + def func(self, images, timeout, ontimeout, uid, node_identifier, tip="", extra1="", extra2="", extra3="", latents=None, masks=None, pick_list:str="", **kwargs): e1, e2, e3 = extra1, extra2, extra3 B = images.shape[0] @@ -118,7 +129,7 @@ class ImageFilter(PreviewImage): urls:list[str] = self.save_images(images=images, **kwargs)['ui']['images'] payload = {"uid": uid, "urls":urls, "allsame":all_the_same, "extras":[extra1, extra2, extra3], "tip":tip} - response = send_with_resend(payload, timeout, uid) + response = send_with_resend(payload, timeout, uid, node_identifier) if response: response, e1, e2, e3 = response.split("|||") @@ -151,7 +162,7 @@ class TextImageFilterWithExtras(PreviewImage): "image" : ("IMAGE", ), "text" : ("STRING", {"default":""}), "timeout": ("INT", {"default": 600, "tooltip": "Timeout in seconds."}), - }, + "node_identifier": ("INT", {"default":0, "max":99999999}), }, "optional": { "mask" : ("MASK", {"tooltip": "Optional - if provided, will be overlaid on image"}), "tip" : ("STRING", {"default":"", "tooltip": "Optional - if provided, will be displayed in popup window"}), @@ -167,13 +178,13 @@ class TextImageFilterWithExtras(PreviewImage): def IS_CHANGED(cls, **kwargs): return float("NaN") - def func(self, image, text, timeout, uid, extra1="", extra2="", extra3="", mask=None, tip="", textareaheight=None, **kwargs): + def func(self, image, text, timeout, uid, node_identifier, extra1="", extra2="", extra3="", mask=None, tip="", textareaheight=None, **kwargs): urls:list[str] = self.save_images(images=image, **kwargs)['ui']['images'] payload = {"uid": uid, "urls":urls, "text":text, "extras":[extra1, extra2, extra3], "tip":tip} if textareaheight is not None: payload['textareaheight'] = textareaheight if mask is not None: payload['mask_urls'] = self.save_images(images=mask_to_image(mask), **kwargs)['ui']['images'] - response = send_with_resend(payload, timeout, uid) + response = send_with_resend(payload, timeout, uid, node_identifier) response = response.split("|||") if response else [text, extra1, extra2, extra3] return (image, *response) @@ -193,7 +204,7 @@ class MaskImageFilter(PreviewImage, LoadImage): "image" : ("IMAGE", ), "timeout": ("INT", {"default": 600, "tooltip": "Timeout in seconds."}), "if_no_mask": (["cancel", "send blank"], {}), - }, + "node_identifier": ("INT", {"default":0, "max":99999999}), }, "optional": { "mask" : ("MASK", {"tooltip":"optional initial mask"}) }, @@ -207,7 +218,7 @@ class MaskImageFilter(PreviewImage, LoadImage): @classmethod def VALIDATE_INPUTS(cls, **kwargs): return True - def func(self, image, timeout, uid, if_no_mask, mask=None, **kwargs): + def func(self, image, timeout, uid, if_no_mask, node_identifier, mask=None, **kwargs): if mask is not None and mask.shape[:3] == image.shape[:3] and not torch.all(mask==0): saveable = torch.cat((image, mask.unsqueeze(-1)), dim=-1) else: @@ -215,7 +226,7 @@ class MaskImageFilter(PreviewImage, LoadImage): urls:list[str] = self.save_images(images=saveable, **kwargs)['ui']['images'] payload = {"uid": uid, "urls":urls, "maskedit":True} - response = send_with_resend(payload, timeout, uid) + response = send_with_resend(payload, timeout, uid, node_identifier) if (response): try: diff --git a/js/image_filter.js b/js/image_filter.js index 48dd389..d0b8dcb 100644 --- a/js/image_filter.js +++ b/js/image_filter.js @@ -3,6 +3,9 @@ import { api } from "../../scripts/api.js"; import { create } from "./utils.js"; import { popup } from "./popup.js"; +import { ComfyWidgets } from "../../scripts/widgets.js"; + +const FILTER_TYPES = ["Image Filter","Text Image Filter","Text Image Filter with Extras","Mask Image Filter"] app.registerExtension({ name: "cg.image_filter", @@ -59,5 +62,25 @@ app.registerExtension({ } } } - } + if (FILTER_TYPES.includes(nodeType.comfyClass )) { + nodeType.prototype.choose_id = function() { + const v = Math.floor(Math.random() * 1000000) + this.widgets.find((n)=>n.name=='node_identifier').value = v + console.log(`Chose ${v}`) + } + + const onNodeCreated = nodeType.prototype.onNodeCreated; + nodeType.prototype.onNodeCreated = function () { + const idx = this.widgets.findIndex((n)=>n.name=='node_identifier') + const old_widget = (idx>=0) ? this.widgets.splice(idx,1)[0] : null + const new_widget = ComfyWidgets["INT"](this, "node_identifier", ["INT", { "default":0 }], app).widget + new_widget.label = new_widget.name + new_widget.type = 'hidden' + + this.choose_id() + return onNodeCreated ? onNodeCreated.apply(this, arguments) : undefined; + } + } + }, + }) \ No newline at end of file diff --git a/js/popup.js b/js/popup.js index 487d8d9..ee9224c 100644 --- a/js/popup.js +++ b/js/popup.js @@ -93,7 +93,7 @@ class Popup extends HTMLSpanElement { this.cancel_button.addEventListener('click', this.send_cancel.bind(this) ) this.counter_reset_button.addEventListener('click', this.request_reset.bind(this) ) - document.addEventListener("keypress", this.on_key_press.bind(this)) + document.addEventListener("keydown", this.on_key_down.bind(this)) document.body.appendChild(this) this.last_response_sent = 0 @@ -114,6 +114,7 @@ class Popup extends HTMLSpanElement { try { const body = new FormData(); + msg = `${msg}!unique!${this.unique}` body.append('response', msg); api.fetchApi("/cg-image-filter-message", { method: "POST", body, }); Log.message_out(msg) @@ -130,6 +131,7 @@ class Popup extends HTMLSpanElement { close() { this.state = State.INACTIVE + this.node.choose_id() State.render(this) } @@ -153,6 +155,22 @@ class Popup extends HTMLSpanElement { _handle_message(message, use_saved) { + const uid = message.detail.uid + const unique = message.detail.unique + const node = app.graph._nodes_by_id[uid] + + if (!node) { + console.log(`Message was for ${uid} which doesn't exist`) + return + } + + if (node.widgets?.find((n)=>n.label=='node_identifier')?.value != message.detail.unique) { + console.log(`Message unique id wasn't mine`) + return + } + + this.node = node + this.allsame = message.detail.allsame || false if (this.state==State.INACTIVE && message.detail.urls && app.ui.settings.getSettingValue("ImageFilter.SmallWindow") && !use_saved && !this.autosend()) { @@ -170,16 +188,9 @@ class Popup extends HTMLSpanElement { try { const detail = message.detail - if (detail.uid) { - const node = app.graph._nodes_by_id[detail.uid] - if (!node) return `Node ${detail.uid} not found` - if (!EXTENSION_NODES.includes(node.type)) return `Node ${detail.uid} is not an image filter node` - } else { - return `No uid in message` - } - if (detail.tick) { this.counter_text.innerText = `${detail.tick}s` + if (this.state==State.INACTIVE) this.request_reset() return } @@ -300,16 +311,35 @@ class Popup extends HTMLSpanElement { this.redraw() } - on_key_press(e) { - if (e.key==' ') { - if (this.state==State.ZOOMED) { - this.state = State.FILTER - } else if (this.mouse_is_over) { + on_key_down(e) { + var used_keypress = false; + + if (this.state==State.FILTER) { + if (e.key==' ' && this.mouse_is_over) { this.state = State.ZOOMED - this.zoomed_image.src = this.mouse_is_over.src + this.zoomed_image_holder = this.mouse_is_over this.on_mouse_out(this.mouse_is_over) + used_keypress = true } - State.render(this) + } else if (this.state==State.ZOOMED) { + if (e.key==' ' || e.key=='ArrowUp') { + this.state = State.FILTER + this.zoomed_image_holder = null + used_keypress = true + } else if (e.key=='ArrowRight') { + this.zoomed_image_holder = this.zoomed_image_holder.nextSibling || this.zoomed_image_holder.parentNode.firstChild + used_keypress = true + } else if (e.key=='ArrowLeft') { + this.zoomed_image_holder = this.zoomed_image_holder.previousSibling || this.zoomed_image_holder.parentNode.lastChild + used_keypress = true + } + } + + if (used_keypress) { + e.stopPropagation() + e.preventDefault() + if (this.zoomed_image_holder) this.zoomed_image.src = this.zoomed_image_holder.src + State.render(this) } } From b5b4e148341c938b90fa9449c8fcf68050af6ac7 Mon Sep 17 00:00:00 2001 From: Chris Date: Fri, 25 Apr 2025 18:34:28 +1000 Subject: [PATCH 2/2] zoomed number --- js/filter.css | 16 ++++++++++++++++ js/popup.js | 18 +++++++++++++++++- 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/js/filter.css b/js/filter.css index d3a493a..ca622b2 100644 --- a/js/filter.css +++ b/js/filter.css @@ -126,6 +126,22 @@ } +.cg_popup .zoomed_number { + position: absolute; + top: 40%; + left: 30px; + color: white; + padding: 10px; + z-index: 110000; + background-color: rgba(128, 128, 128, 0.5); + /*text-align: center;*/ +} + +.cg_popup .zoomed.highlighted .zoomed_image { + border: 3px solid green; + padding: 2px; +} + .cg_popup .buttons button { margin: 5px 0 5px 0; padding: 4px; diff --git a/js/popup.js b/js/popup.js index ee9224c..de64a34 100644 --- a/js/popup.js +++ b/js/popup.js @@ -43,6 +43,10 @@ class State { if (value) item.classList.remove('hidden') else item.classList.add('hidden') } + static highlighted(item, value) { + if (value) item.classList.add('highlighted') + else item.classList.remove('highlighted') + } static render(popup) { const state = popup.state @@ -53,6 +57,12 @@ class State { State.visible(popup.text_edit, state==State.TEXT) State.visible(popup.send_button, true /*!app.ui.settings.getSettingValue("ImageFilter.ClickSends")*/) + if (state==State.ZOOMED) { + const img_index = popup.zoomed_image_holder.image_index + State.highlighted(popup.zoomed, popup.picked.has(`${img_index}`)) + popup.zoomed_number.innerHTML = `${img_index+1}/${popup.n_images}` + } + if (document.getElementById('maskEditor') && state!=State.MASK) { document.getElementById('maskEditor').style.display = 'none' } @@ -70,6 +80,7 @@ class Popup extends HTMLSpanElement { this.overlaygrid = create('span', 'grid overlaygrid', this) this.zoomed = create('span', 'zoomed', this) this.zoomed_image = create('img', 'zoomed_image', this.zoomed) + this.zoomed_number = create('span', 'zoomed_number', this.zoomed) this.text_edit = create('textarea', 'text_edit', this) this.title_bar = create('span', 'title', this) this.buttons = create('span', 'buttons', this) @@ -322,10 +333,15 @@ class Popup extends HTMLSpanElement { used_keypress = true } } else if (this.state==State.ZOOMED) { - if (e.key==' ' || e.key=='ArrowUp') { + if (e.key==' ') { this.state = State.FILTER this.zoomed_image_holder = null used_keypress = true + } else if (e.key=='ArrowUp') { + const fake_event = { target:this.zoomed_image_holder} + this.on_click(fake_event) + } else if (e.key=='ArrowDown') { + // select or unselect } else if (e.key=='ArrowRight') { this.zoomed_image_holder = this.zoomed_image_holder.nextSibling || this.zoomed_image_holder.parentNode.firstChild used_keypress = true