等待选择,循环显示优化

This commit is contained in:
严浪
2024-10-01 17:09:08 +08:00
parent 2d8c32c558
commit 88045a0cdb
11 changed files with 722 additions and 4 deletions
+12 -1
View File
@@ -9,12 +9,23 @@ app.registerExtension({
var showNames=["IfInnerExecute",'MultiIntFormula','MultiParamFormula','ForInnerEnd','DoWhileEnd',"LamSwitcherCase"]
if (showNames.indexOf(nodeData.name)>=0) {
const onDrawForeground = nodeType.prototype.onDrawForeground;
let oldText=''
nodeType.prototype.onDrawForeground = function (ctx) {
try {
const r = onDrawForeground?.apply?.(this, arguments);
const v = app.nodeOutputs?.[this.id + ""];
if (!this.flags.collapsed && v) {
const text = v.value[0] + "";
let text=''
if(['ForInnerEnd','DoWhileEnd'].indexOf(nodeData.name)>=0){
if(v.value[0].indexOf('DOWHILE')!=-1){
text=v.value[0].split('DOWHILE')[1]
oldText=text
}else{
text=oldText
}
}else{
text = v.value[0] + "";
}
ctx.save();
ctx.font = "bold 12px sans-serif";
ctx.fillStyle = "dodgerblue";
+1 -1
View File
@@ -7,7 +7,7 @@ app.registerExtension({
name: "StatusInfo",
async beforeRegisterNodeDef(nodeType, nodeData, app) {
var names=["Image2Video",'Video2TalkingFace','VideoAddAudio','Image2TalkingFace','ForEnd','LoadVideo','VideoFaceFusion',
'Text2AutioEdgeTts','VideoRoopFaceSwap','PromptTranslator','LamCommonPrint','OutDoWhileEnd']
'Text2AutioEdgeTts','VideoRoopFaceSwap','PromptTranslator','LamCommonPrint','LamCommonPrintNoOutput','OutDoWhileEnd']
if (names.indexOf(nodeData.name)>=0) {
// When the node is created we want to add a readonly text widget to display the text
const onNodeCreated = nodeType.prototype.onNodeCreated;
+230
View File
@@ -0,0 +1,230 @@
import { app } from "../../../../scripts/app.js";
import { api } from "../../../../scripts/api.js";
import { ComfyDialog, $el } from "../../../../scripts/ui.js";
import { restart_from_here } from "./image_chooser/prompt.js";
import { hud, FlowState } from "./image_chooser/state.js";
import { send_cancel, send_message, send_onstart, skip_next_restart_message } from "./image_chooser/messaging.js";
import { display_preview_images, additionalDrawBackground, click_is_in_image } from "./image_chooser/preview.js";
class chooserImageDialog extends ComfyDialog {
constructor() {
super();
this.node = null
this.select_index = []
this.dialog_div = null
}
show(image,node){
this.select_index = []
this.node = node
const images_div = image.map((img, index) => {
const imgEl = $el('img', {
src: img.src,
onclick: _ => {
if(this.select_index.includes(index)){
this.select_index = this.select_index.filter(i => i !== index)
imgEl.classList.remove('selected')
} else {
this.select_index.push(index)
imgEl.classList.add('selected')
}
if (node.selected.has(index)) node.selected.delete(index);
else node.selected.add(index);
}
})
return imgEl
})
super.show($el('div.easyuse-chooser-dialog',[
$el('h5.easyuse-chooser-dialog-title', '选择图像继续'),
$el('div.easyuse-chooser-dialog-images',images_div)
]))
}
createButtons() {
const btns = super.createButtons();
btns[0].onclick = _ => {
if (FlowState.running()) { send_cancel();}
super.close()
}
btns.unshift($el('button', {
type: 'button',
textContent: "选择已选图像",
onclick: _ => {
if (FlowState.paused()) {
send_message(this.node.id, [...this.node.selected, -1, ...this.node.anti_selected]);
}
if (FlowState.idle()) {
skip_next_restart_message();
restart_from_here(this.node.id).then(() => { send_message(this.node.id, [...this.node.selected, -1, ...this.node.anti_selected]); });
}
super.close()
}
}))
return btns
}
}
function progressButtonPressed() {
const node = app.graph._nodes_by_id[this.node_id];
if (node) {
const selected = [...node.selected]
if(selected?.length>0){
node.setProperty('values',selected)
}
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();}
}
function enable_disabling(button) {
Object.defineProperty(button, 'clicked', {
get : function() { return this._clicked; },
set : function(v) { this._clicked = (v && this.name!=''); }
})
}
function disable_serialize(widget) {
if (!widget.options) widget.options = { };
widget.options.serialize = false;
}
app.registerExtension({
name:'comfy.WaitImagSelector',
init() {
window.addEventListener("beforeunload", send_cancel, true);
},
setup(app) {
const draw = LGraphCanvas.prototype.draw;
LGraphCanvas.prototype.draw = function() {
if (hud.update()) {
app.graph._nodes.forEach((node)=> { if (node.update) { node.update(); } })
}
draw.apply(this,arguments);
}
function easyuseImageChooser(event) {
const {node,image,isKSampler} = display_preview_images(event);
if(isKSampler) {
const dialog = new chooserImageDialog();
dialog.show(image,node)
}
}
api.addEventListener("lam-wait-image-select", 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(event) {
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 == 'WaitImagSelector'){
node.setProperty('values',[])
/* 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;},
})
}
/* Capture clicks */
const org_onMouseDown = node.onMouseDown;
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));
}
node.send_button_widget = node.addWidget("button", "确认", "", progressButtonPressed);
//node.cancel_button_widget = node.addWidget("button", "", "", cancelButtonPressed);
//enable_disabling(node.cancel_button_widget);
enable_disabling(node.send_button_widget);
//disable_serialize(node.cancel_button_widget);
disable_serialize(node.send_button_widget);
}
},
beforeRegisterNodeDef(nodeType, nodeData, app) {
if(nodeData?.name == 'WaitImagSelector'){
const onDrawBackground = nodeType.prototype.onDrawBackground;
nodeType.prototype.onDrawBackground = function(ctx) {
onDrawBackground.apply(this, arguments);
additionalDrawBackground(this, ctx);
}
nodeType.prototype.imageClicked = function (imageIndex) {
if (nodeType?.comfyClass==="WaitImagSelector") {
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 || 0;
if (FlowState.paused_here(this.id) && selection>0) {
this.send_button_widget.name = (selection>1) ? "确认选择 (" + selection + '/' + maxlength +")" : "确认选择";
}
}
// if (this.cancel_button_widget) {
// const isRunning = FlowState.running()
// this.cancel_button_widget.name = isRunning ? "取消选择" : "";
// }
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("/lam/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 }
+90
View File
@@ -0,0 +1,90 @@
import { app } from "../../../../scripts/app.js";
const kSampler = ['easy kSampler', 'easy kSamplerTiled', 'easy fullkSampler']
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();
const image = showImages(node, event.detail.urls);
return {node,image,isKSampler:kSampler.includes(node.type)}
} 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?.();
return node.imgs
}
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 }
+55
View File
@@ -0,0 +1,55 @@
import { app } from "../../../../scripts/app.js";
class HUD {
constructor() {
this.current_node_id = undefined;
this.class_of_current_node = null;
this.current_node_is_chooser = false;
}
update() {
if (app.runningNodeId==this.current_node_id) return false;
this.current_node_id = app.runningNodeId;
if (this.current_node_id) {
this.class_of_current_node = app.graph?._nodes_by_id[app.runningNodeId.toString()]?.comfyClass;
this.current_node_is_chooser = this.class_of_current_node === "easy imageChooser"
} else {
this.class_of_current_node = undefined;
this.current_node_is_chooser = false;
}
return true;
}
}
const hud = new HUD();
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;
}
export { hud, FlowState}
+13
View File
@@ -32,15 +32,27 @@ class DoWhileEnd:
CATEGORY = "lam"
def getTaskOvers(self,nodeId):
if nodeId.find("Recurse")!= -1:
recId=nodeId.split('Recurse')[0]
teskSize=nodeId.count(recId)+1
return 'DOWHILE'+','.join([str(i+1) for i in range(max(0,teskSize-10),teskSize)])
elif nodeId.find(".")!= -1:
return 'DOWHILE'+'1,2'
else:
return 'DOWHILE'+'1'
def for_end_fun(self,start,ANY,obj,stop=1,dynprompt=None, unique_id=None, **kwargs):
assert dynprompt is not None
graph = GraphBuilder()
open_node = start[0]
overIds=self.getTaskOvers(open_node)
if not ANY:
values = [[open_node,1],obj]
for i in range(NUM_FLOW_SOCKETS):
values.append(kwargs.get(f"initial_value{i}", None))
return {
'ui':{'value':[overIds]},
"result": tuple(values),
"expand": graph.finalize(),
}
@@ -84,6 +96,7 @@ class DoWhileEnd:
assert my_clone is not None
result = map(lambda x: my_clone.out(x), range(NUM_FLOW_SOCKETS+2))
return {
'ui':{'value':[overIds]},
"result": tuple(result),
"expand": graph.finalize(),
}
+28 -2
View File
@@ -22,11 +22,37 @@ class LamCommonPrint:
def common_print(self,obj):
return {"ui": {"text": str(obj)}, "result": (obj, )}
class LamCommonPrintNoOutput:
def __init__(self):
pass
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"obj": (AlwaysEqualProxy("*"),),
}
}
RETURN_TYPES = (AlwaysEqualProxy("*"),)
RETURN_NAMES = ('obj',)
FUNCTION = "common_print"
OUTPUT_NODE = False
CATEGORY = "lam"
def common_print(self,obj):
return {"ui": {"text": str(obj)}, "result": (obj, )}
NODE_CLASS_MAPPINGS = {
"LamCommonPrint": LamCommonPrint
"LamCommonPrint": LamCommonPrint,
"LamCommonPrintNoOutput":LamCommonPrintNoOutput
}
NODE_DISPLAY_NAME_MAPPINGS = {
"LamCommonPrint": "通用打印输出"
"LamCommonPrint": "通用打印输出",
"LamCommonPrintNoOutput": "通用打印输出(非输出节点)"
}
+93
View File
@@ -0,0 +1,93 @@
import folder_paths
from nodes import SaveImage
import random
from PIL import Image, ImageOps
import os
import numpy as np
import json
from PIL.PngImagePlugin import PngInfo
from comfy.cli_args import args
from server import PromptServer
from .src.utils.chooser import ChooserMessage, ChooserCancelled
class WaitImagSelector(SaveImage):
def __init__(self):
self.output_dir = folder_paths.get_temp_directory()
self.type = "temp"
self.prefix_append = "_temp_" + \
''.join(random.choice("abcdefghijklmnopqrstupvxyz")
for x in range(5))
self.compress_level = 1
@classmethod
def INPUT_TYPES(s):
return {"required":
{"images": ("IMAGE", ), },
"hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO", "unique_id": "UNIQUE_ID"},
}
RETURN_TYPES = ("BOOL","IMAGE",)
RETURN_NAMES = ("是否没选","选择图片",)
FUNCTION = "save_images"
CATEGORY = "image"
OUTPUT_NODE = True
last_ic = {}
@classmethod
def IS_CHANGED(cls, unique_id, **kwargs):
return cls.last_ic[unique_id.split(".")[-1]]
def save_images(self, images, filename_prefix="ComfyUI", prompt=None, extra_pnginfo=None, unique_id=None):
id = unique_id.split(".")[-1]
if id not in ChooserMessage.stash:
ChooserMessage.stash[id] = {}
my_stash = ChooserMessage.stash[id]
filename_prefix += self.prefix_append
full_output_folder, filename, counter, subfolder, filename_prefix = folder_paths.get_save_image_path(
filename_prefix, self.output_dir, images[0].shape[1], images[0].shape[0])
results = list()
for (batch_number, image) in enumerate(images):
i = 255. * image.cpu().numpy()
img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8))
metadata = None
if not args.disable_metadata:
metadata = PngInfo()
if prompt is not None:
metadata.add_text("prompt", json.dumps(prompt))
if extra_pnginfo is not None:
for x in extra_pnginfo:
metadata.add_text(x, json.dumps(extra_pnginfo[x]))
filename_with_batch_num = filename.replace(
"%batch_num%", str(batch_number))
file = f"{filename_with_batch_num}_{counter:05}_.png"
img.save(os.path.join(full_output_folder, file),
pnginfo=metadata, compress_level=self.compress_level)
results.append({
"filename": file,
"subfolder": subfolder,
"type": self.type
})
counter += 1
PromptServer.instance.send_sync(
"lam-wait-image-select", {"id": id, "urls": results})
try:
selections = ChooserMessage.waitForMessage(id, asList=True)
if len(selections)==1 and selections[0]==-1:
return {"ui": {"images": results},"result": (True,None,)}
selImgs = images[selections[:-1]]
return {"ui": {"images": results},"result": (False,selImgs,)}
except ChooserCancelled:
return {"ui": {"images": results},"result": (True,None,)}
NODE_CLASS_MAPPINGS = {
"WaitImagSelector": WaitImagSelector
}
NODE_DISPLAY_NAME_MAPPINGS = {
"WaitImagSelector": "等待图片选择器"
}
+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('/lam/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({})