Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d84d71541c | ||
|
|
e8895028a2 | ||
|
|
b5c00450de | ||
|
|
59083391c7 | ||
|
|
8f2adacc20 |
@@ -2,8 +2,6 @@
|
||||
|
||||
Open source comfyui deployment platform, a `vercel` for generative workflow infra. (serverless hosted gpu with vertical intergation with comfyui)
|
||||
|
||||
Check out our latest lcoal demo -> https://github.com/comfy-deploy/comfyui-api-comfydeploy
|
||||
|
||||
> [!NOTE]
|
||||
> Im looking for creative hacker to join ComfyDeploy's core team! DM me on [twitter](https://x.com/BennyKokMusic)
|
||||
|
||||
|
||||
@@ -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)"
|
||||
}
|
||||
@@ -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)"
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
# In file: comfyui-deploy/comfy-nodes/output_exr.py
|
||||
|
||||
import os
|
||||
import numpy as np
|
||||
import folder_paths
|
||||
|
||||
# Try to set up OpenCV for EXR writing.
|
||||
try:
|
||||
os.environ["OPENCV_IO_ENABLE_OPENEXR"] = "1"
|
||||
import cv2
|
||||
OPENCV_AVAILABLE = True
|
||||
except ImportError:
|
||||
print("Warning: OpenCV not found for ComfyDeployOutputEXR. Please add opencv-python-headless to requirements.txt")
|
||||
OPENCV_AVAILABLE = False
|
||||
|
||||
# ALIGNED: Renamed class to match project conventions
|
||||
class ComfyDeployOutputEXR:
|
||||
def __init__(self):
|
||||
self.output_dir = folder_paths.get_output_directory()
|
||||
self.type = "output"
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(s):
|
||||
return {
|
||||
"required": {
|
||||
"images": ("IMAGE", ),
|
||||
"filename_prefix": ("STRING", {"default": "ComfyDeploy_EXR"})
|
||||
},
|
||||
# ADDED: Optional output_id for consistency with other ComfyDeploy nodes
|
||||
"optional": {
|
||||
"output_id": ("STRING", {"multiline": False, "default": "output_exr"}),
|
||||
},
|
||||
}
|
||||
|
||||
RETURN_TYPES = ()
|
||||
# ALIGNED: Changed function name to 'run'
|
||||
FUNCTION = "run"
|
||||
OUTPUT_NODE = True
|
||||
# ALIGNED: Matched the category name
|
||||
CATEGORY = "🔗ComfyDeploy"
|
||||
DESCRIPTION = "Saves the input images as EXR (HDR) files."
|
||||
|
||||
def run(self, images, filename_prefix="ComfyDeploy_EXR", output_id="output_exr"):
|
||||
if not OPENCV_AVAILABLE:
|
||||
raise ImportError("OpenCV is required to save EXR files. Please ensure opencv-python-headless is in requirements.txt.")
|
||||
|
||||
full_output_folder, filename, counter, subfolder, filename_prefix = (
|
||||
folder_paths.get_save_image_path(
|
||||
filename_prefix, self.output_dir, images[0].shape[1], images[0].shape[0]
|
||||
)
|
||||
)
|
||||
results = list()
|
||||
|
||||
for image in images:
|
||||
image_np = image.cpu().numpy()
|
||||
|
||||
if image_np.dtype != np.float32:
|
||||
image_np = image_np.astype(np.float32)
|
||||
|
||||
file = f"{filename}_{counter:05}.exr"
|
||||
file_path = os.path.join(full_output_folder, file)
|
||||
|
||||
image_np_bgr = cv2.cvtColor(image_np, cv2.COLOR_RGB2BGR)
|
||||
cv2.imwrite(file_path, image_np_bgr)
|
||||
|
||||
results.append({
|
||||
"filename": file,
|
||||
"subfolder": subfolder,
|
||||
"type": self.type,
|
||||
"output_id": output_id, # ADDED
|
||||
})
|
||||
counter += 1
|
||||
|
||||
return {"ui": {"images": results}}
|
||||
|
||||
# ALIGNED: Mappings are defined at the bottom of the node file in this project
|
||||
NODE_CLASS_MAPPINGS = {"ComfyDeployOutputEXR": ComfyDeployOutputEXR}
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {"ComfyDeployOutputEXR": "EXR Output (ComfyDeploy)"}
|
||||
+80
-836
File diff suppressed because it is too large
Load Diff
+16
-1
@@ -18,7 +18,6 @@ class Status(Enum):
|
||||
SUCCESS = "success"
|
||||
FAILED = "failed"
|
||||
UPLOADING = "uploading"
|
||||
CANCELLED = "cancelled"
|
||||
|
||||
|
||||
class StreamingPrompt(BaseModel):
|
||||
@@ -57,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]
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
[project]
|
||||
name = "comfyui-deploy"
|
||||
description = "Open source comfyui deployment platform, a vercel for generative workflow infra."
|
||||
version = "2.3.6"
|
||||
version = "2.1.0"
|
||||
license = { file = "LICENSE" }
|
||||
dependencies = ["aiofiles", "pydantic", "opencv-python", "imageio-ffmpeg", "tabulate", "brotli"]
|
||||
|
||||
|
||||
+438
-1303
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,68 +0,0 @@
|
||||
// Snapshot Utilities
|
||||
// Centralized snapshot fetching with ComfyUI version fallback
|
||||
|
||||
/**
|
||||
* Fetches the current snapshot with ComfyUI version fallback
|
||||
* If the snapshot response has null comfyui field, it will fetch the latest ComfyUI version
|
||||
* and update the snapshot with the comfyui_hash
|
||||
*
|
||||
* @param {Function} getDataFn - Function that returns { apiKey, apiUrl } for ComfyUI version API calls
|
||||
* @returns {Promise<Object>} - The snapshot data with comfyui field populated
|
||||
*/
|
||||
export async function fetchSnapshot(getDataFn = null) {
|
||||
try {
|
||||
// Fetch the current snapshot
|
||||
const response = await fetch("/snapshot/get_current");
|
||||
if (!response.ok) {
|
||||
throw new Error(`Snapshot fetch failed: ${response.status}`);
|
||||
}
|
||||
|
||||
const snapshot = await response.json();
|
||||
|
||||
// Check if comfyui field is null and we have getDataFn for fallback
|
||||
if (snapshot.comfyui === null && getDataFn) {
|
||||
console.log(
|
||||
"ComfyUI version is null in snapshot, fetching latest version..."
|
||||
);
|
||||
|
||||
try {
|
||||
const data = getDataFn();
|
||||
if (data && data.apiKey) {
|
||||
const comfyuiVersionResponse = await fetch(
|
||||
`/comfyui-deploy/comfyui-version?api_url=${encodeURIComponent(
|
||||
data.apiUrl || "https://api.comfydeploy.com"
|
||||
)}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${data.apiKey}`,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
if (comfyuiVersionResponse.ok) {
|
||||
const versionData = await comfyuiVersionResponse.json();
|
||||
if (versionData.comfyui_hash) {
|
||||
console.log(
|
||||
`Using ComfyUI hash from API: ${versionData.comfyui_hash}`
|
||||
);
|
||||
snapshot.comfyui = versionData.comfyui_hash;
|
||||
}
|
||||
} else {
|
||||
console.warn(
|
||||
"Failed to fetch ComfyUI version from API:",
|
||||
comfyuiVersionResponse.status
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn("Error fetching ComfyUI version fallback:", error);
|
||||
// Continue with original snapshot even if fallback fails
|
||||
}
|
||||
}
|
||||
|
||||
return snapshot;
|
||||
} catch (error) {
|
||||
console.error("Error fetching snapshot:", error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user