commit cf018b74643116a27e52cafd026b0c259902b184 Author: Andrew F <59752493+shockz0rz@users.noreply.github.com> Date: Tue Dec 19 23:41:50 2023 -0700 Initial commit. Basic grid nodes work. Use at your own risk. diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..6739c83 --- /dev/null +++ b/__init__.py @@ -0,0 +1,6 @@ +from .nodes import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS +from . import api +WEB_DIRECTORY = "js" +__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS', 'WEB_DIRECTORY'] +#__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS' ] +#print( "Easy Grids hopefully loaded?" ) \ No newline at end of file diff --git a/api.py b/api.py new file mode 100644 index 0000000..37be954 --- /dev/null +++ b/api.py @@ -0,0 +1,18 @@ +from aiohttp import web +import json +import server +from .nodes import reset_registry + + +@server.PromptServer.instance.routes.post("/easygrids/reset/{node_id}") +async def ResetLoop(request): + try: + node_id = request.match_info["node_id"] + if node_id in reset_registry: + reset_registry[node_id].curr_x_index = 1 + reset_registry[node_id].curr_y_index = 1 + return web.json_response(status=200) + else: + return web.json_response(dict(error="Node not found"), status=404) + except Exception as e: + return web.json_response(dict(error=str(e)), status=500) \ No newline at end of file diff --git a/js/easyGridExt.js b/js/easyGridExt.js new file mode 100644 index 0000000..6f350aa --- /dev/null +++ b/js/easyGridExt.js @@ -0,0 +1,96 @@ +import { app } from "../../scripts/app.js"; +import { api } from "../../scripts/api.js" + +// Copied from widgetInputs.js because AFAICT it doesn't seem to export these +function hideWidget(node, widget, suffix = "") { + widget.origType = widget.type; + widget.origComputeSize = widget.computeSize; + widget.origSerializeValue = widget.serializeValue; + widget.computeSize = () => [0, -4]; // -4 is due to the gap litegraph adds between widgets automatically + widget.type = CONVERTED_TYPE + suffix; + widget.serializeValue = () => { + // Prevent serializing the widget if we have no input linked + if (!node.inputs) { + return undefined; + } + let node_input = node.inputs.find((i) => i.widget?.name === widget.name); + + if (!node_input || !node_input.link) { + return undefined; + } + return widget.origSerializeValue ? widget.origSerializeValue() : widget.value; + }; + + // Hide any linked widgets, e.g. seed+seedControl + if (widget.linkedWidgets) { + for (const w of widget.linkedWidgets) { + hideWidget(node, w, ":" + widget.name); + } + } +} + +function showWidget(widget) { + widget.type = widget.origType; + widget.computeSize = widget.origComputeSize; + widget.serializeValue = widget.origSerializeValue; + + delete widget.origType; + delete widget.origComputeSize; + delete widget.origSerializeValue; + + // Hide any linked widgets, e.g. seed+seedControl + if (widget.linkedWidgets) { + for (const w of widget.linkedWidgets) { + showWidget(w); + } + } +} + +app.registerExtension({ + name: "easygrids.SimpleLooper", + // node : + async nodeCreated(node, app) { + if ( node.__proto__.comfyClass === "SimpleLooper" ) + { + node.addWidget( "button", "Loop it!", "LoopButton", () => + { + // I don't know how to javascript lol + const target_widget = node.widgets.find((w) => w.name === "loop_count"); + let loop_num = target_widget.value; + for ( let queue_i = 0; queue_i < loop_num; queue_i++ ) + { + app.queuePrompt(1, 1); + } + }) + } + else if (node.__proto__.comfyClass === "ImageGridCommander") + { + node.addWidget( "button", "Queue Full Grid", "QueueButton", () => + { + const x_widget = node.widgets.find((w) => w.name === "x_count"); + const y_widget = node.widgets.find((w) => w.name === "y_count"); + let x_slot = node.findOutputSlot( "x_index" ); + let y_slot = node.findOutputSlot( "y_index" ); + for ( let queue_x = 1; queue_x <= x_widget.value; queue_x++ ) + { + for ( let queue_y = 1; queue_y <= y_widget.value; queue_y++ ) + { + app.queuePrompt(1, 1); + } + } + }); + node.addWidget( "button", "Reset Grid Loop", "ResetButton", () => + { + let req_url = "easygrids/reset/" + node.id.toString(); + const resp = api.fetchApi(req_url, { + method: "POST", + body: "", + } ); + if ( resp.status !== 200 ) + { + console.error( "Failed to reset grid loop" ); + } + }); + } + }, +}); \ No newline at end of file diff --git a/nodes.py b/nodes.py new file mode 100644 index 0000000..7530629 --- /dev/null +++ b/nodes.py @@ -0,0 +1,261 @@ +import re +from copy import deepcopy +import folder_paths +from PIL import Image +import numpy as np +from comfy.cli_args import args +import json +from PIL.PngImagePlugin import PngInfo +import os.path + +static_x = 1 +static_y = 1 + +reset_registry = {} + +class GridFloats: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "index": ( "INT", {"default": 1, "min": 1, "max": 6 } ), + "float1": ("FLOAT", {"default": 1.0}), + "float2": ("FLOAT", {"default": 1.0}), + "float3": ("FLOAT", {"default": 1.0}), + "float4": ("FLOAT", {"default": 1.0}), + "float5": ("FLOAT", {"default": 1.0}), + "float6": ("FLOAT", {"default": 1.0}), + }, + } + + RETURN_TYPES = ("FLOAT",) + FUNCTION = "ReturnFloat" + CATEGORY = "EasyGrids" + + def ReturnFloat( self, index: int, float1 : float, float2 : float, float3 : float, float4 : float, float5 : float, float6: float ): + #TODO: probably a more pythonic way to do this + ret_list = [float1, float2, float3, float4, float5, float6] + if ( index > len(ret_list) ): + return ( ret_list[len(ret_list) - 1], ) + return (ret_list[ index - 1 ], ) + +class GridFloatList: + @classmethod + def INPUT_TYPES(s): + return { + "required": { + "index": ( "INT", {"default": 1, "min": 1, "max": 100 } ), + "float_list": ("STRING", {"multiline": True}), + }} + + RETURN_TYPES = ("FLOAT",) + FUNCTION = "ParseAndReturnFloat" + CATEGORY = "EasyGrids" + + def __init__(self): + self.static_text = "" + self.static_out_arr = [] + + def ParseAndReturnFloat( self, index: int, float_list: str ): + if float_list != self.static_text: + split_str = re.split( ",|;|\s|:", float_list ) + out_arr = [] + for val in split_str: + # let the exception happen if invalid + out_arr.append(float(val)) + self.static_text = float_list + self.static_out_arr = deepcopy( out_arr ) + #import pdb; pdb.set_trace() + if ( index > len(self.static_out_arr) ): + return ( self.static_out_arr[len(self.static_out_arr) - 1], ) + return (self.static_out_arr[ index - 1 ],) + +class ImageGridCommander: + @classmethod + def INPUT_TYPES(s): + return { + "required": { "x_count": ("INT", {"default": 1, "min": 1, "max": 12, "step": 1}), + "y_count": ("INT", {"default": 1, "min": 1, "max": 12, "step": 1}), + }, + "hidden": { + "unique_id": "UNIQUE_ID", + }, + } + + RETURN_TYPES = ("INT","INT","INT","INT",) + RETURN_NAMES = ("x_index", "y_index", "x_size", "y_size",) + FUNCTION = "queue_batch" + CATEGORY = "EasyGrids" + + def __init__(self): + self.curr_x_index = 1 + self.curr_y_index = 1 + self.last_x_count = 0 + self.last_y_count = 0 + + def queue_batch(self, x_count, y_count, unique_id ): + #wish we could do this on init but there doesn't seem to be a way to get the unique_id at that point + #there shouldn't be any need to reset before the first run in any case + if unique_id not in reset_registry: + reset_registry[unique_id] = self + if x_count != self.last_x_count or y_count != self.last_y_count: + self.last_x_count = x_count + self.last_y_count = y_count + self.curr_x_index = 1 + self.curr_y_index = 1 + last_x_index = self.curr_x_index + last_y_index = self.curr_y_index + self.curr_x_index += 1 + if self.curr_x_index > x_count: + self.curr_x_index = 1 + self.curr_y_index += 1 + if self.curr_y_index > y_count: + self.curr_y_index = 1 + return (last_x_index, last_y_index, x_count, y_count,) + + # This node will always be run + @classmethod + def IS_CHANGED( s, x_count, y_count ): + return float("NaN") + + +class SimpleLooper: + @classmethod + def INPUT_TYPES(s): + return { "required": { "loop_count": ("INT", {"default":1, "min": 1, "max": 10, "step": 1}),}} + + RETURN_TYPES = () + FUNCTION = None + CATEGORY = "EasyGrids" + +class TextConcatenator: + @classmethod + def INPUT_TYPES(s): + return { "required" : { "text_1": ("STRING", {"multiline": True}), + "text_2": ("STRING", {"multiline": True}), } } + + RETURN_TYPES = ("STRING",) + FUNCTION = "concat_text" + CATEGORY = "EasyGrids" + + def concat_text( self, text_1, text_2 ): + #simple as! + return ((text_1 + text_2), ) + +class FloatToText: + @classmethod + def INPUT_TYPES(s): + return { "required": { "float_input": ("FLOAT", {"default": 1.0}), + "decimal_places": ("INT", {"default": 3, "min": 1, "max": 10 }), }} + + RETURN_TYPES = ("STRING",) + FUNCTION = "convert_to_str" + CATEGORY = "EasyGrids" + + def convert_to_str(self, float_input, decimal_places): + # if this doesn't work, blame Copilot + formatted_float = "{:.{}f}".format(float_input, decimal_places) + return (formatted_float,) + +class SaveImageGrid: + def __init__(self): + self.output_dir = folder_paths.get_output_directory() + self.type = "output" + self.prefix_append = "" + self.image_grid = [[]] + self.curr_x_size = 1 + self.curr_x_idx = 0 + self.curr_y_size = 1 + self.curr_y_idx = 0 + + + @classmethod + def INPUT_TYPES(s): + return {"required": + {"images": ("IMAGE", ), + "x_size": ("INT", {"default": 1, "min": 1, "max": 12, "step": 1}), + "y_size": ("INT", {"default": 1, "min": 1, "max": 12, "step": 1}), + "filename_prefix": ("STRING", {"default": "ComfyUI"})}, + "hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"}, + } + + RETURN_TYPES = () + FUNCTION = "accumulate_images" + + OUTPUT_NODE = True + + CATEGORY = "EasyGrids" + + def accumulate_images(self, images, x_size, y_size, filename_prefix="ComfyUI", prompt=None, extra_pnginfo=None): + filename_prefix += self.prefix_append + if x_size != self.curr_x_size or y_size != self.curr_y_size: + self.curr_x_size = x_size + self.curr_y_size = y_size + self.curr_x_idx = 0 + self.curr_y_idx = 0 + self.image_grid = [] + for image in images: + self.image_grid.append(image) + self.curr_x_idx += 1 + if self.curr_x_idx >= self.curr_x_size: + self.curr_y_idx += 1 + self.curr_x_idx = 0 + + if len( self.image_grid ) >= self.curr_x_size * self.curr_y_size: + #complete grid + grid_image = self.assemble_grid() + return self.save_grid( grid_image, filename_prefix, prompt, extra_pnginfo ) + return { "ui": { "images": [] } } + + def assemble_grid(self): + space_height = max( [ len(image) for image in self.image_grid ] ) + space_width = max( [ len(image[0]) for image in self.image_grid ] ) + grid_canvas = Image.new("RGBA", (space_width * self.curr_x_size, space_height * self.curr_y_size), color="#ffffff00") + for y_idx in range( self.curr_y_size ): + for x_idx in range( self.curr_x_size ): + pil_image = Image.fromarray( np.clip( ( self.image_grid[ ( y_idx * self.curr_x_size ) + x_idx].cpu().numpy() * 255. ), 0, 255 ).astype( np.uint8 ) ) + grid_canvas.paste(pil_image, (x_idx * space_width, y_idx * space_height )) + return grid_canvas + + + def save_grid( self, grid_image, filename_prefix, prompt=None, extra_pnginfo=None ): + full_output_folder, filename, counter, subfolder, filename_prefix = folder_paths.get_save_image_path(filename_prefix, self.output_dir, grid_image.width, grid_image.height) + results = list() + 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])) + + file = f"{filename}_{counter:05}_.png" + grid_image.save(os.path.join(full_output_folder, file), pnginfo=metadata, compress_level=4) + results.append({ + "filename": file, + "subfolder": subfolder, + "type": self.type + }) + + return { "ui": { "images": results } } + +NODE_CLASS_MAPPINGS = { + "ImageGridCommander": ImageGridCommander, + "GridFloatList": GridFloatList, + "GridFloats": GridFloats, + "SimpleLooper": SimpleLooper, + "TextConcatenator": TextConcatenator, + "FloatToText": FloatToText, + "SaveImageGrid": SaveImageGrid, +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "ImageGridCommander": "Create Image Grid", + "GridFloatList": "Float List fom Text", + "GridFloats" : "Float List", + "SimpleLooper": "Simple Looper", + "TextConcatenator": "Text Concatenator", + "FloatToText": "Float to Text", + "SaveImageGrid": "Save Image Grid", +} \ No newline at end of file