Compare commits
21
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d84d71541c | ||
|
|
e8895028a2 | ||
|
|
b5c00450de | ||
|
|
59083391c7 | ||
|
|
8f2adacc20 | ||
|
|
089bad5560 | ||
|
|
46010e1dd5 | ||
|
|
52d876fa67 | ||
|
|
f7e7eb19d0 | ||
|
|
cb03b6718e | ||
|
|
7b734c415a | ||
|
|
64d3ec6b45 | ||
|
|
c47865ec26 | ||
|
|
b889f79baf | ||
|
|
1d8fed3534 | ||
|
|
a557788e70 | ||
|
|
05cccaffa2 | ||
|
|
85af9dd68f | ||
|
|
233615ea25 | ||
|
|
8b6aabbfaa | ||
|
|
c72539078c |
@@ -1,110 +0,0 @@
|
||||
import os
|
||||
import io
|
||||
import cv2 as cv
|
||||
import numpy as np
|
||||
import torch
|
||||
import requests
|
||||
from folder_paths import get_annotated_filepath
|
||||
|
||||
class ComfyUIDeployExternalEXR:
|
||||
RETURN_TYPES = ("IMAGE", "MASK")
|
||||
RETURN_NAMES = ("image", "mask")
|
||||
FUNCTION = "load_exr"
|
||||
CATEGORY = "🔗ComfyDeploy"
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"input_id": (
|
||||
"STRING",
|
||||
{"multiline": False, "default": "input_exr"},
|
||||
),
|
||||
"exr_file": ("STRING", {"default": ""}),
|
||||
"tonemap": (["linear", "sRGB", "Reinhard"], {"default": "sRGB"}),
|
||||
},
|
||||
"optional": {
|
||||
"default_image": ("IMAGE",),
|
||||
"default_mask": ("MASK",),
|
||||
"display_name": (
|
||||
"STRING",
|
||||
{"multiline": False, "default": ""},
|
||||
),
|
||||
"description": (
|
||||
"STRING",
|
||||
{"multiline": False, "default": ""},
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def VALIDATE_INPUTS(s, exr_file, **kwargs):
|
||||
return True
|
||||
|
||||
def sRGBtoLinear(self, npArray):
|
||||
less = npArray <= 0.0404482362771082
|
||||
npArray[less] = npArray[less] / 12.92
|
||||
npArray[~less] = np.power((npArray[~less] + 0.055) / 1.055, 2.4)
|
||||
|
||||
def linearToSRGB(self, npArray):
|
||||
less = npArray <= 0.0031308
|
||||
npArray[less] = npArray[less] * 12.92
|
||||
npArray[~less] = np.power(npArray[~less], 1/2.4) * 1.055 - 0.055
|
||||
|
||||
def load_exr(self, input_id, exr_file, tonemap="sRGB",
|
||||
default_image=None, default_mask=None,
|
||||
display_name=None, description=None):
|
||||
try:
|
||||
if exr_file and exr_file != "":
|
||||
if exr_file.startswith(('http://', 'https://')):
|
||||
# Handle URL input
|
||||
response = requests.get(exr_file)
|
||||
# Write to temp buffer
|
||||
buffer = io.BytesIO(response.content)
|
||||
nparr = np.frombuffer(buffer.getvalue(), np.uint8)
|
||||
image = cv.imdecode(nparr, cv.IMREAD_UNCHANGED).astype(np.float32)
|
||||
else:
|
||||
# Handle local file
|
||||
exr_path = get_annotated_filepath(exr_file)
|
||||
image = cv.imread(exr_path, cv.IMREAD_UNCHANGED).astype(np.float32)
|
||||
|
||||
if len(image.shape) == 2:
|
||||
image = np.repeat(image[..., np.newaxis], 3, axis=2)
|
||||
|
||||
# Extract RGB and flip channels
|
||||
rgb = np.flip(image[:,:,:3], 2).copy()
|
||||
|
||||
# Apply tonemapping
|
||||
if tonemap == "sRGB":
|
||||
self.linearToSRGB(rgb)
|
||||
rgb = np.clip(rgb, 0, 1)
|
||||
elif tonemap == "Reinhard":
|
||||
rgb = np.clip(rgb, 0, None)
|
||||
rgb = rgb / (rgb + 1)
|
||||
self.linearToSRGB(rgb)
|
||||
rgb = np.clip(rgb, 0, 1)
|
||||
|
||||
rgb = torch.unsqueeze(torch.from_numpy(rgb), 0)
|
||||
|
||||
# Handle alpha/mask
|
||||
mask = torch.zeros((1, image.shape[0], image.shape[1]), dtype=torch.float32)
|
||||
if image.shape[2] > 3:
|
||||
mask[0] = torch.from_numpy(np.clip(image[:,:,3], 0, 1))
|
||||
|
||||
return (rgb, mask)
|
||||
else:
|
||||
# Return defaults if no file provided
|
||||
return (default_image, default_mask)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error loading EXR: {str(e)}")
|
||||
# Return defaults on error
|
||||
return (default_image, default_mask)
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"ComfyUIDeployExternalEXR": ComfyUIDeployExternalEXR
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"ComfyUIDeployExternalEXR": "External EXR (ComfyUI Deploy)"
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
import os
|
||||
os.environ["OPENCV_IO_ENABLE_OPENEXR"] = "1"
|
||||
import cv2 as cv
|
||||
import numpy as np
|
||||
import torch
|
||||
from folder_paths import get_annotated_filepath
|
||||
|
||||
def linear_to_srgb(np_array):
|
||||
"""Converts a linear RGB numpy array to sRGB."""
|
||||
less = np_array <= 0.0031308
|
||||
np_array[less] = np_array[less] * 12.92
|
||||
np_array[~less] = np.power(np_array[~less], 1/2.4) * 1.055 - 0.055
|
||||
return np_array
|
||||
|
||||
class ExternalExrInput:
|
||||
"""
|
||||
Node to load a single EXR image from a local file path.
|
||||
"""
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"exr_file": ("STRING", {"default": "path/to/image.exr"}),
|
||||
"tonemap": (["linear", "sRGB", "Reinhard"], {"default": "sRGB"}),
|
||||
},
|
||||
"optional": {
|
||||
"default_image": ("IMAGE",),
|
||||
"default_mask": ("MASK",),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE", "MASK")
|
||||
RETURN_NAMES = ("image", "mask",)
|
||||
FUNCTION = "run"
|
||||
CATEGORY = "🔗ComfyDeploy/EXR"
|
||||
|
||||
def run(self, exr_file, tonemap, default_image=None, default_mask=None):
|
||||
image = None
|
||||
try:
|
||||
if exr_file and exr_file.strip() != "":
|
||||
exr_path = get_annotated_filepath(exr_file)
|
||||
if os.path.exists(exr_path):
|
||||
image = cv.imread(exr_path, cv.IMREAD_UNCHANGED).astype(np.float32)
|
||||
else:
|
||||
print(f"Warning: File not found at {exr_path}")
|
||||
|
||||
if image is None:
|
||||
raise ValueError("Image could not be loaded.")
|
||||
|
||||
if len(image.shape) == 2: # Grayscale
|
||||
image = np.repeat(image[..., np.newaxis], 3, axis=2)
|
||||
|
||||
rgb = np.flip(image[:, :, :3], 2).copy() # BGR to RGB
|
||||
|
||||
# Apply tonemapping
|
||||
if tonemap == "sRGB":
|
||||
rgb = linear_to_srgb(rgb)
|
||||
rgb = np.clip(rgb, 0, 1)
|
||||
elif tonemap == "Reinhard":
|
||||
rgb = np.clip(rgb, 0, None)
|
||||
rgb = rgb / (rgb + 1)
|
||||
rgb = linear_to_srgb(rgb)
|
||||
rgb = np.clip(rgb, 0, 1)
|
||||
|
||||
rgb_tensor = torch.from_numpy(rgb).unsqueeze(0)
|
||||
|
||||
# Handle alpha/mask
|
||||
if image.shape[2] > 3:
|
||||
mask = np.clip(image[:, :, 3], 0, 1)
|
||||
else:
|
||||
mask = np.ones_like(rgb[:, :, 0])
|
||||
mask_tensor = torch.from_numpy(mask).unsqueeze(0)
|
||||
|
||||
return (rgb_tensor, mask_tensor)
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error loading EXR file '{exr_file}': {e}")
|
||||
if default_image is not None and default_mask is not None:
|
||||
print("Returning default image.")
|
||||
return (default_image, default_mask)
|
||||
|
||||
print("Warning: Error loading EXR and no default image. Returning a black image.")
|
||||
blank_image = torch.zeros((1, 64, 64, 3), dtype=torch.float32)
|
||||
blank_mask = torch.zeros((1, 64, 64), dtype=torch.float32)
|
||||
return (blank_image, blank_mask)
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"ExternalExrInput": ExternalExrInput
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"ExternalExrInput": "External EXR Input (ComfyDeploy)"
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
import os
|
||||
os.environ["OPENCV_IO_ENABLE_OPENEXR"] = "1"
|
||||
import cv2 as cv
|
||||
import torch
|
||||
import numpy as np
|
||||
import folder_paths
|
||||
|
||||
def srgb_to_linear(np_array):
|
||||
"""Converts an sRGB numpy array to linear RGB."""
|
||||
less = np_array <= 0.0404482362771082
|
||||
np_array[less] = np_array[less] / 12.92
|
||||
np_array[~less] = np.power((np_array[~less] + 0.055) / 1.055, 2.4)
|
||||
return np_array
|
||||
|
||||
class ExternalExrOutput:
|
||||
"""
|
||||
Node to save a single image as an EXR file to a local path.
|
||||
"""
|
||||
def __init__(self):
|
||||
self.output_dir = folder_paths.get_output_directory()
|
||||
self.type = "output"
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"images": ("IMAGE",),
|
||||
"filepath": ("STRING", {"default": "/tmp/output.exr"}),
|
||||
"tonemap": (["linear", "sRGB"], {"default": "linear"}),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ()
|
||||
FUNCTION = "run"
|
||||
OUTPUT_NODE = True
|
||||
CATEGORY = "🔗ComfyDeploy/EXR"
|
||||
|
||||
def run(self, images, filepath, tonemap):
|
||||
if not filepath.endswith(".exr"):
|
||||
raise ValueError("Filepath must end with '.exr'")
|
||||
|
||||
output_dir = os.path.dirname(filepath)
|
||||
if not os.path.isabs(output_dir):
|
||||
raise ValueError("Filepath must be an absolute path.")
|
||||
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
|
||||
# We only process the first image in the batch
|
||||
image_tensor = images[0]
|
||||
|
||||
linear = image_tensor.cpu().numpy().astype(np.float32)
|
||||
|
||||
# If the source is sRGB, convert to linear
|
||||
if tonemap == "sRGB":
|
||||
linear[...,:3] = srgb_to_linear(linear[...,:3])
|
||||
|
||||
# Convert RGB to BGR for OpenCV
|
||||
bgr = np.flip(linear, 2).copy()
|
||||
|
||||
# Save the image
|
||||
cv.imwrite(filepath, bgr)
|
||||
|
||||
print(f"Saved EXR file to: {filepath}")
|
||||
|
||||
return {"ui": {"images": [{"filename": os.path.basename(filepath), "subfolder": os.path.dirname(filepath), "type": self.type}]}}
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"ExternalExrOutput": ExternalExrOutput
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"ExternalExrOutput": "External EXR Output (ComfyDeploy)"
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
import os
|
||||
os.environ["OPENCV_IO_ENABLE_OPENEXR"] = "1"
|
||||
import cv2 as cv
|
||||
import numpy as np
|
||||
import torch
|
||||
import re
|
||||
from folder_paths import get_annotated_filepath
|
||||
|
||||
def linear_to_srgb(np_array):
|
||||
"""Converts a linear RGB numpy array to sRGB."""
|
||||
less = np_array <= 0.0031308
|
||||
np_array[less] = np_array[less] * 12.92
|
||||
np_array[~less] = np.power(np_array[~less], 1/2.4) * 1.055 - 0.055
|
||||
return np_array
|
||||
|
||||
class ExternalExrSequenceInput:
|
||||
"""
|
||||
Node to load a sequence of EXR images from a local filepath pattern, a directory,
|
||||
or a single file within a sequence.
|
||||
"""
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"path_or_pattern": ("STRING", {"default": "path/to/frames_or_pattern"}),
|
||||
"tonemap": (["linear", "sRGB", "Reinhard"], {"default": "sRGB"}),
|
||||
"start_frame": ("INT", {"default": 1, "min": 1}),
|
||||
"end_frame": ("INT", {"default": 50, "min": 1}),
|
||||
},
|
||||
"optional": {
|
||||
"default_image": ("IMAGE",),
|
||||
"default_mask": ("MASK",),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE", "MASK")
|
||||
RETURN_NAMES = ("image", "mask",)
|
||||
FUNCTION = "run"
|
||||
CATEGORY = "🔗ComfyDeploy/EXR"
|
||||
|
||||
def get_image_paths(self, path_input, start_frame, end_frame):
|
||||
image_paths = []
|
||||
|
||||
# Case 1: Input is a C-style pattern
|
||||
if '%' in path_input:
|
||||
print(f"Pattern detected: {path_input}")
|
||||
for i in range(start_frame, end_frame + 1):
|
||||
fpath = get_annotated_filepath(path_input % i)
|
||||
if os.path.exists(fpath):
|
||||
image_paths.append(fpath)
|
||||
return image_paths
|
||||
|
||||
annotated_path = get_annotated_filepath(path_input)
|
||||
|
||||
# Case 2: Input is a directory
|
||||
if os.path.isdir(annotated_path):
|
||||
print(f"Directory detected: {annotated_path}")
|
||||
files_in_dir = sorted(os.listdir(annotated_path))
|
||||
for filename in files_in_dir:
|
||||
if not filename.lower().endswith('.exr'):
|
||||
continue
|
||||
|
||||
matches = re.findall(r'\d+', filename)
|
||||
if not matches:
|
||||
continue
|
||||
|
||||
frame_number = int(matches[-1])
|
||||
if start_frame <= frame_number <= end_frame:
|
||||
image_paths.append(os.path.join(annotated_path, filename))
|
||||
return image_paths
|
||||
|
||||
# Case 3: Input is a single file from a sequence
|
||||
if os.path.isfile(annotated_path):
|
||||
print(f"Single file detected: {annotated_path}. Attempting to find sequence.")
|
||||
base_dir = os.path.dirname(annotated_path)
|
||||
filename = os.path.basename(annotated_path)
|
||||
|
||||
matches = list(re.finditer(r'(\d+)', filename))
|
||||
if not matches: # It's a single file with no frame number
|
||||
return [annotated_path]
|
||||
|
||||
last_match = matches[-1]
|
||||
num_start_pos, num_end_pos = last_match.span()
|
||||
prefix = filename[:num_start_pos]
|
||||
suffix = filename[num_end_pos:]
|
||||
padding = len(last_match.group(0))
|
||||
|
||||
for i in range(start_frame, end_frame + 1):
|
||||
potential_filename = f"{prefix}{str(i).zfill(padding)}{suffix}"
|
||||
potential_path = os.path.join(base_dir, potential_filename)
|
||||
if os.path.exists(potential_path):
|
||||
image_paths.append(potential_path)
|
||||
return image_paths
|
||||
|
||||
return [] # Return empty if no cases match
|
||||
|
||||
def run(self, path_or_pattern, tonemap, start_frame, end_frame, default_image=None, default_mask=None):
|
||||
try:
|
||||
image_paths = self.get_image_paths(path_or_pattern, start_frame, end_frame)
|
||||
if not image_paths:
|
||||
raise ValueError(f"No EXR files found for '{path_or_pattern}' between frames {start_frame}-{end_frame}.")
|
||||
|
||||
print(f"Found {len(image_paths)} EXR files to load.")
|
||||
rgb_frames = []
|
||||
mask_frames = []
|
||||
|
||||
for path in image_paths:
|
||||
image = cv.imread(path, cv.IMREAD_UNCHANGED)
|
||||
if image is None:
|
||||
print(f"Warning: Could not read file {path}, skipping.")
|
||||
continue
|
||||
|
||||
image = image.astype(np.float32)
|
||||
if len(image.shape) == 2:
|
||||
image = np.repeat(image[..., np.newaxis], 3, axis=2)
|
||||
|
||||
rgb = np.flip(image[:, :, :3], 2).copy()
|
||||
|
||||
if tonemap == "sRGB":
|
||||
rgb = linear_to_srgb(rgb)
|
||||
rgb = np.clip(rgb, 0, 1)
|
||||
elif tonemap == "Reinhard":
|
||||
rgb = np.clip(rgb, 0, None)
|
||||
rgb = rgb / (rgb + 1)
|
||||
rgb = linear_to_srgb(rgb)
|
||||
rgb = np.clip(rgb, 0, 1)
|
||||
|
||||
rgb_frames.append(torch.from_numpy(rgb))
|
||||
|
||||
if image.shape[2] > 3:
|
||||
mask = np.clip(image[:, :, 3], 0, 1)
|
||||
else:
|
||||
mask = np.ones_like(rgb[:, :, 0])
|
||||
mask_frames.append(torch.from_numpy(mask))
|
||||
|
||||
if not rgb_frames:
|
||||
raise ValueError("No frames were loaded successfully.")
|
||||
|
||||
print(f"Successfully loaded {len(rgb_frames)} frames into a batch.")
|
||||
return (torch.stack(rgb_frames, 0), torch.stack(mask_frames, 0))
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error loading EXR sequence: {e}")
|
||||
if default_image is not None and default_mask is not None:
|
||||
print("Returning default image.")
|
||||
return (default_image, default_mask)
|
||||
|
||||
print("Warning: Error loading sequence and no default image. Returning a black image.")
|
||||
blank_image = torch.zeros((1, 64, 64, 3), dtype=torch.float32)
|
||||
blank_mask = torch.zeros((1, 64, 64), dtype=torch.float32)
|
||||
return (blank_image, blank_mask)
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"ExternalExrSequenceInput": ExternalExrSequenceInput
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"ExternalExrSequenceInput": "External EXR Sequence Input (ComfyDeploy)"
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import os
|
||||
os.environ["OPENCV_IO_ENABLE_OPENEXR"] = "1"
|
||||
import cv2 as cv
|
||||
import torch
|
||||
import numpy as np
|
||||
import re
|
||||
|
||||
def srgb_to_linear(np_array):
|
||||
"""Converts an sRGB numpy array to linear RGB."""
|
||||
less = np_array <= 0.0404482362771082
|
||||
np_array[less] = np_array[less] / 12.92
|
||||
np_array[~less] = np.power((np_array[~less] + 0.055) / 1.055, 2.4)
|
||||
return np_array
|
||||
|
||||
class ExternalExrSequenceOutput:
|
||||
"""
|
||||
Node to save a sequence of images as EXR files to a local directory.
|
||||
It uses a filepath pattern like 'path/to/frame_%04d.exr' to save each frame.
|
||||
"""
|
||||
def __init__(self):
|
||||
self.type = "output"
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"images": ("IMAGE",),
|
||||
"filepath_pattern": ("STRING", {"default": "/tmp/exr_sequence/frame_%04d.exr"}),
|
||||
"tonemap": (["linear", "sRGB"], {"default": "linear"}),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ()
|
||||
FUNCTION = "run"
|
||||
OUTPUT_NODE = True
|
||||
CATEGORY = "🔗ComfyDeploy/EXR"
|
||||
|
||||
def run(self, images, filepath_pattern, tonemap):
|
||||
# Basic validation for the filepath pattern
|
||||
if not re.search(r'%0?\d+d', filepath_pattern):
|
||||
raise ValueError("Filepath pattern must contain a C-style format specifier like '%04d'.")
|
||||
|
||||
if not filepath_pattern.endswith(".exr"):
|
||||
raise ValueError("Filepath pattern must end with '.exr'.")
|
||||
|
||||
output_dir = os.path.dirname(filepath_pattern)
|
||||
if not os.path.isabs(output_dir):
|
||||
raise ValueError("Filepath must be an absolute path.")
|
||||
|
||||
os.makedirs(output_dir, exist_ok=True)
|
||||
|
||||
# Convert tensor to numpy array
|
||||
linear_images = images.cpu().numpy().astype(np.float32)
|
||||
|
||||
# If the source is sRGB, convert to linear
|
||||
if tonemap == "sRGB":
|
||||
srgb_to_linear(linear_images[...,:3])
|
||||
|
||||
# Convert RGB to BGR for OpenCV
|
||||
bgr_images = np.flip(linear_images, 3).copy()
|
||||
|
||||
results = []
|
||||
for i, bgr_image in enumerate(bgr_images):
|
||||
frame_num = i + 1
|
||||
try:
|
||||
# Use the pattern to format the full file path
|
||||
file_path = filepath_pattern % frame_num
|
||||
except TypeError:
|
||||
raise ValueError("Invalid format specifier in filepath_pattern. Use '%d', '%04d', etc.")
|
||||
|
||||
# Save the image
|
||||
cv.imwrite(file_path, bgr_image)
|
||||
|
||||
results.append({
|
||||
"filename": os.path.basename(file_path),
|
||||
"subfolder": os.path.dirname(file_path),
|
||||
"type": self.type,
|
||||
})
|
||||
|
||||
return {"ui": {"images": results}}
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"ExternalExrSequenceOutput": ExternalExrSequenceOutput
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"ExternalExrSequenceOutput": "External EXR Sequence Output (ComfyDeploy)"
|
||||
}
|
||||
@@ -1,8 +1,4 @@
|
||||
import folder_paths
|
||||
from PIL import Image, ImageOps
|
||||
import numpy as np
|
||||
import torch
|
||||
import folder_paths
|
||||
|
||||
|
||||
class AnyType(str):
|
||||
@@ -41,6 +37,10 @@ class ComfyUIDeployExternalLora:
|
||||
"STRING",
|
||||
{"multiline": False, "default": ""},
|
||||
),
|
||||
"bearer_token": (
|
||||
"STRING",
|
||||
{"multiline": False, "default": ""},
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
@@ -57,6 +57,7 @@ class ComfyUIDeployExternalLora:
|
||||
display_name=None,
|
||||
description=None,
|
||||
lora_url=None,
|
||||
bearer_token=None,
|
||||
):
|
||||
import requests
|
||||
import os
|
||||
@@ -84,9 +85,13 @@ class ComfyUIDeployExternalLora:
|
||||
+ " to "
|
||||
+ destination_path
|
||||
)
|
||||
headers = {"User-Agent": "Mozilla/5.0"}
|
||||
if bearer_token:
|
||||
headers["Authorization"] = f"Bearer {bearer_token}"
|
||||
print("using bearer token")
|
||||
response = requests.get(
|
||||
lora_url,
|
||||
headers={"User-Agent": "Mozilla/5.0"},
|
||||
headers=headers,
|
||||
allow_redirects=True,
|
||||
)
|
||||
with open(destination_path, "wb") as out_file:
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
class ComfyUIDeployExternalNumberSliderInt:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"input_id": (
|
||||
"STRING",
|
||||
{"multiline": False, "default": "input_number_slider_int"},
|
||||
),
|
||||
},
|
||||
"optional": {
|
||||
"default_value": (
|
||||
"INT",
|
||||
{"multiline": True, "display": "number", "min": -2147483647, "max": 2147483647, "default": 1, "step": 1},
|
||||
),
|
||||
"min_value": (
|
||||
"INT",
|
||||
{"multiline": True, "display": "number", "min": -2147483647, "max": 2147483647, "default": 0, "step": 1},
|
||||
),
|
||||
"max_value": (
|
||||
"INT",
|
||||
{"multiline": True, "display": "number", "min": -2147483647, "max": 2147483647, "default": 10, "step": 1},
|
||||
),
|
||||
"display_name": (
|
||||
"STRING",
|
||||
{"multiline": False, "default": ""},
|
||||
),
|
||||
"description": (
|
||||
"STRING",
|
||||
{"multiline": True, "default": ""},
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("INT",)
|
||||
RETURN_NAMES = ("value",)
|
||||
FUNCTION = "run"
|
||||
CATEGORY = "🔗ComfyDeploy"
|
||||
|
||||
def run(self, input_id, default_value=None, min_value=0, max_value=10, display_name=None, description=None):
|
||||
try:
|
||||
int_value = int(round(float(input_id)))
|
||||
if min_value <= int_value <= max_value:
|
||||
print("my integer", int_value)
|
||||
return [int_value]
|
||||
else:
|
||||
print("Integer out of range. Returning default value:", default_value)
|
||||
return [default_value]
|
||||
except (ValueError, TypeError):
|
||||
print("Invalid input. Returning default value:", default_value)
|
||||
return [default_value]
|
||||
|
||||
NODE_CLASS_MAPPINGS = {"ComfyUIDeployExternalNumberSliderInt": ComfyUIDeployExternalNumberSliderInt}
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {"ComfyUIDeployExternalNumberSliderInt": "External Number Slider Int (ComfyUI Deploy)"}
|
||||
@@ -0,0 +1,116 @@
|
||||
import random
|
||||
|
||||
|
||||
class ComfyUIDeployExternalSeed:
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"input_id": (
|
||||
"STRING",
|
||||
{"multiline": False, "default": "input_seed"},
|
||||
),
|
||||
"default_value": (
|
||||
"INT",
|
||||
{"default": -1},
|
||||
),
|
||||
"min_value": (
|
||||
"INT",
|
||||
{"default": 1, "min": 1, "max": 999999999999999},
|
||||
),
|
||||
"max_value": (
|
||||
"INT",
|
||||
{"default": 4294967295, "min": 1, "max": 999999999999999},
|
||||
),
|
||||
},
|
||||
"optional": {
|
||||
"display_name": (
|
||||
"STRING",
|
||||
{"multiline": False, "default": ""},
|
||||
),
|
||||
"description": (
|
||||
"STRING",
|
||||
{
|
||||
"multiline": True,
|
||||
"default": 'For default value:\n"-1" (i.e. not in range): Randomize within the min and max value range. \nin range: Fixed, always the same value\n',
|
||||
},
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("INT",)
|
||||
RETURN_NAMES = ("seed",)
|
||||
FUNCTION = "run"
|
||||
CATEGORY = "🔗ComfyDeploy"
|
||||
|
||||
# Limits
|
||||
_MAX_LIMIT = 999_999_999_999_999 # 15 digits
|
||||
|
||||
# Store cached seed when fixed flag is enabled
|
||||
_cached_seed = None
|
||||
|
||||
@classmethod
|
||||
def IS_CHANGED(
|
||||
cls,
|
||||
input_id,
|
||||
min_value,
|
||||
max_value,
|
||||
default_value=None,
|
||||
**kwargs,
|
||||
):
|
||||
"""Inform ComfyUI whether the node output should be considered changed.
|
||||
|
||||
If default_value is within range (Fixed mode), we return the inputs tuple
|
||||
so the cached result is reused until the user changes something.
|
||||
For Randomize mode, we force re-execution each queue.
|
||||
"""
|
||||
# Clamp values to allowed range for check
|
||||
min_value = max(1, min_value)
|
||||
max_value = min(cls._MAX_LIMIT, max_value)
|
||||
|
||||
# Fixed mode when default_value is within range
|
||||
if (
|
||||
default_value is not None
|
||||
and default_value >= min_value
|
||||
and default_value <= max_value
|
||||
):
|
||||
return (input_id, default_value)
|
||||
|
||||
# For Randomize (default_value is -1 or out of range) we force re-execution
|
||||
import random as _rnd
|
||||
|
||||
return _rnd.random()
|
||||
|
||||
def run(
|
||||
self,
|
||||
input_id,
|
||||
min_value: int,
|
||||
max_value: int,
|
||||
display_name=None,
|
||||
description=None,
|
||||
default_value: int = -1,
|
||||
):
|
||||
# Clamp values to allowed range
|
||||
min_value = max(1, min_value)
|
||||
max_value = min(self._MAX_LIMIT, max_value)
|
||||
|
||||
# Ensure limits are in correct order after clamping
|
||||
if min_value > max_value:
|
||||
min_value, max_value = max_value, min_value
|
||||
|
||||
# Fixed mode: default_value is within range
|
||||
if default_value >= min_value and default_value <= max_value:
|
||||
seed = int(default_value)
|
||||
self._cached_seed = seed
|
||||
return [seed]
|
||||
|
||||
# Randomize mode: default_value is -1 or out of range
|
||||
seed = random.randint(min_value, max_value)
|
||||
self._cached_seed = seed
|
||||
return [seed]
|
||||
|
||||
|
||||
NODE_CLASS_MAPPINGS = {"ComfyUIDeployExternalSeed": ComfyUIDeployExternalSeed}
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"ComfyUIDeployExternalSeed": "External Seed (ComfyUI Deploy)"
|
||||
}
|
||||
@@ -748,36 +748,64 @@ class ComfyUIDeployExternalVideo:
|
||||
file_parts = f.split(".")
|
||||
if len(file_parts) > 1 and (file_parts[-1] in video_extensions):
|
||||
files.append(f)
|
||||
return {"required": {
|
||||
"input_id": (
|
||||
"STRING",
|
||||
{"multiline": False, "default": "input_video"},
|
||||
),
|
||||
"force_rate": ("INT", {"default": 0, "min": 0, "max": 60, "step": 1}),
|
||||
"force_size": (["Disabled", "Custom Height", "Custom Width", "Custom", "256x?", "?x256", "256x256", "512x?", "?x512", "512x512"],),
|
||||
"custom_width": ("INT", {"default": 512, "min": 0, "max": DIMMAX, "step": 8}),
|
||||
"custom_height": ("INT", {"default": 512, "min": 0, "max": DIMMAX, "step": 8}),
|
||||
"frame_load_cap": ("INT", {"default": 0, "min": 0, "max": BIGMAX, "step": 1}),
|
||||
"skip_first_frames": ("INT", {"default": 0, "min": 0, "max": BIGMAX, "step": 1}),
|
||||
"select_every_nth": ("INT", {"default": 1, "min": 1, "max": BIGMAX, "step": 1}),
|
||||
},
|
||||
"optional": {
|
||||
"meta_batch": ("VHS_BatchManager",),
|
||||
"vae": ("VAE",),
|
||||
"default_video": (sorted(files),),
|
||||
"display_name": (
|
||||
"STRING",
|
||||
{"multiline": False, "default": ""},
|
||||
),
|
||||
"description": (
|
||||
"STRING",
|
||||
{"multiline": True, "default": ""},
|
||||
),
|
||||
},
|
||||
"hidden": {
|
||||
"unique_id": "UNIQUE_ID"
|
||||
},
|
||||
}
|
||||
return {
|
||||
"required": {
|
||||
"input_id": (
|
||||
"STRING",
|
||||
{"multiline": False, "default": "input_video"},
|
||||
),
|
||||
"force_rate": ("INT", {"default": 0, "min": 0, "max": 60, "step": 1}),
|
||||
"force_size": (
|
||||
[
|
||||
"Disabled",
|
||||
"Custom Height",
|
||||
"Custom Width",
|
||||
"Custom",
|
||||
"256x?",
|
||||
"?x256",
|
||||
"256x256",
|
||||
"512x?",
|
||||
"?x512",
|
||||
"512x512",
|
||||
],
|
||||
),
|
||||
"custom_width": (
|
||||
"INT",
|
||||
{"default": 512, "min": 0, "max": DIMMAX, "step": 8},
|
||||
),
|
||||
"custom_height": (
|
||||
"INT",
|
||||
{"default": 512, "min": 0, "max": DIMMAX, "step": 8},
|
||||
),
|
||||
"frame_load_cap": (
|
||||
"INT",
|
||||
{"default": 0, "min": 0, "max": BIGMAX, "step": 1},
|
||||
),
|
||||
"skip_first_frames": (
|
||||
"INT",
|
||||
{"default": 0, "min": 0, "max": BIGMAX, "step": 1},
|
||||
),
|
||||
"select_every_nth": (
|
||||
"INT",
|
||||
{"default": 1, "min": 1, "max": BIGMAX, "step": 1},
|
||||
),
|
||||
},
|
||||
"optional": {
|
||||
"meta_batch": ("VHS_BatchManager",),
|
||||
"vae": ("VAE",),
|
||||
"default_video": (sorted(files),),
|
||||
"display_name": (
|
||||
"STRING",
|
||||
{"multiline": False, "default": ""},
|
||||
),
|
||||
"description": (
|
||||
"STRING",
|
||||
{"multiline": True, "default": ""},
|
||||
),
|
||||
"default_value_url": ("STRING", {"image_preview": True, "default": ""}),
|
||||
},
|
||||
"hidden": {"unique_id": "UNIQUE_ID"},
|
||||
}
|
||||
|
||||
CATEGORY = "Video Helper Suite 🎥🅥🅗🅢"
|
||||
|
||||
@@ -804,16 +832,21 @@ class ComfyUIDeployExternalVideo:
|
||||
select_every_nth = kwargs.get("select_every_nth")
|
||||
meta_batch = kwargs.get("meta_batch")
|
||||
unique_id = kwargs.get("unique_id")
|
||||
|
||||
default_value_url = kwargs.get("default_value_url")
|
||||
|
||||
input_dir = folder_paths.get_input_directory()
|
||||
if input_id.startswith("http"):
|
||||
if input_id.startswith("http") or (
|
||||
default_value_url and default_value_url.startswith("http")
|
||||
):
|
||||
import requests
|
||||
|
||||
print("Fetching video from URL: ", input_id)
|
||||
response = requests.get(input_id, stream=True)
|
||||
# Use input_id if it's a URL, otherwise use default_value_url
|
||||
url = input_id if input_id.startswith("http") else default_value_url
|
||||
|
||||
print("Fetching video from URL: ", url)
|
||||
response = requests.get(url, stream=True)
|
||||
file_size = int(response.headers.get("Content-Length", 0))
|
||||
file_extension = input_id.split(".")[-1].split("?")[
|
||||
file_extension = url.split(".")[-1].split("?")[
|
||||
0
|
||||
] # Extract extension and handle URLs with parameters
|
||||
if file_extension not in video_extensions:
|
||||
|
||||
@@ -0,0 +1,111 @@
|
||||
import os
|
||||
os.environ["OPENCV_IO_ENABLE_OPENEXR"] = "1"
|
||||
import cv2 as cv
|
||||
import numpy as np
|
||||
import torch
|
||||
import requests
|
||||
|
||||
def linear_to_srgb(np_array):
|
||||
"""Converts a linear RGB numpy array to sRGB."""
|
||||
less = np_array <= 0.0031308
|
||||
np_array[less] = np_array[less] * 12.92
|
||||
np_array[~less] = np.power(np_array[~less], 1/2.4) * 1.055 - 0.055
|
||||
return np_array
|
||||
|
||||
class HttpExrInput:
|
||||
"""
|
||||
Node to load a single EXR image from a URL, with optional tonemapping.
|
||||
This node is designed to be used in a ComfyDeploy environment where input files are provided via signed URLs.
|
||||
"""
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"get_signed_url": ("STRING", {"multiline": True, "default": ""}),
|
||||
"tonemap": (["linear", "sRGB", "Reinhard"], {"default": "sRGB"}),
|
||||
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
|
||||
},
|
||||
"optional": {
|
||||
"default_image": ("IMAGE",),
|
||||
"default_mask": ("MASK",),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE", "MASK")
|
||||
RETURN_NAMES = ("image", "mask",)
|
||||
FUNCTION = "run"
|
||||
CATEGORY = "🔗ComfyDeploy/EXR"
|
||||
|
||||
def load_exr_from_data(self, exr_data):
|
||||
try:
|
||||
nparr = np.frombuffer(exr_data, np.uint8)
|
||||
# Use cv.IMREAD_UNCHANGED to keep all channels (e.g., alpha)
|
||||
image = cv.imdecode(nparr, cv.IMREAD_UNCHANGED)
|
||||
if image is None:
|
||||
raise ValueError("Failed to decode EXR data.")
|
||||
return image.astype(np.float32)
|
||||
except Exception as e:
|
||||
print(f"Error decoding EXR data: {e}")
|
||||
return None
|
||||
|
||||
def run(self, get_signed_url, tonemap, seed, default_image=None, default_mask=None):
|
||||
if not get_signed_url or get_signed_url.strip() == "":
|
||||
print("Warning: No input URL provided. Returning default image if available.")
|
||||
if default_image is not None and default_mask is not None:
|
||||
return (default_image, default_mask)
|
||||
|
||||
print("Warning: No input URL and no default image. Returning a black image.")
|
||||
blank_image = torch.zeros((1, 64, 64, 3), dtype=torch.float32)
|
||||
blank_mask = torch.zeros((1, 64, 64), dtype=torch.float32)
|
||||
return (blank_image, blank_mask)
|
||||
|
||||
image = None
|
||||
try:
|
||||
print(f"Fetching EXR from URL: {get_signed_url}")
|
||||
response = requests.get(get_signed_url)
|
||||
response.raise_for_status()
|
||||
image = self.load_exr_from_data(response.content)
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(f"Error fetching EXR from URL {get_signed_url}: {e}")
|
||||
|
||||
if image is None:
|
||||
print("Warning: Could not load or decode EXR image. Returning default image if available.")
|
||||
if default_image is not None and default_mask is not None:
|
||||
return (default_image, default_mask)
|
||||
|
||||
print("Warning: Failed to load EXR and no default image. Returning a black image.")
|
||||
blank_image = torch.zeros((1, 64, 64, 3), dtype=torch.float32)
|
||||
blank_mask = torch.zeros((1, 64, 64), dtype=torch.float32)
|
||||
return (blank_image, blank_mask)
|
||||
|
||||
# BGR to RGB conversion and channel handling
|
||||
if len(image.shape) == 2: # Grayscale
|
||||
image = np.repeat(image[..., np.newaxis], 3, axis=2)
|
||||
|
||||
rgb = np.flip(image[:, :, :3], 2).copy() # OpenCV loads as BGR, convert to RGB
|
||||
|
||||
# Tonemapping
|
||||
if tonemap == "sRGB":
|
||||
rgb = linear_to_srgb(rgb)
|
||||
rgb = np.clip(rgb, 0, 1)
|
||||
elif tonemap == "Reinhard":
|
||||
rgb = np.clip(rgb, 0, None) # Ensure no negative values
|
||||
rgb = rgb / (rgb + 1)
|
||||
rgb = linear_to_srgb(rgb)
|
||||
rgb = np.clip(rgb, 0, 1)
|
||||
|
||||
# Handle alpha channel if it exists
|
||||
if image.shape[2] > 3:
|
||||
mask = np.clip(image[:, :, 3], 0, 1)
|
||||
else:
|
||||
mask = np.ones_like(rgb[:, :, 0]) # Create a full white mask if no alpha
|
||||
|
||||
return (torch.from_numpy(rgb).unsqueeze(0), torch.from_numpy(mask).unsqueeze(0),)
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"HttpExrInput": HttpExrInput
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"HttpExrInput": "HTTP EXR Input (ComfyDeploy)"
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
import os
|
||||
os.environ["OPENCV_IO_ENABLE_OPENEXR"] = "1"
|
||||
import cv2 as cv
|
||||
import torch
|
||||
import numpy as np
|
||||
import requests
|
||||
|
||||
def srgb_to_linear(np_array):
|
||||
"""Converts an sRGB numpy array to linear RGB."""
|
||||
less = np_array <= 0.0404482362771082
|
||||
np_array[less] = np_array[less] / 12.92
|
||||
np_array[~less] = np.power((np_array[~less] + 0.055) / 1.055, 2.4)
|
||||
return np_array
|
||||
|
||||
class HttpExrOutput:
|
||||
"""
|
||||
Node to save a single EXR image to a pre-signed URL.
|
||||
This node is designed for ComfyDeploy to upload the generated EXR file.
|
||||
"""
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"images": ("IMAGE",),
|
||||
"put_signed_url": ("STRING", {"multiline": True, "default": ""}),
|
||||
"tonemap": (["linear", "sRGB"], {"default": "linear"}),
|
||||
},
|
||||
"hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ()
|
||||
FUNCTION = "run"
|
||||
OUTPUT_NODE = True
|
||||
CATEGORY = "ComfyDeploy/EXR"
|
||||
|
||||
def run(self, images, put_signed_url, tonemap, prompt=None, extra_pnginfo=None):
|
||||
if not put_signed_url or put_signed_url.strip() == "":
|
||||
print("Warning: No put_signed_url provided. Nothing will be uploaded.")
|
||||
return {"ui": {"images": []}}
|
||||
|
||||
# We process only the first image of the batch
|
||||
image_tensor = images[0]
|
||||
|
||||
# Convert tensor to numpy array, assuming it's in range [0, 1]
|
||||
linear = image_tensor.cpu().numpy().astype(np.float32)
|
||||
|
||||
# If the source is sRGB, convert to linear
|
||||
if tonemap == "sRGB":
|
||||
linear[...,:3] = srgb_to_linear(linear[...,:3])
|
||||
|
||||
# Convert RGB to BGR for OpenCV
|
||||
bgr = np.flip(linear, 2).copy()
|
||||
|
||||
results = []
|
||||
try:
|
||||
# Encode the image to the EXR format in memory
|
||||
is_success, buffer = cv.imencode(".exr", bgr)
|
||||
if not is_success:
|
||||
raise Exception("Failed to encode image to EXR format.")
|
||||
|
||||
# Upload the image data to the pre-signed URL
|
||||
response = requests.put(put_signed_url, data=buffer.tobytes(), headers={'Content-Type': 'image/x-exr'})
|
||||
response.raise_for_status()
|
||||
|
||||
print(f"Successfully uploaded EXR to: {put_signed_url}")
|
||||
# The UI can optionally display a link or confirmation
|
||||
results.append({"url": put_signed_url, "output_id": "output_http_exr"})
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error uploading EXR to signed URL: {e}")
|
||||
|
||||
return {"ui": {"images": results}}
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"HttpExrOutput": HttpExrOutput
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"HttpExrOutput": "HTTP EXR Output (ComfyDeploy)"
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
import os
|
||||
os.environ["OPENCV_IO_ENABLE_OPENEXR"] = "1"
|
||||
import cv2 as cv
|
||||
import numpy as np
|
||||
import torch
|
||||
import requests
|
||||
import json
|
||||
|
||||
def linear_to_srgb(np_array):
|
||||
"""Converts a linear RGB numpy array to sRGB."""
|
||||
less = np_array <= 0.0031308
|
||||
np_array[less] = np_array[less] * 12.92
|
||||
np_array[~less] = np.power(np_array[~less], 1/2.4) * 1.055 - 0.055
|
||||
return np_array
|
||||
|
||||
class HttpExrSequenceInput:
|
||||
"""
|
||||
Node to load a sequence of EXR images from a list of URLs provided as a JSON string.
|
||||
"""
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"urls_json": ("STRING", {"multiline": True, "default": "[]"}),
|
||||
"tonemap": (["linear", "sRGB", "Reinhard"], {"default": "sRGB"}),
|
||||
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
|
||||
},
|
||||
"optional": {
|
||||
"default_image": ("IMAGE",),
|
||||
"default_mask": ("MASK",),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ("IMAGE", "MASK")
|
||||
RETURN_NAMES = ("image", "mask",)
|
||||
FUNCTION = "run"
|
||||
CATEGORY = "🔗ComfyDeploy/EXR"
|
||||
|
||||
def load_exr_from_data(self, exr_data):
|
||||
try:
|
||||
nparr = np.frombuffer(exr_data, np.uint8)
|
||||
image = cv.imdecode(nparr, cv.IMREAD_UNCHANGED)
|
||||
if image is None:
|
||||
raise ValueError("Failed to decode EXR data.")
|
||||
return image.astype(np.float32)
|
||||
except Exception as e:
|
||||
print(f"Error decoding EXR data: {e}")
|
||||
return None
|
||||
|
||||
def run(self, urls_json, tonemap, seed, default_image=None, default_mask=None):
|
||||
try:
|
||||
urls = json.loads(urls_json)
|
||||
if not isinstance(urls, list) or not all(isinstance(u, str) for u in urls):
|
||||
raise ValueError("urls_json must be a JSON array of URL strings.")
|
||||
except (json.JSONDecodeError, ValueError) as e:
|
||||
print(f"Error parsing urls_json: {e}. Using default image if available.")
|
||||
urls = []
|
||||
|
||||
if not urls:
|
||||
if default_image is not None and default_mask is not None:
|
||||
return (default_image, default_mask)
|
||||
|
||||
print("Warning: No valid URLs and no default image. Returning a black image.")
|
||||
blank_image = torch.zeros((1, 64, 64, 3), dtype=torch.float32)
|
||||
blank_mask = torch.zeros((1, 64, 64), dtype=torch.float32)
|
||||
return (blank_image, blank_mask)
|
||||
|
||||
rgb_frames = []
|
||||
mask_frames = []
|
||||
|
||||
for url in urls:
|
||||
image = None
|
||||
try:
|
||||
print(f"Fetching EXR from URL: {url}")
|
||||
response = requests.get(url)
|
||||
response.raise_for_status()
|
||||
image = self.load_exr_from_data(response.content)
|
||||
except requests.exceptions.RequestException as e:
|
||||
print(f"Error fetching EXR from URL {url}: {e}")
|
||||
|
||||
if image is None:
|
||||
print(f"Warning: Could not decode EXR from {url}. Skipping frame.")
|
||||
continue
|
||||
|
||||
if len(image.shape) == 2: # Grayscale
|
||||
image = np.repeat(image[..., np.newaxis], 3, axis=2)
|
||||
|
||||
rgb = np.flip(image[:, :, :3], 2).copy() # BGR to RGB
|
||||
|
||||
if tonemap == "sRGB":
|
||||
rgb = linear_to_srgb(rgb)
|
||||
rgb = np.clip(rgb, 0, 1)
|
||||
elif tonemap == "Reinhard":
|
||||
rgb = np.clip(rgb, 0, None)
|
||||
rgb = rgb / (rgb + 1)
|
||||
rgb = linear_to_srgb(rgb)
|
||||
rgb = np.clip(rgb, 0, 1)
|
||||
|
||||
rgb_frames.append(torch.from_numpy(rgb))
|
||||
|
||||
if image.shape[2] > 3:
|
||||
mask = np.clip(image[:, :, 3], 0, 1)
|
||||
else:
|
||||
mask = np.ones_like(rgb[:, :, 0])
|
||||
mask_frames.append(torch.from_numpy(mask))
|
||||
|
||||
if not rgb_frames:
|
||||
print("Could not load any frames. Returning default image if available.")
|
||||
if default_image is not None and default_mask is not None:
|
||||
return (default_image, default_mask)
|
||||
|
||||
print("Warning: Failed to load any frames and no default image. Returning a black image.")
|
||||
blank_image = torch.zeros((1, 64, 64, 3), dtype=torch.float32)
|
||||
blank_mask = torch.zeros((1, 64, 64), dtype=torch.float32)
|
||||
return (blank_image, blank_mask)
|
||||
|
||||
print(f"Loaded {len(rgb_frames)} frames successfully.")
|
||||
return (torch.stack(rgb_frames, 0), torch.stack(mask_frames, 0))
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"HttpExrSequenceInput": HttpExrSequenceInput
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"HttpExrSequenceInput": "HTTP EXR Sequence Input (ComfyDeploy)"
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import os
|
||||
os.environ["OPENCV_IO_ENABLE_OPENEXR"] = "1"
|
||||
import cv2 as cv
|
||||
import torch
|
||||
import numpy as np
|
||||
import requests
|
||||
import json
|
||||
|
||||
def srgb_to_linear(np_array):
|
||||
"""Converts an sRGB numpy array to linear RGB."""
|
||||
less = np_array <= 0.0404482362771082
|
||||
np_array[less] = np_array[less] / 12.92
|
||||
np_array[~less] = np.power((np_array[~less] + 0.055) / 1.055, 2.4)
|
||||
return np_array
|
||||
|
||||
class HttpExrSequenceOutput:
|
||||
"""
|
||||
Node to save a sequence of images as EXR files to a list of pre-signed URLs.
|
||||
"""
|
||||
def __init__(self):
|
||||
self.type = "output"
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
return {
|
||||
"required": {
|
||||
"images": ("IMAGE",),
|
||||
"upload_urls_json": ("STRING", {"multiline": True, "default": "[]"}),
|
||||
"tonemap": (["linear", "sRGB"], {"default": "linear"}),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ()
|
||||
FUNCTION = "run"
|
||||
OUTPUT_NODE = True
|
||||
CATEGORY = "🔗ComfyDeploy/EXR"
|
||||
|
||||
def run(self, images, upload_urls_json, tonemap):
|
||||
try:
|
||||
upload_urls = json.loads(upload_urls_json)
|
||||
if not isinstance(upload_urls, list) or not all(isinstance(u, str) for u in upload_urls):
|
||||
raise ValueError("upload_urls_json must be a JSON array of URL strings.")
|
||||
except (json.JSONDecodeError, ValueError) as e:
|
||||
print(f"Error parsing upload_urls_json: {e}. Aborting upload.")
|
||||
return {"ui": {"images": []}}
|
||||
|
||||
if not upload_urls:
|
||||
print("Warning: No upload URLs provided. Nothing will be uploaded.")
|
||||
return {"ui": {"images": []}}
|
||||
|
||||
if len(images) != len(upload_urls):
|
||||
print(f"Warning: Mismatch between number of images ({len(images)}) and upload URLs ({len(upload_urls)}). Aborting upload.")
|
||||
return {"ui": {"images": []}}
|
||||
|
||||
# Convert tensor to numpy array
|
||||
linear_images = images.cpu().numpy().astype(np.float32)
|
||||
|
||||
# If the source is sRGB, convert all images to linear
|
||||
if tonemap == "sRGB":
|
||||
srgb_to_linear(linear_images[...,:3])
|
||||
|
||||
# Convert RGB to BGR for OpenCV
|
||||
bgr_images = np.flip(linear_images, 3).copy()
|
||||
|
||||
results = []
|
||||
for i, (bgr_image, url) in enumerate(zip(bgr_images, upload_urls)):
|
||||
try:
|
||||
# Encode the image to the EXR format in memory
|
||||
is_success, buffer = cv.imencode(".exr", bgr_image)
|
||||
if not is_success:
|
||||
raise Exception("Failed to encode image to EXR format.")
|
||||
|
||||
# Upload the image data to the pre-signed URL
|
||||
response = requests.put(url, data=buffer.tobytes(), headers={'Content-Type': 'image/x-exr'})
|
||||
response.raise_for_status()
|
||||
|
||||
print(f"Successfully uploaded frame {i+1} to: {url}")
|
||||
results.append({"url": url})
|
||||
|
||||
except Exception as e:
|
||||
print(f"Error uploading frame {i+1} to {url}: {e}")
|
||||
|
||||
return {"ui": {"images": results}}
|
||||
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"HttpExrSequenceOutput": HttpExrSequenceOutput
|
||||
}
|
||||
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"HttpExrSequenceOutput": "HTTP EXR Sequence Output (ComfyDeploy)"
|
||||
}
|
||||
+79
-29
@@ -158,6 +158,8 @@ from logging import basicConfig, getLogger
|
||||
# Check for an environment variable to enable/disable Logfire
|
||||
use_logfire = os.environ.get("USE_LOGFIRE", "false").lower() == "true"
|
||||
|
||||
API_KEY_COMFY_ORG = os.environ.get("API_KEY_COMFY_ORG", None)
|
||||
|
||||
if use_logfire:
|
||||
try:
|
||||
import logfire
|
||||
@@ -283,6 +285,9 @@ def post_prompt(json_data):
|
||||
if "extra_data" in json_data:
|
||||
extra_data = json_data["extra_data"]
|
||||
|
||||
if API_KEY_COMFY_ORG is not None:
|
||||
extra_data["api_key_comfy_org"] = API_KEY_COMFY_ORG
|
||||
|
||||
if "client_id" in json_data:
|
||||
extra_data["client_id"] = json_data["client_id"]
|
||||
if valid[0]:
|
||||
@@ -316,7 +321,7 @@ def randomSeed(num_digits=15):
|
||||
return random.randint(range_start, range_end)
|
||||
|
||||
|
||||
def apply_random_seed_to_workflow(workflow_api):
|
||||
def apply_random_seed_to_workflow(workflow_api, workflow):
|
||||
"""
|
||||
Applies a random seed to each element in the workflow_api that has a 'seed' input.
|
||||
|
||||
@@ -329,6 +334,41 @@ def apply_random_seed_to_workflow(workflow_api):
|
||||
# If seed is a list, it's an input from another node (generally `external number int`)
|
||||
if isinstance(workflow_api[key]["inputs"]["seed"], list):
|
||||
continue
|
||||
|
||||
# Check node type in workflow to determine if we should randomize
|
||||
node_id = key
|
||||
should_skip = (
|
||||
False # Add a flag to track if we should skip randomization
|
||||
)
|
||||
|
||||
for node in workflow["nodes"]:
|
||||
if str(node["id"]) == node_id and node["type"] == "KSampler":
|
||||
# Check if this node has widgets_values and if seed setting is not "fixed"
|
||||
if "widgets_values" in node and len(node["widgets_values"]) > 1:
|
||||
seed_mode = node["widgets_values"][1]
|
||||
if seed_mode == "fixed":
|
||||
# Skip randomization for fixed seeds
|
||||
logger.info(
|
||||
f"Skipping random seed for KSampler (node {node_id}) as it's set to fixed"
|
||||
)
|
||||
should_skip = True # Set the flag to skip randomization
|
||||
break # Exit the inner loop
|
||||
|
||||
# Apply random seed for non-fixed seeds (randomize, iter, etc.)
|
||||
workflow_api[key]["inputs"]["seed"] = randomSeed()
|
||||
logger.info(
|
||||
f"Applied random seed {workflow_api[key]['inputs']['seed']} to KSampler (node {node_id})"
|
||||
)
|
||||
should_skip = (
|
||||
True # Set the flag to skip default randomization
|
||||
)
|
||||
break # Exit the inner loop
|
||||
break # This break will skip checking other nodes if widgets_values doesn't exist
|
||||
|
||||
# Skip the rest of the code for this key if we already handled it
|
||||
if should_skip:
|
||||
continue
|
||||
|
||||
# Special case for SONICSampler
|
||||
if workflow_api[key]["class_type"] == "SONICSampler":
|
||||
workflow_api[key]["inputs"]["seed"] = randomSeed("sonic")
|
||||
@@ -436,6 +476,12 @@ def apply_inputs_to_workflow(workflow_api: Any, inputs: Any, sid: str = None):
|
||||
if value["class_type"] == "ComfyUIDeployExternalEXR":
|
||||
value["inputs"]["exr_file"] = new_value
|
||||
|
||||
if value["class_type"] == "ComfyUIDeployExternalSeed":
|
||||
logger.info(
|
||||
f"Applied random seed {new_value} to {value['class_type']}"
|
||||
)
|
||||
value["inputs"]["default_value"] = new_value
|
||||
|
||||
|
||||
def send_prompt(sid: str, inputs: StreamingPrompt):
|
||||
# workflow_api = inputs.workflow_api
|
||||
@@ -443,7 +489,7 @@ def send_prompt(sid: str, inputs: StreamingPrompt):
|
||||
workflow = copy.deepcopy(inputs.workflow)
|
||||
|
||||
# Random seed
|
||||
apply_random_seed_to_workflow(workflow_api)
|
||||
apply_random_seed_to_workflow(workflow_api, workflow)
|
||||
|
||||
logger.info("getting inputs", inputs.inputs)
|
||||
|
||||
@@ -529,7 +575,7 @@ async def comfy_deploy_run(request):
|
||||
workflow = data.get("workflow")
|
||||
|
||||
# Now it handles directly in here
|
||||
apply_random_seed_to_workflow(workflow_api)
|
||||
apply_random_seed_to_workflow(workflow_api, workflow)
|
||||
apply_inputs_to_workflow(workflow_api, inputs)
|
||||
|
||||
prompt = {
|
||||
@@ -596,7 +642,7 @@ async def stream_prompt(data, token):
|
||||
gpu_event_id = data.get("gpu_event_id", None)
|
||||
|
||||
# Now it handles directly in here
|
||||
apply_random_seed_to_workflow(workflow_api)
|
||||
apply_random_seed_to_workflow(workflow_api, workflow)
|
||||
apply_inputs_to_workflow(workflow_api, inputs)
|
||||
|
||||
prompt = {
|
||||
@@ -2010,13 +2056,14 @@ async def upload_in_background(
|
||||
("files", "content_type", "image/png"),
|
||||
("gifs", "format", "image/gif"),
|
||||
("model_file", "format", "application/octet-stream"),
|
||||
("result", "format", "application/octet-stream"),
|
||||
("text_file", "format", "text/plain"),
|
||||
]:
|
||||
items = data.get(file_type, [])
|
||||
|
||||
for item in items:
|
||||
# if is model_file, just add it to the data
|
||||
if file_type == "model_file":
|
||||
if file_type == "model_file" or file_type == "result":
|
||||
if isinstance(item, str):
|
||||
filename = os.path.basename(item)
|
||||
# Extract folder name from the path
|
||||
@@ -2096,6 +2143,7 @@ async def update_run_with_output(
|
||||
or "files" in data
|
||||
or "gifs" in data
|
||||
or "model_file" in data
|
||||
or "result" in data
|
||||
or "text_file" in data
|
||||
)
|
||||
if bypass_upload and have_upload_media:
|
||||
@@ -2719,30 +2767,32 @@ class UploadQueue:
|
||||
# If this was the last upload for this node, clean up node data
|
||||
if not self.node_uploads[prompt_id][node_id]:
|
||||
del self.node_uploads[prompt_id][node_id]
|
||||
if self.node_output_data[prompt_id][node_id]["data"]:
|
||||
# Send final node data to API before cleanup
|
||||
if prompt_metadata[prompt_id].status_endpoint:
|
||||
body = {
|
||||
"run_id": prompt_id,
|
||||
"output_data": self.node_output_data[
|
||||
prompt_id
|
||||
][node_id]["data"],
|
||||
"node_meta": {"node_id": node_id},
|
||||
}
|
||||
try:
|
||||
await async_request_with_retry(
|
||||
"POST",
|
||||
prompt_metadata[
|
||||
prompt_id
|
||||
].status_endpoint,
|
||||
token=prompt_metadata[prompt_id].token,
|
||||
json=body,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Failed to send final node data: {str(e)}"
|
||||
)
|
||||
del self.node_output_data[prompt_id][node_id]
|
||||
if prompt_id in self.node_output_data:
|
||||
if node_id in self.node_output_data[prompt_id]:
|
||||
if self.node_output_data[prompt_id][node_id]["data"]:
|
||||
# Send final node data to API before cleanup
|
||||
if prompt_metadata[prompt_id].status_endpoint:
|
||||
body = {
|
||||
"run_id": prompt_id,
|
||||
"output_data": self.node_output_data[
|
||||
prompt_id
|
||||
][node_id]["data"],
|
||||
"node_meta": {"node_id": node_id},
|
||||
}
|
||||
try:
|
||||
await async_request_with_retry(
|
||||
"POST",
|
||||
prompt_metadata[
|
||||
prompt_id
|
||||
].status_endpoint,
|
||||
token=prompt_metadata[prompt_id].token,
|
||||
json=body,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Failed to send final node data: {str(e)}"
|
||||
)
|
||||
del self.node_output_data[prompt_id][node_id]
|
||||
|
||||
# Send status update
|
||||
await self.update_queue_status(prompt_id)
|
||||
|
||||
+16
@@ -56,11 +56,27 @@ streaming_prompt_metadata: dict[str, StreamingPrompt] = {}
|
||||
class BinaryEventTypes:
|
||||
PREVIEW_IMAGE = 1
|
||||
UNENCODED_PREVIEW_IMAGE = 2
|
||||
EXR_IMAGE = 4
|
||||
|
||||
|
||||
max_output_id_length = 24
|
||||
|
||||
|
||||
async def send_exr(image_data, sid=None, output_id: str = None):
|
||||
max_length = max_output_id_length
|
||||
output_id = output_id[:max_length]
|
||||
padded_output_id = output_id.ljust(max_length, "\x00")
|
||||
encoded_output_id = padded_output_id.encode("ascii", "replace")
|
||||
|
||||
bytesIO = BytesIO()
|
||||
# 10 bytes for the output_id
|
||||
bytesIO.write(encoded_output_id)
|
||||
bytesIO.write(image_data)
|
||||
|
||||
preview_bytes = bytesIO.getvalue()
|
||||
await send_bytes(BinaryEventTypes.EXR_IMAGE, preview_bytes, sid=sid)
|
||||
|
||||
|
||||
async def send_image(image_data, sid=None, output_id: str = None):
|
||||
max_length = max_output_id_length
|
||||
output_id = output_id[:max_length]
|
||||
|
||||
+103
-175
@@ -5,7 +5,7 @@ LGraphNode = LiteGraph.LGraphNode;
|
||||
import { ComfyDialog, $el } from "../../scripts/ui.js";
|
||||
|
||||
import { generateDependencyGraph } from "https://esm.sh/comfyui-json@0.1.25";
|
||||
import { ComfyDeploy } from "https://esm.sh/comfydeploy@2.0.0-beta.69";
|
||||
// import { ComfyDeploy } from "https://esm.sh/comfydeploy@2.0.0-beta.69";
|
||||
|
||||
const styles = `
|
||||
.comfydeploy-menu-item {
|
||||
@@ -330,8 +330,7 @@ async function convertToInput(node, widget, config) {
|
||||
inputNode.configure({
|
||||
widgets_values: [inputId, widget.value, JSON.stringify(options)],
|
||||
});
|
||||
} else
|
||||
{
|
||||
} else {
|
||||
inputNode.configure({
|
||||
widgets_values: [inputId, widget.value],
|
||||
});
|
||||
@@ -345,7 +344,7 @@ async function convertToInput(node, widget, config) {
|
||||
console.log(options);
|
||||
inputNode.widgets.find((x) => x.name == "default_value").options.values = options;
|
||||
}
|
||||
|
||||
|
||||
app.graph.add(inputNode);
|
||||
inputNode.connect(0, node, index);
|
||||
|
||||
@@ -615,9 +614,9 @@ const ext = {
|
||||
// return r
|
||||
// };
|
||||
},
|
||||
|
||||
|
||||
// async nodeCreated(node) {
|
||||
|
||||
|
||||
// },
|
||||
|
||||
registerCustomNodes() {
|
||||
@@ -1396,85 +1395,6 @@ async function deployWorkflow() {
|
||||
}
|
||||
}
|
||||
|
||||
// Add this function to refresh the workflows list
|
||||
function refreshWorkflowsList(el) {
|
||||
const workflowsList = el.querySelector("#workflows-list");
|
||||
const workflowsLoading = el.querySelector("#workflows-loading");
|
||||
|
||||
workflowsLoading.style.display = "flex";
|
||||
workflowsList.style.display = "none";
|
||||
workflowsList.innerHTML = "";
|
||||
|
||||
client.workflows
|
||||
.getAll({
|
||||
page: "1",
|
||||
pageSize: "10",
|
||||
})
|
||||
.then((result) => {
|
||||
workflowsLoading.style.display = "none";
|
||||
workflowsList.style.display = "block";
|
||||
|
||||
if (result.length === 0) {
|
||||
workflowsList.innerHTML =
|
||||
"<li style='color: #bdbdbd;'>No workflows found</li>";
|
||||
return;
|
||||
}
|
||||
|
||||
result.forEach((workflow) => {
|
||||
const li = document.createElement("li");
|
||||
li.style.marginBottom = "15px";
|
||||
li.style.padding = "15px";
|
||||
li.style.backgroundColor = "#2a2a2a";
|
||||
li.style.borderRadius = "8px";
|
||||
li.style.boxShadow = "0 2px 4px rgba(0,0,0,0.1)";
|
||||
|
||||
const lastRun = workflow.runs[0];
|
||||
const lastRunStatus = lastRun ? lastRun.status : "No runs";
|
||||
const statusColor =
|
||||
lastRunStatus === "success"
|
||||
? "#4CAF50"
|
||||
: lastRunStatus === "error"
|
||||
? "#F44336"
|
||||
: "#FFC107";
|
||||
|
||||
const timeAgo = getTimeAgo(new Date(workflow.updatedAt));
|
||||
|
||||
li.innerHTML = `
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 10px;">
|
||||
<div style="flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">
|
||||
<strong style="font-size: 18px; color: #e0e0e0;">${workflow.name}</strong>
|
||||
</div>
|
||||
<span style="font-size: 12px; color: ${statusColor}; margin-left: 10px;">Last run: ${lastRunStatus}</span>
|
||||
</div>
|
||||
<div style="font-size: 14px; color: #bdbdbd; margin-bottom: 10px;">Last updated ${timeAgo}</div>
|
||||
<div style="display: flex; gap: 10px;">
|
||||
<button class="open-cloud-btn" style="padding: 5px 10px; background-color: #4CAF50; color: white; border: none; border-radius: 4px; cursor: pointer;">Open in Cloud</button>
|
||||
<button class="load-api-btn" style="padding: 5px 10px; background-color: #2196F3; color: white; border: none; border-radius: 4px; cursor: pointer;">Load Workflow</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
const openCloudBtn = li.querySelector(".open-cloud-btn");
|
||||
openCloudBtn.onclick = () =>
|
||||
window.open(
|
||||
`${getData().endpoint}/workflows/${workflow.id}?workspace=true`,
|
||||
"_blank",
|
||||
);
|
||||
|
||||
const loadApiBtn = li.querySelector(".load-api-btn");
|
||||
loadApiBtn.onclick = () => loadWorkflowApi(workflow.versions[0].id);
|
||||
|
||||
workflowsList.appendChild(li);
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error fetching workflows:", error);
|
||||
workflowsLoading.style.display = "none";
|
||||
workflowsList.style.display = "block";
|
||||
workflowsList.innerHTML =
|
||||
"<li style='color: #F44336;'>Error fetching workflows</li>";
|
||||
});
|
||||
}
|
||||
|
||||
function addButton() {
|
||||
const menu = document.querySelector(".comfy-menu");
|
||||
|
||||
@@ -1961,85 +1881,93 @@ export class ConfigDialog extends ComfyDialog {
|
||||
export const configDialog = new ConfigDialog();
|
||||
|
||||
const currentOrigin = window.location.origin;
|
||||
const client = new ComfyDeploy({
|
||||
bearerAuth: getData().apiKey,
|
||||
serverURL: `${currentOrigin}/comfydeploy/api/`,
|
||||
});
|
||||
// const client = new ComfyDeploy({
|
||||
// bearerAuth: getData().apiKey,
|
||||
// serverURL: `${currentOrigin}/comfydeploy/api/`,
|
||||
// });
|
||||
|
||||
app.extensionManager.registerSidebarTab({
|
||||
id: "search",
|
||||
icon: "pi pi-cloud-upload",
|
||||
title: "Deploy",
|
||||
tooltip: "Deploy and Configure",
|
||||
type: "custom",
|
||||
render: (el) => {
|
||||
el.innerHTML = `
|
||||
<div style="padding: 20px;">
|
||||
<h3>Comfy Deploy</h3>
|
||||
<div id="deploy-container" style="margin-bottom: 20px;"></div>
|
||||
<div id="workflows-container" style="display: none;">
|
||||
<h4>Your Workflows</h4>
|
||||
<div id="workflows-loading" style="display: flex; justify-content: center; align-items: center; height: 100px;">
|
||||
${loadingIcon}
|
||||
// Check if the current URL hostname starts with localhost or 127.0.0.1
|
||||
const isLocalhost =
|
||||
window.location.hostname === "localhost" ||
|
||||
window.location.hostname === "127.0.0.1";
|
||||
|
||||
// Only register the sidebar tab if we're on localhost
|
||||
if (isLocalhost) {
|
||||
app.extensionManager.registerSidebarTab({
|
||||
id: "search",
|
||||
icon: "pi pi-cloud-upload",
|
||||
title: "Deploy",
|
||||
tooltip: "Deploy and Configure",
|
||||
type: "custom",
|
||||
render: (el) => {
|
||||
el.innerHTML = `
|
||||
<div style="padding: 20px;">
|
||||
<h3>Comfy Deploy</h3>
|
||||
<div id="deploy-container" style="margin-bottom: 20px;"></div>
|
||||
<div id="workflows-container" style="display: none;">
|
||||
<h4>Your Workflows</h4>
|
||||
<div id="workflows-loading" style="display: flex; justify-content: center; align-items: center; height: 100px;">
|
||||
${loadingIcon}
|
||||
</div>
|
||||
<ul id="workflows-list" style="list-style-type: none; padding: 0; display: none;"></ul>
|
||||
</div>
|
||||
<ul id="workflows-list" style="list-style-type: none; padding: 0; display: none;"></ul>
|
||||
<div id="config-container"></div>
|
||||
</div>
|
||||
<div id="config-container"></div>
|
||||
</div>
|
||||
`;
|
||||
`;
|
||||
|
||||
// Add deploy button
|
||||
const deployContainer = el.querySelector("#deploy-container");
|
||||
const deployButton = document.createElement("button");
|
||||
deployButton.id = "sidebar-deploy-button";
|
||||
deployButton.style.display = "flex";
|
||||
deployButton.style.alignItems = "center";
|
||||
deployButton.style.justifyContent = "center";
|
||||
deployButton.style.width = "100%";
|
||||
deployButton.style.marginBottom = "10px";
|
||||
deployButton.style.padding = "10px";
|
||||
deployButton.style.fontSize = "16px";
|
||||
deployButton.style.fontWeight = "bold";
|
||||
deployButton.style.backgroundColor = "#4CAF50";
|
||||
deployButton.style.color = "white";
|
||||
deployButton.style.border = "none";
|
||||
deployButton.style.borderRadius = "5px";
|
||||
deployButton.style.cursor = "pointer";
|
||||
deployButton.innerHTML = `<i class="pi pi-cloud-upload" style="margin-right: 8px;"></i><div id='sidebar-button-title'>Deploy</div>`;
|
||||
deployButton.onclick = async () => {
|
||||
await deployWorkflow();
|
||||
// Refresh the workflows list after deployment
|
||||
refreshWorkflowsList(el);
|
||||
};
|
||||
deployContainer.appendChild(deployButton);
|
||||
// Add deploy button
|
||||
const deployContainer = el.querySelector("#deploy-container");
|
||||
const deployButton = document.createElement("button");
|
||||
deployButton.id = "sidebar-deploy-button";
|
||||
deployButton.style.display = "flex";
|
||||
deployButton.style.alignItems = "center";
|
||||
deployButton.style.justifyContent = "center";
|
||||
deployButton.style.width = "100%";
|
||||
deployButton.style.marginBottom = "10px";
|
||||
deployButton.style.padding = "10px";
|
||||
deployButton.style.fontSize = "16px";
|
||||
deployButton.style.fontWeight = "bold";
|
||||
deployButton.style.backgroundColor = "#4CAF50";
|
||||
deployButton.style.color = "white";
|
||||
deployButton.style.border = "none";
|
||||
deployButton.style.borderRadius = "5px";
|
||||
deployButton.style.cursor = "pointer";
|
||||
deployButton.innerHTML = `<i class="pi pi-cloud-upload" style="margin-right: 8px;"></i><div id='sidebar-button-title'>Deploy</div>`;
|
||||
deployButton.onclick = async () => {
|
||||
await deployWorkflow();
|
||||
// Refresh the workflows list after deployment
|
||||
// refreshWorkflowsList(el);
|
||||
};
|
||||
deployContainer.appendChild(deployButton);
|
||||
|
||||
// Add config button
|
||||
const configContainer = el.querySelector("#config-container");
|
||||
const configButton = document.createElement("button");
|
||||
configButton.style.display = "flex";
|
||||
configButton.style.alignItems = "center";
|
||||
configButton.style.justifyContent = "center";
|
||||
configButton.style.width = "100%";
|
||||
configButton.style.padding = "8px";
|
||||
configButton.style.fontSize = "14px";
|
||||
configButton.style.backgroundColor = "#f0f0f0";
|
||||
configButton.style.color = "#333";
|
||||
configButton.style.border = "1px solid #ccc";
|
||||
configButton.style.borderRadius = "5px";
|
||||
configButton.style.cursor = "pointer";
|
||||
configButton.innerHTML = `<i class="pi pi-cog" style="margin-right: 8px;"></i>Configure`;
|
||||
configButton.onclick = () => {
|
||||
configDialog.show();
|
||||
};
|
||||
deployContainer.appendChild(configButton);
|
||||
// Add config button
|
||||
const configContainer = el.querySelector("#config-container");
|
||||
const configButton = document.createElement("button");
|
||||
configButton.style.display = "flex";
|
||||
configButton.style.alignItems = "center";
|
||||
configButton.style.justifyContent = "center";
|
||||
configButton.style.width = "100%";
|
||||
configButton.style.padding = "8px";
|
||||
configButton.style.fontSize = "14px";
|
||||
configButton.style.backgroundColor = "#f0f0f0";
|
||||
configButton.style.color = "#333";
|
||||
configButton.style.border = "1px solid #ccc";
|
||||
configButton.style.borderRadius = "5px";
|
||||
configButton.style.cursor = "pointer";
|
||||
configButton.innerHTML = `<i class="pi pi-cog" style="margin-right: 8px;"></i>Configure`;
|
||||
configButton.onclick = () => {
|
||||
configDialog.show();
|
||||
};
|
||||
deployContainer.appendChild(configButton);
|
||||
|
||||
// Fetch and display workflows
|
||||
const workflowsList = el.querySelector("#workflows-list");
|
||||
const workflowsLoading = el.querySelector("#workflows-loading");
|
||||
// Fetch and display workflows
|
||||
const workflowsList = el.querySelector("#workflows-list");
|
||||
const workflowsLoading = el.querySelector("#workflows-loading");
|
||||
|
||||
refreshWorkflowsList(el);
|
||||
},
|
||||
});
|
||||
// refreshWorkflowsList(el);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function getTimeAgo(date) {
|
||||
const seconds = Math.floor((new Date() - date) / 1000);
|
||||
@@ -2056,24 +1984,24 @@ function getTimeAgo(date) {
|
||||
return Math.floor(seconds) + " seconds ago";
|
||||
}
|
||||
|
||||
async function loadWorkflowApi(versionId) {
|
||||
try {
|
||||
const response = await client.comfyui.getWorkflowVersionVersionId({
|
||||
versionId: versionId,
|
||||
});
|
||||
// Implement the logic to load the workflow API into the ComfyUI interface
|
||||
console.log("Workflow API loaded:", response);
|
||||
await window["app"].ui.settings.setSettingValueAsync(
|
||||
"Comfy.Validation.Workflows",
|
||||
true,
|
||||
);
|
||||
app.loadGraphData(response.workflow);
|
||||
// You might want to update the UI or trigger some action in ComfyUI here
|
||||
} catch (error) {
|
||||
console.error("Error loading workflow API:", error);
|
||||
// Show an error message to the user
|
||||
}
|
||||
}
|
||||
// async function loadWorkflowApi(versionId) {
|
||||
// try {
|
||||
// const response = await client.comfyui.getWorkflowVersionVersionId({
|
||||
// versionId: versionId,
|
||||
// });
|
||||
// // Implement the logic to load the workflow API into the ComfyUI interface
|
||||
// console.log("Workflow API loaded:", response);
|
||||
// await window["app"].ui.settings.setSettingValueAsync(
|
||||
// "Comfy.Validation.Workflows",
|
||||
// true,
|
||||
// );
|
||||
// app.loadGraphData(response.workflow);
|
||||
// // You might want to update the UI or trigger some action in ComfyUI here
|
||||
// } catch (error) {
|
||||
// console.error("Error loading workflow API:", error);
|
||||
// // Show an error message to the user
|
||||
// }
|
||||
// }
|
||||
|
||||
const orginal_fetch_api = api.fetchApi;
|
||||
api.fetchApi = async (route, options) => {
|
||||
|
||||
Reference in New Issue
Block a user