Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file not shown.
Binary file not shown.
155 changes: 155 additions & 0 deletions ComfyUI/custom_nodes/example_node.py.example
Original file line number Diff line number Diff line change
@@ -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"
}
108 changes: 108 additions & 0 deletions ComfyUI/custom_nodes/runway_text2img/__init__.py
Original file line number Diff line number Diff line change
@@ -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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security (python.lang.best-practice.arbitrary-sleep): time.sleep() call; did you mean to leave this in?

Source: opengrep


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

Comment on lines +55 to +103
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Replace debug prints with proper logging.

The API integration logic is solid with good error handling and correct tensor conversion. However, the debug print statements should be replaced with proper logging for production use.

+import logging
+
+logger = logging.getLogger(__name__)
+
 def run(prompt, ratio, timeout, seed, mock):
     # ... existing code ...
     
-    print("got prompt")
-    print("=== Runway Payload ===")
-    print(payload)
+    logger.info("Processing prompt for Runway API")
+    logger.debug("Runway payload: %s", payload)
     
     response = requests.post(RUNWAY_API_URL, json=payload, headers=headers)
-    print("=== Response Code ===", response.status_code)
-    print("=== Response Body ===", response.text)
+    logger.debug("Response code: %s", response.status_code)
+    logger.debug("Response body: %s", response.text)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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
import logging
logger = logging.getLogger(__name__)
def run(prompt, ratio, timeout, seed, mock):
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
}
logger.info("Processing prompt for Runway API")
logger.debug("Runway payload: %s", payload)
response = requests.post(RUNWAY_API_URL, json=payload, headers=headers)
logger.debug("Response code: %s", response.status_code)
logger.debug("Response body: %s", 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
🧰 Tools
🪛 Ruff (0.12.2)

72-72: print found

Remove print

(T201)


73-73: print found

Remove print

(T201)


74-74: print found

Remove print

(T201)


77-77: print found

Remove print

(T201)


78-78: print found

Remove print

(T201)

🤖 Prompt for AI Agents
In ComfyUI/custom_nodes/runway_text2img/__init__.py between lines 55 and 103,
replace all print statements used for debugging with calls to a configured
logger instance. Import the logging module, set up a logger at the top of the
file, and use logger.debug or logger.info for these messages instead of print.
This will ensure debug information is properly managed and can be enabled or
disabled via logging configuration in production.

RunwayTextToImage.run = staticmethod(run)

NODE_CLASS_MAPPINGS = {
"RunwayTextToImage": RunwayTextToImage
}
Binary file not shown.
44 changes: 44 additions & 0 deletions ComfyUI/custom_nodes/websocket_image_save.py
Original file line number Diff line number Diff line change
@@ -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):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (code-quality): The first argument to class methods should be cls (class-method-first-arg-name)

Suggested change
def INPUT_TYPES(s):
def INPUT_TYPES(cls):

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
Comment on lines +30 to +34
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (code-quality): Replace manual loop counter with call to enumerate (convert-to-enumerate)


return {}

@classmethod
def IS_CHANGED(s, images):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (code-quality): The first argument to class methods should be cls (class-method-first-arg-name)

Suggested change
def IS_CHANGED(s, images):
def IS_CHANGED(cls, images):

return time.time()

NODE_CLASS_MAPPINGS = {
"SaveImageWebsocket": SaveImageWebsocket,
}