Compare commits

..
Author SHA1 Message Date
nick 649b61c580 default vid 2024-08-26 13:46:01 -07:00
nick edff5685f9 fix: random seed 2024-08-22 17:39:03 -07:00
bennykok 9fc0c2b4a2 chore: upload node data 2024-08-21 16:34:25 -07:00
bennykok d34e2e99b1 fix: external lora for new comfyui 2024-08-21 09:46:13 -07:00
bennykok f85043db07 fix: remove default value 2024-08-20 19:14:43 -07:00
bennykok 894d8e1503 Merge branch 'benny/async-upload-file' into public-main 2024-08-20 18:02:57 -07:00
bennykok 08d631d1eb feat: async file upload for the same node 2024-08-20 17:07:50 -07:00
karrix a1031487e1 add: all node support name and description 2024-08-20 20:15:29 +08:00
bennykok ca41207192 feat: max min int for all number inputs to enable negative number input 2024-08-19 13:27:46 -07:00
bennykok 507d5ef631 feat: add a init timeout of 10 seconds for retry logic 2024-08-18 17:31:48 -07:00
bennykok dd1d9df23f fix: resolve false possible error 2024-08-18 15:38:16 -07:00
bennykok 3a14e49ca5 fix: refresh workflows list 2024-08-17 16:04:14 -07:00
nick 8147c4bfb7 video node' 2024-08-15 12:50:29 -07:00
bennykok 10268825d9 feat: support new frontend! 2024-08-14 11:09:58 -07:00
bennykok f6ea252652 fix: log when random seed is applied 2024-08-10 10:35:48 -07:00
bennykok 98cd5ef79c fix: randomize noise RandomNoise, KSamplerAdvanced, SamplerCustom 2024-08-10 10:02:01 -07:00
Emmanuel Morales 4bce5cadfb fix(text): return correctly the text in external_text_list node 2024-08-10 09:44:37 -06:00
Nick Kao f362671041 Merge pull request #61 from BennyKok/node-error-no-throw
block on bad prompt
2024-08-08 10:01:33 -07:00
15 changed files with 1439 additions and 1087 deletions
+11 -1
View File
@@ -8,6 +8,16 @@ class ComfyUIDeployExternalBoolean:
{"multiline": False, "default": "input_bool"}, {"multiline": False, "default": "input_bool"},
), ),
"default_value": ("BOOLEAN", {"default": False}) "default_value": ("BOOLEAN", {"default": False})
},
"optional": {
"display_name": (
"STRING",
{"multiline": False, "default": ""},
),
"description": (
"STRING",
{"multiline": True, "default": ""},
),
} }
} }
@@ -16,7 +26,7 @@ class ComfyUIDeployExternalBoolean:
FUNCTION = "run" 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}") print(f"Node '{input_id}' processing with switch set to {default_value}")
return [default_value] return [default_value]
+9 -1
View File
@@ -23,6 +23,14 @@ class ComfyUIDeployExternalCheckpoint:
}, },
"optional": { "optional": {
"default_value": (folder_paths.get_filename_list("checkpoints"), ), "default_value": (folder_paths.get_filename_list("checkpoints"), ),
"display_name": (
"STRING",
{"multiline": False, "default": ""},
),
"description": (
"STRING",
{"multiline": True, "default": ""},
),
} }
} }
@@ -33,7 +41,7 @@ class ComfyUIDeployExternalCheckpoint:
CATEGORY = "deploy" 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 requests
import os import os
import uuid import uuid
+9 -1
View File
@@ -15,6 +15,14 @@ class ComfyUIDeployExternalImage:
}, },
"optional": { "optional": {
"default_value": ("IMAGE",), "default_value": ("IMAGE",),
"display_name": (
"STRING",
{"multiline": False, "default": ""},
),
"description": (
"STRING",
{"multiline": True, "default": ""},
),
} }
} }
@@ -25,7 +33,7 @@ class ComfyUIDeployExternalImage:
CATEGORY = "image" 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 image = default_value
try: try:
if input_id.startswith('http'): if input_id.startswith('http'):
+9 -1
View File
@@ -15,6 +15,14 @@ class ComfyUIDeployExternalImageAlpha:
}, },
"optional": { "optional": {
"default_value": ("IMAGE",), "default_value": ("IMAGE",),
"display_name": (
"STRING",
{"multiline": False, "default": ""},
),
"description": (
"STRING",
{"multiline": True, "default": ""},
),
} }
} }
@@ -25,7 +33,7 @@ class ComfyUIDeployExternalImageAlpha:
CATEGORY = "image" 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 image = default_value
try: try:
if input_id.startswith('http'): if input_id.startswith('http'):
+9 -1
View File
@@ -21,6 +21,14 @@ class ComfyUIDeployExternalImageBatch:
}, },
"optional": { "optional": {
"default_value": ("IMAGE",), "default_value": ("IMAGE",),
"display_name": (
"STRING",
{"multiline": False, "default": ""},
),
"description": (
"STRING",
{"multiline": True, "default": ""},
),
} }
} }
@@ -31,7 +39,7 @@ class ComfyUIDeployExternalImageBatch:
CATEGORY = "image" 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 = [] processed_images = []
try: try:
images_list = json.loads(images) # Assuming images is a JSON array string images_list = json.loads(images) # Assuming images is a JSON array string
+14 -2
View File
@@ -29,6 +29,18 @@ class ComfyUIDeployExternalLora:
"STRING", "STRING",
{"multiline": False, "default": ""}, {"multiline": False, "default": ""},
), ),
"display_name": (
"STRING",
{"multiline": False, "default": ""},
),
"description": (
"STRING",
{"multiline": True, "default": ""},
),
"lora_url": (
"STRING",
{"multiline": False, "default": ""},
),
}, },
} }
@@ -39,12 +51,12 @@ class ComfyUIDeployExternalLora:
CATEGORY = "deploy" 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, lora_url=None):
import requests import requests
import os import os
import uuid import uuid
if default_lora_name.startswith("http"): if lora_url and lora_url.startswith("http"):
if lora_save_name: if lora_save_name:
existing_loras = folder_paths.get_filename_list("loras") existing_loras = folder_paths.get_filename_list("loras")
# Check if lora_save_name exists in the list # Check if lora_save_name exists in the list
+10 -2
View File
@@ -16,7 +16,15 @@ class ComfyUIDeployExternalNumber:
"optional": { "optional": {
"default_value": ( "default_value": (
"FLOAT", "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": ""},
),
"description": (
"STRING",
{"multiline": True, "default": ""},
), ),
} }
} }
@@ -28,7 +36,7 @@ class ComfyUIDeployExternalNumber:
CATEGORY = "number" CATEGORY = "number"
def run(self, input_id, default_value=None): def run(self, input_id, default_value=None, display_name=None, description=None):
try: try:
float_value = float(input_id) float_value = float(input_id)
print("my number", float_value) print("my number", float_value)
+10 -2
View File
@@ -16,7 +16,15 @@ class ComfyUIDeployExternalNumberInt:
"optional": { "optional": {
"default_value": ( "default_value": (
"INT", "INT",
{"multiline": True, "display": "number", "default": 0}, {"multiline": True, "display": "number", "min": -2147483647, "max": 2147483647, "default": 0},
),
"display_name": (
"STRING",
{"multiline": False, "default": ""},
),
"description": (
"STRING",
{"multiline": True, "default": ""},
), ),
} }
} }
@@ -28,7 +36,7 @@ class ComfyUIDeployExternalNumberInt:
CATEGORY = "number" 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()): if not input_id or (isinstance(input_id, str) and not input_id.strip().isdigit()):
return [default_value] return [default_value]
return [int(input_id)] return [int(input_id)]
+12 -4
View File
@@ -11,15 +11,23 @@ class ComfyUIDeployExternalNumberSlider:
"optional": { "optional": {
"default_value": ( "default_value": (
"FLOAT", "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": ( "min_value": (
"FLOAT", "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": ( "max_value": (
"FLOAT", "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": ""},
),
"description": (
"STRING",
{"multiline": True, "default": ""},
), ),
} }
} }
@@ -31,7 +39,7 @@ class ComfyUIDeployExternalNumberSlider:
CATEGORY = "number" 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: try:
float_value = float(input_id) float_value = float(input_id)
if min_value <= float_value <= max_value: if min_value <= float_value <= max_value:
+9 -1
View File
@@ -18,6 +18,14 @@ class ComfyUIDeployExternalText:
"STRING", "STRING",
{"multiline": True, "default": ""}, {"multiline": True, "default": ""},
), ),
"display_name": (
"STRING",
{"multiline": False, "default": ""},
),
"description": (
"STRING",
{"multiline": True, "default": ""},
),
} }
} }
@@ -28,7 +36,7 @@ class ComfyUIDeployExternalText:
CATEGORY = "text" 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] return [default_value]
+12 -3
View File
@@ -17,6 +17,16 @@ class ComfyUIDeployExternalTextList:
"STRING", "STRING",
{"multiline": True, "default": "[]"}, {"multiline": True, "default": "[]"},
), ),
},
"optional": {
"display_name": (
"STRING",
{"multiline": False, "default": ""},
),
"description": (
"STRING",
{"multiline": True, "default": ""},
),
} }
} }
@@ -29,15 +39,14 @@ class ComfyUIDeployExternalTextList:
CATEGORY = "text" CATEGORY = "text"
def run(self, input_id, text=None): def run(self, input_id, text=None, display_name=None, description=None):
text_list = [] text_list = []
try: try:
text_list = json.loads(text) # Assuming text is a JSON array string text_list = json.loads(text) # Assuming text is a JSON array string
except Exception as e: except Exception as e:
print(f"Error processing images: {e}") print(f"Error processing images: {e}")
pass pass
return [text_list] return ([text_list],)
NODE_CLASS_MAPPINGS = {"ComfyUIDeployExternalTextList": ComfyUIDeployExternalTextList} NODE_CLASS_MAPPINGS = {"ComfyUIDeployExternalTextList": ComfyUIDeployExternalTextList}
NODE_DISPLAY_NAME_MAPPINGS = {"ComfyUIDeployExternalTextList": "External Text List (ComfyUI Deploy)"} NODE_DISPLAY_NAME_MAPPINGS = {"ComfyUIDeployExternalTextList": "External Text List (ComfyUI Deploy)"}
+14 -5
View File
@@ -764,7 +764,15 @@ class ComfyUIDeployExternalVideo:
"optional": { "optional": {
"meta_batch": ("VHS_BatchManager",), "meta_batch": ("VHS_BatchManager",),
"vae": ("VAE",), "vae": ("VAE",),
"default_value": (sorted(files),), "default_video": (sorted(files),),
"display_name": (
"STRING",
{"multiline": False, "default": ""},
),
"description": (
"STRING",
{"multiline": True, "default": ""},
),
}, },
"hidden": { "hidden": {
"unique_id": "UNIQUE_ID" "unique_id": "UNIQUE_ID"
@@ -796,8 +804,6 @@ class ComfyUIDeployExternalVideo:
meta_batch = kwargs.get("meta_batch") meta_batch = kwargs.get("meta_batch")
unique_id = kwargs.get("unique_id") 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() input_dir = folder_paths.get_input_directory()
if input_id.startswith("http"): if input_id.startswith("http"):
@@ -827,8 +833,11 @@ class ComfyUIDeployExternalVideo:
leave=True, leave=True,
): ):
out_file.write(chunk) out_file.write(chunk)
else:
print("video path: ", video_path) video = kwargs.get("default_video", "")
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( return load_video_cv(
video=video_path, video=video_path,
+117 -25
View File
@@ -17,12 +17,13 @@ from urllib.parse import quote
import threading import threading
import hashlib import hashlib
import aiohttp import aiohttp
from aiohttp import ClientSession, web
import aiofiles import aiofiles
from typing import Dict, List, Union, Any, Optional from typing import Dict, List, Union, Any, Optional
from PIL import Image from PIL import Image
import copy import copy
import struct import struct
from aiohttp import ClientError from aiohttp import web, ClientSession, ClientError, ClientTimeout
import atexit import atexit
# Global session # Global session
@@ -50,29 +51,45 @@ def exit_handler():
atexit.register(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')) retry_delay_multiplier = float(os.environ.get('RETRY_DELAY_MULTIPLIER', '2'))
print(f"max_retries: {max_retries}, retry_delay_multiplier: {retry_delay_multiplier}") 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 global client_session
await ensure_client_session() await ensure_client_session()
# async with aiohttp.ClientSession() as client_session:
retry_delay = 1 # Start with 1 second delay retry_delay = 1 # Start with 1 second delay
initial_timeout = 5 # 5 seconds timeout for the initial connection
for attempt in range(max_retries): for attempt in range(max_retries):
try: 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: async with client_session.request(method, url, **kwargs) as response:
response.raise_for_status() response.raise_for_status()
if method.upper() == 'GET':
await response.read()
return response return response
except asyncio.TimeoutError:
logger.warning(f"Request timed out after {initial_timeout} seconds (attempt {attempt + 1}/{max_retries})")
except ClientError as e: except ClientError as e:
if attempt == max_retries - 1: if attempt == max_retries - 1:
logger.error(f"Request failed after {max_retries} attempts: {e}") logger.error(f"Request failed after {max_retries} attempts: {e}")
# raise # raise
logger.warning(f"Request failed (attempt {attempt + 1}/{max_retries}): {e}") logger.warning(f"Request failed (attempt {attempt + 1}/{max_retries}): {e}")
# Wait before retrying
await asyncio.sleep(retry_delay) await asyncio.sleep(retry_delay)
retry_delay *= retry_delay_multiplier # Exponential backoff 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 from logging import basicConfig, getLogger
# Check for an environment variable to enable/disable Logfire # Check for an environment variable to enable/disable Logfire
@@ -217,13 +234,30 @@ def apply_random_seed_to_workflow(workflow_api):
workflow_api (dict): The workflow API dictionary to modify. workflow_api (dict): The workflow API dictionary to modify.
""" """
for key in workflow_api: for key in workflow_api:
if 'inputs' in workflow_api[key] and 'seed' in workflow_api[key]['inputs']: if 'inputs' in workflow_api[key]:
if 'seed' in workflow_api[key]['inputs']:
if isinstance(workflow_api[key]['inputs']['seed'], list): if isinstance(workflow_api[key]['inputs']['seed'], list):
continue continue
if workflow_api[key]['class_type'] == "PromptExpansion": if workflow_api[key]['class_type'] == "PromptExpansion":
workflow_api[key]['inputs']['seed'] = randomSeed(8); 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 continue
workflow_api[key]['inputs']['seed'] = randomSeed();
def apply_inputs_to_workflow(workflow_api: Any, inputs: Any, sid: str = None): def apply_inputs_to_workflow(workflow_api: Any, inputs: Any, sid: str = None):
# Loop through each of the inputs and replace them # Loop through each of the inputs and replace them
@@ -258,7 +292,7 @@ def apply_inputs_to_workflow(workflow_api: Any, inputs: Any, sid: str = None):
value['inputs']["images"] = new_value value['inputs']["images"] = new_value
if value["class_type"] == "ComfyUIDeployExternalLora": if value["class_type"] == "ComfyUIDeployExternalLora":
value["inputs"]["default_lora_name"] = new_value value["inputs"]["lora_url"] = new_value
if value["class_type"] == "ComfyUIDeployExternalSlider": if value["class_type"] == "ComfyUIDeployExternalSlider":
value["inputs"]["default_value"] = new_value value["inputs"]["default_value"] = new_value
@@ -349,7 +383,7 @@ async def comfy_deploy_run(request):
status = 200 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 # Even tho there are node_errors it can still be run
status = 400 status = 400
await update_run_with_output(prompt_id, { await update_run_with_output(prompt_id, {
@@ -410,7 +444,7 @@ async def stream_prompt(data):
status = 200 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 # Even tho there are node_errors it can still be run
status = 400 status = 400
await update_run_with_output(prompt_id, { await update_run_with_output(prompt_id, {
@@ -822,6 +856,50 @@ async def send(event, data, sid=None):
logger.info(f"Exception: {e}") logger.info(f"Exception: {e}")
traceback.print_exc() 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 prompt_server = server.PromptServer.instance
@@ -914,17 +992,22 @@ async def send_json_override(self, event, data, sid=None):
# await update_run_with_output(prompt_id, data) # await update_run_with_output(prompt_id, data)
if event == 'executed' and 'node' in data and 'output' in data: if event == 'executed' and 'node' in data and 'output' in data:
node_meta = None
if prompt_id in prompt_metadata: if prompt_id in prompt_metadata:
node = data.get('node') node = data.get('node')
class_type = prompt_metadata[prompt_id].workflow_api[node]['class_type'] class_type = prompt_metadata[prompt_id].workflow_api[node]['class_type']
logger.info(f"Executed {class_type} {data}") logger.info(f"Executed {class_type} {data}")
node_meta = {
"node_id": node,
"node_class": class_type,
}
if class_type == "PreviewImage": if class_type == "PreviewImage":
logger.info("Skipping preview image") logger.info("Skipping preview image")
return return
else: else:
logger.info(f"Executed {data}") 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'), 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')) # update_run_with_output(prompt_id, data.get('output'))
@@ -1079,18 +1162,18 @@ async def upload_file(prompt_id, filename, subfolder=None, content_type="image/p
prompt_id = quote(prompt_id) prompt_id = quote(prompt_id)
content_type = quote(content_type) 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 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 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)) 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 start_time = time.time() # Start timing here
with open(file, 'rb') as f: async with aiofiles.open(file, 'rb') as f:
data = f.read() data = await f.read()
headers = { headers = {
# "x-amz-acl": "public-read", # "x-amz-acl": "public-read",
"Content-Type": content_type, "Content-Type": content_type,
@@ -1193,8 +1276,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): async def handle_upload(prompt_id: str, data, key: str, content_type_key: str, default_content_type: str):
items = data.get(key, []) items = data.get(key, [])
upload_tasks = []
for item in items: for item in items:
# # Skipping temp files # Skipping temp files
if item.get("type") == "temp": if item.get("type") == "temp":
continue continue
@@ -1207,29 +1292,35 @@ async def handle_upload(prompt_id: str, data, key: str, content_type_key: str, d
elif file_extension == '.webp': elif file_extension == '.webp':
file_type = 'image/webp' file_type = 'image/webp'
await upload_file( upload_tasks.append(upload_file(
prompt_id, prompt_id,
item.get("filename"), item.get("filename"),
subfolder=item.get("subfolder"), subfolder=item.get("subfolder"),
type=item.get("type"), type=item.get("type"),
content_type=file_type content_type=file_type
) ))
# Execute all upload tasks concurrently
await asyncio.gather(*upload_tasks)
# Upload files in the background # Upload files in the background
async def upload_in_background(prompt_id: str, data, node_id=None, have_upload=True): async def upload_in_background(prompt_id: str, data, node_id=None, have_upload=True):
try: try:
await handle_upload(prompt_id, data, 'images', "content_type", "image/png") upload_tasks = [
await handle_upload(prompt_id, data, 'files', "content_type", "image/png") handle_upload(prompt_id, data, 'images', "content_type", "image/png"),
# This will also be mp4 handle_upload(prompt_id, data, 'files', "content_type", "image/png"),
await handle_upload(prompt_id, data, 'gifs', "format", "image/gif") handle_upload(prompt_id, data, 'gifs', "format", "image/gif"),
await handle_upload(prompt_id, data, 'mesh', "format", "application/octet-stream") handle_upload(prompt_id, data, 'mesh', "format", "application/octet-stream")
]
await asyncio.gather(*upload_tasks)
if have_upload: if have_upload:
await update_file_status(prompt_id, data, False, node_id=node_id) await update_file_status(prompt_id, data, False, node_id=node_id)
except Exception as e: except Exception as e:
await handle_error(prompt_id, data, e) await handle_error(prompt_id, data, e)
async def update_run_with_output(prompt_id, data, node_id=None): async def update_run_with_output(prompt_id, data, node_id=None, node_meta=None):
if prompt_id not in prompt_metadata: if prompt_id not in prompt_metadata:
return return
@@ -1240,7 +1331,8 @@ async def update_run_with_output(prompt_id, data, node_id=None):
body = { body = {
"run_id": prompt_id, "run_id": prompt_id,
"output_data": data "output_data": data,
"node_meta": node_meta,
} }
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: if bypass_upload and have_upload_media:
+1
View File
@@ -2,4 +2,5 @@ aiofiles
pydantic pydantic
opencv-python opencv-python
imageio-ffmpeg imageio-ffmpeg
brotli
# logfire # logfire
+285 -130
View File
@@ -2,6 +2,7 @@ import { app } from "./app.js";
import { api } from "./api.js"; import { api } from "./api.js";
import { ComfyWidgets, LGraphNode } from "./widgets.js"; import { ComfyWidgets, LGraphNode } from "./widgets.js";
import { generateDependencyGraph } from "https://esm.sh/[email protected]"; 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>`; 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>`;
@@ -24,10 +25,7 @@ function dispatchAPIEventData(data) {
message += "\n" + nodeError.class_type + ":"; message += "\n" + nodeError.class_type + ":";
for (const errorReason of nodeError.errors) { for (const errorReason of nodeError.errors) {
message += message +=
"\n - " + "\n - " + errorReason.message + ": " + errorReason.details;
errorReason.message +
": " +
errorReason.details;
} }
} }
@@ -47,38 +45,32 @@ function dispatchAPIEventData(data) {
// window.name = this.clientId; // use window name so it isnt reused when duplicating tabs // window.name = this.clientId; // use window name so it isnt reused when duplicating tabs
// sessionStorage.setItem("clientId", this.clientId); // store in session storage so duplicate tab can load correct workflow // sessionStorage.setItem("clientId", this.clientId); // store in session storage so duplicate tab can load correct workflow
} }
api.dispatchEvent( api.dispatchEvent(new CustomEvent("status", { detail: msg.data.status }));
new CustomEvent("status", { detail: msg.data.status })
);
break; break;
case "progress": case "progress":
api.dispatchEvent( api.dispatchEvent(new CustomEvent("progress", { detail: msg.data }));
new CustomEvent("progress", { detail: msg.data })
);
break; break;
case "executing": case "executing":
api.dispatchEvent( api.dispatchEvent(
new CustomEvent("executing", { detail: msg.data.node }) new CustomEvent("executing", { detail: msg.data.node }),
); );
break; break;
case "executed": case "executed":
api.dispatchEvent( api.dispatchEvent(new CustomEvent("executed", { detail: msg.data }));
new CustomEvent("executed", { detail: msg.data })
);
break; break;
case "execution_start": case "execution_start":
api.dispatchEvent( api.dispatchEvent(
new CustomEvent("execution_start", { detail: msg.data }) new CustomEvent("execution_start", { detail: msg.data }),
); );
break; break;
case "execution_error": case "execution_error":
api.dispatchEvent( api.dispatchEvent(
new CustomEvent("execution_error", { detail: msg.data }) new CustomEvent("execution_error", { detail: msg.data }),
); );
break; break;
case "execution_cached": case "execution_cached":
api.dispatchEvent( api.dispatchEvent(
new CustomEvent("execution_cached", { detail: msg.data }) new CustomEvent("execution_cached", { detail: msg.data }),
); );
break; break;
default: default:
@@ -152,13 +144,11 @@ const ext = {
} }
if (!workflow_version_id) { if (!workflow_version_id) {
console.error( console.error("No workflow_version_id provided in query parameters.");
"No workflow_version_id provided in query parameters."
);
} else { } else {
loadingDialog.showLoading( loadingDialog.showLoading(
"Loading workflow from " + org_display, "Loading workflow from " + org_display,
"Please wait..." "Please wait...",
); );
fetch(endpoint + "/api/workflow-version/" + workflow_version_id, { fetch(endpoint + "/api/workflow-version/" + workflow_version_id, {
method: "GET", method: "GET",
@@ -171,10 +161,7 @@ const ext = {
const data = await res.json(); const data = await res.json();
const { workflow, workflow_id, error } = data; const { workflow, workflow_id, error } = data;
if (error) { if (error) {
infoDialog.showMessage( infoDialog.showMessage("Unable to load this workflow", error);
"Unable to load this workflow",
error
);
return; return;
} }
@@ -197,7 +184,7 @@ const ext = {
window.history.replaceState( window.history.replaceState(
{}, {},
document.title, document.title,
window.location.pathname window.location.pathname,
); );
}); });
} }
@@ -227,7 +214,7 @@ const ext = {
multiline: false, multiline: false,
}, },
], ],
app app,
); );
ComfyWidgets.STRING( ComfyWidgets.STRING(
@@ -240,17 +227,14 @@ const ext = {
multiline: false, multiline: false,
}, },
], ],
app app,
); );
ComfyWidgets.STRING( ComfyWidgets.STRING(
this, this,
"version", "version",
[ ["", { default: this.properties.version, multiline: false }],
"", app,
{ default: this.properties.version, multiline: false },
],
app
); );
// this.widgets.forEach((w) => { // this.widgets.forEach((w) => {
@@ -277,7 +261,7 @@ const ext = {
title_mode: LiteGraph.NORMAL_TITLE, title_mode: LiteGraph.NORMAL_TITLE,
title: "Comfy Deploy", title: "Comfy Deploy",
collapsable: true, collapsable: true,
}) }),
); );
ComfyDeploy.category = "deploy"; ComfyDeploy.category = "deploy";
@@ -310,9 +294,7 @@ const ext = {
if (typeof api.handlePromptGenerated === "function") { if (typeof api.handlePromptGenerated === "function") {
api.handlePromptGenerated(prompt); api.handlePromptGenerated(prompt);
} else { } else {
console.warn( console.warn("api.handlePromptGenerated is not a function");
"api.handlePromptGenerated is not a function"
);
} }
sendEventToCD("cd_plugin_onQueuePrompt", prompt); sendEventToCD("cd_plugin_onQueuePrompt", prompt);
} else if (message.type === "get_prompt") { } else if (message.type === "get_prompt") {
@@ -346,13 +328,9 @@ const ext = {
const canvas = app.canvas; const canvas = app.canvas;
const targetScale = 1; const targetScale = 1;
const targetOffsetX = const targetOffsetX =
canvas.canvas.width / 4 - canvas.canvas.width / 4 - position[0] - node.size[0] / 2;
position[0] -
node.size[0] / 2;
const targetOffsetY = const targetOffsetY =
canvas.canvas.height / 4 - canvas.canvas.height / 4 - position[1] - node.size[1] / 2;
position[1] -
node.size[1] / 2;
const startScale = canvas.ds.scale; const startScale = canvas.ds.scale;
const startOffsetX = canvas.ds.offset[0]; const startOffsetX = canvas.ds.offset[0];
@@ -376,21 +354,9 @@ const ext = {
const easedT = easeOutCubic(t); const easedT = easeOutCubic(t);
const currentScale = lerp( const currentScale = lerp(startScale, targetScale, easedT);
startScale, const currentOffsetX = lerp(startOffsetX, targetOffsetX, easedT);
targetScale, const currentOffsetY = lerp(startOffsetY, targetOffsetY, easedT);
easedT
);
const currentOffsetX = lerp(
startOffsetX,
targetOffsetX,
easedT
);
const currentOffsetY = lerp(
startOffsetY,
targetOffsetY,
easedT
);
canvas.setZoom(currentScale); canvas.setZoom(currentScale);
canvas.ds.offset = [currentOffsetX, currentOffsetY]; canvas.ds.offset = [currentOffsetX, currentOffsetY];
@@ -442,7 +408,7 @@ const ext = {
function showError(title, message) { function showError(title, message) {
infoDialog.show( infoDialog.show(
`<h3 style="margin: 0px; color: red;">${title}</h3><br><span>${message}</span> ` `<h3 style="margin: 0px; color: red;">${title}</h3><br><span>${message}</span> `,
); );
} }
@@ -532,6 +498,7 @@ function createDynamicUIHtml(data) {
return html; return html;
} }
// Modify the existing deployWorkflow function
async function deployWorkflow() { async function deployWorkflow() {
const deploy = document.getElementById("deploy-button"); const deploy = document.getElementById("deploy-button");
@@ -550,7 +517,7 @@ async function deployWorkflow() {
if (deployMeta.length == 0) { if (deployMeta.length == 0) {
const text = await inputDialog.input( const text = await inputDialog.input(
"Create your deployment", "Create your deployment",
"Workflow name" "Workflow name",
); );
if (!text) return; if (!text) return;
console.log(text); console.log(text);
@@ -591,7 +558,7 @@ async function deployWorkflow() {
<input id="reuse-hash" type="checkbox" checked>Reuse hash from last version</input> <input id="reuse-hash" type="checkbox" checked>Reuse hash from last version</input>
</label> </label>
</div> </div>
` `,
); );
if (!ok) return; if (!ok) return;
@@ -610,7 +577,7 @@ async function deployWorkflow() {
if (!snapshot) { if (!snapshot) {
showError( showError(
"Error when deploying", "Error when deploying",
"Unable to generate snapshot, please install ComfyUI Manager" "Unable to generate snapshot, please install ComfyUI Manager",
); );
return; return;
} }
@@ -631,7 +598,7 @@ async function deployWorkflow() {
"Content-Type": "application/json", "Content-Type": "application/json",
Authorization: "Bearer " + apiKey, Authorization: "Bearer " + apiKey,
}, },
} },
) )
.then((x) => x.json()) .then((x) => x.json())
.catch(() => { .catch(() => {
@@ -650,7 +617,7 @@ async function deployWorkflow() {
// Match previous hash for models // Match previous hash for models
if (reuseHash && existing_workflow?.dependencies?.models) { if (reuseHash && existing_workflow?.dependencies?.models) {
const previousModelHash = Object.entries( const previousModelHash = Object.entries(
existing_workflow?.dependencies?.models existing_workflow?.dependencies?.models,
).flatMap(([key, value]) => { ).flatMap(([key, value]) => {
return Object.values(value).map((x) => ({ return Object.values(value).map((x) => ({
...x, ...x,
@@ -672,44 +639,36 @@ async function deployWorkflow() {
console.log(file); console.log(file);
loadingDialog.showLoading("Generating hash", file); loadingDialog.showLoading("Generating hash", file);
const hash = await fetch( const hash = await fetch(
`/comfyui-deploy/get-file-hash?file_path=${encodeURIComponent( `/comfyui-deploy/get-file-hash?file_path=${encodeURIComponent(file)}`,
file
)}`
).then((x) => x.json()); ).then((x) => x.json());
loadingDialog.showLoading("Generating hash", file); loadingDialog.showLoading("Generating hash", file);
console.log(hash); console.log(hash);
return hash.file_hash; return hash.file_hash;
}, },
handleFileUpload: async (file, hash, prevhash) => { // handleFileUpload: async (file, hash, prevhash) => {
console.log("Uploading ", file); // console.log("Uploading ", file);
loadingDialog.showLoading("Uploading file", file); // loadingDialog.showLoading("Uploading file", file);
try { // try {
const { download_url } = await fetch( // const { download_url } = await fetch(`/comfyui-deploy/upload-file`, {
`/comfyui-deploy/upload-file`, // method: "POST",
{ // body: JSON.stringify({
method: "POST", // file_path: file,
body: JSON.stringify({ // token: apiKey,
file_path: file, // url: endpoint + "/api/upload-url",
token: apiKey, // }),
url: endpoint + "/api/upload-url", // })
}), // .then((x) => x.json())
} // .catch(() => {
) // loadingDialog.close();
.then((x) => x.json()) // confirmDialog.confirm("Error", "Unable to upload file " + file);
.catch(() => { // });
loadingDialog.close(); // loadingDialog.showLoading("Uploaded file", file);
confirmDialog.confirm( // console.log(download_url);
"Error", // return download_url;
"Unable to upload file " + file // } catch (error) {
); // return undefined;
}); // }
loadingDialog.showLoading("Uploaded file", file); // },
console.log(download_url);
return download_url;
} catch (error) {
return undefined;
}
},
existingDependencies: existing_workflow.dependencies, existingDependencies: existing_workflow.dependencies,
}); });
@@ -734,12 +693,21 @@ async function deployWorkflow() {
"Check dependencies", "Check dependencies",
// JSON.stringify(deps, null, 2), // 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> <div style="position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%);">${loadingIcon}</div>
<iframe <iframe
style="z-index: 10; min-width: 600px; max-width: 1024px; min-height: 600px; border: none; background-color: transparent;" style="z-index: 10; min-width: 600px; max-width: 1024px; min-height: 600px; border: none; background-color: transparent;"
src="https://www.comfydeploy.com/dependency-graph?deps=${encodeURIComponent( src="https://www.comfydeploy.com/dependency-graph?deps=${encodeURIComponent(
JSON.stringify(deps) JSON.stringify(deps),
)}" />` )}" />`,
// createDynamicUIHtml(deps), // createDynamicUIHtml(deps),
); );
if (!depsOk) return; if (!depsOk) return;
@@ -800,9 +768,17 @@ async function deployWorkflow() {
graph.change(); graph.change();
infoDialog.show( infoDialog.show(
`<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/>` `<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(() => { setTimeout(() => {
title.textContent = "Deploy"; title.textContent = "Deploy";
title.style.color = "white"; title.style.color = "white";
@@ -820,6 +796,85 @@ 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() { function addButton() {
const menu = document.querySelector(".comfy-menu"); const menu = document.querySelector(".comfy-menu");
@@ -997,22 +1052,17 @@ export class InputDialog extends InfoDialog {
type: "button", type: "button",
textContent: "Save", textContent: "Save",
onclick: () => { onclick: () => {
const input = const input = this.textElement.querySelector("#input").value;
this.textElement.querySelector("#input").value;
if (input.trim() === "") { if (input.trim() === "") {
showError( showError("Input validation", "Input cannot be empty");
"Input validation",
"Input cannot be empty"
);
} else { } else {
this.callback?.(input); this.callback?.(input);
this.close(); this.close();
this.textElement.querySelector("#input").value = this.textElement.querySelector("#input").value = "";
"";
} }
}, },
}), }),
] ],
), ),
]; ];
} }
@@ -1073,7 +1123,7 @@ export class ConfirmDialog extends InfoDialog {
this.close(); this.close();
}, },
}), }),
] ],
), ),
]; ];
} }
@@ -1130,7 +1180,7 @@ function getData(environment) {
function saveData(data) { function saveData(data) {
localStorage.setItem( localStorage.setItem(
"comfy_deploy_env_data_" + data.environment, "comfy_deploy_env_data_" + data.environment,
JSON.stringify(data) JSON.stringify(data),
); );
} }
@@ -1145,9 +1195,7 @@ export class ConfigDialog extends ComfyDialog {
this.element.style.paddingBottom = "20px"; this.element.style.paddingBottom = "20px";
this.container = document.createElement("div"); this.container = document.createElement("div");
this.element this.element.querySelector(".comfy-modal-content").prepend(this.container);
.querySelector(".comfy-modal-content")
.prepend(this.container);
} }
createButtons() { createButtons() {
@@ -1181,7 +1229,7 @@ export class ConfigDialog extends ComfyDialog {
this.close(); this.close();
}, },
}), }),
] ],
), ),
]; ];
} }
@@ -1193,8 +1241,7 @@ export class ConfigDialog extends ComfyDialog {
} }
save(api_key, displayName) { save(api_key, displayName) {
const deployOption = const deployOption = this.container.querySelector("#deployOption").value;
this.container.querySelector("#deployOption").value;
localStorage.setItem("comfy_deploy_env", deployOption); localStorage.setItem("comfy_deploy_env", deployOption);
const endpoint = this.container.querySelector("#endpoint").value; const endpoint = this.container.querySelector("#endpoint").value;
@@ -1226,12 +1273,8 @@ export class ConfigDialog extends ComfyDialog {
<h3 style="margin: 0px;">Comfy Deploy Config</h3> <h3 style="margin: 0px;">Comfy Deploy Config</h3>
<label style="color: white; width: 100%;"> <label style="color: white; width: 100%;">
<select id="deployOption" style="margin: 8px 0px; width: 100%; height:30px; box-sizing: border-box;" > <select id="deployOption" style="margin: 8px 0px; width: 100%; height:30px; box-sizing: border-box;" >
<option value="cloud" ${ <option value="cloud" ${data.environment === "cloud" ? "selected" : ""}>Cloud</option>
data.environment === "cloud" ? "selected" : "" <option value="local" ${data.environment === "local" ? "selected" : ""}>Local</option>
}>Cloud</option>
<option value="local" ${
data.environment === "local" ? "selected" : ""
}>Local</option>
</select> </select>
</label> </label>
<label style="color: white; width: 100%;"> <label style="color: white; width: 100%;">
@@ -1249,9 +1292,7 @@ export class ConfigDialog extends ComfyDialog {
}"> }">
<button id="loginButton" style="margin-top: 8px; width: 100%; height:40px; box-sizing: border-box; padding: 0px 6px;"> <button id="loginButton" style="margin-top: 8px; width: 100%; height:40px; box-sizing: border-box; padding: 0px 6px;">
${ ${
data.apiKey data.apiKey ? "Re-login with ComfyDeploy" : "Login with ComfyDeploy"
? "Re-login with ComfyDeploy"
: "Login with ComfyDeploy"
} }
</button> </button>
</div> </div>
@@ -1272,7 +1313,7 @@ export class ConfigDialog extends ComfyDialog {
clearInterval(poll); clearInterval(poll);
infoDialog.showMessage( infoDialog.showMessage(
"Timeout", "Timeout",
"Wait too long for the response, please try re-login" "Wait too long for the response, please try re-login",
); );
}, 30000); // Stop polling after 30 seconds }, 30000); // Stop polling after 30 seconds
@@ -1283,15 +1324,14 @@ export class ConfigDialog extends ComfyDialog {
if (json.api_key) { if (json.api_key) {
this.save(json.api_key, json.name); this.save(json.api_key, json.name);
this.close(); this.close();
this.container.querySelector("#apiKey").value = this.container.querySelector("#apiKey").value = json.api_key;
json.api_key;
// infoDialog.show(); // infoDialog.show();
clearInterval(this.poll); clearInterval(this.poll);
clearTimeout(this.timeout); clearTimeout(this.timeout);
// Refresh dialog // Refresh dialog
const a = await confirmDialog.confirm( const a = await confirmDialog.confirm(
"Authenticated", "Authenticated",
`<div>You will be able to upload workflow to <button style="font-size: 18px; width: fit;">${json.name}</button></div>` `<div>You will be able to upload workflow to <button style="font-size: 18px; width: fit;">${json.name}</button></div>`,
); );
configDialog.show(); configDialog.show();
} }
@@ -1327,3 +1367,118 @@ export class ConfigDialog extends ComfyDialog {
} }
export const configDialog = new ConfigDialog(); 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
}
}