diff --git a/README.md b/README.md index 2337cd7..74dfce4 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# ๐Ÿ”— Comfyui : Bjornulf_custom_nodes v0.23 ๐Ÿ”— +# ๐Ÿ”— Comfyui : Bjornulf_custom_nodes v0.24 ๐Ÿ”— # โ˜ Usage in cloud : @@ -66,6 +66,7 @@ wget --content-disposition -P /workspace/ComfyUI/models/checkpoints "https://civ - **v0.21**: Add a new write text node that also display the text in the comfyui console (good for debugging) - **v0.22**: Allow write text node to use random selection like this {hood|helmet} will randomly choose between hood or helmet. - **v0.23**: And a new node: Pause, resume or stop workflow. +- **v0.24**: And a new node: Pause, select input, pick one. # ๐Ÿ“ Nodes descriptions @@ -338,4 +339,13 @@ Just connect this node with your workflow, it takes an image as input and return Automatically pause the workflow, and rings a bell when it does. (play the audio `bell.m4a` file provided) You can then manually resume or stop the workflow by clicking on the node's buttons. I do that let's say for example if I have a very long upscaling process, I can check if the input is good before continuing. Sometimes I might stop the workflow and restart it with another seed. -You can connect any type of node to the pause node, above is an example with text, but you can send an IMAGE or whatever else, in the node `input = output`. (Of course you need to send the output to something that has the correct format...) \ No newline at end of file +You can connect any type of node to the pause node, above is an example with text, but you can send an IMAGE or whatever else, in the node `input = output`. (Of course you need to send the output to something that has the correct format...) + +### 36 - โธ๏ธ๐Ÿ” Paused. Select input, Pick one + +![pick input](screenshots/pick.png) + +**Description:** +Automatically pause the workflow, and rings a bell when it does. (play the audio `bell.m4a` file provided) +You can then manually select the input you want to use, and resume the workflow with it. +You can connect this node to anything you want, above is an example with IMAGE. But you can pick whatever you want, in the node `input = output`. \ No newline at end of file diff --git a/__init__.py b/__init__.py index 4ecbbcd..e25e0da 100644 --- a/__init__.py +++ b/__init__.py @@ -41,6 +41,8 @@ from .text_to_speech import TextToSpeech from .loop_combine_texts_by_lines import CombineTextsByLines from .free_vram_hack import FreeVRAM from .pause_resume_stop import PauseResume +from .pick_input import PickInput +# from .pass_preview_image import PassPreviewImage # from .check_black_image import CheckBlackImage # from .clear_vram import ClearVRAM @@ -49,6 +51,8 @@ from .pause_resume_stop import PauseResume NODE_CLASS_MAPPINGS = { # "Bjornulf_CustomStringType": CustomStringType, "Bjornulf_ollamaLoader": ollamaLoader, + # "Bjornulf_PassPreviewImage": PassPreviewImage, + "Bjornulf_PickInput": PickInput, "Bjornulf_PauseResume": PauseResume, "Bjornulf_FreeVRAM": FreeVRAM, "Bjornulf_CombineTextsByLines": CombineTextsByLines, @@ -98,6 +102,8 @@ NODE_CLASS_MAPPINGS = { NODE_DISPLAY_NAME_MAPPINGS = { # "Bjornulf_CustomStringType": "!!! CUSTOM STRING TYPE !!!", "Bjornulf_ollamaLoader": "๐Ÿฆ™ Ollama (Description)", + # "Bjornulf_PassPreviewImage": "๐Ÿ–ผโฎ• Pass Preview Image", + "Bjornulf_PickInput": "โธ๏ธ๐Ÿ” Paused. Select input, Pick one", "Bjornulf_PauseResume": "โธ๏ธ Paused. Resume or Stop ?", "Bjornulf_FreeVRAM": "๐Ÿงน Free VRAM hack", "Bjornulf_CombineTextsByLines": "โ™ป Loop (All Lines from input ๐Ÿ”— combine by lines)", diff --git a/load_image_alpha.py b/load_image_alpha.py index b63bbf6..9a17f5f 100644 --- a/load_image_alpha.py +++ b/load_image_alpha.py @@ -17,7 +17,8 @@ class LoadImageWithTransparency: CATEGORY = "image" - RETURN_TYPES = ("IMAGE",) + RETURN_TYPES = ("IMAGE", "MASK", "STRING") # Added "STRING" for the image path + RETURN_NAMES = ("image", "mask", "image_path") FUNCTION = "load_image_alpha" def load_image_alpha(self, image): @@ -62,7 +63,7 @@ class LoadImageWithTransparency: output_image = output_images[0] output_mask = output_masks[0] - return (output_image, output_mask) + return (output_image, output_mask, image_path) # Added image_path to the return tuple @classmethod def IS_CHANGED(s, image): diff --git a/pause_resume_stop.py b/pause_resume_stop.py index 069b9c8..b69cffe 100644 --- a/pause_resume_stop.py +++ b/pause_resume_stop.py @@ -42,7 +42,6 @@ class PauseResume: if PauseResume.should_stop: PauseResume.should_stop = False # Reset for next run PauseResume.is_paused = True - PauseResume.should_stop = False raise Exception("Workflow stopped by user") PauseResume.is_paused = True diff --git a/pick_input.py b/pick_input.py new file mode 100644 index 0000000..0666760 --- /dev/null +++ b/pick_input.py @@ -0,0 +1,152 @@ +import time +from aiohttp import web +from server import PromptServer +import logging +from pydub import AudioSegment +from pydub.playback import play +import os + +class Everything(str): + def __ne__(self, __value: object) -> bool: + return False + + +class PickInput: + is_paused = True + should_stop = False + selected_input = None + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "number_of_inputs": ("INT", {"default": 2, "min": 1, "max": 10, "step": 1}), + }, + "hidden": { + **{f"input_{i}": (Everything("*"), {"forceInput": "True"}) for i in range(2, 11)} + } + } + + RETURN_TYPES = (Everything("*"),) + RETURN_NAMES = ("output",) + FUNCTION = "pick_input" + CATEGORY = "Bjornulf" + + def play_audio(self): + audio_file = os.path.join(os.path.dirname(__file__), 'bell.m4a') + sound = AudioSegment.from_file(audio_file, format="m4a") + play(sound) + + def pick_input(self, **kwargs): + logging.info(f"Selected input at the start: {PickInput.selected_input}") + self.play_audio() + + while PickInput.is_paused and not PickInput.should_stop: + logging.info(f"PickInput.is_paused: {PickInput.is_paused}, PickInput.should_stop: {PickInput.should_stop}") + time.sleep(1) # Sleep to prevent busy waiting + + if PickInput.should_stop: + PickInput.should_stop = False # Reset for next run + PickInput.is_paused = True + raise Exception("Workflow stopped by user") + + PickInput.is_paused = True + PickInput.should_stop = False + + # Check if the selected input exists in kwargs + if PickInput.selected_input not in kwargs: + logging.error(f"Selected input '{PickInput.selected_input}' not found in kwargs") + logging.info(f"Available kwargs: {list(kwargs.keys())}") + return (None,) # or handle this error as appropriate + + selected_value = kwargs.get(PickInput.selected_input) + logging.info(f"Value of selected input '{PickInput.selected_input}': {selected_value}") + + # Store the value in self.target if needed + self.target = selected_value + + return (selected_value,) + + def create_select_handler(self, index): + async def select_input(request): + self.selected_input = index + self.is_waiting = False + return web.Response(text=f"Input {index + 1} selected") + return select_input + +@PromptServer.instance.routes.get("/bjornulf_stop_pick") +async def stop_node_pick(request): + logging.info("Stop node pick called") + PickInput.should_stop = True + PickInput.is_paused = False # Ensure the loop exits + return web.Response(text="Workflow stopped") + +@PromptServer.instance.routes.get("/bjornulf_select_input_1") +async def bjornulf_select_input_1(request): + logging.info("Resume node called") + PickInput.is_paused = False + PickInput.selected_input="input_1" + return web.Response(text="Node resumed") + +@PromptServer.instance.routes.get("/bjornulf_select_input_2") +async def bjornulf_select_input_2(request): + logging.info("Resume node called") + PickInput.is_paused = False + PickInput.selected_input="input_2" + return web.Response(text="Node resumed") + +@PromptServer.instance.routes.get("/bjornulf_select_input_3") +async def bjornulf_select_input_3(request): + logging.info("Resume node called") + PickInput.is_paused = False + PickInput.selected_input="input_3" + return web.Response(text="Node resumed") + +@PromptServer.instance.routes.get("/bjornulf_select_input_4") +async def bjornulf_select_input_4(request): + logging.info("Resume node called") + PickInput.is_paused = False + PickInput.selected_input="input_4" + return web.Response(text="Node resumed") + +@PromptServer.instance.routes.get("/bjornulf_select_input_5") +async def bjornulf_select_input_5(request): + logging.info("Resume node called") + PickInput.is_paused = False + PickInput.selected_input="input_5" + return web.Response(text="Node resumed") + +@PromptServer.instance.routes.get("/bjornulf_select_input_6") +async def bjornulf_select_input_6(request): + logging.info("Resume node called") + PickInput.is_paused = False + PickInput.selected_input="input_6" + return web.Response(text="Node resumed") + +@PromptServer.instance.routes.get("/bjornulf_select_input_7") +async def bjornulf_select_input_7(request): + logging.info("Resume node called") + PickInput.is_paused = False + PickInput.selected_input="input_7" + return web.Response(text="Node resumed") + +@PromptServer.instance.routes.get("/bjornulf_select_input_8") +async def bjornulf_select_input_8(request): + logging.info("Resume node called") + PickInput.is_paused = False + PickInput.selected_input="input_8" + return web.Response(text="Node resumed") + +@PromptServer.instance.routes.get("/bjornulf_select_input_9") +async def bjornulf_select_input_9(request): + logging.info("Resume node called") + PickInput.is_paused = False + PickInput.selected_input="input_9" + return web.Response(text="Node resumed") + +@PromptServer.instance.routes.get("/bjornulf_select_input_10") +async def bjornulf_select_input_10(request): + logging.info("Resume node called") + PickInput.is_paused = False + PickInput.selected_input="input_10" + return web.Response(text="Node resumed") \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index da98d4e..4c699a1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "bjornulf_custom_nodes" description = "Nodes: Ollama, Text to Speech, Save image for Bjornulf LobeChat, Text with random Seed, Random line from input, Combine images (Background + Overlay alpha), Image to grayscale (black & white), Remove image Transparency (alpha), Resize Image, ..." -version = "0.23" +version = "0.24" license = {file = "LICENSE"} [project.urls] diff --git a/screenshots/pick.png b/screenshots/pick.png new file mode 100644 index 0000000..7955fb2 Binary files /dev/null and b/screenshots/pick.png differ diff --git a/web/js/pauseresume.js b/web/js/pause_resume_stop.js similarity index 100% rename from web/js/pauseresume.js rename to web/js/pause_resume_stop.js diff --git a/web/js/pick_input.js b/web/js/pick_input.js new file mode 100644 index 0000000..27319e0 --- /dev/null +++ b/web/js/pick_input.js @@ -0,0 +1,114 @@ +import { app } from "../../../scripts/app.js"; + +app.registerExtension({ + name: "Bjornulf.PickInput", + async nodeCreated(node) { + if (node.comfyClass === "Bjornulf_PickInput") { + + const updateInputButtons = (numInputs) => { + // Remove all existing widgets + node.widgets.length = 1; + + // Re-add the number_of_inputs widget + // const numInputsWidget = node.addWidget("number", "Number of Inputs", "number_of_inputs", (v) => { + // updateInputs(); + // app.graph.setDirtyCanvas(true); + // return v; + // }, { min: 1, max: 10, step: 1, precision: 0 }); + + // Add new input buttons + for (let i = 1; i < numInputs + 1; i++) { + node.addWidget("button", `Input ${i}`, `input_button_${i}`, () => { + fetch(`/bjornulf_select_input_${i}`, { method: "GET" }) + .then((response) => response.text()) + .then((data) => { + console.log(`Input ${i} response:`, data); + // You can update the UI here if needed + }) + .catch((error) => console.error("Error:", error)); + }); + } + + // Re-add the Stop button + node.addWidget("button", "Stop", "Stop", () => { + fetch("/bjornulf_stop_pick", { method: "GET" }) + .then((response) => response.text()) + .then((data) => { + console.log("Stop response:", data); + // You can update the UI here if needed + }) + .catch((error) => console.error("Error:", error)); + }); + + node.setSize(node.computeSize()); + }; + + const updateInputs = () => { + const numInputsWidget = node.widgets.find( + (w) => w.name === "number_of_inputs" + ); + if (!numInputsWidget) return; + + const numInputs = numInputsWidget.value; + + // Initialize node.inputs if it doesn't exist + if (!node.inputs) { + node.inputs = []; + } + + // Filter existing text inputs + const existingInputs = node.inputs.filter((input) => + input.name.startsWith("input_") + ); + + // const Everything = Symbol('Everything'); + // Determine if we need to add or remove inputs + if (existingInputs.length < numInputs) { + // Add new inputs if not enough existing + for (let i = existingInputs.length + 1; i <= numInputs; i++) { + const inputName = `input_${i}`; + if (!node.inputs.find((input) => input.name === inputName)) { + node.addInput(inputName, "*"); + } + } + } else { + // Remove excess inputs if too many + node.inputs = node.inputs.filter( + (input) => + !input.name.startsWith("input_") || + parseInt(input.name.split("_")[1]) <= numInputs + ); + } + + // Update input buttons + updateInputButtons(numInputs); + + node.setSize(node.computeSize()); + }; + + // Set seed widget to hidden input + const seedWidget = node.widgets.find((w) => w.name === "seed"); + if (seedWidget) { + seedWidget.type = "HIDDEN"; + } + + // Move number_of_inputs to the top initially + const numInputsWidget = node.widgets.find( + (w) => w.name === "number_of_inputs" + ); + if (numInputsWidget) { + node.widgets = [ + numInputsWidget, + ...node.widgets.filter((w) => w !== numInputsWidget), + ]; + numInputsWidget.callback = () => { + updateInputs(); + app.graph.setDirtyCanvas(true); + }; + } + + // Delay the initial update to ensure node is fully initialized + setTimeout(updateInputs, 0); + } + }, +}); \ No newline at end of file