diff --git a/ComfyUI/custom_nodes/__pycache__/runway_text2img.cpython-311.pyc b/ComfyUI/custom_nodes/__pycache__/runway_text2img.cpython-311.pyc new file mode 100644 index 00000000..65c64904 Binary files /dev/null and b/ComfyUI/custom_nodes/__pycache__/runway_text2img.cpython-311.pyc differ diff --git a/ComfyUI/custom_nodes/__pycache__/websocket_image_save.cpython-311.pyc b/ComfyUI/custom_nodes/__pycache__/websocket_image_save.cpython-311.pyc new file mode 100644 index 00000000..00891713 Binary files /dev/null and b/ComfyUI/custom_nodes/__pycache__/websocket_image_save.cpython-311.pyc differ diff --git a/ComfyUI/custom_nodes/example_node.py.example b/ComfyUI/custom_nodes/example_node.py.example new file mode 100644 index 00000000..e86b4f11 --- /dev/null +++ b/ComfyUI/custom_nodes/example_node.py.example @@ -0,0 +1,155 @@ +class Example: + """ + A example node + + Class methods + ------------- + INPUT_TYPES (dict): + Tell the main program input parameters of nodes. + IS_CHANGED: + optional method to control when the node is re executed. + + Attributes + ---------- + RETURN_TYPES (`tuple`): + The type of each element in the output tuple. + RETURN_NAMES (`tuple`): + Optional: The name of each output in the output tuple. + FUNCTION (`str`): + The name of the entry-point method. For example, if `FUNCTION = "execute"` then it will run Example().execute() + OUTPUT_NODE ([`bool`]): + If this node is an output node that outputs a result/image from the graph. The SaveImage node is an example. + The backend iterates on these output nodes and tries to execute all their parents if their parent graph is properly connected. + Assumed to be False if not present. + CATEGORY (`str`): + The category the node should appear in the UI. + DEPRECATED (`bool`): + Indicates whether the node is deprecated. Deprecated nodes are hidden by default in the UI, but remain + functional in existing workflows that use them. + EXPERIMENTAL (`bool`): + Indicates whether the node is experimental. Experimental nodes are marked as such in the UI and may be subject to + significant changes or removal in future versions. Use with caution in production workflows. + execute(s) -> tuple || None: + The entry point method. The name of this method must be the same as the value of property `FUNCTION`. + For example, if `FUNCTION = "execute"` then this method's name must be `execute`, if `FUNCTION = "foo"` then it must be `foo`. + """ + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(s): + """ + Return a dictionary which contains config for all input fields. + Some types (string): "MODEL", "VAE", "CLIP", "CONDITIONING", "LATENT", "IMAGE", "INT", "STRING", "FLOAT". + Input types "INT", "STRING" or "FLOAT" are special values for fields on the node. + The type can be a list for selection. + + Returns: `dict`: + - Key input_fields_group (`string`): Can be either required, hidden or optional. A node class must have property `required` + - Value input_fields (`dict`): Contains input fields config: + * Key field_name (`string`): Name of a entry-point method's argument + * Value field_config (`tuple`): + + First value is a string indicate the type of field or a list for selection. + + Second value is a config for type "INT", "STRING" or "FLOAT". + """ + return { + "required": { + "image": ("IMAGE",), + "int_field": ("INT", { + "default": 0, + "min": 0, #Minimum value + "max": 4096, #Maximum value + "step": 64, #Slider's step + "display": "number", # Cosmetic only: display as "number" or "slider" + "lazy": True # Will only be evaluated if check_lazy_status requires it + }), + "float_field": ("FLOAT", { + "default": 1.0, + "min": 0.0, + "max": 10.0, + "step": 0.01, + "round": 0.001, #The value representing the precision to round to, will be set to the step value by default. Can be set to False to disable rounding. + "display": "number", + "lazy": True + }), + "print_to_screen": (["enable", "disable"],), + "string_field": ("STRING", { + "multiline": False, #True if you want the field to look like the one on the ClipTextEncode node + "default": "Hello World!", + "lazy": True + }), + }, + } + + RETURN_TYPES = ("IMAGE",) + #RETURN_NAMES = ("image_output_name",) + + FUNCTION = "test" + + #OUTPUT_NODE = False + + CATEGORY = "Example" + + def check_lazy_status(self, image, string_field, int_field, float_field, print_to_screen): + """ + Return a list of input names that need to be evaluated. + + This function will be called if there are any lazy inputs which have not yet been + evaluated. As long as you return at least one field which has not yet been evaluated + (and more exist), this function will be called again once the value of the requested + field is available. + + Any evaluated inputs will be passed as arguments to this function. Any unevaluated + inputs will have the value None. + """ + if print_to_screen == "enable": + return ["int_field", "float_field", "string_field"] + else: + return [] + + def test(self, image, string_field, int_field, float_field, print_to_screen): + if print_to_screen == "enable": + print(f"""Your input contains: + string_field aka input text: {string_field} + int_field: {int_field} + float_field: {float_field} + """) + #do some processing on the image, in this example I just invert it + image = 1.0 - image + return (image,) + + """ + The node will always be re executed if any of the inputs change but + this method can be used to force the node to execute again even when the inputs don't change. + You can make this node return a number or a string. This value will be compared to the one returned the last time the node was + executed, if it is different the node will be executed again. + This method is used in the core repo for the LoadImage node where they return the image hash as a string, if the image hash + changes between executions the LoadImage node is executed again. + """ + #@classmethod + #def IS_CHANGED(s, image, string_field, int_field, float_field, print_to_screen): + # return "" + +# Set the web directory, any .js file in that directory will be loaded by the frontend as a frontend extension +# WEB_DIRECTORY = "./somejs" + + +# Add custom API routes, using router +from aiohttp import web +from server import PromptServer + +@PromptServer.instance.routes.get("/hello") +async def get_hello(request): + return web.json_response("hello") + + +# A dictionary that contains all nodes you want to export with their names +# NOTE: names should be globally unique +NODE_CLASS_MAPPINGS = { + "Example": Example +} + +# A dictionary that contains the friendly/humanly readable titles for the nodes +NODE_DISPLAY_NAME_MAPPINGS = { + "Example": "Example Node" +} diff --git a/ComfyUI/custom_nodes/runway_text2img/__init__.py b/ComfyUI/custom_nodes/runway_text2img/__init__.py new file mode 100644 index 00000000..bed0f3ad --- /dev/null +++ b/ComfyUI/custom_nodes/runway_text2img/__init__.py @@ -0,0 +1,108 @@ +import requests +import os +import time +from io import BytesIO +from PIL import Image +import numpy as np +import torch +from dotenv import load_dotenv + +# Load API key +load_dotenv() +RUNWAY_API_KEY = os.getenv("RUNWAY_API_KEY") +RUNWAY_API_URL = "https://api.dev.runwayml.com/v1/text_to_image" + +class RunwayTextToImage: + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "prompt": ("STRING", { + "multiline": True, + "default": "a fantasy landscape with mountains and rivers" + }), + "ratio": ([ + "1920:1080", "1080:1920", "1024:1024", "1280:720", "720:1280", + "720:720", "960:720", "720:960", "1360:768", "1168:880", + "1440:1080", "1080:1440", "1808:768", "2112:912", "1680:720" + ], {"default": "1024:1024"}), + "timeout": ("INT", {"default": 30, "min": 1, "max": 1200}), + "seed": ("INT", {"default": 42, "min": 0, "max": 4294967295}), + "mock": ("BOOLEAN", {"default": False}), + } + } + + RETURN_TYPES = ("IMAGE",) + RETURN_NAMES = ("images",) + FUNCTION = "run" + CATEGORY = "Runway" + +# ✅ ComfyUI expects this function to exist at module level +def run(prompt, ratio, timeout, seed, mock): + width, height = map(int, ratio.split(":")) + + if mock: + print("✅ Mock mode enabled – skipping real API call.") + + dummy_np = (np.random.rand(height, width, 3) * 255).astype(np.uint8) # [H, W, 3] + dummy_tensor = torch.from_numpy(dummy_np).permute(2, 0, 1).float() / 255.0 # [3, H, W] + print(f"✅ Dummy tensor created with shape: {dummy_tensor.shape}, dtype: {dummy_tensor.dtype}") + + return (dummy_tensor,) + + + api_key = os.getenv("RUNWAY_API_KEY") + if not api_key: + raise RuntimeError("❌ Missing RUNWAY_API_KEY environment variable.") + + headers = { + "Authorization": f"Bearer {api_key}", + "Content-Type": "application/json", + "X-Runway-Version": "2024-11-06" + } + + payload = { + "model": "gen4_image", + "promptText": prompt, + "ratio": ratio, + "seed": seed + } + + print("got prompt") + print("=== Runway Payload ===") + print(payload) + + response = requests.post(RUNWAY_API_URL, json=payload, headers=headers) + print("=== Response Code ===", response.status_code) + print("=== Response Body ===", response.text) + response.raise_for_status() + + job_id = response.json()["id"] + + # Polling for result + image_url = None + for _ in range(timeout): + poll = requests.get(f"{RUNWAY_API_URL}/{job_id}", headers=headers) + poll_data = poll.json() + if poll_data.get("status") == "succeeded": + image_url = poll_data["outputs"][0]["uri"] + break + elif poll_data.get("status") == "failed": + raise RuntimeError("❌ Runway generation failed.") + time.sleep(1) + + if not image_url: + raise RuntimeError("⏰ Timed out waiting for Runway result.") + + image_bytes = requests.get(image_url).content + image = Image.open(BytesIO(image_bytes)).convert("RGB") + image_tensor = torch.from_numpy(np.array(image)).float() / 255.0 + image_tensor = image_tensor.permute(2, 0, 1) # [3, H, W] + return (image_tensor,) # no batch dim + +RunwayTextToImage.run = staticmethod(run) + +NODE_CLASS_MAPPINGS = { + "RunwayTextToImage": RunwayTextToImage +} diff --git a/ComfyUI/custom_nodes/runway_text2img/__pycache__/__init__.cpython-311.pyc b/ComfyUI/custom_nodes/runway_text2img/__pycache__/__init__.cpython-311.pyc new file mode 100644 index 00000000..b8786474 Binary files /dev/null and b/ComfyUI/custom_nodes/runway_text2img/__pycache__/__init__.cpython-311.pyc differ diff --git a/ComfyUI/custom_nodes/websocket_image_save.py b/ComfyUI/custom_nodes/websocket_image_save.py new file mode 100644 index 00000000..ca56ebc8 --- /dev/null +++ b/ComfyUI/custom_nodes/websocket_image_save.py @@ -0,0 +1,44 @@ +from PIL import Image +import numpy as np +import comfy.utils +import time + +#You can use this node to save full size images through the websocket, the +#images will be sent in exactly the same format as the image previews: as +#binary images on the websocket with a 8 byte header indicating the type +#of binary message (first 4 bytes) and the image format (next 4 bytes). + +#Note that no metadata will be put in the images saved with this node. + +class SaveImageWebsocket: + @classmethod + def INPUT_TYPES(s): + return {"required": + {"images": ("IMAGE", ),} + } + + RETURN_TYPES = () + FUNCTION = "save_images" + + OUTPUT_NODE = True + + CATEGORY = "api/image" + + def save_images(self, images): + pbar = comfy.utils.ProgressBar(images.shape[0]) + step = 0 + for image in images: + i = 255. * image.cpu().numpy() + img = Image.fromarray(np.clip(i, 0, 255).astype(np.uint8)) + pbar.update_absolute(step, images.shape[0], ("PNG", img, None)) + step += 1 + + return {} + + @classmethod + def IS_CHANGED(s, images): + return time.time() + +NODE_CLASS_MAPPINGS = { + "SaveImageWebsocket": SaveImageWebsocket, +}