fix:wildcard not match space #131

This commit is contained in:
yolain
2024-04-15 19:26:46 +08:00
parent f52a8fd40b
commit b333bf05c6
16 changed files with 624 additions and 62 deletions
+17 -14
View File
@@ -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🐍
[ComfyUI-Custom-Scripts](https://github.com/pythongosssss/ComfyUI-Custom-Scripts) - pyssss🐍
[cg-image-picker](https://github.com/chrisgoringe/cg-image-picker) - Image Preview Chooser
+16 -13
View File
@@ -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) - 图片选择器
+9
View File
@@ -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):
+1
View File
@@ -2618,6 +2618,7 @@ class instantID:
del pipe
return (new_pipe, model, positive, negative)
class instantIDApply(instantID):
def __init__(self):
+75 -1
View File
@@ -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"
}
+52
View File
@@ -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({})
+1 -2
View File
@@ -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:
+3
View File
@@ -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": "已停用",
+41
View File
@@ -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);
}
});
}
+14 -1
View File
@@ -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;
};
},
+2 -31
View File
@@ -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)
+133
View File
@@ -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);
}
}
}
})
+34
View File
@@ -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 }
+86
View File
@@ -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<node.imageRects.length; i++) {
const dx = pos[0] - node.imageRects[i][0];
const dy = pos[1] - node.imageRects[i][1];
if ( dx > 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 }
+114
View File
@@ -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 }
+26
View File
@@ -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;
}