From e011711600874689f520a72b667350cc1f011e94 Mon Sep 17 00:00:00 2001 From: bennykok Date: Mon, 9 Sep 2024 17:49:39 -0700 Subject: [PATCH 1/7] feat: zip batch image support --- comfy-nodes/external_image_batch.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/comfy-nodes/external_image_batch.py b/comfy-nodes/external_image_batch.py index 6d1587c..4004882 100644 --- a/comfy-nodes/external_image_batch.py +++ b/comfy-nodes/external_image_batch.py @@ -39,18 +39,38 @@ 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 + 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'): - import requests from io import BytesIO print("Fetching image from url: ", img_input) response = requests.get(img_input) image = Image.open(BytesIO(response.content)) + elif 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('data:image/png;base64,') or img_input.startswith('data:image/jpeg;base64,') or img_input.startswith('data:image/jpg;base64,'): import base64 from io import BytesIO From 71d60a5dd11bba92bbf309ed5f80a4c333e5ce53 Mon Sep 17 00:00:00 2001 From: EdwinWong Date: Tue, 10 Sep 2024 01:03:50 -0700 Subject: [PATCH 2/7] fix: comfydeploy node backward compatible in every comfyui --- web-plugin/index.js | 177 ++++++++++++++++++++++---------------------- 1 file changed, 89 insertions(+), 88 deletions(-) diff --git a/web-plugin/index.js b/web-plugin/index.js index d269693..38fb9f7 100644 --- a/web-plugin/index.js +++ b/web-plugin/index.js @@ -192,11 +192,13 @@ const ext = { registerCustomNodes() { /** @type {LGraphNode}*/ - class ComfyDeploy { - color = LGraphCanvas.node_colors.yellow.color; - bgcolor = LGraphCanvas.node_colors.yellow.bgcolor; - groupcolor = LGraphCanvas.node_colors.yellow.groupcolor; + class ComfyDeploy extends LGraphNode { 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 = ""; @@ -204,65 +206,75 @@ const ext = { this.properties.version = ""; } - ComfyWidgets.STRING( - this, + this.addWidget( + "text", "workflow_name", - [ - "", - { - default: this.properties.workflow_name, - multiline: false, - }, - ], - app, + this.properties.workflow_name, + (v) => { + this.properties.workflow_name = v; + }, + { multiline: false } ); - ComfyWidgets.STRING( - this, + this.addWidget( + "text", "workflow_id", - [ - "", - { - default: this.properties.workflow_id, - multiline: false, - }, - ], - app, + this.properties.workflow_id, + (v) => { + this.properties.workflow_id = v; + }, + { multiline: false } ); - ComfyWidgets.STRING( - this, + this.addWidget( + "text", "version", - ["", { default: this.properties.version, multiline: false }], - app, + this.properties.version, + (v) => { + this.properties.version = v; + }, + { multiline: false } ); - // 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"; + } + } } - // Load default visibility - - LiteGraph.registerNodeType( - "ComfyDeploy", - Object.assign(ComfyDeploy, { - title_mode: LiteGraph.NORMAL_TITLE, - title: "Comfy Deploy", - collapsable: true, - }), - ); + // Register the node type + LiteGraph.registerNodeType("ComfyDeploy", Object.assign(ComfyDeploy, { + title: "Comfy Deploy", + title_mode: LiteGraph.NORMAL_TITLE, + collapsable: true, + })); ComfyDeploy.category = "deploy"; }, @@ -431,10 +443,10 @@ function createDynamicUIHtml(data) {

Missing Nodes

These nodes are not found with any matching custom_nodes in the ComfyUI Manager Database

${data.missing_nodes - .map((node) => { - return `

${node}

`; - }) - .join("")} + .map((node) => { + return `

${node}

`; + }) + .join("")} `; } @@ -442,17 +454,14 @@ function createDynamicUIHtml(data) { Object.values(data.custom_nodes).forEach((node) => { html += `
- ${ - node.name - } + ${node.name + }

${node.hash}

- ${ - node.warning - ? `

${node.warning}

` - : "" - } + ${node.warning + ? `

${node.warning}

` + : "" + }
`; }); @@ -466,9 +475,8 @@ function createDynamicUIHtml(data) { Object.entries(data.models).forEach(([section, items]) => { html += `
-

${ - section.charAt(0).toUpperCase() + section.slice(1) - }

