Compare commits

..
15 changed files with 1429 additions and 1087 deletions
+11 -1
View File
@@ -8,6 +8,16 @@ class ComfyUIDeployExternalBoolean:
{"multiline": False, "default": "input_bool"},
),
"default_value": ("BOOLEAN", {"default": False})
},
"optional": {
"display_name": (
"STRING",
{"multiline": False, "default": "Name of the node (optional)"},
),
"description": (
"STRING",
{"multiline": True, "default": "Description of the node (optional)"},
),
}
}
@@ -16,7 +26,7 @@ class ComfyUIDeployExternalBoolean:
FUNCTION = "run"
def run(self, input_id, default_value=None):
def run(self, input_id, default_value=None, display_name=None, description=None):
print(f"Node '{input_id}' processing with switch set to {default_value}")
return [default_value]
+9 -1
View File
@@ -23,6 +23,14 @@ class ComfyUIDeployExternalCheckpoint:
},
"optional": {
"default_value": (folder_paths.get_filename_list("checkpoints"), ),
"display_name": (
"STRING",
{"multiline": False, "default": "Name of the node (optional)"},
),
"description": (
"STRING",
{"multiline": True, "default": "Description of the node (optional)"},
),
}
}
@@ -33,7 +41,7 @@ class ComfyUIDeployExternalCheckpoint:
CATEGORY = "deploy"
def run(self, input_id, default_value=None):
def run(self, input_id, default_value=None, display_name=None, description=None):
import requests
import os
import uuid
+9 -1
View File
@@ -15,6 +15,14 @@ class ComfyUIDeployExternalImage:
},
"optional": {
"default_value": ("IMAGE",),
"display_name": (
"STRING",
{"multiline": False, "default": "Name of the node (optional)"},
),
"description": (
"STRING",
{"multiline": True, "default": "Description of the node (optional)"},
),
}
}
@@ -25,7 +33,7 @@ class ComfyUIDeployExternalImage:
CATEGORY = "image"
def run(self, input_id, default_value=None):
def run(self, input_id, default_value=None, display_name=None, description=None):
image = default_value
try:
if input_id.startswith('http'):
+9 -1
View File
@@ -15,6 +15,14 @@ class ComfyUIDeployExternalImageAlpha:
},
"optional": {
"default_value": ("IMAGE",),
"display_name": (
"STRING",
{"multiline": False, "default": "Name of the node (optional)"},
),
"description": (
"STRING",
{"multiline": True, "default": "Description of the node (optional)"},
),
}
}
@@ -25,7 +33,7 @@ class ComfyUIDeployExternalImageAlpha:
CATEGORY = "image"
def run(self, input_id, default_value=None):
def run(self, input_id, default_value=None, display_name=None, description=None):
image = default_value
try:
if input_id.startswith('http'):
+9 -1
View File
@@ -21,6 +21,14 @@ class ComfyUIDeployExternalImageBatch:
},
"optional": {
"default_value": ("IMAGE",),
"display_name": (
"STRING",
{"multiline": False, "default": "Name of the node (optional)"},
),
"description": (
"STRING",
{"multiline": True, "default": "Description of the node (optional)"},
),
}
}
@@ -31,7 +39,7 @@ class ComfyUIDeployExternalImageBatch:
CATEGORY = "image"
def run(self, input_id, images=None, default_value=None):
def run(self, input_id, images=None, default_value=None, display_name=None, description=None):
processed_images = []
try:
images_list = json.loads(images) # Assuming images is a JSON array string
+9 -1
View File
@@ -29,6 +29,14 @@ class ComfyUIDeployExternalLora:
"STRING",
{"multiline": False, "default": ""},
),
"display_name": (
"STRING",
{"multiline": False, "default": "Name of the node (optional)"},
),
"description": (
"STRING",
{"multiline": True, "default": "Description of the node (optional)"},
),
},
}
@@ -39,7 +47,7 @@ class ComfyUIDeployExternalLora:
CATEGORY = "deploy"
def run(self, input_id, default_lora_name=None, lora_save_name=None):
def run(self, input_id, default_lora_name=None, lora_save_name=None, display_name=None, description=None):
import requests
import os
import uuid
+10 -2
View File
@@ -16,7 +16,15 @@ class ComfyUIDeployExternalNumber:
"optional": {
"default_value": (
"FLOAT",
{"multiline": True, "display": "number", "default": 0, "step": 0.01},
{"multiline": True, "display": "number", "default": 0, "min": -2147483647, "max": 2147483647, "step": 0.01},
),
"display_name": (
"STRING",
{"multiline": False, "default": "Name of the node (optional)"},
),
"description": (
"STRING",
{"multiline": True, "default": "Description of the node (optional)"},
),
}
}
@@ -28,7 +36,7 @@ class ComfyUIDeployExternalNumber:
CATEGORY = "number"
def run(self, input_id, default_value=None):
def run(self, input_id, default_value=None, display_name=None, description=None):
try:
float_value = float(input_id)
print("my number", float_value)
+10 -2
View File
@@ -16,7 +16,15 @@ class ComfyUIDeployExternalNumberInt:
"optional": {
"default_value": (
"INT",
{"multiline": True, "display": "number", "default": 0},
{"multiline": True, "display": "number", "min": -2147483647, "max": 2147483647, "default": 0},
),
"display_name": (
"STRING",
{"multiline": False, "default": "Name of the node (optional)"},
),
"description": (
"STRING",
{"multiline": True, "default": "Description of the node (optional)"},
),
}
}
@@ -28,7 +36,7 @@ class ComfyUIDeployExternalNumberInt:
CATEGORY = "number"
def run(self, input_id, default_value=None):
def run(self, input_id, default_value=None, display_name=None, description=None):
if not input_id or (isinstance(input_id, str) and not input_id.strip().isdigit()):
return [default_value]
return [int(input_id)]
+12 -4
View File
@@ -11,15 +11,23 @@ class ComfyUIDeployExternalNumberSlider:
"optional": {
"default_value": (
"FLOAT",
{"multiline": True, "display": "number", "default": 0.5, "step": 0.01},
{"multiline": True, "display": "number", "min": -2147483647, "max": 2147483647, "default": 0.5, "step": 0.01},
),
"min_value": (
"FLOAT",
{"multiline": True, "display": "number", "default": 0, "step": 0.01},
{"multiline": True, "display": "number", "min": -2147483647, "max": 2147483647, "default": 0, "step": 0.01},
),
"max_value": (
"FLOAT",
{"multiline": True, "display": "number", "default": 1, "step": 0.01},
{"multiline": True, "display": "number", "min": -2147483647, "max": 2147483647, "default": 1, "step": 0.01},
),
"display_name": (
"STRING",
{"multiline": False, "default": "Name of the node (optional)"},
),
"description": (
"STRING",
{"multiline": True, "default": "Description of the node (optional)"},
),
}
}
@@ -31,7 +39,7 @@ class ComfyUIDeployExternalNumberSlider:
CATEGORY = "number"
def run(self, input_id, default_value=None, min_value=0, max_value=1):
def run(self, input_id, default_value=None, min_value=0, max_value=1, display_name=None, description=None):
try:
float_value = float(input_id)
if min_value <= float_value <= max_value:
+9 -1
View File
@@ -18,6 +18,14 @@ class ComfyUIDeployExternalText:
"STRING",
{"multiline": True, "default": ""},
),
"display_name": (
"STRING",
{"multiline": False, "default": "Name of the node (optional)"},
),
"description": (
"STRING",
{"multiline": True, "default": "Description of the node (optional)"},
),
}
}
@@ -28,7 +36,7 @@ class ComfyUIDeployExternalText:
CATEGORY = "text"
def run(self, input_id, default_value=None):
def run(self, input_id, default_value=None, display_name=None, description=None):
return [default_value]
+12 -3
View File
@@ -17,6 +17,16 @@ class ComfyUIDeployExternalTextList:
"STRING",
{"multiline": True, "default": "[]"},
),
},
"optional": {
"display_name": (
"STRING",
{"multiline": False, "default": "Name of the node (optional)"},
),
"description": (
"STRING",
{"multiline": True, "default": "Description of the node (optional)"},
),
}
}
@@ -29,15 +39,14 @@ class ComfyUIDeployExternalTextList:
CATEGORY = "text"
def run(self, input_id, text=None):
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]
return ([text_list],)
NODE_CLASS_MAPPINGS = {"ComfyUIDeployExternalTextList": ComfyUIDeployExternalTextList}
NODE_DISPLAY_NAME_MAPPINGS = {"ComfyUIDeployExternalTextList": "External Text List (ComfyUI Deploy)"}
+13 -4
View File
@@ -765,6 +765,14 @@ class ComfyUIDeployExternalVideo:
"meta_batch": ("VHS_BatchManager",),
"vae": ("VAE",),
"default_value": (sorted(files),),
"display_name": (
"STRING",
{"multiline": False, "default": "Name of the node (optional)"},
),
"description": (
"STRING",
{"multiline": True, "default": "Description of the node (optional)"},
),
},
"hidden": {
"unique_id": "UNIQUE_ID"
@@ -796,8 +804,6 @@ 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"):
@@ -827,8 +833,11 @@ class ComfyUIDeployExternalVideo:
leave=True,
):
out_file.write(chunk)
print("video path: ", video_path)
else:
video = kwargs.get("default_value", "")
if video is None:
raise "No default video given and no external video provided"
video_path = folder_paths.get_annotated_filepath(video.strip('"'))
return load_video_cv(
video=video_path,
+119 -33
View File
@@ -17,12 +17,13 @@ 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 ClientError
from aiohttp import web, ClientSession, ClientError, ClientTimeout
import atexit
# Global session
@@ -32,7 +33,7 @@ client_session = None
# global client_session
# if client_session is None:
# client_session = aiohttp.ClientSession()
async def ensure_client_session():
global client_session
if client_session is None:
@@ -42,7 +43,7 @@ async def cleanup():
global client_session
if client_session:
await client_session.close()
def exit_handler():
print("Exiting the application. Initiating cleanup...")
loop = asyncio.get_event_loop()
@@ -50,28 +51,44 @@ def exit_handler():
atexit.register(exit_handler)
max_retries = int(os.environ.get('MAX_RETRIES', '3'))
max_retries = int(os.environ.get('MAX_RETRIES', '5'))
retry_delay_multiplier = float(os.environ.get('RETRY_DELAY_MULTIPLIER', '2'))
print(f"max_retries: {max_retries}, retry_delay_multiplier: {retry_delay_multiplier}")
async def async_request_with_retry(method, url, **kwargs):
async def async_request_with_retry(method, url, disable_timeout=False, **kwargs):
global client_session
await ensure_client_session()
# async with aiohttp.ClientSession() as client_session:
retry_delay = 1 # Start with 1 second delay
initial_timeout = 5 # 5 seconds timeout for the initial connection
for attempt in range(max_retries):
try:
# Set a timeout for the initial connection
if not disable_timeout:
timeout = ClientTimeout(total=None, connect=initial_timeout)
kwargs['timeout'] = timeout
async with client_session.request(method, url, **kwargs) as response:
response.raise_for_status()
if method.upper() == 'GET':
await response.read()
return response
except asyncio.TimeoutError:
logger.warning(f"Request timed out after {initial_timeout} seconds (attempt {attempt + 1}/{max_retries})")
except ClientError as e:
if attempt == max_retries - 1:
logger.error(f"Request failed after {max_retries} attempts: {e}")
# raise
logger.warning(f"Request failed (attempt {attempt + 1}/{max_retries}): {e}")
await asyncio.sleep(retry_delay)
retry_delay *= retry_delay_multiplier # Exponential backoff
# Wait before retrying
await asyncio.sleep(retry_delay)
retry_delay *= retry_delay_multiplier # Exponential backoff
# If all retries fail, raise an exception
raise Exception(f"Request failed after {max_retries} attempts")
from logging import basicConfig, getLogger
@@ -99,7 +116,7 @@ def log(level, message, **kwargs):
getattr(logger, level)(message, **kwargs)
else:
getattr(logger, level)(f"{message} {kwargs}")
# For a span, you might need to create a context manager
from contextlib import contextmanager
@@ -217,15 +234,32 @@ 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] 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();
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']}")
def apply_inputs_to_workflow(workflow_api: Any, inputs: Any, sid: str = None):
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
def apply_inputs_to_workflow(workflow_api: Any, inputs: Any, sid: str | None = None):
# Loop through each of the inputs and replace them
for key, value in workflow_api.items():
if 'inputs' in value:
@@ -349,7 +383,7 @@ async def comfy_deploy_run(request):
status = 200
if "node_errors" in res and res["node_errors"] is not None:
if "node_errors" in res and res["node_errors"] is not None and len(res["node_errors"]) > 0:
# Even tho there are node_errors it can still be run
status = 400
await update_run_with_output(prompt_id, {
@@ -410,7 +444,7 @@ async def stream_prompt(data):
status = 200
if "node_errors" in res and res["node_errors"] is not None:
if "node_errors" in res and res["node_errors"] is not None and len(res["node_errors"]) > 0:
# Even tho there are node_errors it can still be run
status = 400
await update_run_with_output(prompt_id, {
@@ -822,6 +856,50 @@ async def send(event, data, sid=None):
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
@@ -923,7 +1001,7 @@ async def send_json_override(self, event, data, sid=None):
return
else:
logger.info(f"Executed {data}")
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'))
@@ -1079,18 +1157,18 @@ async def upload_file(prompt_id, filename, subfolder=None, content_type="image/p
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}"
target_url = f"{file_upload_endpoint}?file_name={filename}&run_id={prompt_id}&type={content_type}&version=v2"
start_time = time.time() # Start timing here
result = requests.get(target_url)
result = await async_request_with_retry("GET", target_url, disable_timeout=True)
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 = result.json()
ok = await result.json()
start_time = time.time() # Start timing here
with open(file, 'rb') as f:
data = f.read()
async with aiofiles.open(file, 'rb') as f:
data = await f.read()
headers = {
# "x-amz-acl": "public-read",
"Content-Type": content_type,
@@ -1193,8 +1271,10 @@ 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
@@ -1207,22 +1287,28 @@ async def handle_upload(prompt_id: str, data, key: str, content_type_key: str, d
elif file_extension == '.webp':
file_type = 'image/webp'
await upload_file(
upload_tasks.append(upload_file(
prompt_id,
item.get("filename"),
subfolder=item.get("subfolder"),
type=item.get("type"),
content_type=file_type
)
))
# Execute all upload tasks concurrently
await asyncio.gather(*upload_tasks)
# Upload files in the background
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")
# 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")
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 asyncio.gather(*upload_tasks)
if have_upload:
await update_file_status(prompt_id, data, False, node_id=node_id)
@@ -1321,4 +1407,4 @@ if cd_enable_log:
@server.PromptServer.instance.routes.get("/comfyui-deploy/filename_list_cache")
async def get_filename_list_cache(_):
from folder_paths import filename_list_cache
return web.json_response({'filename_list': filename_list_cache})
return web.json_response({'filename_list': filename_list_cache})
+1
View File
@@ -2,4 +2,5 @@ aiofiles
pydantic
opencv-python
imageio-ffmpeg
brotli
# logfire
+1187 -1032
View File
File diff suppressed because it is too large Load Diff