From b333bf05c6981f58b9debbd6cf4de06b6e69a69c Mon Sep 17 00:00:00 2001 From: yolain Date: Mon, 15 Apr 2024 19:26:46 +0800 Subject: [PATCH] fix:wildcard not match space #131 --- README.en.md | 31 +++---- README.md | 29 ++++--- py/api.py | 9 ++ py/easyNodes.py | 1 + py/image.py | 76 ++++++++++++++++- py/libs/chooser.py | 52 ++++++++++++ py/wildcards.py | 3 +- web/js/common/i18n.js | 3 + web/js/common/utils.js | 41 +++++++++ web/js/easy/easy.js | 15 +++- web/js/easy/easyDynamicWidgets.js | 33 +------- web/js/image_chooser/chooser.js | 133 ++++++++++++++++++++++++++++++ web/js/image_chooser/messaging.js | 34 ++++++++ web/js/image_chooser/preview.js | 86 +++++++++++++++++++ web/js/image_chooser/prompt.js | 114 +++++++++++++++++++++++++ web/js/image_chooser/state.js | 26 ++++++ 16 files changed, 624 insertions(+), 62 deletions(-) create mode 100644 py/libs/chooser.py create mode 100644 web/js/image_chooser/chooser.js create mode 100644 web/js/image_chooser/messaging.js create mode 100644 web/js/image_chooser/preview.js create mode 100644 web/js/image_chooser/prompt.js create mode 100644 web/js/image_chooser/state.js diff --git a/README.en.md b/README.en.md index 73f1fc8..3b0d003 100644 --- a/README.en.md +++ b/README.en.md @@ -271,20 +271,21 @@ Disclaimer: Opened source was not easy. I have a lot of respect for the contributions of these original authors. I just did some integration and optimization. -| Nodes Name(Search Name) | Related libraries | Library-related node | -|:---------------------------|:----------------------------------------------------------------------------|:-------------------------| -| easy setNode | [ComfyUI-extensions](https://github.com/diffus3/ComfyUI-extensions) | diffus3.SetNode | -| easy getNode | [ComfyUI-extensions](https://github.com/diffus3/ComfyUI-extensions) | diffus3.GetNode | -| easy bookmark | [rgthree-comfy](https://github.com/rgthree/rgthree-comfy) | Bookmark 🔖 | -| easy portraitMarker | [comfyui-portrait-master](https://github.com/florestefano1975/comfyui-portrait-master) | Portrait Master | -| easy LLLiteLoader | [ControlNet-LLLite-ComfyUI](https://github.com/kohya-ss/ControlNet-LLLite-ComfyUI) | LLLiteLoader | -| easy globalSeed | [ComfyUI-Inspire-Pack](https://github.com/ltdrdata/ComfyUI-Inspire-Pack) | Global Seed (Inspire) | -| easy preSamplingDynamicCFG | [sd-dynamic-thresholding](https://github.com/mcmonkeyprojects/sd-dynamic-thresholding) | DynamicThresholdingFull | -| dynamicThresholdingFull | [sd-dynamic-thresholding](https://github.com/mcmonkeyprojects/sd-dynamic-thresholding) | DynamicThresholdingFull | -| easy imageInsetCrop | [rgthree-comfy](https://github.com/rgthree/rgthree-comfy) | ImageInsetCrop | -| easy poseEditor | [ComfyUI_Custom_Nodes_AlekPet](https://github.com/AlekPet/ComfyUI_Custom_Nodes_AlekPet) | poseNode | +| Nodes Name(Search Name) | Related libraries | Library-related node | +|:-------------------------------|:----------------------------------------------------------------------------|:-------------------------| +| easy setNode | [ComfyUI-extensions](https://github.com/diffus3/ComfyUI-extensions) | diffus3.SetNode | +| easy getNode | [ComfyUI-extensions](https://github.com/diffus3/ComfyUI-extensions) | diffus3.GetNode | +| easy bookmark | [rgthree-comfy](https://github.com/rgthree/rgthree-comfy) | Bookmark 🔖 | +| easy portraitMarker | [comfyui-portrait-master](https://github.com/florestefano1975/comfyui-portrait-master) | Portrait Master | +| easy LLLiteLoader | [ControlNet-LLLite-ComfyUI](https://github.com/kohya-ss/ControlNet-LLLite-ComfyUI) | LLLiteLoader | +| easy globalSeed | [ComfyUI-Inspire-Pack](https://github.com/ltdrdata/ComfyUI-Inspire-Pack) | Global Seed (Inspire) | +| easy preSamplingDynamicCFG | [sd-dynamic-thresholding](https://github.com/mcmonkeyprojects/sd-dynamic-thresholding) | DynamicThresholdingFull | +| dynamicThresholdingFull | [sd-dynamic-thresholding](https://github.com/mcmonkeyprojects/sd-dynamic-thresholding) | DynamicThresholdingFull | +| easy imageInsetCrop | [rgthree-comfy](https://github.com/rgthree/rgthree-comfy) | ImageInsetCrop | +| easy poseEditor | [ComfyUI_Custom_Nodes_AlekPet](https://github.com/AlekPet/ComfyUI_Custom_Nodes_AlekPet) | poseNode | | easy preSamplingLayerDiffusion | [ComfyUI-layerdiffusion](https://github.com/huchenlei/ComfyUI-layerdiffusion) | LayeredDiffusionApply... | -| easy dynamiCrafterLoader | [ComfyUI-layerdiffusion](https://github.com/ExponentialML/ComfyUI_Native_DynamiCrafter) | Apply Dynamicrafter | +| easy dynamiCrafterLoader | [ComfyUI-layerdiffusion](https://github.com/ExponentialML/ComfyUI_Native_DynamiCrafter) | Apply Dynamicrafter | +| easy imageChooser | [cg-image-picker](https://github.com/chrisgoringe/cg-image-picker) | Preview Chooser | ## Workflow Examples @@ -334,4 +335,6 @@ Disclaimer: Opened source was not easy. I have a lot of respect for the contribu [ComfyUI_InstantID](https://github.com/cubiq/ComfyUI_InstantID) - Face migration -[ComfyUI-Custom-Scripts](https://github.com/pythongosssss/ComfyUI-Custom-Scripts) - pyssss🐍 \ No newline at end of file +[ComfyUI-Custom-Scripts](https://github.com/pythongosssss/ComfyUI-Custom-Scripts) - pyssss🐍 + +[cg-image-picker](https://github.com/chrisgoringe/cg-image-picker) - Image Preview Chooser diff --git a/README.md b/README.md index 183f60b..6e595e3 100644 --- a/README.md +++ b/README.md @@ -281,21 +281,22 @@ 声明: 非常尊重这些原作者们的付出,开源不易,我仅仅只是做了一些整合与优化。 -| 节点名 (搜索名) | 相关的库 | 库相关的节点 | -|:-------------------------------|:----------------------------------------------------------------------------|:-----------------------| -| easy setNode | [ComfyUI-extensions](https://github.com/diffus3/ComfyUI-extensions) | diffus3.SetNode | -| easy getNode | [ComfyUI-extensions](https://github.com/diffus3/ComfyUI-extensions) | diffus3.GetNode | -| easy bookmark | [rgthree-comfy](https://github.com/rgthree/rgthree-comfy) | Bookmark 🔖 | -| easy portraitMarker | [comfyui-portrait-master](https://github.com/florestefano1975/comfyui-portrait-master) | Portrait Master | -| easy LLLiteLoader | [ControlNet-LLLite-ComfyUI](https://github.com/kohya-ss/ControlNet-LLLite-ComfyUI) | LLLiteLoader | -| easy globalSeed | [ComfyUI-Inspire-Pack](https://github.com/ltdrdata/ComfyUI-Inspire-Pack) | Global Seed (Inspire) | +| 节点名 (搜索名) | 相关的库 | 库相关的节点 | +|:-------------------------------|:----------------------------------------------------------------------------|:------------------------| +| easy setNode | [ComfyUI-extensions](https://github.com/diffus3/ComfyUI-extensions) | diffus3.SetNode | +| easy getNode | [ComfyUI-extensions](https://github.com/diffus3/ComfyUI-extensions) | diffus3.GetNode | +| easy bookmark | [rgthree-comfy](https://github.com/rgthree/rgthree-comfy) | Bookmark 🔖 | +| easy portraitMarker | [comfyui-portrait-master](https://github.com/florestefano1975/comfyui-portrait-master) | Portrait Master | +| easy LLLiteLoader | [ControlNet-LLLite-ComfyUI](https://github.com/kohya-ss/ControlNet-LLLite-ComfyUI) | LLLiteLoader | +| easy globalSeed | [ComfyUI-Inspire-Pack](https://github.com/ltdrdata/ComfyUI-Inspire-Pack) | Global Seed (Inspire) | | easy preSamplingDynamicCFG | [sd-dynamic-thresholding](https://github.com/mcmonkeyprojects/sd-dynamic-thresholding) | DynamicThresholdingFull | | dynamicThresholdingFull | [sd-dynamic-thresholding](https://github.com/mcmonkeyprojects/sd-dynamic-thresholding) | DynamicThresholdingFull | -| easy imageInsetCrop | [rgthree-comfy](https://github.com/rgthree/rgthree-comfy) | ImageInsetCrop | -| easy poseEditor | [ComfyUI_Custom_Nodes_AlekPet](https://github.com/AlekPet/ComfyUI_Custom_Nodes_AlekPet) | poseNode | -| easy if | [ComfyUI-Logic](https://github.com/theUpsider/ComfyUI-Logic) | IfExecute | -| easy preSamplingLayerDiffusion | [ComfyUI-layerdiffusion](https://github.com/huchenlei/ComfyUI-layerdiffusion) | LayeredDiffusionApply等 | -| easy dynamiCrafterLoader | [ComfyUI-layerdiffusion](https://github.com/ExponentialML/ComfyUI_Native_DynamiCrafter) | Apply Dynamicrafter | +| easy imageInsetCrop | [rgthree-comfy](https://github.com/rgthree/rgthree-comfy) | ImageInsetCrop | +| easy poseEditor | [ComfyUI_Custom_Nodes_AlekPet](https://github.com/AlekPet/ComfyUI_Custom_Nodes_AlekPet) | poseNode | +| easy if | [ComfyUI-Logic](https://github.com/theUpsider/ComfyUI-Logic) | IfExecute | +| easy preSamplingLayerDiffusion | [ComfyUI-layerdiffusion](https://github.com/huchenlei/ComfyUI-layerdiffusion) | LayeredDiffusionApply等 | +| easy dynamiCrafterLoader | [ComfyUI-layerdiffusion](https://github.com/ExponentialML/ComfyUI_Native_DynamiCrafter) | Apply Dynamicrafter | +| easy imageChooser | [cg-image-picker](https://github.com/chrisgoringe/cg-image-picker) | Preview Chooser | ## 示例 @@ -352,3 +353,5 @@ [ComfyUI_InstantID](https://github.com/cubiq/ComfyUI_InstantID) - 人脸迁移 [ComfyUI-Custom-Scripts](https://github.com/pythongosssss/ComfyUI-Custom-Scripts) - pyssss 小蛇🐍脚本 + +[cg-image-picker](https://github.com/chrisgoringe/cg-image-picker) - 图片选择器 diff --git a/py/api.py b/py/api.py index ee66ee5..8ceea02 100644 --- a/py/api.py +++ b/py/api.py @@ -19,6 +19,15 @@ except ImportError: print("pip install aiohttp") sys.exit() +@PromptServer.instance.routes.get("/easyuse/reboot") +def reboot(self): + try: + sys.stdout.close_log() + except Exception as e: + pass + + return os.execv(sys.executable, [sys.executable] + sys.argv) + # parse csv @PromptServer.instance.routes.post("/easyuse/upload/csv") async def parse_csv(request): diff --git a/py/easyNodes.py b/py/easyNodes.py index 5c8b2b7..d83f582 100644 --- a/py/easyNodes.py +++ b/py/easyNodes.py @@ -2618,6 +2618,7 @@ class instantID: del pipe return (new_pipe, model, positive, negative) + class instantIDApply(instantID): def __init__(self): diff --git a/py/image.py b/py/image.py index 12c1a13..9c3db05 100644 --- a/py/image.py +++ b/py/image.py @@ -4,9 +4,13 @@ import hashlib import folder_paths import torch import numpy as np +import comfy.model_management +from server import PromptServer from nodes import MAX_RESOLUTION from .log import log_node_info from .libs.image import pil2tensor, tensor2pil, ResizeMode, get_new_bounds +from .libs.chooser import ChooserMessage, ChooserCancelled +from nodes import PreviewImage # 图像裁切 class imageInsetCrop: @@ -582,7 +586,75 @@ class imageRemBg: else: return (None, None) - # 姿势编辑器 +# 图像选择器 +class imageChooser(PreviewImage): + @classmethod + def INPUT_TYPES(self): + return { + "required":{ + + }, + "optional": { + "images": ("IMAGE",), + }, + "hidden": {"prompt": "PROMPT", "my_unique_id": "UNIQUE_ID"}, + } + + RETURN_TYPES = ("IMAGE",) + RETURN_NAMES = ("image",) + FUNCTION = "chooser" + OUTPUT_NODE = True + INPUT_IS_LIST = True + CATEGORY = "EasyUse/Image" + + last_ic = {} + @classmethod + def IS_CHANGED(cls, my_unique_id, **kwargs): + return cls.last_ic[my_unique_id[0]] + + def tensor_bundle(self, tensor_in: torch.Tensor, picks): + if tensor_in is not None and len(picks): + batch = tensor_in.shape[0] + return torch.cat(tuple([tensor_in[(x) % batch].unsqueeze_(0) for x in picks])).reshape( + [-1] + list(tensor_in.shape[1:])) + else: + return None + + def chooser(self, prompt=None, my_unique_id=None, **kwargs): + + id = my_unique_id[0] + if id not in ChooserMessage.stash: + ChooserMessage.stash[id] = {} + my_stash = ChooserMessage.stash[id] + + # enable stashing. If images is None, we are operating in read-from-stash mode + if 'images' in kwargs: + my_stash['images'] = kwargs['images'] + else: + kwargs['images'] = my_stash.get('images', None) + + if (kwargs['images'] is None): + return (None, None, None, "") + + images_in = torch.cat(kwargs.pop('images')) + self.batch = images_in.shape[0] + for x in kwargs: kwargs[x] = kwargs[x][0] + result = self.save_images(images=images_in, prompt=prompt) + + images = result['ui']['images'] + PromptServer.instance.send_sync("easyuse-image-choose", {"id": id, "urls": images}) + + # wait for selection + try: + selections = ChooserMessage.waitForMessage(id, asList=True) + choosen = [x for x in selections if x >= 0] if len(selections)>1 else [0] + except ChooserCancelled: + raise comfy.model_management.InterruptProcessingException() + + return {"ui": {"images": images}, + "result": (self.tensor_bundle(images_in, choosen),)} + +# 姿势编辑器 class poseEditor: @classmethod def INPUT_TYPES(self): @@ -638,6 +710,7 @@ NODE_CLASS_MAPPINGS = { "easy imageSplitList": imageSplitList, "easy imageSave": imageSaveSimple, "easy imageRemBg": imageRemBg, + "easy imageChooser": imageChooser, "easy joinImageBatch": JoinImageBatch, "easy poseEditor": poseEditor } @@ -657,6 +730,7 @@ NODE_DISPLAY_NAME_MAPPINGS = { "easy imageSplitList": "imageSplitList", "easy imageSave": "SaveImage (Simple)", "easy imageRemBg": "Image Remove Bg", + "easy imageChooser": "Image Chooser", "easy joinImageBatch": "JoinImageBatch", "easy poseEditor": "PoseEditor" } \ No newline at end of file diff --git a/py/libs/chooser.py b/py/libs/chooser.py new file mode 100644 index 0000000..559eaff --- /dev/null +++ b/py/libs/chooser.py @@ -0,0 +1,52 @@ +from server import PromptServer +from aiohttp import web +import time + +class ChooserCancelled(Exception): + pass + +class ChooserMessage: + stash = {} + messages = {} + cancelled = False + + @classmethod + def addMessage(cls, id, message): + if message == '__cancel__': + cls.messages = {} + cls.cancelled = True + elif message == '__start__': + cls.messages = {} + cls.stash = {} + cls.cancelled = False + else: + cls.messages[str(id)] = message + + @classmethod + def waitForMessage(cls, id, period=0.1, asList=False): + sid = str(id) + while not (sid in cls.messages) and not ("-1" in cls.messages): + if cls.cancelled: + cls.cancelled = False + raise ChooserCancelled() + time.sleep(period) + if cls.cancelled: + cls.cancelled = False + raise ChooserCancelled() + message = cls.messages.pop(str(id), None) or cls.messages.pop("-1") + try: + if asList: + return [int(x.strip()) for x in message.split(",")] + else: + return int(message.strip()) + except ValueError: + print( + f"ERROR IN IMAGE_CHOOSER - failed to parse '${message}' as ${'comma separated list of ints' if asList else 'int'}") + return [1] if asList else 1 + + +@PromptServer.instance.routes.post('/easyuse/image_chooser_message') +async def make_image_selection(request): + post = await request.post() + ChooserMessage.addMessage(post.get("id"), post.get("message")) + return web.json_response({}) \ No newline at end of file diff --git a/py/wildcards.py b/py/wildcards.py index d5fcada..28e5ed6 100644 --- a/py/wildcards.py +++ b/py/wildcards.py @@ -156,9 +156,8 @@ def process(text, seed=None): def replace_wildcard(string): global easy_wildcard_dict - pattern = r"__([\w.\-+/*\\]+)__" + pattern = r"__([\w\s.\-+/*\\]+?)__" matches = re.findall(pattern, string) - replacements_found = False for match in matches: diff --git a/web/js/common/i18n.js b/web/js/common/i18n.js index 4158f52..1191244 100644 --- a/web/js/common/i18n.js +++ b/web/js/common/i18n.js @@ -25,8 +25,11 @@ const zhCN = { "Saving Succeed":"保存成功", "Saving Failed":"保存失败", "No COMBO link": "沒有找到COMBO连接", + "Reboot ComfyUI":"重启ComfyUI", + "Are you sure you'd like to reboot the server?": "是否要重启ComfyUI?", // GroupMap "Groups Map (EasyUse)": "管理组 (EasyUse)", + "Reboot ComfyUI (EasyUse)": "重启服务 (EasyUse)", "Always": "启用中", "Bypass": "已忽略", "Never": "已停用", diff --git a/web/js/common/utils.js b/web/js/common/utils.js index 9baebae..0bae058 100644 --- a/web/js/common/utils.js +++ b/web/js/common/utils.js @@ -70,3 +70,44 @@ export function formatTime(time, format) { }; return format.replace(/([Mmdhs]|y{2})\1/g, (key) => time[_mapping[key]]); } + + +let origProps = {}; +export const findWidgetByName = (node, name) => node.widgets.find((w) => w.name === name); + +export const doesInputWithNameExist = (node, name) => node.inputs ? node.inputs.some((input) => input.name === name) : false; + +export function updateNodeHeight(node) {node.setSize([node.size[0], node.computeSize()[1]]);} + +export function toggleWidget(node, widget, show = false, suffix = "") { + if (!widget || doesInputWithNameExist(node, widget.name)) return; + if (!origProps[widget.name]) { + origProps[widget.name] = { origType: widget.type, origComputeSize: widget.computeSize }; + } + const origSize = node.size; + + widget.type = show ? origProps[widget.name].origType : "easyHidden" + suffix; + widget.computeSize = show ? origProps[widget.name].origComputeSize : () => [0, -4]; + + widget.linkedWidgets?.forEach(w => toggleWidget(node, w, ":" + widget.name, show)); + + const height = show ? Math.max(node.computeSize()[1], origSize[1]) : node.size[1]; + node.setSize([node.size[0], height]); +} + +export function isLocalNetwork(ip) { + const localNetworkRanges = [ + '192.168.', + '10.', + '127.', + /^172\.((1[6-9]|2[0-9]|3[0-1])\.)/ + ]; + + return localNetworkRanges.some(range => { + if (typeof range === 'string') { + return ip.startsWith(range); + } else { + return range.test(ip); + } + }); +} \ No newline at end of file diff --git a/web/js/easy/easy.js b/web/js/easy/easy.js index 02c195e..a34998f 100644 --- a/web/js/easy/easy.js +++ b/web/js/easy/easy.js @@ -1,6 +1,6 @@ import { api } from "/scripts/api.js"; import { app } from "/scripts/app.js"; -import {deepEqual,addCss} from "../common/utils.js"; +import {deepEqual, addCss, isLocalNetwork} from "../common/utils.js"; import {$t} from '../common/i18n.js'; import {toast} from "../common/toast.js"; @@ -270,6 +270,19 @@ app.registerExtension({ } } ); + // Only show the reboot option if the server is running on a local network 仅在本地或局域网环境可重启服务 + if(isLocalNetwork(window.location.host)){ + options.push(null,{ + content: '🔴 '+ $t('Reboot ComfyUI (EasyUse)'), + callback: _ =>{ + if (confirm($t("Are you sure you'd like to reboot the server?"))){ + try { + api.fetchApi("/easyuse/reboot"); + } catch (exception) {} + } + } + }) + } return options; }; }, diff --git a/web/js/easy/easyDynamicWidgets.js b/web/js/easy/easyDynamicWidgets.js index 5db36b6..b9db63d 100644 --- a/web/js/easy/easyDynamicWidgets.js +++ b/web/js/easy/easyDynamicWidgets.js @@ -4,36 +4,11 @@ import { ComfyWidgets } from "/scripts/widgets.js"; import { toast} from "../common/toast.js"; import { $t } from '../common/i18n.js'; -let origProps = {}; +import { findWidgetByName, toggleWidget, updateNodeHeight} from "../common/utils.js"; const seedNodes = ["easy seed", "easy latentNoisy", "easy wildcards", "easy preSampling", "easy preSamplingAdvanced", "easy preSamplingNoiseIn", "easy preSamplingSdTurbo", "easy preSamplingCascade", "easy preSamplingDynamicCFG", "easy preSamplingLayerDiffusion", "easy fullkSampler", "easy fullCascadeKSampler"] const loaderNodes = ["easy fullLoader", "easy a1111Loader", "easy comfyLoader"] -const findWidgetByName = (node, name) => node.widgets.find((w) => w.name === name); -const doesInputWithNameExist = (node, name) => node.inputs ? node.inputs.some((input) => input.name === name) : false; - -function updateNodeHeight(node) {node.setSize([node.size[0], node.computeSize()[1]]);} - -function toggleWidget(node, widget, show = false, suffix = "") { - if (!widget || doesInputWithNameExist(node, widget.name)) return; - if (!origProps[widget.name]) { - origProps[widget.name] = { origType: widget.type, origComputeSize: widget.computeSize }; - } - const origSize = node.size; - - widget.type = show ? origProps[widget.name].origType : "easyHidden" + suffix; - widget.computeSize = show ? origProps[widget.name].origComputeSize : () => [0, -4]; - - widget.linkedWidgets?.forEach(w => toggleWidget(node, w, ":" + widget.name, show)); - - const height = show ? Math.max(node.computeSize()[1], origSize[1]) : node.size[1]; - node.setSize([node.size[0], height]); -} - -function toggleInput(node, name, show = false) { - if(!show){ - } -} function widgetLogic(node, widget) { if (widget.name === 'lora_name') { @@ -81,7 +56,7 @@ function widgetLogic(node, widget) { }else { toggleWidget(node, findWidgetByName(node, 'link_id')) } - if (widget.value === 'Hide' || widget.value === 'Preview' || widget.value === 'Sender') { + if (widget.value === 'Hide' || widget.value === 'Preview' || widget.value == 'PreviewChooser' || widget.value === 'Sender') { toggleWidget(node, findWidgetByName(node, 'save_prefix')) toggleWidget(node, findWidgetByName(node, 'output_path')) toggleWidget(node, findWidgetByName(node, 'embed_workflow')) @@ -272,13 +247,9 @@ function widgetLogic(node, widget) { if (widget.name === 'num_embeds') { let number_to_show = widget.value + 1 for (let i = 0; i < number_to_show; i++) { - toggleInput(node, 'image'+i, true) - toggleInput(node, 'mask'+i, true) toggleWidget(node, findWidgetByName(node, 'weight'+i), true) } for (let i = number_to_show; i < 6; i++) { - toggleInput(node, 'image'+i) - toggleInput(node, 'mask'+i) toggleWidget(node, findWidgetByName(node, 'weight'+i)) } updateNodeHeight(node) diff --git a/web/js/image_chooser/chooser.js b/web/js/image_chooser/chooser.js new file mode 100644 index 0000000..fa53e98 --- /dev/null +++ b/web/js/image_chooser/chooser.js @@ -0,0 +1,133 @@ +import { app } from "/scripts/app.js"; +import { api } from "/scripts/api.js"; + +import { restart_from_here } from "./prompt.js"; +import { FlowState } from "./state.js"; +import { send_cancel, send_message, send_onstart, skip_next_restart_message } from "./messaging.js"; +import { display_preview_images, additionalDrawBackground, click_is_in_image } from "./preview.js"; +import {toggleWidget} from "../common/utils.js"; + +function progressButtonPressed() { + const node = app.graph._nodes_by_id[this.node_id]; + if (node) { + if (FlowState.paused()) { + send_message(node.id, [...node.selected, -1, ...node.anti_selected]); + } + if (FlowState.idle()) { + skip_next_restart_message(); + restart_from_here(node.id).then(() => { send_message(node.id, [...node.selected, -1, ...node.anti_selected]); }); + } + } +} + +function cancelButtonPressed() { if (FlowState.running()) { send_cancel(); } } + +app.registerExtension({ + name:'comfy.easyuse.imageChooser', + init() { + window.addEventListener("beforeunload", send_cancel, true); + }, + setup(app) { + function easyuseImageChooser(event) { + display_preview_images(event); + } + api.addEventListener("easyuse-image-choose", easyuseImageChooser); + + /* + If a run is interrupted, send a cancel message (unless we're doing the cancelling, to avoid infinite loop) + */ + const original_api_interrupt = api.interrupt; + api.interrupt = function () { + if (FlowState.paused() && !FlowState.cancelling) send_cancel(); + original_api_interrupt.apply(this, arguments); + } + + /* + At the start of execution + */ + function on_execution_start() { + if (send_onstart()) { + app.graph._nodes.forEach((node)=> { + if (node.selected || node.anti_selected) { + node.selected.clear(); + node.anti_selected.clear(); + node.update(); + } + }) + } + } + api.addEventListener("execution_start", on_execution_start); + }, + + async nodeCreated(node, app) { + if(node.comfyClass == 'easy imageChooser'){ + node.send_button_widget = node.addWidget("button", "", "", progressButtonPressed, {serialize: false}); + node.cancel_button_widget = node.addWidget("button", "", "", cancelButtonPressed, {serialize: false}); + /* Capture clicks */ + const org_onMouseDown = node.onMouseDown; + + /* A property defining the top of the image when there is just one */ + if(node?.imageIndex === undefined){ + Object.defineProperty(node, 'imageIndex', { + get : function() { return null; }, + set: function (v) {node.overIndex= v}, + }) + } + if(node?.imagey === undefined){ + Object.defineProperty(node, 'imagey', { + get : function() { return null; }, + set: function (v) {return node.widgets[node.widgets.length-1].last_y+LiteGraph.NODE_WIDGET_HEIGHT;}, + }) + } + + node.onMouseDown = function( e, pos, canvas ) { + if (e.isPrimary) { + const i = click_is_in_image(node, pos); + if (i>=0) { this.imageClicked(i); } + } + return (org_onMouseDown && org_onMouseDown.apply(this, arguments)); + } + + } + }, + + beforeRegisterNodeDef(nodeType, nodeData, app) { + if(nodeData?.name == 'easy imageChooser'){ + + const onDrawBackground = nodeType.prototype.onDrawBackground; + nodeType.prototype.onDrawBackground = function(ctx) { + onDrawBackground.apply(this, arguments); + additionalDrawBackground(this, ctx); + } + + nodeType.prototype.imageClicked = function (imageIndex) { + if (this.selected.has(imageIndex)) this.selected.delete(imageIndex); + else this.selected.add(imageIndex); + this.update(); + } + + const update = nodeType.prototype.update; + nodeType.prototype.update = function() { + if (update) update.apply(this,arguments); + if (this.send_button_widget) { + this.send_button_widget.node_id = this.id; + const selection = ( this.selected ? this.selected.size : 0 ) + ( this.anti_selected ? this.anti_selected.size : 0 ) + const maxlength = this.imgs.length; + if (FlowState.paused_here(this.id) && selection>0) { + this.send_button_widget.name = (selection>1) ? "Progress selected (" + selection + '/' + maxlength +")" : "Progress selected image"; + } else if (FlowState.idle() && selection>0) { + this.send_button_widget.name = (selection>1) ? "Progress selected (" + selection + '/' + maxlength +")" : "Progress selected image as restart"; + } + else { + this.send_button_widget.name = ""; + } + } + if (this.cancel_button_widget) { + const isRunning = FlowState.running() + this.cancel_button_widget.name = isRunning ? "Cancel current run" : ""; + } + this.setDirtyCanvas(true,true); + } + } + } +}) \ No newline at end of file diff --git a/web/js/image_chooser/messaging.js b/web/js/image_chooser/messaging.js new file mode 100644 index 0000000..a4799bb --- /dev/null +++ b/web/js/image_chooser/messaging.js @@ -0,0 +1,34 @@ +import { api } from "/scripts/api.js"; +import { FlowState } from "./state.js"; + +function send_message_from_pausing_node(message) { + const id = app.runningNodeId; + send_message(id, message); +} + +function send_message(id, message) { + const body = new FormData(); + body.append('message',message); + body.append('id', id); + api.fetchApi("/easyuse/image_chooser_message", { method: "POST", body, }); +} + +function send_cancel() { + send_message(-1,'__cancel__'); + //FlowState.cancelling = true; + //api.interrupt(); + //FlowState.cancelling = false; +} + +var skip_next = 0; +function skip_next_restart_message() { skip_next += 1; } +function send_onstart() { + if (skip_next>0) { + skip_next -= 1; + return false; + } + send_message(-1,'__start__'); + return true; +} + +export { send_message_from_pausing_node, send_cancel, send_message, send_onstart, skip_next_restart_message } \ No newline at end of file diff --git a/web/js/image_chooser/preview.js b/web/js/image_chooser/preview.js new file mode 100644 index 0000000..2bb6a89 --- /dev/null +++ b/web/js/image_chooser/preview.js @@ -0,0 +1,86 @@ +import { app } from "/scripts/app.js"; + +function display_preview_images(event) { + const node = app.graph._nodes_by_id[event.detail.id]; + if (node) { + node.selected = new Set(); + node.anti_selected = new Set(); + showImages(node, event.detail.urls); + } else { + console.log(`Image Chooser Preview - failed to find ${event.detail.id}`) + } +} + +function showImages(node, urls) { + node.imgs = []; + urls.forEach((u)=> { + const img = new Image(); + node.imgs.push(img); + img.onload = () => { app.graph.setDirtyCanvas(true); }; + img.src = `/view?filename=${encodeURIComponent(u.filename)}&type=temp&subfolder=${app.getPreviewFormatParam()}` + }) + node.setSizeForImage?.(); +} + +function drawRect(node, s, ctx) { + const padding = 1; + var rect; + if (node.imageRects) { + rect = node.imageRects[s]; + } else { + const y = node.imagey; + rect = [padding,y+padding,node.size[0]-2*padding,node.size[1]-y-2*padding]; + } + ctx.strokeRect(rect[0]+padding, rect[1]+padding, rect[2]-padding*2, rect[3]-padding*2); +} + +function additionalDrawBackground(node, ctx) { + if (!node.imgs) return; + if (node.imageRects) { + for (let i = 0; i < node.imgs.length; i++) { + // delete underlying image + ctx.fillStyle = "#000"; + ctx.fillRect(...node.imageRects[i]) + // draw the new one + const img = node.imgs[i]; + const cellWidth = node.imageRects[i][2]; + const cellHeight = node.imageRects[i][3]; + + let wratio = cellWidth/img.width; + let hratio = cellHeight/img.height; + var ratio = Math.min(wratio, hratio); + + let imgHeight = ratio * img.height; + let imgWidth = ratio * img.width; + + const imgX = node.imageRects[i][0] + (cellWidth - imgWidth)/2; + const imgY = node.imageRects[i][1] + (cellHeight - imgHeight)/2; + const cell_padding = 2; + ctx.drawImage(img, imgX+cell_padding, imgY+cell_padding, imgWidth-cell_padding*2, imgHeight-cell_padding*2); + + } + } + ctx.lineWidth = 2; + ctx.strokeStyle = "green"; + node?.selected?.forEach((s) => { drawRect(node,s, ctx) }) + ctx.strokeStyle = "#F88"; + node?.anti_selected?.forEach((s) => { drawRect(node,s, ctx) }) +} + +function click_is_in_image(node, pos) { + if (node.imgs?.length>1) { + for (var i = 0; i 0 && dx < node.imageRects[i][2] && + dy > 0 && dy < node.imageRects[i][3] ) { + return i; + } + } + } else if (node.imgs?.length==1) { + if (pos[1]>node.imagey) return 0; + } + return -1; +} + +export { display_preview_images, additionalDrawBackground, click_is_in_image } \ No newline at end of file diff --git a/web/js/image_chooser/prompt.js b/web/js/image_chooser/prompt.js new file mode 100644 index 0000000..a978b75 --- /dev/null +++ b/web/js/image_chooser/prompt.js @@ -0,0 +1,114 @@ +import { app } from "/scripts/app.js"; + +function links_with(p, node_id, down, up) { + const links_with = []; + p.workflow.links.forEach((l) => { + if (down && l[1]===node_id && !links_with.includes(l[3])) links_with.push(l[3]) + if (up && l[3]===node_id && !links_with.includes(l[1])) links_with.push(l[1]) + }); + return links_with; +} + +function _all_v_nodes(p, here_id) { + /* + Make a list of all downstream nodes. + */ + const downstream = []; + const to_process = [here_id] + while(to_process.length>0) { + const id = to_process.pop(); + downstream.push(id); + to_process.push( + ...links_with(p,id,true,false).filter((nid)=>{ + return !(downstream.includes(nid) || to_process.includes(nid)) + }) + ) + } + + /* + Now all upstream nodes from any of the downstream nodes (except us). + Put us on the result list so we don't flow up through us + */ + to_process.push(...downstream.filter((n)=>{ return n!=here_id})); + const back_upstream = [here_id]; + while(to_process.length>0) { + const id = to_process.pop(); + back_upstream.push(id); + to_process.push( + ...links_with(p,id,false,true).filter((nid)=>{ + return !(back_upstream.includes(nid) || to_process.includes(nid)) + }) + ) + } + + const keep = []; + keep.push(...downstream); + keep.push(...back_upstream.filter((n)=>{return !keep.includes(n)})); + + console.log(`Nodes to keep: ${keep}`); + return keep; +} + +async function all_v_nodes(here_id) { + const p = structuredClone(await app.graphToPrompt()); + const all_nodes = []; + p.workflow.nodes.forEach((node)=>{all_nodes.push(node.id)}) + p.workflow.links = p.workflow.links.filter((l)=>{ return (all_nodes.includes(l[1]) && all_nodes.includes(l[3]))} ) + return _all_v_nodes(p,here_id); +} + +async function restart_from_here(here_id, go_down_to_chooser=false) { + const p = structuredClone(await app.graphToPrompt()); + /* + Make a list of all nodes, and filter out links that are no longer valid + */ + const all_nodes = []; + p.workflow.nodes.forEach((node)=>{all_nodes.push(node.id)}) + p.workflow.links = p.workflow.links.filter((l)=>{ return (all_nodes.includes(l[1]) && all_nodes.includes(l[3]))} ) + + /* Move downstream to a chooser */ + if (go_down_to_chooser) { + while (!app.graph._nodes_by_id[here_id].isChooser) { + here_id = links_with(p, here_id, true, false)[0]; + } + } + + const keep = _all_v_nodes(p, here_id); + + /* + Filter p.workflow.nodes and p.workflow.links + */ + p.workflow.nodes = p.workflow.nodes.filter((node) => { + if (node.id===here_id) node.inputs.forEach((i)=>{i.link=null}) // remove our upstream links + return (keep.includes(node.id)) // only keep keepers + }) + p.workflow.links = p.workflow.links.filter((l) => {return (keep.includes(l[1]) && keep.includes(l[3]))}) + + /* + Filter the p.output object to only include nodes we're keeping + */ + const new_output = {} + for (const [key, value] of Object.entries(p.output)) { + if (keep.includes(parseInt(key))) new_output[key] = value; + } + /* + Filter the p.output entry for the start node to remove any list (ie link) inputs + */ + const new_inputs = {}; + for (const [key, value] of Object.entries(new_output[here_id.toString()].inputs)) { + if (!Array.isArray(value)) new_inputs[key] = value; + } + new_output[here_id.toString()].inputs = new_inputs; + + p.output = new_output; + + // temporarily hijack graph_to_prompt with a version that restores the old one but returns this prompt + const gtp_was = app.graphToPrompt; + app.graphToPrompt = () => { + app.graphToPrompt = gtp_was; + return p; + } + app.queuePrompt(0); +} + +export { restart_from_here, all_v_nodes } \ No newline at end of file diff --git a/web/js/image_chooser/state.js b/web/js/image_chooser/state.js new file mode 100644 index 0000000..b12ce99 --- /dev/null +++ b/web/js/image_chooser/state.js @@ -0,0 +1,26 @@ +import { app } from "/scripts/app.js"; + +export class FlowState { + constructor(){} + static idle() { + return (!app.runningNodeId); + } + static paused() { + return true; + } + static paused_here(node_id) { + return (FlowState.paused() && FlowState.here(node_id)) + } + static running() { + return (!FlowState.idle()); + } + static here(node_id) { + return (app.runningNodeId==node_id); + } + static state() { + if (FlowState.paused()) return "Paused"; + if (FlowState.running()) return "Running"; + return "Idle"; + } + static cancelling = false; +} \ No newline at end of file