`; +

${section.charAt(0).toUpperCase() + section.slice(1) + }

`; items.forEach((item) => { html += `

${item.name}

`; }); @@ -484,9 +492,8 @@ function createDynamicUIHtml(data) { Object.entries(data.files).forEach(([section, items]) => { html += `
-

${ - section.charAt(0).toUpperCase() + section.slice(1) - }

`; +

${section.charAt(0).toUpperCase() + section.slice(1) + }

`; items.forEach((item) => { html += `

${item.name}

`; }); @@ -1006,14 +1013,12 @@ export class LoadingDialog extends ComfyDialog { showLoading(title, message) { this.show(`
-

${title} ${ - this.loadingIcon - }

- ${ - message - ? `` - : "" - } +

${title} ${this.loadingIcon + }

+ ${message + ? `` + : "" + }
`); } @@ -1279,21 +1284,17 @@ export class ConfigDialog extends ComfyDialog {
- API Key: User / Org - + API Key: User / Org +
From 2d72cd8175c63e90c443594b5d636615e289c5a9 Mon Sep 17 00:00:00 2001 From: bennykok Date: Sat, 14 Sep 2024 21:49:17 -0700 Subject: [PATCH 3/7] fix: batch zip image input --- comfy-nodes/external_image_batch.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/comfy-nodes/external_image_batch.py b/comfy-nodes/external_image_batch.py index 4004882..03c8474 100644 --- a/comfy-nodes/external_image_batch.py +++ b/comfy-nodes/external_image_batch.py @@ -56,12 +56,7 @@ class ComfyUIDeployExternalImageBatch: 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'): - from io import BytesIO - print("Fetching image from url: ", img_input) - response = requests.get(img_input) - image = Image.open(BytesIO(response.content)) - elif img_input.startswith('http') and img_input.endswith('.zip'): + 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)) @@ -71,6 +66,11 @@ class ComfyUIDeployExternalImageBatch: image = Image.open(file) image = self.process_image(image) processed_images.append(image) + elif img_input.startswith('http'): + from io import BytesIO + print("Fetching image from url: ", img_input) + response = requests.get(img_input) + image = Image.open(BytesIO(response.content)) elif img_input.startswith('data:image/png;base64,') or img_input.startswith('data:image/jpeg;base64,') or img_input.startswith('data:image/jpg;base64,'): import base64 from io import BytesIO From 65f757674800bbfe79dfc76c02f90f082e897d1b Mon Sep 17 00:00:00 2001 From: karrix Date: Mon, 16 Sep 2024 12:45:53 -0700 Subject: [PATCH 4/7] fix: non type error when upload output --- custom_routes.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/custom_routes.py b/custom_routes.py index 6af88a3..ba974da 100644 --- a/custom_routes.py +++ b/custom_routes.py @@ -1407,7 +1407,9 @@ async def update_run_with_output(prompt_id, data, node_id=None, node_meta=None): "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 = False + if data is not None: + 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 From 3d099f88ea8799a80be9b860793e9c64a7cf4843 Mon Sep 17 00:00:00 2001 From: bennykok Date: Mon, 16 Sep 2024 13:55:02 -0700 Subject: [PATCH 5/7] fix: back to sequential file upload --- custom_routes.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/custom_routes.py b/custom_routes.py index ba974da..979b298 100644 --- a/custom_routes.py +++ b/custom_routes.py @@ -1338,7 +1338,7 @@ 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 = [] + # upload_tasks = [] for item in items: # Skipping temp files @@ -1354,17 +1354,25 @@ 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( + # upload_tasks.append(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) + # 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, node_meta=None): @@ -1407,6 +1415,7 @@ async def update_run_with_output(prompt_id, data, node_id=None, node_meta=None): "output_data": data, "node_meta": node_meta, } + 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 From 1f5a88b88805f8f01ba1803b0ecec2e796937417 Mon Sep 17 00:00:00 2001 From: bennykok Date: Mon, 16 Sep 2024 23:55:16 -0700 Subject: [PATCH 6/7] Revert "fix: back to sequential file upload" This reverts commit 3d099f88ea8799a80be9b860793e9c64a7cf4843. --- custom_routes.py | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/custom_routes.py b/custom_routes.py index 979b298..ba974da 100644 --- a/custom_routes.py +++ b/custom_routes.py @@ -1338,7 +1338,7 @@ 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 = [] + upload_tasks = [] for item in items: # Skipping temp files @@ -1354,25 +1354,17 @@ 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( - # prompt_id, - # item.get("filename"), - # subfolder=item.get("subfolder"), - # type=item.get("type"), - # content_type=file_type, - # item=item - # )) - 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, item=item - ) + )) # Execute all upload tasks concurrently - # await asyncio.gather(*upload_tasks) + 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, node_meta=None): @@ -1415,7 +1407,6 @@ async def update_run_with_output(prompt_id, data, node_id=None, node_meta=None): "output_data": data, "node_meta": node_meta, } - 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 From 5a78ca97bd1eeeb8de6d96a18aa9f1a2d51869b6 Mon Sep 17 00:00:00 2001 From: bennykok Date: Mon, 16 Sep 2024 23:57:40 -0700 Subject: [PATCH 7/7] fix: roll back to unique session per request --- custom_routes.py | 121 ++++++++++++++++++++++++----------------------- 1 file changed, 61 insertions(+), 60 deletions(-) diff --git a/custom_routes.py b/custom_routes.py index ba974da..af99bf4 100644 --- a/custom_routes.py +++ b/custom_routes.py @@ -28,27 +28,27 @@ from aiohttp import web, ClientSession, ClientError, ClientTimeout import atexit # Global session -client_session = None +# client_session = None # def create_client_session(): # 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: - client_session = aiohttp.ClientSession() +# async def ensure_client_session(): +# global client_session +# if client_session is None: +# client_session = aiohttp.ClientSession() -async def cleanup(): - global client_session - if client_session: - await client_session.close() +# 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() - loop.run_until_complete(cleanup()) + # loop = asyncio.get_event_loop() + # loop.run_until_complete(cleanup()) atexit.register(exit_handler) @@ -60,62 +60,63 @@ print(f"max_retries: {max_retries}, retry_delay_multiplier: {retry_delay_multipl import time async def async_request_with_retry(method, url, disable_timeout=False, token=None, **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 + # 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 - 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 + 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}" + 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") + 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") - 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}") + # 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}") - 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") + if attempt == max_retries - 1: + logger.error(f"Request failed after {max_retries} attempts: {e}") + raise - # 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 + 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") + total_time = time.time() - start_time + raise Exception(f"Request failed after {max_retries} attempts and {total_time:.2f} seconds") from logging import basicConfig, getLogger