Compare commits

..
20 changed files with 156 additions and 1069 deletions
+1 -11
View File
@@ -8,16 +8,6 @@ class ComfyUIDeployExternalBoolean:
{"multiline": False, "default": "input_bool"},
),
"default_value": ("BOOLEAN", {"default": False})
},
"optional": {
"display_name": (
"STRING",
{"multiline": False, "default": ""},
),
"description": (
"STRING",
{"multiline": True, "default": ""},
),
}
}
@@ -26,7 +16,7 @@ class ComfyUIDeployExternalBoolean:
FUNCTION = "run"
def run(self, input_id, default_value=None, display_name=None, description=None):
def run(self, input_id, default_value=None):
print(f"Node '{input_id}' processing with switch set to {default_value}")
return [default_value]
+1 -9
View File
@@ -23,14 +23,6 @@ class ComfyUIDeployExternalCheckpoint:
},
"optional": {
"default_value": (folder_paths.get_filename_list("checkpoints"), ),
"display_name": (
"STRING",
{"multiline": False, "default": ""},
),
"description": (
"STRING",
{"multiline": True, "default": ""},
),
}
}
@@ -41,7 +33,7 @@ class ComfyUIDeployExternalCheckpoint:
CATEGORY = "deploy"
def run(self, input_id, default_value=None, display_name=None, description=None):
def run(self, input_id, default_value=None):
import requests
import os
import uuid
-108
View File
@@ -1,108 +0,0 @@
from PIL import Image, ImageOps
import numpy as np
import torch
import folder_paths
class AnyType(str):
def __ne__(self, __value: object) -> bool:
return False
WILDCARD = AnyType("*")
class ComfyUIDeployExternalFaceModel:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"input_id": (
"STRING",
{"multiline": False, "default": "input_reactor_face_model"},
),
},
"optional": {
"default_face_model_name": (
"STRING",
{"multiline": False, "default": ""},
),
"face_model_save_name": ( # if `default_face_model_name` is a link to download a file, we will attempt to save it with this name
"STRING",
{"multiline": False, "default": ""},
),
"display_name": (
"STRING",
{"multiline": False, "default": ""},
),
"description": (
"STRING",
{"multiline": True, "default": ""},
),
"face_model_url": (
"STRING",
{"multiline": False, "default": ""},
),
},
}
RETURN_TYPES = (WILDCARD,)
RETURN_NAMES = ("path",)
FUNCTION = "run"
CATEGORY = "deploy"
def run(
self,
input_id,
default_face_model_name=None,
face_model_save_name=None,
display_name=None,
description=None,
face_model_url=None,
):
import requests
import os
import uuid
if face_model_url and face_model_url.startswith("http"):
if face_model_save_name:
existing_face_models = folder_paths.get_filename_list("reactor/faces")
# Check if face_model_save_name exists in the list
if face_model_save_name in existing_face_models:
print(f"using face model: {face_model_save_name}")
return (face_model_save_name,)
else:
face_model_save_name = str(uuid.uuid4()) + ".safetensors"
print(face_model_save_name)
print(folder_paths.folder_names_and_paths["reactor/faces"][0][0])
destination_path = os.path.join(
folder_paths.folder_names_and_paths["reactor/faces"][0][0],
face_model_save_name,
)
print(destination_path)
print(
"Downloading external face model - "
+ face_model_url
+ " to "
+ destination_path
)
response = requests.get(
face_model_url,
headers={"User-Agent": "Mozilla/5.0"},
allow_redirects=True,
)
with open(destination_path, "wb") as out_file:
out_file.write(response.content)
return (face_model_save_name,)
else:
print(f"using face model: {default_face_model_name}")
return (default_face_model_name,)
NODE_CLASS_MAPPINGS = {"ComfyUIDeployExternalFaceModel": ComfyUIDeployExternalFaceModel}
NODE_DISPLAY_NAME_MAPPINGS = {
"ComfyUIDeployExternalFaceModel": "External Face Model (ComfyUI Deploy)"
}
+1 -9
View File
@@ -15,14 +15,6 @@ class ComfyUIDeployExternalImage:
},
"optional": {
"default_value": ("IMAGE",),
"display_name": (
"STRING",
{"multiline": False, "default": ""},
),
"description": (
"STRING",
{"multiline": True, "default": ""},
),
}
}
@@ -33,7 +25,7 @@ class ComfyUIDeployExternalImage:
CATEGORY = "image"
def run(self, input_id, default_value=None, display_name=None, description=None):
def run(self, input_id, default_value=None):
image = default_value
try:
if input_id.startswith('http'):
+1 -9
View File
@@ -15,14 +15,6 @@ class ComfyUIDeployExternalImageAlpha:
},
"optional": {
"default_value": ("IMAGE",),
"display_name": (
"STRING",
{"multiline": False, "default": ""},
),
"description": (
"STRING",
{"multiline": True, "default": ""},
),
}
}
@@ -33,7 +25,7 @@ class ComfyUIDeployExternalImageAlpha:
CATEGORY = "image"
def run(self, input_id, default_value=None, display_name=None, description=None):
def run(self, input_id, default_value=None):
image = default_value
try:
if input_id.startswith('http'):
+3 -31
View File
@@ -21,14 +21,6 @@ class ComfyUIDeployExternalImageBatch:
},
"optional": {
"default_value": ("IMAGE",),
"display_name": (
"STRING",
{"multiline": False, "default": ""},
),
"description": (
"STRING",
{"multiline": True, "default": ""},
),
}
}
@@ -39,34 +31,14 @@ class ComfyUIDeployExternalImageBatch:
CATEGORY = "image"
def process_image(self, image):
image = ImageOps.exif_transpose(image)
image = image.convert("RGB")
image = np.array(image).astype(np.float32) / 255.0
image_tensor = torch.from_numpy(image)[None,]
return image_tensor
def run(self, input_id, images=None, default_value=None, display_name=None, description=None):
import requests
import zipfile
import io
def run(self, input_id, images=None, default_value=None):
processed_images = []
try:
images_list = json.loads(images) # Assuming images is a JSON array string
print(images_list)
for img_input in images_list:
if img_input.startswith('http') and img_input.endswith('.zip'):
print("Fetching zip file from url: ", img_input)
response = requests.get(img_input)
zip_file = zipfile.ZipFile(io.BytesIO(response.content))
for file_name in zip_file.namelist():
if file_name.lower().endswith(('.png', '.jpg', '.jpeg')):
with zip_file.open(file_name) as file:
image = Image.open(file)
image = self.process_image(image)
processed_images.append(image)
elif img_input.startswith('http'):
if img_input.startswith('http'):
import requests
from io import BytesIO
print("Fetching image from url: ", img_input)
response = requests.get(img_input)
+9 -43
View File
@@ -4,15 +4,12 @@ import numpy as np
import torch
import folder_paths
class AnyType(str):
def __ne__(self, __value: object) -> bool:
return False
WILDCARD = AnyType("*")
class ComfyUIDeployExternalLora:
@classmethod
def INPUT_TYPES(s):
@@ -25,22 +22,6 @@ class ComfyUIDeployExternalLora:
},
"optional": {
"default_lora_name": (folder_paths.get_filename_list("loras"),),
"lora_save_name": ( # if `default_lora_name` is a link to download a file, we will attempt to save it with this name
"STRING",
{"multiline": False, "default": ""},
),
"display_name": (
"STRING",
{"multiline": False, "default": ""},
),
"description": (
"STRING",
{"multiline": True, "default": ""},
),
"lora_url": (
"STRING",
{"multiline": False, "default": ""},
),
},
}
@@ -51,43 +32,28 @@ class ComfyUIDeployExternalLora:
CATEGORY = "deploy"
def run(
self,
input_id,
default_lora_name=None,
lora_save_name=None,
display_name=None,
description=None,
lora_url=None,
):
def run(self, input_id, default_lora_name=None):
import requests
import os
import uuid
if lora_url and lora_url.startswith("http"):
if lora_save_name:
existing_loras = folder_paths.get_filename_list("loras")
# Check if lora_save_name exists in the list
if lora_save_name in existing_loras:
print(f"using lora: {lora_save_name}")
return (lora_save_name,)
else:
lora_save_name = str(uuid.uuid4()) + ".safetensors"
print(lora_save_name)
if default_lora_name.startswith("http"):
unique_filename = str(uuid.uuid4()) + ".safetensors"
print(unique_filename)
print(folder_paths.folder_names_and_paths["loras"][0][0])
destination_path = os.path.join(
folder_paths.folder_names_and_paths["loras"][0][0], lora_save_name
folder_paths.folder_names_and_paths["loras"][0][0], unique_filename
)
print(destination_path)
print("Downloading external lora - " + lora_url + " to " + destination_path)
print("Downloading external lora - " + input_id + " to " + destination_path)
response = requests.get(
lora_url,
input_id,
headers={"User-Agent": "Mozilla/5.0"},
allow_redirects=True,
)
with open(destination_path, "wb") as out_file:
out_file.write(response.content)
return (lora_save_name,)
return (unique_filename,)
else:
print(f"using lora: {default_lora_name}")
return (default_lora_name,)
+2 -10
View File
@@ -16,15 +16,7 @@ class ComfyUIDeployExternalNumber:
"optional": {
"default_value": (
"FLOAT",
{"multiline": True, "display": "number", "default": 0, "min": -2147483647, "max": 2147483647, "step": 0.01},
),
"display_name": (
"STRING",
{"multiline": False, "default": ""},
),
"description": (
"STRING",
{"multiline": True, "default": ""},
{"multiline": True, "display": "number", "default": 0, "step": 0.01},
),
}
}
@@ -36,7 +28,7 @@ class ComfyUIDeployExternalNumber:
CATEGORY = "number"
def run(self, input_id, default_value=None, display_name=None, description=None):
def run(self, input_id, default_value=None):
try:
float_value = float(input_id)
print("my number", float_value)
+2 -10
View File
@@ -16,15 +16,7 @@ class ComfyUIDeployExternalNumberInt:
"optional": {
"default_value": (
"INT",
{"multiline": True, "display": "number", "min": -2147483647, "max": 2147483647, "default": 0},
),
"display_name": (
"STRING",
{"multiline": False, "default": ""},
),
"description": (
"STRING",
{"multiline": True, "default": ""},
{"multiline": True, "display": "number", "default": 0},
),
}
}
@@ -36,7 +28,7 @@ class ComfyUIDeployExternalNumberInt:
CATEGORY = "number"
def run(self, input_id, default_value=None, display_name=None, description=None):
def run(self, input_id, default_value=None):
if not input_id or (isinstance(input_id, str) and not input_id.strip().isdigit()):
return [default_value]
return [int(input_id)]
+4 -12
View File
@@ -11,23 +11,15 @@ class ComfyUIDeployExternalNumberSlider:
"optional": {
"default_value": (
"FLOAT",
{"multiline": True, "display": "number", "min": -2147483647, "max": 2147483647, "default": 0.5, "step": 0.01},
{"multiline": True, "display": "number", "default": 0.5, "step": 0.01},
),
"min_value": (
"FLOAT",
{"multiline": True, "display": "number", "min": -2147483647, "max": 2147483647, "default": 0, "step": 0.01},
{"multiline": True, "display": "number", "default": 0, "step": 0.01},
),
"max_value": (
"FLOAT",
{"multiline": True, "display": "number", "min": -2147483647, "max": 2147483647, "default": 1, "step": 0.01},
),
"display_name": (
"STRING",
{"multiline": False, "default": ""},
),
"description": (
"STRING",
{"multiline": True, "default": ""},
{"multiline": True, "display": "number", "default": 1, "step": 0.01},
),
}
}
@@ -39,7 +31,7 @@ class ComfyUIDeployExternalNumberSlider:
CATEGORY = "number"
def run(self, input_id, default_value=None, min_value=0, max_value=1, display_name=None, description=None):
def run(self, input_id, default_value=None, min_value=0, max_value=1):
try:
float_value = float(input_id)
if min_value <= float_value <= max_value:
+1 -9
View File
@@ -18,14 +18,6 @@ class ComfyUIDeployExternalText:
"STRING",
{"multiline": True, "default": ""},
),
"display_name": (
"STRING",
{"multiline": False, "default": ""},
),
"description": (
"STRING",
{"multiline": True, "default": ""},
),
}
}
@@ -36,7 +28,7 @@ class ComfyUIDeployExternalText:
CATEGORY = "text"
def run(self, input_id, default_value=None, display_name=None, description=None):
def run(self, input_id, default_value=None):
return [default_value]
-46
View File
@@ -1,46 +0,0 @@
class AnyType(str):
def __ne__(self, __value: object) -> bool:
return False
WILDCARD = AnyType("*")
class ComfyUIDeployExternalTextAny:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"input_id": (
"STRING",
{"multiline": False, "default": "input_text"},
),
},
"optional": {
"default_value": (
"STRING",
{"multiline": True, "default": ""},
),
"display_name": (
"STRING",
{"multiline": False, "default": ""},
),
"description": (
"STRING",
{"multiline": True, "default": ""},
),
}
}
RETURN_TYPES = (WILDCARD,)
RETURN_NAMES = ("text",)
FUNCTION = "run"
CATEGORY = "text"
def run(self, input_id, default_value=None, display_name=None, description=None):
return [default_value]
NODE_CLASS_MAPPINGS = {"ComfyUIDeployExternalTextAny": ComfyUIDeployExternalTextAny}
NODE_DISPLAY_NAME_MAPPINGS = {"ComfyUIDeployExternalTextAny": "External Text Any (ComfyUI Deploy)"}
-52
View File
@@ -1,52 +0,0 @@
import folder_paths
from PIL import Image, ImageOps
import numpy as np
import torch
import json
class ComfyUIDeployExternalTextList:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"input_id": (
"STRING",
{"multiline": False, "default": 'input_text_list'},
),
"text": (
"STRING",
{"multiline": True, "default": "[]"},
),
},
"optional": {
"display_name": (
"STRING",
{"multiline": False, "default": ""},
),
"description": (
"STRING",
{"multiline": True, "default": ""},
),
}
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("text",)
OUTPUT_IS_LIST = (True,)
FUNCTION = "run"
CATEGORY = "text"
def run(self, input_id, text=None, display_name=None, description=None):
text_list = []
try:
text_list = json.loads(text) # Assuming text is a JSON array string
except Exception as e:
print(f"Error processing images: {e}")
pass
return ([text_list],)
NODE_CLASS_MAPPINGS = {"ComfyUIDeployExternalTextList": ComfyUIDeployExternalTextList}
NODE_DISPLAY_NAME_MAPPINGS = {"ComfyUIDeployExternalTextList": "External Text List (ComfyUI Deploy)"}
+5 -14
View File
@@ -764,15 +764,7 @@ class ComfyUIDeployExternalVideo:
"optional": {
"meta_batch": ("VHS_BatchManager",),
"vae": ("VAE",),
"default_video": (sorted(files),),
"display_name": (
"STRING",
{"multiline": False, "default": ""},
),
"description": (
"STRING",
{"multiline": True, "default": ""},
),
"default_value": (sorted(files),),
},
"hidden": {
"unique_id": "UNIQUE_ID"
@@ -804,6 +796,8 @@ class ComfyUIDeployExternalVideo:
meta_batch = kwargs.get("meta_batch")
unique_id = kwargs.get("unique_id")
video = kwargs.get("default_value")
video_path = folder_paths.get_annotated_filepath(video.strip('"'))
input_dir = folder_paths.get_input_directory()
if input_id.startswith("http"):
@@ -833,11 +827,8 @@ class ComfyUIDeployExternalVideo:
leave=True,
):
out_file.write(chunk)
else:
video = kwargs.get("default_video", None)
if video is None:
raise "No default video given and no external video provided"
video_path = folder_paths.get_annotated_filepath(video.strip('"'))
print("video path: ", video_path)
return load_video_cv(
video=video_path,
+64 -336
View File
@@ -1,5 +1,4 @@
from io import BytesIO
from pprint import pprint
from aiohttp import web
import os
import requests
@@ -18,13 +17,12 @@ from urllib.parse import quote
import threading
import hashlib
import aiohttp
from aiohttp import ClientSession, web
import aiofiles
from typing import Dict, List, Union, Any, Optional
from PIL import Image
import copy
import struct
from aiohttp import web, ClientSession, ClientError, ClientTimeout, ClientResponseError
from aiohttp import ClientError
import atexit
# Global session
@@ -52,70 +50,28 @@ def exit_handler():
atexit.register(exit_handler)
max_retries = int(os.environ.get('MAX_RETRIES', '5'))
max_retries = int(os.environ.get('MAX_RETRIES', '3'))
retry_delay_multiplier = float(os.environ.get('RETRY_DELAY_MULTIPLIER', '2'))
print(f"max_retries: {max_retries}, retry_delay_multiplier: {retry_delay_multiplier}")
import time
async def async_request_with_retry(method, url, disable_timeout=False, token=None, **kwargs):
async def async_request_with_retry(method, url, **kwargs):
global client_session
await ensure_client_session()
retry_delay = 1 # Start with 1 second delay
initial_timeout = 5 # 5 seconds timeout for the initial connection
start_time = time.time()
for attempt in range(max_retries):
try:
if not disable_timeout:
timeout = ClientTimeout(total=None, connect=initial_timeout)
kwargs['timeout'] = timeout
if token is not None:
if 'headers' not in kwargs:
kwargs['headers'] = {}
kwargs['headers']['Authorization'] = f"Bearer {token}"
request_start = time.time()
async with client_session.request(method, url, **kwargs) as response:
request_end = time.time()
# logger.info(f"Request attempt {attempt + 1} took {request_end - request_start:.2f} seconds")
if response.status != 200:
error_body = await response.text()
# logger.error(f"Request failed with status {response.status} and body {error_body}")
# raise Exception(f"Request failed with status {response.status}")
response.raise_for_status()
if method.upper() == 'GET':
await response.read()
total_time = time.time() - start_time
# logger.info(f"Request succeeded after {total_time:.2f} seconds (attempt {attempt + 1}/{max_retries})")
return response
except asyncio.TimeoutError:
logger.warning(f"Request timed out after {initial_timeout} seconds (attempt {attempt + 1}/{max_retries})")
except ClientError as e:
end_time = time.time()
logger.error(f"Request failed (attempt {attempt + 1}/{max_retries}): {e}")
logger.error(f"Time taken for failed attempt: {end_time - request_start:.2f} seconds")
logger.error(f"Total time elapsed: {end_time - start_time:.2f} seconds")
# Log the response body for ClientError as well
if hasattr(e, 'response') and e.response is not None:
error_body = await e.response.text()
logger.error(f"Error response body: {error_body}")
if attempt == max_retries - 1:
logger.error(f"Request failed after {max_retries} attempts: {e}")
raise
await asyncio.sleep(retry_delay)
retry_delay *= retry_delay_multiplier
total_time = time.time() - start_time
raise Exception(f"Request failed after {max_retries} attempts and {total_time:.2f} seconds")
# raise
logger.warning(f"Request failed (attempt {attempt + 1}/{max_retries}): {e}")
await asyncio.sleep(retry_delay)
retry_delay *= retry_delay_multiplier # Exponential backoff
from logging import basicConfig, getLogger
@@ -152,8 +108,7 @@ def log_span(name):
if use_logfire:
with logger.span(name):
yield
else:
yield
# else:
# logger.info(f"Start: {name}")
# yield
# logger.info(f"End: {name}")
@@ -261,30 +216,13 @@ def apply_random_seed_to_workflow(workflow_api):
workflow_api (dict): The workflow API dictionary to modify.
"""
for key in workflow_api:
if 'inputs' in workflow_api[key]:
if 'seed' in workflow_api[key]['inputs']:
if isinstance(workflow_api[key]['inputs']['seed'], list):
continue
if workflow_api[key]['class_type'] == "PromptExpansion":
workflow_api[key]['inputs']['seed'] = randomSeed(8)
logger.info(f"Applied random seed {workflow_api[key]['inputs']['seed']} to PromptExpansion")
continue
workflow_api[key]['inputs']['seed'] = randomSeed()
logger.info(f"Applied random seed {workflow_api[key]['inputs']['seed']} to {workflow_api[key]['class_type']}")
if 'noise_seed' in workflow_api[key]['inputs']:
if workflow_api[key]['class_type'] == "RandomNoise":
workflow_api[key]['inputs']['noise_seed'] = randomSeed()
logger.info(f"Applied random noise_seed {workflow_api[key]['inputs']['noise_seed']} to RandomNoise")
continue
if workflow_api[key]['class_type'] == "KSamplerAdvanced":
workflow_api[key]['inputs']['noise_seed'] = randomSeed()
logger.info(f"Applied random noise_seed {workflow_api[key]['inputs']['noise_seed']} to KSamplerAdvanced")
continue
if workflow_api[key]['class_type'] == "SamplerCustom":
workflow_api[key]['inputs']['noise_seed'] = randomSeed()
logger.info(f"Applied random noise_seed {workflow_api[key]['inputs']['noise_seed']} to SamplerCustom")
continue
if 'inputs' in workflow_api[key] and 'seed' in workflow_api[key]['inputs']:
if isinstance(workflow_api[key]['inputs']['seed'], list):
continue
if workflow_api[key]['class_type'] == "PromptExpansion":
workflow_api[key]['inputs']['seed'] = randomSeed(8);
continue
workflow_api[key]['inputs']['seed'] = randomSeed();
def apply_inputs_to_workflow(workflow_api: Any, inputs: Any, sid: str = None):
# Loop through each of the inputs and replace them
@@ -309,7 +247,7 @@ def apply_inputs_to_workflow(workflow_api: Any, inputs: Any, sid: str = None):
value['inputs']["input_id"] = new_value
# Fix for external text default value
if (value["class_type"] == "ComfyUIDeployExternalText" or value["class_type"] == "ComfyUIDeployExternalTextAny"):
if (value["class_type"] == "ComfyUIDeployExternalText"):
value['inputs']["default_value"] = new_value
if (value["class_type"] == "ComfyUIDeployExternalCheckpoint"):
@@ -319,7 +257,7 @@ def apply_inputs_to_workflow(workflow_api: Any, inputs: Any, sid: str = None):
value['inputs']["images"] = new_value
if value["class_type"] == "ComfyUIDeployExternalLora":
value["inputs"]["lora_url"] = new_value
value["inputs"]["default_lora_name"] = new_value
if value["class_type"] == "ComfyUIDeployExternalSlider":
value["inputs"]["default_value"] = new_value
@@ -327,13 +265,9 @@ def apply_inputs_to_workflow(workflow_api: Any, inputs: Any, sid: str = None):
if value["class_type"] == "ComfyUIDeployExternalBoolean":
value["inputs"]["default_value"] = new_value
if value["class_type"] == "ComfyUIDeployExternalFaceModel":
value["inputs"]["face_model_url"] = new_value
def send_prompt(sid: str, inputs: StreamingPrompt):
# workflow_api = inputs.workflow_api
workflow_api = copy.deepcopy(inputs.workflow_api)
workflow = copy.deepcopy(inputs.workflow)
# Random seed
apply_random_seed_to_workflow(workflow_api)
@@ -349,8 +283,7 @@ def send_prompt(sid: str, inputs: StreamingPrompt):
prompt = {
"prompt": workflow_api,
"client_id": sid, #"comfy_deploy_instance", #api.client_id
"prompt_id": prompt_id,
"extra_data": {"extra_pnginfo": {"workflow": workflow}},
"prompt_id": prompt_id
}
try:
@@ -371,25 +304,13 @@ def send_prompt(sid: str, inputs: StreamingPrompt):
@server.PromptServer.instance.routes.post("/comfyui-deploy/run")
async def comfy_deploy_run(request):
# Extract the bearer token from the Authorization header
data = await request.json()
if "cd_token" in data:
token = data["cd_token"]
else:
auth_header = request.headers.get('Authorization')
token = None
if auth_header:
parts = auth_header.split()
if len(parts) == 2 and parts[0].lower() == 'bearer':
token = parts[1]
# In older version, we use workflow_api, but this has inputs already swapped in nextjs frontend, which is tricky
workflow_api = data.get("workflow_api_raw")
# The prompt id generated from comfy deploy, can be None
prompt_id = data.get("prompt_id")
inputs = data.get("inputs")
workflow = data.get("workflow")
# Now it handles directly in here
apply_random_seed_to_workflow(workflow_api)
@@ -398,15 +319,13 @@ async def comfy_deploy_run(request):
prompt = {
"prompt": workflow_api,
"client_id": "comfy_deploy_instance", #api.client_id
"prompt_id": prompt_id,
"extra_data": {"extra_pnginfo": {"workflow": workflow}}
"prompt_id": prompt_id
}
prompt_metadata[prompt_id] = SimplePrompt(
status_endpoint=data.get('status_endpoint'),
file_upload_endpoint=data.get('file_upload_endpoint'),
workflow_api=workflow_api,
token=token
workflow_api=workflow_api
)
try:
@@ -429,7 +348,7 @@ async def comfy_deploy_run(request):
status = 200
if "node_errors" in res and res["node_errors"] is not None and len(res["node_errors"]) > 0:
if "node_errors" in res and res["node_errors"]:
# Even tho there are node_errors it can still be run
status = 400
await update_run_with_output(prompt_id, {
@@ -444,13 +363,12 @@ async def comfy_deploy_run(request):
return web.json_response(res, status=status)
async def stream_prompt(data, token):
async def stream_prompt(data):
# In older version, we use workflow_api, but this has inputs already swapped in nextjs frontend, which is tricky
workflow_api = data.get("workflow_api_raw")
# The prompt id generated from comfy deploy, can be None
prompt_id = data.get("prompt_id")
inputs = data.get("inputs")
workflow = data.get("workflow")
# Now it handles directly in here
apply_random_seed_to_workflow(workflow_api)
@@ -459,18 +377,16 @@ async def stream_prompt(data, token):
prompt = {
"prompt": workflow_api,
"client_id": "comfy_deploy_instance", #api.client_id
"prompt_id": prompt_id,
"extra_data": {"extra_pnginfo": {"workflow": workflow}},
"prompt_id": prompt_id
}
prompt_metadata[prompt_id] = SimplePrompt(
status_endpoint=data.get('status_endpoint'),
file_upload_endpoint=data.get('file_upload_endpoint'),
workflow_api=workflow_api,
token=token
workflow_api=workflow_api
)
# log('info', "Begin prompt", prompt=prompt)
log('info', "Begin prompt", prompt=prompt)
try:
res = post_prompt(prompt)
@@ -493,7 +409,7 @@ async def stream_prompt(data, token):
status = 200
if "node_errors" in res and res["node_errors"] is not None and len(res["node_errors"]) > 0:
if "node_errors" in res and res["node_errors"]:
# Even tho there are node_errors it can still be run
status = 400
await update_run_with_output(prompt_id, {
@@ -516,14 +432,6 @@ comfy_message_queues: Dict[str, asyncio.Queue] = {}
async def stream_response(request):
response = web.StreamResponse(status=200, reason='OK', headers={'Content-Type': 'text/event-stream'})
await response.prepare(request)
# Extract the bearer token from the Authorization header
auth_header = request.headers.get('Authorization')
token = None
if auth_header:
parts = auth_header.split()
if len(parts) == 2 and parts[0].lower() == 'bearer':
token = parts[1]
pending = True
data = await request.json()
@@ -535,7 +443,7 @@ async def stream_response(request):
log('info', 'Streaming prompt')
try:
result = await stream_prompt(data=data, token=token)
result = await stream_prompt(data=data)
await response.write(f"event: event_update\ndata: {json.dumps(result)}\n\n".encode('utf-8'))
# await response.write(.encode('utf-8'))
await response.drain() # Ensure the buffer is flushed
@@ -545,7 +453,7 @@ async def stream_response(request):
if not comfy_message_queues[prompt_id].empty():
data = await comfy_message_queues[prompt_id].get()
# log('info', data["event"], data=json.dumps(data))
log('info', data["event"], data=json.dumps(data))
# logger.info("listener", data)
await response.write(f"event: event_update\ndata: {json.dumps(data)}\n\n".encode('utf-8'))
await response.drain() # Ensure the buffer is flushed
@@ -664,10 +572,9 @@ async def upload_file_endpoint(request):
with open(file_path, 'rb') as f:
headers = {
"Content-Type": file_type,
# "Content-Length": str(file_size)
# "x-amz-acl": "public-read",
"Content-Length": str(file_size)
}
if content.get('include_acl') is True:
headers["x-amz-acl"] = "public-read"
upload_response = await async_request_with_retry('PUT', upload_url, data=f, headers=headers)
if upload_response.status == 200:
return web.json_response({
@@ -794,7 +701,6 @@ async def websocket_handler(request):
inputs={},
status_endpoint=status_endpoint,
file_upload_endpoint=request.rel_url.query.get('file_upload_endpoint', None),
workflow=workflow["workflow"],
)
await update_realtime_run_status(realtime_id, status_endpoint, Status.RUNNING)
@@ -914,51 +820,7 @@ async def send(event, data, sid=None):
except Exception as e:
logger.info(f"Exception: {e}")
traceback.print_exc()
@server.PromptServer.instance.routes.get('/comfydeploy/{tail:.*}')
@server.PromptServer.instance.routes.post('/comfydeploy/{tail:.*}')
async def proxy_to_comfydeploy(request):
# Get the base URL
base_url = f'https://www.comfydeploy.com/{request.match_info["tail"]}'
# Get all query parameters
query_params = request.query_string
# Construct the full target URL with query parameters
target_url = f"{base_url}?{query_params}" if query_params else base_url
# print(f"Proxying request to: {target_url}")
try:
# Create a new ClientSession for each request
async with ClientSession() as client_session:
# Forward the request
client_req = await client_session.request(
method=request.method,
url=target_url,
headers={k: v for k, v in request.headers.items() if k.lower() not in ('host', 'content-length')},
data=await request.read(),
allow_redirects=False,
)
# Read the entire response content
content = await client_req.read()
# Try to decode the content as JSON
try:
json_data = json.loads(content)
# If successful, return a JSON response
return web.json_response(json_data, status=client_req.status)
except json.JSONDecodeError:
# If it's not valid JSON, return the content as-is
return web.Response(body=content, status=client_req.status, headers=client_req.headers)
except ClientError as e:
print(f"Client error occurred while proxying request: {str(e)}")
return web.Response(status=502, text=f"Bad Gateway: {str(e)}")
except Exception as e:
print(f"Error occurred while proxying request: {str(e)}")
return web.Response(status=500, text=f"Internal Server Error: {str(e)}")
prompt_server = server.PromptServer.instance
@@ -984,7 +846,6 @@ async def send_json_override(self, event, data, sid=None):
"data": data
})
asyncio.create_task(update_run_ws_event(prompt_id, event, data))
# event_emitter.emit("send_json", {
# "event": event,
# "data": data
@@ -1052,22 +913,17 @@ async def send_json_override(self, event, data, sid=None):
# await update_run_with_output(prompt_id, data)
if event == 'executed' and 'node' in data and 'output' in data:
node_meta = None
if prompt_id in prompt_metadata:
node = data.get('node')
class_type = prompt_metadata[prompt_id].workflow_api[node]['class_type']
logger.info(f"Executed {class_type} {data}")
node_meta = {
"node_id": node,
"node_class": class_type,
}
if class_type == "PreviewImage":
logger.info("Skipping preview image")
return
else:
logger.info(f"Executed {data}")
await update_run_with_output(prompt_id, data.get('output'), node_id=data.get('node'), node_meta=node_meta)
await update_run_with_output(prompt_id, data.get('output'), node_id=data.get('node'))
# await update_run_with_output(prompt_id, data.get('output'), node_id=data.get('node'))
# update_run_with_output(prompt_id, data.get('output'))
@@ -1082,7 +938,6 @@ async def update_run_live_status(prompt_id, live_status, calculated_progress: fl
return
status_endpoint = prompt_metadata[prompt_id].status_endpoint
token = prompt_metadata[prompt_id].token
if (status_endpoint is None):
return
@@ -1106,27 +961,7 @@ async def update_run_live_status(prompt_id, live_status, calculated_progress: fl
})
# requests.post(status_endpoint, json=body)
await async_request_with_retry('POST', status_endpoint, token=token, json=body)
async def update_run_ws_event(prompt_id: str, event: str, data: dict):
if prompt_id not in prompt_metadata:
return
# print("update_run_ws_event", prompt_id, event, data)
status_endpoint = prompt_metadata[prompt_id].status_endpoint
if status_endpoint is None:
return
token = prompt_metadata[prompt_id].token
body = {
"run_id": prompt_id,
"ws_event": {
"event": event,
"data": data,
},
}
await async_request_with_retry('POST', status_endpoint, token=token, json=body)
await async_request_with_retry('POST', status_endpoint, json=body)
async def update_run(prompt_id: str, status: Status):
@@ -1157,8 +992,7 @@ async def update_run(prompt_id: str, status: Status):
try:
# requests.post(status_endpoint, json=body)
if (status_endpoint is not None):
token = prompt_metadata[prompt_id].token
await async_request_with_retry('POST', status_endpoint, token=token, json=body)
await async_request_with_retry('POST', status_endpoint, json=body)
if (status_endpoint is not None) and cd_enable_run_log and (status == Status.SUCCESS or status == Status.FAILED):
try:
@@ -1188,7 +1022,7 @@ async def update_run(prompt_id: str, status: Status):
]
}
await async_request_with_retry('POST', status_endpoint, token=token, json=body)
await async_request_with_retry('POST', status_endpoint, json=body)
# requests.post(status_endpoint, json=body)
except Exception as log_error:
logger.info(f"Error reading log file: {log_error}")
@@ -1209,48 +1043,7 @@ async def update_run(prompt_id: str, status: Status):
})
async def file_sender(file_object, chunk_size):
while True:
chunk = await file_object.read(chunk_size)
if not chunk:
break
yield chunk
chunk_size = 1024 * 1024 # 1MB chunks, adjust as needed
async def upload_with_retry(session, url, headers, data, max_retries=3, initial_delay=1):
start_time = time.time() # Start timing here
for attempt in range(max_retries):
try:
async with session.put(url, headers=headers, data=data) as response:
upload_duration = time.time() - start_time
logger.info(f"Upload attempt {attempt + 1} completed in {upload_duration:.2f} seconds")
logger.info(f"Upload response status: {response.status}")
response.raise_for_status() # This will raise an exception for 4xx and 5xx status codes
response_text = await response.text()
logger.info(f"Response body: {response_text[:1000]}...")
logger.info("Upload successful")
return response # Successful upload, exit the retry loop
except (ClientError, ClientResponseError) as e:
logger.error(f"Upload attempt {attempt + 1} failed: {str(e)}")
if attempt < max_retries - 1: # If it's not the last attempt
delay = initial_delay * (2 ** attempt) # Exponential backoff
logger.info(f"Retrying in {delay} seconds...")
await asyncio.sleep(delay)
else:
logger.error("Max retries reached. Upload failed.")
raise # Re-raise the last exception if all retries are exhausted
except Exception as e:
logger.error(f"Unexpected error during upload: {str(e)}")
logger.error(traceback.format_exc())
raise # Re-raise unexpected exceptions immediately
async def upload_file(prompt_id, filename, subfolder=None, content_type="image/png", type="output", item=None):
async def upload_file(prompt_id, filename, subfolder=None, content_type="image/png", type="output"):
"""
Uploads file to S3 bucket using S3 client object
:return: None
@@ -1280,60 +1073,33 @@ async def upload_file(prompt_id, filename, subfolder=None, content_type="image/p
logger.info(f"Uploading file {file}")
file_upload_endpoint = prompt_metadata[prompt_id].file_upload_endpoint
token = prompt_metadata[prompt_id].token
filename = quote(filename)
prompt_id = quote(prompt_id)
content_type = quote(content_type)
target_url = f"{file_upload_endpoint}?file_name={filename}&run_id={prompt_id}&type={content_type}&version=v2"
target_url = f"{file_upload_endpoint}?file_name={filename}&run_id={prompt_id}&type={content_type}"
start_time = time.time() # Start timing here
logger.info(f"Target URL: {target_url}")
result = await async_request_with_retry("GET", target_url, disable_timeout=True, token=token)
result = requests.get(target_url)
end_time = time.time() # End timing after the request is complete
logger.info("Time taken for getting file upload endpoint: {:.2f} seconds".format(end_time - start_time))
ok = await result.json()
logger.info(f"Result: {ok}")
ok = result.json()
async with aiofiles.open(file, 'rb') as f:
data = await f.read()
size = str(len(data))
# logger.info(f"Image size: {size}")
start_time = time.time() # Start timing here
start_time = time.time() # Start timing here
with open(file, 'rb') as f:
data = f.read()
headers = {
# "x-amz-acl": "public-read",
"Content-Type": content_type,
"Content-Length": size,
"Content-Length": str(len(data)),
}
logger.info(headers)
if ok.get('include_acl') is True:
headers["x-amz-acl"] = "public-read"
# response = requests.put(ok.get("url"), headers=headers, data=data)
# response = await async_request_with_retry('PUT', ok.get("url"), headers=headers, data=data)
# logger.info(f"Upload file response status: {response.status}, status text: {response.reason}")
async with aiohttp.ClientSession() as session:
try:
response = await upload_with_retry(session, ok.get("url"), headers, data)
# Process successful response...
except Exception as e:
# Handle final failure...
logger.error(f"Upload ultimately failed: {str(e)}")
response = await async_request_with_retry('PUT', ok.get("url"), headers=headers, data=data)
logger.info(f"Upload file response status: {response.status}, status text: {response.reason}")
end_time = time.time() # End timing after the request is complete
logger.info("Upload time: {:.2f} seconds".format(end_time - start_time))
if item is not None:
file_download_url = ok.get("download_url")
if file_download_url is not None:
item["url"] = file_download_url
item["upload_duration"] = end_time - start_time
if ok.get("is_public") is not None:
item["is_public"] = ok.get("is_public")
def have_pending_upload(prompt_id):
if prompt_id in prompt_metadata and len(prompt_metadata[prompt_id].uploading_nodes) > 0:
@@ -1426,10 +1192,8 @@ async def update_file_status(prompt_id: str, data, uploading, have_error=False,
async def handle_upload(prompt_id: str, data, key: str, content_type_key: str, default_content_type: str):
items = data.get(key, [])
upload_tasks = []
for item in items:
# Skipping temp files
# # Skipping temp files
if item.get("type") == "temp":
continue
@@ -1442,60 +1206,29 @@ async def handle_upload(prompt_id: str, data, key: str, content_type_key: str, d
elif file_extension == '.webp':
file_type = 'image/webp'
upload_tasks.append(upload_file(
await upload_file(
prompt_id,
item.get("filename"),
subfolder=item.get("subfolder"),
type=item.get("type"),
content_type=file_type,
item=item
))
# await upload_file(
# prompt_id,
# item.get("filename"),
# subfolder=item.get("subfolder"),
# type=item.get("type"),
# content_type=file_type,
# item=item
# )
# Execute all upload tasks concurrently
await asyncio.gather(*upload_tasks)
content_type=file_type
)
# Upload files in the background
async def upload_in_background(prompt_id: str, data, node_id=None, have_upload=True, node_meta=None):
async def upload_in_background(prompt_id: str, data, node_id=None, have_upload=True):
try:
# await handle_upload(prompt_id, data, 'images', "content_type", "image/png")
# await handle_upload(prompt_id, data, 'files', "content_type", "image/png")
# await handle_upload(prompt_id, data, 'gifs', "format", "image/gif")
# await handle_upload(prompt_id, data, 'mesh', "format", "application/octet-stream")
upload_tasks = [
handle_upload(prompt_id, data, "images", "content_type", "image/png"),
handle_upload(prompt_id, data, "files", "content_type", "image/png"),
handle_upload(prompt_id, data, "gifs", "format", "image/gif"),
handle_upload(
prompt_id, data, "mesh", "format", "application/octet-stream"
),
]
await handle_upload(prompt_id, data, 'images', "content_type", "image/png")
await handle_upload(prompt_id, data, 'files', "content_type", "image/png")
# This will also be mp4
await handle_upload(prompt_id, data, 'gifs', "format", "image/gif")
await handle_upload(prompt_id, data, 'mesh', "format", "application/octet-stream")
await asyncio.gather(*upload_tasks)
status_endpoint = prompt_metadata[prompt_id].status_endpoint
token = prompt_metadata[prompt_id].token
if have_upload:
if status_endpoint is not None:
body = {
"run_id": prompt_id,
"output_data": data,
"node_meta": node_meta,
}
# pprint(body)
await async_request_with_retry('POST', status_endpoint, token=token, json=body)
await update_file_status(prompt_id, data, False, node_id=node_id)
except Exception as e:
await handle_error(prompt_id, data, e)
async def update_run_with_output(prompt_id, data, node_id=None, node_meta=None):
async def update_run_with_output(prompt_id, data, node_id=None):
if prompt_id not in prompt_metadata:
return
@@ -1506,13 +1239,9 @@ async def update_run_with_output(prompt_id, data, node_id=None, node_meta=None):
body = {
"run_id": prompt_id,
"output_data": data,
"node_meta": node_meta,
"output_data": data
}
pprint(body)
have_upload_media = False
if data is not None:
have_upload_media = 'images' in data or 'files' in data or 'gifs' in data or 'mesh' in data
have_upload_media = 'images' in data or 'files' in data or 'gifs' in data or 'mesh' in data
if bypass_upload and have_upload_media:
print("CD_BYPASS_UPLOAD is enabled, skipping the upload of the output:", node_id)
return
@@ -1524,16 +1253,15 @@ async def update_run_with_output(prompt_id, data, node_id=None, node_meta=None):
if have_upload_media:
await update_file_status(prompt_id, data, True, node_id=node_id)
# asyncio.create_task(upload_in_background(prompt_id, data, node_id=node_id, have_upload=have_upload_media, node_meta=node_meta))
await upload_in_background(prompt_id, data, node_id=node_id, have_upload=have_upload_media, node_meta=node_meta)
asyncio.create_task(upload_in_background(prompt_id, data, node_id=node_id, have_upload=have_upload_media))
# await upload_in_background(prompt_id, data, node_id=node_id, have_upload=have_upload)
except Exception as e:
await handle_error(prompt_id, data, e)
# requests.post(status_endpoint, json=body)
elif status_endpoint is not None:
token = prompt_metadata[prompt_id].token
await async_request_with_retry('POST', status_endpoint, token=token, json=body)
if status_endpoint is not None:
await async_request_with_retry('POST', status_endpoint, json=body)
await send('outputs_uploaded', {
"prompt_id": prompt_id
-3
View File
@@ -24,14 +24,11 @@ class StreamingPrompt(BaseModel):
running_prompt_ids: set[str] = set()
status_endpoint: Optional[str]
file_upload_endpoint: Optional[str]
workflow: Any
class SimplePrompt(BaseModel):
status_endpoint: Optional[str]
file_upload_endpoint: Optional[str]
token: Optional[str]
workflow_api: dict
status: Status = Status.NOT_STARTED
progress: set = set()
-1
View File
@@ -2,5 +2,4 @@ aiofiles
pydantic
opencv-python
imageio-ffmpeg
brotli
# logfire
+61 -352
View File
@@ -2,7 +2,6 @@ import { app } from "./app.js";
import { api } from "./api.js";
import { ComfyWidgets, LGraphNode } from "./widgets.js";
import { generateDependencyGraph } from "https://esm.sh/[email protected]";
import { ComfyDeploy } from "https://esm.sh/[email protected]";
const loadingIcon = `<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 24 24"><g fill="none" stroke="#888888" stroke-linecap="round" stroke-width="2"><path stroke-dasharray="60" stroke-dashoffset="60" stroke-opacity=".3" d="M12 3C16.9706 3 21 7.02944 21 12C21 16.9706 16.9706 21 12 21C7.02944 21 3 16.9706 3 12C3 7.02944 7.02944 3 12 3Z"><animate fill="freeze" attributeName="stroke-dashoffset" dur="1.3s" values="60;0"/></path><path stroke-dasharray="15" stroke-dashoffset="15" d="M12 3C16.9706 3 21 7.02944 21 12"><animate fill="freeze" attributeName="stroke-dashoffset" dur="0.3s" values="15;0"/><animateTransform attributeName="transform" dur="1.5s" repeatCount="indefinite" type="rotate" values="0 12 12;360 12 12"/></path></g></svg>`;
@@ -192,13 +191,11 @@ const ext = {
registerCustomNodes() {
/** @type {LGraphNode}*/
class ComfyDeploy extends LGraphNode {
class ComfyDeploy {
color = LGraphCanvas.node_colors.yellow.color;
bgcolor = LGraphCanvas.node_colors.yellow.bgcolor;
groupcolor = LGraphCanvas.node_colors.yellow.groupcolor;
constructor() {
super();
this.color = LGraphCanvas.node_colors.yellow.color;
this.bgcolor = LGraphCanvas.node_colors.yellow.bgcolor;
this.groupcolor = LGraphCanvas.node_colors.yellow.groupcolor;
if (!this.properties) {
this.properties = {};
this.properties.workflow_name = "";
@@ -206,75 +203,53 @@ const ext = {
this.properties.version = "";
}
this.addWidget(
"text",
ComfyWidgets.STRING(
this,
"workflow_name",
this.properties.workflow_name,
(v) => {
this.properties.workflow_name = v;
},
{ multiline: false }
["", { default: this.properties.workflow_name, multiline: false }],
app,
);
this.addWidget(
"text",
ComfyWidgets.STRING(
this,
"workflow_id",
this.properties.workflow_id,
(v) => {
this.properties.workflow_id = v;
},
{ multiline: false }
["", { default: this.properties.workflow_id, multiline: false }],
app,
);
this.addWidget(
"text",
ComfyWidgets.STRING(
this,
"version",
this.properties.version,
(v) => {
this.properties.version = v;
},
{ multiline: false }
["", { default: this.properties.version, multiline: false }],
app,
);
// this.widgets.forEach((w) => {
// // w.computeSize = () => [200,10]
// w.computedHeight = 2;
// })
this.widgets_start_y = 10;
this.setSize(this.computeSize());
// const config = { };
// console.log(this);
this.serialize_widgets = true;
this.isVirtualNode = true;
}
onExecute() {
// This method is called when the node is executed
// You can add any necessary logic here
}
onSerialize(o) {
// This method is called when the node is being serialized
// Ensure all necessary data is saved
if (!o.properties) {
o.properties = {};
}
o.properties.workflow_name = this.properties.workflow_name;
o.properties.workflow_id = this.properties.workflow_id;
o.properties.version = this.properties.version;
}
onConfigure(o) {
// This method is called when the node is being configured (e.g., when loading a saved graph)
// Ensure all necessary data is restored
if (o.properties) {
this.properties = { ...this.properties, ...o.properties };
this.widgets[0].value = this.properties.workflow_name || "";
this.widgets[1].value = this.properties.workflow_id || "";
this.widgets[2].value = this.properties.version || "1";
}
}
}
// Register the node type
LiteGraph.registerNodeType("ComfyDeploy", Object.assign(ComfyDeploy, {
title: "Comfy Deploy",
title_mode: LiteGraph.NORMAL_TITLE,
collapsable: true,
}));
// Load default visibility
LiteGraph.registerNodeType(
"ComfyDeploy",
Object.assign(ComfyDeploy, {
title_mode: LiteGraph.NORMAL_TITLE,
title: "Comfy Deploy",
collapsable: true,
}),
);
ComfyDeploy.category = "deploy";
},
@@ -303,11 +278,7 @@ const ext = {
sendEventToCD("cd_plugin_onDeployChanges", prompt);
} else if (message.type === "queue_prompt") {
const prompt = await app.graphToPrompt();
if (typeof api.handlePromptGenerated === "function") {
api.handlePromptGenerated(prompt);
} else {
console.warn("api.handlePromptGenerated is not a function");
}
api.handlePromptGenerated(prompt);
sendEventToCD("cd_plugin_onQueuePrompt", prompt);
} else if (message.type === "get_prompt") {
const prompt = await app.graphToPrompt();
@@ -330,56 +301,6 @@ const ext = {
app.graph.add(node);
app.graph.afterChange();
} else if (message.type === "zoom_to_node") {
const nodeId = message.data.nodeId;
const position = message.data.position;
const node = app.graph.getNodeById(nodeId);
if (!node) return;
const canvas = app.canvas;
const targetScale = 1;
const targetOffsetX =
canvas.canvas.width / 4 - position[0] - node.size[0] / 2;
const targetOffsetY =
canvas.canvas.height / 4 - position[1] - node.size[1] / 2;
const startScale = canvas.ds.scale;
const startOffsetX = canvas.ds.offset[0];
const startOffsetY = canvas.ds.offset[1];
const duration = 400; // Animation duration in milliseconds
const startTime = Date.now();
function easeOutCubic(t) {
return 1 - Math.pow(1 - t, 3);
}
function lerp(start, end, t) {
return start * (1 - t) + end * t;
}
function animate() {
const currentTime = Date.now();
const elapsedTime = currentTime - startTime;
const t = Math.min(elapsedTime / duration, 1);
const easedT = easeOutCubic(t);
const currentScale = lerp(startScale, targetScale, easedT);
const currentOffsetX = lerp(startOffsetX, targetOffsetX, easedT);
const currentOffsetY = lerp(startOffsetY, targetOffsetY, easedT);
canvas.setZoom(currentScale);
canvas.ds.offset = [currentOffsetX, currentOffsetY];
canvas.draw(true, true);
if (t < 1) {
requestAnimationFrame(animate);
}
}
animate();
}
// else if (message.type === "refresh") {
// sendEventToCD("cd_plugin_onRefresh");
@@ -505,7 +426,6 @@ function createDynamicUIHtml(data) {
return html;
}
// Modify the existing deployWorkflow function
async function deployWorkflow() {
const deploy = document.getElementById("deploy-button");
@@ -652,30 +572,30 @@ async function deployWorkflow() {
console.log(hash);
return hash.file_hash;
},
// handleFileUpload: async (file, hash, prevhash) => {
// console.log("Uploading ", file);
// loadingDialog.showLoading("Uploading file", file);
// try {
// const { download_url } = await fetch(`/comfyui-deploy/upload-file`, {
// method: "POST",
// body: JSON.stringify({
// file_path: file,
// token: apiKey,
// url: endpoint + "/api/upload-url",
// }),
// })
// .then((x) => x.json())
// .catch(() => {
// loadingDialog.close();
// confirmDialog.confirm("Error", "Unable to upload file " + file);
// });
// loadingDialog.showLoading("Uploaded file", file);
// console.log(download_url);
// return download_url;
// } catch (error) {
// return undefined;
// }
// },
handleFileUpload: async (file, hash, prevhash) => {
console.log("Uploading ", file);
loadingDialog.showLoading("Uploading file", file);
try {
const { download_url } = await fetch(`/comfyui-deploy/upload-file`, {
method: "POST",
body: JSON.stringify({
file_path: file,
token: apiKey,
url: endpoint + "/api/upload-url",
}),
})
.then((x) => x.json())
.catch(() => {
loadingDialog.close();
confirmDialog.confirm("Error", "Unable to upload file " + file);
});
loadingDialog.showLoading("Uploaded file", file);
console.log(download_url);
return download_url;
} catch (error) {
return undefined;
}
},
existingDependencies: existing_workflow.dependencies,
});
@@ -700,15 +620,6 @@ async function deployWorkflow() {
"Check dependencies",
// JSON.stringify(deps, null, 2),
`
<div>
You will need to create a cloud machine with the following configuration on ComfyDeploy
<ol style="text-align: left; margin-top: 10px;">
<li>Review the dependencies listed in the graph below</li>
<li>Create a new cloud machine with the required configuration</li>
<li>Install missing models and check missing files</li>
<li>Deploy your workflow to the newly created machine</li>
</ol>
</div>
<div style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);">${loadingIcon}</div>
<iframe
style="z-index: 10; min-width: 600px; max-width: 1024px; min-height: 600px; border: none; background-color: transparent;"
@@ -778,14 +689,6 @@ async function deployWorkflow() {
`<span style="color:green;">Deployed successfully!</span> <a style="color:white;" target="_blank" href=${endpoint}/workflows/${data.workflow_id}>-> View here</a> <br/> <br/> Workflow ID: ${data.workflow_id} <br/> Workflow Name: ${workflow_name} <br/> Workflow Version: ${data.version} <br/>`,
);
// // Refresh the workflows list in the sidebar
// const sidebarEl = document.querySelector(
// '.comfy-sidebar-tab[data-id="search"]',
// );
// if (sidebarEl) {
// refreshWorkflowsList(sidebarEl);
// }
setTimeout(() => {
title.textContent = "Deploy";
title.style.color = "white";
@@ -803,85 +706,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");
@@ -1368,118 +1192,3 @@ 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/`,
});
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">
<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>
<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 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");
refreshWorkflowsList(el);
},
});
function getTimeAgo(date) {
const seconds = Math.floor((new Date() - date) / 1000);
let interval = seconds / 31536000;
if (interval > 1) return Math.floor(interval) + " years ago";
interval = seconds / 2592000;
if (interval > 1) return Math.floor(interval) + " months ago";
interval = seconds / 86400;
if (interval > 1) return Math.floor(interval) + " days ago";
interval = seconds / 3600;
if (interval > 1) return Math.floor(interval) + " hours ago";
interval = seconds / 60;
if (interval > 1) return Math.floor(interval) + " minutes ago";
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",
false,
);
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
}
}
-1
View File
@@ -6,5 +6,4 @@ export const customInputNodes: Record<string, string> = {
ComfyUIDeployExternalNumberInt: "integer",
ComfyUIDeployExternalLora: "string - (public lora download url)",
ComfyUIDeployExternalCheckpoint: "string - (public checkpoints download url)",
ComfyUIDeployExternalFaceModel: "string - (public face model download url)",
};
+1 -3
View File
@@ -51,9 +51,7 @@ const createRunRoute = createRoute({
export const registerCreateRunRoute = (app: App) => {
app.openapi(createRunRoute, async (c) => {
const data = c.req.valid("json");
const proto = c.req.headers.get('x-forwarded-proto') || "http";
const host = c.req.headers.get('x-forwarded-host') || c.req.headers.get('host');
const origin = `${proto}://${host}` || new URL(c.req.url).origin;
const origin = new URL(c.req.url).origin;
const apiKeyTokenData = c.get("apiKeyTokenData")!;
const { deployment_id, inputs } = data;