├── .gitignore ├── Comflyapi.json ├── LICENSE ├── README.md ├── __init__.py ├── chatgpt_api.py ├── requirements.txt ├── utils.py └── workflow └── comfyui-chatgpt-api.json /.gitignore: -------------------------------------------------------------------------------- 1 | # API key 2 | Comflyapi.json merge=ours 3 | 4 | # Byte-compiled / optimized / DLL files 5 | __pycache__/ 6 | *.py[cod] 7 | *$py.class 8 | 9 | # C extensions 10 | *.so 11 | 12 | # Distribution / packaging 13 | .Python 14 | build/ 15 | develop-eggs/ 16 | dist/ 17 | downloads/ 18 | eggs/ 19 | .eggs/ 20 | lib/ 21 | lib64/ 22 | parts/ 23 | sdist/ 24 | var/ 25 | wheels/ 26 | share/python-wheels/ 27 | *.egg-info/ 28 | .installed.cfg 29 | *.egg 30 | MANIFEST 31 | 32 | # PyInstaller 33 | # Usually these files are written by a python script from a template 34 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 35 | *.manifest 36 | *.spec 37 | 38 | # Installer logs 39 | pip-log.txt 40 | pip-delete-this-directory.txt 41 | 42 | # Unit test / coverage reports 43 | htmlcov/ 44 | .tox/ 45 | .nox/ 46 | .coverage 47 | .coverage.* 48 | .cache 49 | nosetests.xml 50 | coverage.xml 51 | *.cover 52 | *.py,cover 53 | .hypothesis/ 54 | .pytest_cache/ 55 | cover/ 56 | 57 | # Translations 58 | *.mo 59 | *.pot 60 | 61 | # Django stuff: 62 | *.log 63 | local_settings.py 64 | db.sqlite3 65 | db.sqlite3-journal 66 | 67 | # Flask stuff: 68 | instance/ 69 | .webassets-cache 70 | 71 | # Scrapy stuff: 72 | .scrapy 73 | 74 | # Sphinx documentation 75 | docs/_build/ 76 | 77 | # PyBuilder 78 | .pybuilder/ 79 | target/ 80 | 81 | # Jupyter Notebook 82 | .ipynb_checkpoints 83 | 84 | # IPython 85 | profile_default/ 86 | ipython_config.py 87 | 88 | # pyenv 89 | # For a library or package, you might want to ignore these files since the code is 90 | # intended to run in multiple environments; otherwise, check them in: 91 | # .python-version 92 | 93 | # pipenv 94 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 95 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 96 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 97 | # install all needed dependencies. 98 | #Pipfile.lock 99 | 100 | # UV 101 | # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. 102 | # This is especially recommended for binary packages to ensure reproducibility, and is more 103 | # commonly ignored for libraries. 104 | #uv.lock 105 | 106 | # poetry 107 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 108 | # This is especially recommended for binary packages to ensure reproducibility, and is more 109 | # commonly ignored for libraries. 110 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 111 | #poetry.lock 112 | 113 | # pdm 114 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 115 | #pdm.lock 116 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 117 | # in version control. 118 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control 119 | .pdm.toml 120 | .pdm-python 121 | .pdm-build/ 122 | 123 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 124 | __pypackages__/ 125 | 126 | # Celery stuff 127 | celerybeat-schedule 128 | celerybeat.pid 129 | 130 | # SageMath parsed files 131 | *.sage.py 132 | 133 | # Environments 134 | .env 135 | .venv 136 | env/ 137 | venv/ 138 | ENV/ 139 | env.bak/ 140 | venv.bak/ 141 | 142 | # Spyder project settings 143 | .spyderproject 144 | .spyproject 145 | 146 | # Rope project settings 147 | .ropeproject 148 | 149 | # mkdocs documentation 150 | /site 151 | 152 | # mypy 153 | .mypy_cache/ 154 | .dmypy.json 155 | dmypy.json 156 | 157 | # Pyre type checker 158 | .pyre/ 159 | 160 | # pytype static type analyzer 161 | .pytype/ 162 | 163 | # Cython debug symbols 164 | cython_debug/ 165 | 166 | # PyCharm 167 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 168 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 169 | # and can be added to the global gitignore or merged into this file. For a more nuclear 170 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 171 | #.idea/ 172 | 173 | # Ruff stuff: 174 | .ruff_cache/ 175 | 176 | # PyPI configuration file 177 | .pypirc 178 | -------------------------------------------------------------------------------- /Comflyapi.json: -------------------------------------------------------------------------------- 1 | { 2 | "api_key": "" 3 | } 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | > **Warning** 5 | > 6 | > 插件只在windows11上测试,mac电脑后续我也会测试,如果其他系统有任何问题,可以提交issues 7 | > 速度不快,因为官网速度也不快,所以需要点耐心。 8 | 9 | # 插件 Node: 10 | 11 | 20250429: 12 | 13 | `Chatgpt节点`: Comfyui_gpt_image_1_edit新增chats输出口,输出多轮对话。 14 | 新增clear_chats,当为Ture的时候,只能image输入什么图片修改什么图片,不支持显示上下文对话。 15 | 当为Flase的时候,支持对上一次生成的图片进行二次修改。支持显示上下文对话。并且支持多图模式下新增图片参考。 16 | 17 |
18 | 查看更新/Update 19 | 20 | ![2eaf76b077612170647f6861e43e2af](https://github.com/user-attachments/assets/1c4c484f-c3c6-48c6-96c5-58c4ef4e59d5) 21 | 22 | ![6a43cb051fece84815ac6036bee3a4c](https://github.com/user-attachments/assets/f0fbf71e-8cfb-448e-87cd-1e147bb2f552) 23 | 24 |
25 | 26 | 27 | 20250425: 28 | 29 | `Chatgpt节点`: 30 | 新增Comfyui_gpt_image_1和Comfyui_gpt_image_1_edit官方gpt_image_1模型api接口节点。 31 | 32 | ![image](https://github.com/user-attachments/assets/9d08d5fc-dde9-4523-955c-31652a74f1a5) 33 | 34 | 模型名都是gpt_image_1,区别只是分组不同: 35 | 36 | 一共四个分组:default默认分组为官方逆向,价格便宜,缺点就是不稳定,速度慢。按次收费。不支持额外参数选择。这个分组的apikey只能用于ComfyuiChatGPTApi节点。 37 | 38 | 其他三个组都是官方api组,最优惠的目前是ssvip组。分组需要再令牌里面去修改选择。这3个官方分组优点就是速度快,稳定性高。支持官方参数调整。 39 | 缺点就是贵,但是也比官方便宜。大家可以按照自己的情况选择。这3个分组的令牌的apikey只能用在下面2个新节点上面!!! 40 | 41 | 1. Comfyui_gpt_image_1 节点:文生图,有耕读参数调整,支持调整生图限制为low。 42 | 43 | 2. Comfyui_gpt_image_1_edit 节点:图生图,支持mask遮罩,支持多图参考。 44 | 45 |
46 | 查看更新/Update 47 | 48 | ![3bc790641c44e373aca97ea4a1de47e](https://github.com/user-attachments/assets/1a7a0615-46e5-46b3-af04-32246a23d6f4) 49 | 50 | ![5efe58fcf7055d675962f40c1ad1cbb](https://github.com/user-attachments/assets/8a90eab5-4242-43bb-ae01-74493b90b6ce) 51 | 52 |
53 | 54 | 55 | 56 | 20250424: 57 | 58 | `Chatgpt节点`: 59 | 60 | ComfyuiChatGPTApi节点新增官方gpt-image-1,按次计费 0.06, 61 | ComfyuiChatGPTApi节点新增chats输出口,输出多轮对话。 62 | 新增clear_chats,当为Ture的时候,只能image输入什么图片修改什么图片,不支持显示上下文对话。 63 | 当为Flase的时候,支持对上一次生成的图片进行二次修改。支持显示上下文对话。 64 | 65 |
66 | 查看更新/Update 67 | 68 | ![cad243f2bf4a3aa11163f1a007db469](https://github.com/user-attachments/assets/ef0f6a34-3de7-42a2-8543-c1930575e1bb) 69 | 70 | ![bd6493050affdf156143c8dc5286988](https://github.com/user-attachments/assets/0906caf3-35ec-4061-bfc9-5f611a19abf2) 71 | 72 | ![e5b3d375b700dcbf921b12a8aa527c4](https://github.com/user-attachments/assets/75537100-e5d2-403c-b2e0-1f662680092f) 73 | 74 | 75 |
76 | 77 | 78 | `Chatgpt节点`: 79 | 80 | 新增openai的comfyui-chatgpt-api节点,。 81 | 目前单图和多图输入,文本输入,生成图片,图片编辑.使用的是 https://ai.comfly.chat 的 api key 82 | 固定一次生成消耗0.06元(显示是逆向api,稳定性还不高,想尝鲜的可以注册网站用免费送的0.2美金玩玩) 83 | 速度不快,因为官网速度也不快,所以需要点耐心。 files输入接口还没有完善,先忽略。 84 | 85 | 86 | 87 | ![fdedd73cffa278d2a8cf81478b58e90](https://github.com/user-attachments/assets/36e78cdd-33b2-41ed-a15c-ad9c1886bede) 88 | 89 | 90 | ![0a2394c0b41efe190a5d0880f4c584b](https://github.com/user-attachments/assets/267fbe73-7113-4120-a829-a7aa2247bd4d) 91 | 92 | 93 | 94 | # 🥵 Comfly的QQ群 / my wechat 95 | 96 | ![86601b471a343671e7240c74aa8e1fd](https://github.com/ainewsto/Comfyui_Comfly/assets/113163264/3e1c2d15-ba5b-4aa5-a76b-08f87e7c8e2c) 97 | 98 | ![86601b471a343671e7240c74aa8e1fd](https://github.com/ainewsto/Comfyui_Comfly/assets/113163264/fdc2f849-5937-4cce-a36d-8444ecca3030) 99 | 100 | 101 | # :dizzy:插件有参考项目 Comfy Resources: 102 | 103 | 感谢原项目: 104 | https://github.com/comfyanonymous/ComfyUI 105 | 106 | 107 | ## 🚀 About me 108 | * website: https://comfly.chat 109 | * Welcome valuable suggestions! 📧 **Email**: [3508432500@qq.com](mailto:1544007699@qq.com) 110 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | from .chatgpt_api import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS 2 | 3 | __all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS'] 4 | 5 | 6 | -------------------------------------------------------------------------------- /chatgpt_api.py: -------------------------------------------------------------------------------- 1 | import mimetypes 2 | import os 3 | import io 4 | import math 5 | import random 6 | import torch 7 | import requests 8 | import time 9 | from PIL import Image 10 | from io import BytesIO 11 | import json 12 | import comfy.utils 13 | import re 14 | import aiohttp 15 | import asyncio 16 | import base64 17 | import uuid 18 | import numpy as np 19 | import folder_paths 20 | from .utils import pil2tensor, tensor2pil 21 | from comfy.utils import common_upscale 22 | 23 | 24 | def get_config(): 25 | try: 26 | config_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'Comflyapi.json') 27 | with open(config_path, 'r') as f: 28 | config = json.load(f) 29 | return config 30 | except: 31 | return {} 32 | 33 | def save_config(config): 34 | config_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), 'Comflyapi.json') 35 | with open(config_path, 'w') as f: 36 | json.dump(config, f, indent=4) 37 | 38 | 39 | # reference: OpenAIGPTImage1 node from comfyui node 40 | def downscale_input(image): 41 | samples = image.movedim(-1,1) 42 | 43 | total = int(1536 * 1024) 44 | scale_by = math.sqrt(total / (samples.shape[3] * samples.shape[2])) 45 | if scale_by >= 1: 46 | return image 47 | width = round(samples.shape[3] * scale_by) 48 | height = round(samples.shape[2] * scale_by) 49 | 50 | s = common_upscale(samples, width, height, "lanczos", "disabled") 51 | s = s.movedim(1,-1) 52 | return s 53 | 54 | class Comfyui_gpt_image_1_edit: 55 | 56 | _last_edited_image = None 57 | _conversation_history = [] 58 | 59 | @classmethod 60 | def INPUT_TYPES(cls): 61 | return { 62 | "required": { 63 | "image": ("IMAGE",), 64 | "prompt": ("STRING", {"multiline": True}), 65 | }, 66 | "optional": { 67 | "mask": ("MASK",), 68 | "api_key": ("STRING", {"default": ""}), 69 | "model": ("STRING", {"default": "gpt-image-1"}), 70 | "n": ("INT", {"default": 1, "min": 1, "max": 10}), 71 | "quality": (["auto", "high", "medium", "low"], {"default": "auto"}), 72 | "size": (["auto", "1024x1024", "1536x1024", "1024x1536"], {"default": "auto"}), 73 | "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}), 74 | "clear_chats": ("BOOLEAN", {"default": True}), 75 | } 76 | } 77 | 78 | RETURN_TYPES = ("IMAGE", "STRING", "STRING") 79 | RETURN_NAMES = ("edited_image", "response", "chats") 80 | FUNCTION = "edit_image" 81 | CATEGORY = "ainewsto/Chatgpt" 82 | 83 | def __init__(self): 84 | self.api_key = get_config().get('api_key', '') 85 | self.timeout = 300 86 | 87 | def get_headers(self): 88 | return { 89 | "Authorization": f"Bearer {self.api_key}" 90 | } 91 | 92 | def format_conversation_history(self): 93 | """Format the conversation history for display""" 94 | if not Comfyui_gpt_image_1_edit._conversation_history: 95 | return "" 96 | formatted_history = "" 97 | for entry in Comfyui_gpt_image_1_edit._conversation_history: 98 | formatted_history += f"**User**: {entry['user']}\n\n" 99 | formatted_history += f"**AI**: {entry['ai']}\n\n" 100 | formatted_history += "---\n\n" 101 | return formatted_history.strip() 102 | 103 | def edit_image(self, image, prompt, model="gpt-image-1", n=1, quality="auto", 104 | seed=0, mask=None, api_key="", size="auto", clear_chats=True): 105 | if api_key.strip(): 106 | self.api_key = api_key 107 | config = get_config() 108 | config['api_key'] = api_key 109 | save_config(config) 110 | 111 | original_image = image 112 | original_batch_size = image.shape[0] 113 | use_saved_image = False 114 | 115 | if not clear_chats and Comfyui_gpt_image_1_edit._last_edited_image is not None: 116 | if original_batch_size > 1: 117 | last_batch_size = Comfyui_gpt_image_1_edit._last_edited_image.shape[0] 118 | last_image_first = Comfyui_gpt_image_1_edit._last_edited_image[0:1] 119 | if last_image_first.shape[1:] == original_image.shape[1:]: 120 | image = torch.cat([last_image_first, original_image[1:]], dim=0) 121 | use_saved_image = True 122 | else: 123 | 124 | image = Comfyui_gpt_image_1_edit._last_edited_image 125 | use_saved_image = True 126 | 127 | if clear_chats: 128 | Comfyui_gpt_image_1_edit._conversation_history = [] 129 | 130 | 131 | try: 132 | if not self.api_key: 133 | error_message = "API key not found in Comflyapi.json" 134 | print(error_message) 135 | return (original_image, error_message, self.format_conversation_history()) 136 | 137 | pbar = comfy.utils.ProgressBar(100) 138 | pbar.update_absolute(10) 139 | 140 | files = {} 141 | 142 | if image is not None: 143 | batch_size = image.shape[0] 144 | for i in range(batch_size): 145 | single_image = image[i:i+1] 146 | scaled_image = downscale_input(single_image).squeeze() 147 | 148 | image_np = (scaled_image.numpy() * 255).astype(np.uint8) 149 | img = Image.fromarray(image_np) 150 | img_byte_arr = io.BytesIO() 151 | img.save(img_byte_arr, format='PNG') 152 | img_byte_arr.seek(0) 153 | 154 | if batch_size == 1: 155 | files['image'] = ('image.png', img_byte_arr, 'image/png') 156 | else: 157 | if 'image[]' not in files: 158 | files['image[]'] = [] 159 | files['image[]'].append(('image_{}.png'.format(i), img_byte_arr, 'image/png')) 160 | 161 | if mask is not None: 162 | if image.shape[0] != 1: 163 | raise Exception("Cannot use a mask with multiple images") 164 | if image is None: 165 | raise Exception("Cannot use a mask without an input image") 166 | if mask.shape[1:] != image.shape[1:-1]: 167 | raise Exception("Mask and Image must be the same size") 168 | 169 | batch, height, width = mask.shape 170 | rgba_mask = torch.zeros(height, width, 4, device="cpu") 171 | rgba_mask[:,:,3] = (1-mask.squeeze().cpu()) 172 | scaled_mask = downscale_input(rgba_mask.unsqueeze(0)).squeeze() 173 | mask_np = (scaled_mask.numpy() * 255).astype(np.uint8) 174 | mask_img = Image.fromarray(mask_np) 175 | mask_byte_arr = io.BytesIO() 176 | mask_img.save(mask_byte_arr, format='PNG') 177 | mask_byte_arr.seek(0) 178 | files['mask'] = ('mask.png', mask_byte_arr, 'image/png') 179 | 180 | if 'image[]' in files: 181 | 182 | data = { 183 | 'prompt': prompt, 184 | 'model': model, 185 | 'n': str(n), 186 | 'quality': quality 187 | } 188 | 189 | if size != "auto": 190 | data['size'] = size 191 | 192 | image_files = [] 193 | for file_tuple in files['image[]']: 194 | image_files.append(('image', file_tuple)) 195 | 196 | if 'mask' in files: 197 | image_files.append(('mask', files['mask'])) 198 | 199 | response = requests.post( 200 | "https://ai.comfly.chat/v1/images/edits", 201 | headers=self.get_headers(), 202 | data=data, 203 | files=image_files, 204 | timeout=self.timeout 205 | ) 206 | else: 207 | data = { 208 | 'prompt': prompt, 209 | 'model': model, 210 | 'n': str(n), 211 | 'quality': quality 212 | } 213 | 214 | if size != "auto": 215 | data['size'] = size 216 | 217 | request_files = [] 218 | 219 | if 'image' in files: 220 | request_files.append(('image', files['image'])) 221 | 222 | if 'mask' in files: 223 | request_files.append(('mask', files['mask'])) 224 | 225 | response = requests.post( 226 | "https://ai.comfly.chat/v1/images/edits", 227 | headers=self.get_headers(), 228 | data=data, 229 | files=request_files, 230 | timeout=self.timeout 231 | ) 232 | 233 | pbar.update_absolute(50) 234 | 235 | if response.status_code != 200: 236 | error_message = f"API Error: {response.status_code} - {response.text}" 237 | print(error_message) 238 | return (original_image, error_message, self.format_conversation_history()) 239 | result = response.json() 240 | 241 | if "data" not in result or not result["data"]: 242 | error_message = "No image data in response" 243 | print(error_message) 244 | return (original_image, error_message, self.format_conversation_history()) 245 | 246 | edited_images = [] 247 | image_urls = [] 248 | 249 | for item in result["data"]: 250 | if "b64_json" in item: 251 | image_data = base64.b64decode(item["b64_json"]) 252 | edited_image = Image.open(BytesIO(image_data)) 253 | edited_tensor = pil2tensor(edited_image) 254 | edited_images.append(edited_tensor) 255 | elif "url" in item: 256 | image_urls.append(item["url"]) 257 | try: 258 | img_response = requests.get(item["url"]) 259 | if img_response.status_code == 200: 260 | edited_image = Image.open(BytesIO(img_response.content)) 261 | edited_tensor = pil2tensor(edited_image) 262 | edited_images.append(edited_tensor) 263 | except Exception as e: 264 | print(f"Error downloading image from URL: {str(e)}") 265 | 266 | pbar.update_absolute(90) 267 | 268 | if edited_images: 269 | combined_tensor = torch.cat(edited_images, dim=0) 270 | response_info = f"Successfully edited {len(edited_images)} image(s)\n" 271 | response_info += f"Prompt: {prompt}\n" 272 | response_info += f"Model: {model}\n" 273 | response_info += f"Quality: {quality}\n" 274 | 275 | if use_saved_image: 276 | response_info += "[Using previous edited image as input]\n" 277 | 278 | if size != "auto": 279 | response_info += f"Size: {size}\n" 280 | 281 | Comfyui_gpt_image_1_edit._conversation_history.append({ 282 | "user": f"Edit image with prompt: {prompt}", 283 | "ai": f"Generated edited image with {model}" 284 | }) 285 | 286 | Comfyui_gpt_image_1_edit._last_edited_image = combined_tensor 287 | 288 | pbar.update_absolute(100) 289 | return (combined_tensor, response_info, self.format_conversation_history()) 290 | else: 291 | error_message = "No edited images in response" 292 | print(error_message) 293 | return (original_image, error_message, self.format_conversation_history()) 294 | 295 | except Exception as e: 296 | error_message = f"Error in image editing: {str(e)}" 297 | import traceback 298 | print(traceback.format_exc()) 299 | print(error_message) 300 | return (original_image, error_message, self.format_conversation_history()) 301 | 302 | 303 | 304 | class Comfyui_gpt_image_1: 305 | @classmethod 306 | def INPUT_TYPES(cls): 307 | return { 308 | "required": { 309 | "prompt": ("STRING", {"multiline": True}), 310 | }, 311 | "optional": { 312 | "api_key": ("STRING", {"default": ""}), 313 | "model": ("STRING", {"default": "gpt-image-1"}), 314 | "n": ("INT", {"default": 1, "min": 1, "max": 10}), 315 | "quality": (["auto", "high", "medium", "low"], {"default": "auto"}), 316 | "size": (["auto", "1024x1024", "1536x1024", "1024x1536"], {"default": "auto"}), 317 | "background": (["auto", "transparent", "opaque"], {"default": "auto"}), 318 | "output_format": (["png", "jpeg", "webp"], {"default": "png"}), 319 | "moderation": (["auto", "low"], {"default": "auto"}), 320 | "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}), 321 | } 322 | } 323 | 324 | RETURN_TYPES = ("IMAGE", "STRING") 325 | RETURN_NAMES = ("generated_image", "response") 326 | FUNCTION = "generate_image" 327 | CATEGORY = "ainewsto/Chatgpt" 328 | 329 | def __init__(self): 330 | self.api_key = get_config().get('api_key', '') 331 | self.timeout = 300 332 | 333 | def get_headers(self): 334 | return { 335 | "Content-Type": "application/json", 336 | "Authorization": f"Bearer {self.api_key}" 337 | } 338 | 339 | def generate_image(self, prompt, model="gpt-image-1", n=1, quality="auto", 340 | size="auto", background="auto", output_format="png", 341 | moderation="auto", seed=0, api_key=""): 342 | 343 | if api_key.strip(): 344 | self.api_key = api_key 345 | config = get_config() 346 | config['api_key'] = api_key 347 | save_config(config) 348 | 349 | try: 350 | if not self.api_key: 351 | error_message = "API key not found in Comflyapi.json" 352 | print(error_message) 353 | blank_image = Image.new('RGB', (1024, 1024), color='white') 354 | blank_tensor = pil2tensor(blank_image) 355 | return (blank_tensor, error_message) 356 | pbar = comfy.utils.ProgressBar(100) 357 | pbar.update_absolute(10) 358 | payload = { 359 | "prompt": prompt, 360 | "model": model, 361 | "n": n, 362 | "quality": quality, 363 | "background": background, 364 | "output_format": output_format, 365 | "moderation": moderation, 366 | } 367 | 368 | # Only include size if it's not "auto" 369 | if size != "auto": 370 | payload["size"] = size 371 | 372 | response = requests.post( 373 | "https://ai.comfly.chat/v1/images/generations", 374 | headers=self.get_headers(), 375 | json=payload, 376 | timeout=self.timeout 377 | ) 378 | 379 | pbar.update_absolute(50) 380 | if response.status_code != 200: 381 | error_message = f"API Error: {response.status_code} - {response.text}" 382 | print(error_message) 383 | blank_image = Image.new('RGB', (1024, 1024), color='white') 384 | blank_tensor = pil2tensor(blank_image) 385 | return (blank_tensor, error_message) 386 | 387 | # Parse the response 388 | result = response.json() 389 | 390 | # Format the response information 391 | timestamp = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) 392 | response_info = f"**GPT-image-1 Generation ({timestamp})**\n\n" 393 | response_info += f"Prompt: {prompt}\n" 394 | response_info += f"Model: {model}\n" 395 | response_info += f"Quality: {quality}\n" 396 | if size != "auto": 397 | response_info += f"Size: {size}\n" 398 | response_info += f"Background: {background}\n" 399 | response_info += f"Seed: {seed} (Note: Seed not used by API)\n\n" 400 | 401 | # Process the generated images 402 | generated_images = [] 403 | image_urls = [] 404 | 405 | if "data" in result and result["data"]: 406 | for i, item in enumerate(result["data"]): 407 | pbar.update_absolute(50 + (i+1) * 50 // len(result["data"])) 408 | 409 | if "b64_json" in item: 410 | # Decode base64 image 411 | image_data = base64.b64decode(item["b64_json"]) 412 | generated_image = Image.open(BytesIO(image_data)) 413 | generated_tensor = pil2tensor(generated_image) 414 | generated_images.append(generated_tensor) 415 | elif "url" in item: 416 | image_urls.append(item["url"]) 417 | # Download and process the image from URL 418 | try: 419 | img_response = requests.get(item["url"]) 420 | if img_response.status_code == 200: 421 | generated_image = Image.open(BytesIO(img_response.content)) 422 | generated_tensor = pil2tensor(generated_image) 423 | generated_images.append(generated_tensor) 424 | except Exception as e: 425 | print(f"Error downloading image from URL: {str(e)}") 426 | else: 427 | error_message = "No generated images in response" 428 | print(error_message) 429 | response_info += f"Error: {error_message}\n" 430 | blank_image = Image.new('RGB', (1024, 1024), color='white') 431 | blank_tensor = pil2tensor(blank_image) 432 | return (blank_tensor, response_info) 433 | 434 | # Add usage information to the response if available 435 | if "usage" in result: 436 | response_info += "Usage Information:\n" 437 | if "total_tokens" in result["usage"]: 438 | response_info += f"Total Tokens: {result['usage']['total_tokens']}\n" 439 | if "input_tokens" in result["usage"]: 440 | response_info += f"Input Tokens: {result['usage']['input_tokens']}\n" 441 | if "output_tokens" in result["usage"]: 442 | response_info += f"Output Tokens: {result['usage']['output_tokens']}\n" 443 | 444 | # Add detailed token usage if available 445 | if "input_tokens_details" in result["usage"]: 446 | response_info += "Input Token Details:\n" 447 | details = result["usage"]["input_tokens_details"] 448 | if "text_tokens" in details: 449 | response_info += f" Text Tokens: {details['text_tokens']}\n" 450 | if "image_tokens" in details: 451 | response_info += f" Image Tokens: {details['image_tokens']}\n" 452 | 453 | if generated_images: 454 | # Combine all generated images into a single tensor 455 | combined_tensor = torch.cat(generated_images, dim=0) 456 | 457 | pbar.update_absolute(100) 458 | return (combined_tensor, response_info) 459 | else: 460 | error_message = "No images were successfully processed" 461 | print(error_message) 462 | response_info += f"Error: {error_message}\n" 463 | blank_image = Image.new('RGB', (1024, 1024), color='white') 464 | blank_tensor = pil2tensor(blank_image) 465 | return (blank_tensor, response_info) 466 | 467 | except Exception as e: 468 | error_message = f"Error in image generation: {str(e)}" 469 | print(error_message) 470 | blank_image = Image.new('RGB', (1024, 1024), color='white') 471 | blank_tensor = pil2tensor(blank_image) 472 | return (blank_tensor, error_message) 473 | 474 | 475 | class ComfyuiChatGPTApi: 476 | 477 | _last_generated_image_urls = "" 478 | 479 | @classmethod 480 | def INPUT_TYPES(cls): 481 | return { 482 | "required": { 483 | "prompt": ("STRING", {"multiline": True}), 484 | "model": ("STRING", {"default": "gpt-image-1", "multiline": False}), 485 | }, 486 | "optional": { 487 | "api_key": ("STRING", {"default": ""}), 488 | "files": ("FILES",), 489 | "image_url": ("STRING", {"multiline": False, "default": ""}), 490 | "images": ("IMAGE", {"default": None}), 491 | "temperature": ("FLOAT", {"default": 0.7, "min": 0.0, "max": 2.0, "step": 0.01}), 492 | "max_tokens": ("INT", {"default": 4096, "min": 1, "max": 16384, "step": 1}), 493 | "top_p": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0, "step": 0.01}), 494 | "frequency_penalty": ("FLOAT", {"default": -2.0, "min": -2.0, "max": 2.0, "step": 0.01}), 495 | "presence_penalty": ("FLOAT", {"default": 0.0, "min": -2.0, "max": 2.0, "step": 0.01}), 496 | "seed": ("INT", {"default": -1, "min": -1, "max": 2147483647}), 497 | "image_download_timeout": ("INT", {"default": 600, "min": 300, "max": 1200, "step": 10}), 498 | "clear_chats": ("BOOLEAN", {"default": True}), 499 | } 500 | } 501 | 502 | RETURN_TYPES = ("IMAGE", "STRING", "STRING", "STRING") 503 | RETURN_NAMES = ("images", "response", "image_urls", "chats") 504 | FUNCTION = "process" 505 | CATEGORY = "ainewsto/Chatgpt" 506 | 507 | def __init__(self): 508 | self.api_key = get_config().get('api_key', '') 509 | self.timeout = 800 510 | self.image_download_timeout = 600 511 | self.api_endpoint = "https://ai.comfly.chat/v1/chat/completions" 512 | self.conversation_history = [] 513 | 514 | def get_headers(self): 515 | return { 516 | "Content-Type": "application/json", 517 | "Authorization": f"Bearer {self.api_key}" 518 | } 519 | 520 | def image_to_base64(self, image): 521 | """Convert PIL image to base64 string""" 522 | buffered = BytesIO() 523 | image.save(buffered, format="PNG") 524 | return base64.b64encode(buffered.getvalue()).decode('utf-8') 525 | 526 | def file_to_base64(self, file_path): 527 | """Convert file to base64 string and return appropriate MIME type""" 528 | try: 529 | with open(file_path, "rb") as file: 530 | file_content = file.read() 531 | encoded_content = base64.b64encode(file_content).decode('utf-8') 532 | # Get MIME type 533 | mime_type, _ = mimetypes.guess_type(file_path) 534 | if not mime_type: 535 | # Default to binary if MIME type can't be determined 536 | mime_type = "application/octet-stream" 537 | return encoded_content, mime_type 538 | except Exception as e: 539 | print(f"Error encoding file: {str(e)}") 540 | return None, None 541 | 542 | def extract_image_urls(self, response_text): 543 | """Extract image URLs from markdown format in response""" 544 | 545 | image_pattern = r'!\[.*?\]\((.*?)\)' 546 | matches = re.findall(image_pattern, response_text) 547 | 548 | if not matches: 549 | url_pattern = r'https?://\S+\.(?:jpg|jpeg|png|gif|webp)' 550 | matches = re.findall(url_pattern, response_text) 551 | 552 | if not matches: 553 | all_urls_pattern = r'https?://\S+' 554 | matches = re.findall(all_urls_pattern, response_text) 555 | return matches if matches else [] 556 | 557 | def download_image(self, url, timeout=30): 558 | """Download image from URL and convert to tensor with improved error handling""" 559 | try: 560 | 561 | headers = { 562 | 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36', 563 | 'Accept': 'image/avif,image/webp,image/apng,image/svg+xml,image/*,*/*;q=0.8', 564 | 'Accept-Language': 'en-US,en;q=0.9', 565 | 'Referer': 'https://comfyui.com/' 566 | } 567 | 568 | response = requests.get(url, headers=headers, timeout=timeout) 569 | response.raise_for_status() 570 | 571 | content_type = response.headers.get('Content-Type', '') 572 | if not content_type.startswith('image/'): 573 | print(f"Warning: URL doesn't point to an image. Content-Type: {content_type}") 574 | 575 | image = Image.open(BytesIO(response.content)) 576 | return pil2tensor(image) 577 | except requests.exceptions.Timeout: 578 | print(f"Timeout error downloading image from {url} (timeout: {timeout}s)") 579 | return None 580 | except requests.exceptions.SSLError as e: 581 | print(f"SSL Error downloading image from {url}: {str(e)}") 582 | return None 583 | except requests.exceptions.ConnectionError: 584 | print(f"Connection error downloading image from {url}") 585 | return None 586 | except requests.exceptions.RequestException as e: 587 | print(f"Request error downloading image from {url}: {str(e)}") 588 | return None 589 | except Exception as e: 590 | print(f"Error downloading image from {url}: {str(e)}") 591 | return None 592 | 593 | def format_conversation_history(self): 594 | """Format the conversation history for display""" 595 | if not self.conversation_history: 596 | return "" 597 | formatted_history = "" 598 | for entry in self.conversation_history: 599 | formatted_history += f"**User**: {entry['user']}\n\n" 600 | formatted_history += f"**AI**: {entry['ai']}\n\n" 601 | formatted_history += "---\n\n" 602 | return formatted_history.strip() 603 | 604 | def process(self, prompt, model, clear_chats=True, files=None, image_url="", images=None, temperature=0.7, 605 | max_tokens=4096, top_p=1.0, frequency_penalty=0.0, presence_penalty=0.0, seed=-1, 606 | image_download_timeout=100, api_key=""): 607 | if api_key.strip(): 608 | self.api_key = api_key 609 | config = get_config() 610 | config['api_key'] = api_key 611 | save_config(config) 612 | try: 613 | 614 | self.image_download_timeout = image_download_timeout 615 | 616 | if clear_chats: 617 | self.conversation_history = [] 618 | 619 | if not self.api_key: 620 | error_message = "API key not found in Comflyapi.json" 621 | print(error_message) 622 | 623 | blank_img = Image.new('RGB', (512, 512), color='white') 624 | return (pil2tensor(blank_img), error_message, "", self.format_conversation_history()) 625 | 626 | pbar = comfy.utils.ProgressBar(100) 627 | pbar.update_absolute(10) 628 | timestamp = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime()) 629 | 630 | if seed < 0: 631 | seed = random.randint(0, 2147483647) 632 | print(f"Using random seed: {seed}") 633 | 634 | content = [] 635 | 636 | content.append({"type": "text", "text": prompt}) 637 | 638 | 639 | if not clear_chats and ComfyuiChatGPTApi._last_generated_image_urls: 640 | prev_image_url = ComfyuiChatGPTApi._last_generated_image_urls.split('\n')[0].strip() 641 | if prev_image_url: 642 | print(f"Using previous image URL: {prev_image_url}") 643 | content.append({ 644 | "type": "image_url", 645 | "image_url": {"url": prev_image_url} 646 | }) 647 | 648 | elif clear_chats: 649 | if images is not None: 650 | batch_size = images.shape[0] 651 | max_images = min(batch_size, 4) 652 | for i in range(max_images): 653 | pil_image = tensor2pil(images)[i] 654 | image_base64 = self.image_to_base64(pil_image) 655 | content.append({ 656 | "type": "image_url", 657 | "image_url": {"url": f"data:image/png;base64,{image_base64}"} 658 | }) 659 | if batch_size > max_images: 660 | content.append({ 661 | "type": "text", 662 | "text": f"\n(Note: {batch_size-max_images} additional images were omitted due to API limitations)" 663 | }) 664 | 665 | elif image_url: 666 | content.append({ 667 | "type": "image_url", 668 | "image_url": {"url": image_url} 669 | }) 670 | 671 | elif image_url: 672 | content.append({ 673 | "type": "image_url", 674 | "image_url": {"url": image_url} 675 | }) 676 | 677 | if files: 678 | file_paths = files if isinstance(files, list) else [files] 679 | for file_path in file_paths: 680 | encoded_content, mime_type = self.file_to_base64(file_path) 681 | if encoded_content and mime_type: 682 | 683 | if mime_type.startswith('image/'): 684 | 685 | content.append({ 686 | "type": "image_url", 687 | "image_url": {"url": f"data:{mime_type};base64,{encoded_content}"} 688 | }) 689 | else: 690 | 691 | content.append({ 692 | "type": "text", 693 | "text": f"\n\nI've attached a file ({os.path.basename(file_path)}) for analysis." 694 | }) 695 | content.append({ 696 | "type": "file_url", 697 | "file_url": { 698 | "url": f"data:{mime_type};base64,{encoded_content}", 699 | "name": os.path.basename(file_path) 700 | } 701 | }) 702 | 703 | messages = [] 704 | 705 | messages.append({ 706 | "role": "user", 707 | "content": content 708 | }) 709 | 710 | payload = { 711 | "model": model, 712 | "messages": messages, 713 | "temperature": temperature, 714 | "max_tokens": max_tokens, 715 | "top_p": top_p, 716 | "frequency_penalty": frequency_penalty, 717 | "presence_penalty": presence_penalty, 718 | "seed": seed, 719 | "stream": True 720 | } 721 | 722 | loop = asyncio.new_event_loop() 723 | asyncio.set_event_loop(loop) 724 | response_text = loop.run_until_complete(self.stream_response(payload, pbar)) 725 | loop.close() 726 | 727 | self.conversation_history.append({ 728 | "user": prompt, 729 | "ai": response_text 730 | }) 731 | 732 | technical_response = f"**Model**: {model}\n**Temperature**: {temperature}\n**Seed**: {seed}\n**Time**: {timestamp}" 733 | 734 | image_urls = self.extract_image_urls(response_text) 735 | image_urls_string = "\n".join(image_urls) if image_urls else "" 736 | 737 | if image_urls: 738 | ComfyuiChatGPTApi._last_generated_image_urls = image_urls_string 739 | 740 | chat_history = self.format_conversation_history() 741 | if image_urls: 742 | try: 743 | 744 | img_tensors = [] 745 | successful_downloads = 0 746 | for i, url in enumerate(image_urls): 747 | print(f"Attempting to download image {i+1}/{len(image_urls)} from: {url}") 748 | 749 | pbar.update_absolute(min(80, 40 + (i+1) * 40 // len(image_urls))) 750 | img_tensor = self.download_image(url, self.image_download_timeout) 751 | if img_tensor is not None: 752 | img_tensors.append(img_tensor) 753 | successful_downloads += 1 754 | print(f"Successfully downloaded {successful_downloads} out of {len(image_urls)} images") 755 | if img_tensors: 756 | 757 | combined_tensor = torch.cat(img_tensors, dim=0) 758 | pbar.update_absolute(100) 759 | return (combined_tensor, technical_response, image_urls_string, chat_history) 760 | except Exception as e: 761 | print(f"Error processing image URLs: {str(e)}") 762 | 763 | if images is not None: 764 | pbar.update_absolute(100) 765 | return (images, technical_response, image_urls_string, chat_history) 766 | else: 767 | blank_img = Image.new('RGB', (512, 512), color='white') 768 | blank_tensor = pil2tensor(blank_img) 769 | pbar.update_absolute(100) 770 | return (blank_tensor, technical_response, image_urls_string, chat_history) 771 | 772 | except Exception as e: 773 | error_message = f"Error calling ChatGPT API: {str(e)}" 774 | print(error_message) 775 | 776 | if images is not None: 777 | return (images, error_message, "", self.format_conversation_history()) 778 | else: 779 | blank_img = Image.new('RGB', (512, 512), color='white') 780 | blank_tensor = pil2tensor(blank_img) 781 | return (blank_tensor, error_message, "", self.format_conversation_history()) 782 | 783 | async def stream_response(self, payload, pbar): 784 | """Stream response from API""" 785 | full_response = "" 786 | try: 787 | async with aiohttp.ClientSession() as session: 788 | async with session.post( 789 | self.api_endpoint, 790 | headers=self.get_headers(), 791 | json=payload, 792 | timeout=self.timeout 793 | ) as response: 794 | if response.status != 200: 795 | error_text = await response.text() 796 | raise Exception(f"API Error {response.status}: {error_text}") 797 | 798 | async for line in response.content: 799 | line = line.decode('utf-8').strip() 800 | if line.startswith('data: '): 801 | data = line[6:] 802 | if data == '[DONE]': 803 | break 804 | try: 805 | chunk = json.loads(data) 806 | if 'choices' in chunk and chunk['choices']: 807 | delta = chunk['choices'][0].get('delta', {}) 808 | if 'content' in delta: 809 | content = delta['content'] 810 | full_response += content 811 | 812 | pbar.update_absolute(min(40, 20 + len(full_response) // 100)) 813 | except json.JSONDecodeError: 814 | continue 815 | return full_response 816 | 817 | except asyncio.TimeoutError: 818 | raise TimeoutError(f"API request timed out after {self.timeout} seconds") 819 | except Exception as e: 820 | raise Exception(f"Error in streaming response: {str(e)}") 821 | 822 | 823 | NODE_CLASS_MAPPINGS = { 824 | "ComfyuiChatGPTApi": ComfyuiChatGPTApi, 825 | "Comfyui_gpt_image_1_edit": Comfyui_gpt_image_1_edit, 826 | "Comfyui_gpt_image_1": Comfyui_gpt_image_1, 827 | } 828 | 829 | NODE_DISPLAY_NAME_MAPPINGS = { 830 | "ComfyuiChatGPTApi": "ComfyuiChatGPTApi", 831 | "Comfyui_gpt_image_1_edit": "Comfyui_gpt_image_1_edit", 832 | "Comfyui_gpt_image_1": "Comfyui_gpt_image_1", 833 | } 834 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aiohttp 2 | aiohttp-cors 3 | GitPython 4 | numpy 5 | Pillow 6 | requests 7 | torch 8 | 9 | -------------------------------------------------------------------------------- /utils.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import torch 3 | from PIL import Image 4 | from typing import List, Union 5 | 6 | def pil2tensor(image: Union[Image.Image, List[Image.Image]]) -> torch.Tensor: 7 | """ 8 | Convert PIL image(s) to tensor, matching ComfyUI's implementation. 9 | 10 | Args: 11 | image: Single PIL Image or list of PIL Images 12 | 13 | Returns: 14 | torch.Tensor: Image tensor with values normalized to [0, 1] 15 | """ 16 | if isinstance(image, list): 17 | if len(image) == 0: 18 | return torch.empty(0) 19 | 20 | tensors = [] 21 | for img in image: 22 | if img.mode == 'RGBA': 23 | img = img.convert('RGB') 24 | elif img.mode != 'RGB': 25 | img = img.convert('RGB') 26 | 27 | img_array = np.array(img).astype(np.float32) / 255.0 28 | tensor = torch.from_numpy(img_array)[None,] 29 | tensors.append(tensor) 30 | 31 | if len(tensors) == 1: 32 | return tensors[0] 33 | else: 34 | shapes = [t.shape[1:3] for t in tensors] 35 | if all(shape == shapes[0] for shape in shapes): 36 | return torch.cat(tensors, dim=0) 37 | else: 38 | max_h = max(t.shape[1] for t in tensors) 39 | max_w = max(t.shape[2] for t in tensors) 40 | 41 | padded_tensors = [] 42 | for t in tensors: 43 | h, w = t.shape[1:3] 44 | if h == max_h and w == max_w: 45 | padded_tensors.append(t) 46 | else: 47 | padded = torch.zeros((1, max_h, max_w, 3), dtype=t.dtype) 48 | padded[0, :h, :w, :] = t[0, :h, :w, :] 49 | padded_tensors.append(padded) 50 | 51 | return torch.cat(padded_tensors, dim=0) 52 | 53 | # Convert PIL image to RGB if needed 54 | if image.mode == 'RGBA': 55 | image = image.convert('RGB') 56 | elif image.mode != 'RGB': 57 | image = image.convert('RGB') 58 | 59 | # Convert to numpy array and normalize to [0, 1] 60 | img_array = np.array(image).astype(np.float32) / 255.0 61 | 62 | # Return tensor with shape [1, H, W, 3] 63 | return torch.from_numpy(img_array)[None,] 64 | 65 | 66 | def tensor2pil(image: torch.Tensor) -> List[Image.Image]: 67 | """ 68 | Convert tensor to PIL image(s), matching ComfyUI's implementation. 69 | 70 | Args: 71 | image: Tensor with shape [B, H, W, 3] or [H, W, 3], values in range [0, 1] 72 | 73 | Returns: 74 | List[Image.Image]: List of PIL Images 75 | """ 76 | batch_count = image.size(0) if len(image.shape) > 3 else 1 77 | if batch_count > 1: 78 | out = [] 79 | for i in range(batch_count): 80 | out.extend(tensor2pil(image[i])) 81 | return out 82 | 83 | # Convert tensor to numpy array, scale to [0, 255], and clip values 84 | numpy_image = np.clip(255.0 * image.cpu().numpy().squeeze(), 0, 255).astype(np.uint8) 85 | 86 | # Convert numpy array to PIL Image 87 | return [Image.fromarray(numpy_image)] -------------------------------------------------------------------------------- /workflow/comfyui-chatgpt-api.json: -------------------------------------------------------------------------------- 1 | { 2 | "last_node_id": 28, 3 | "last_link_id": 44, 4 | "nodes": [ 5 | { 6 | "id": 17, 7 | "type": "ShowText|pysssss", 8 | "pos": [ 9 | -890.126708984375, 10 | -759.1605224609375 11 | ], 12 | "size": [ 13 | 404.01727294921875, 14 | 116.56401824951172 15 | ], 16 | "flags": {}, 17 | "order": 7, 18 | "mode": 0, 19 | "inputs": [ 20 | { 21 | "name": "text", 22 | "type": "STRING", 23 | "link": 44, 24 | "widget": { 25 | "name": "text" 26 | } 27 | } 28 | ], 29 | "outputs": [ 30 | { 31 | "name": "STRING", 32 | "type": "STRING", 33 | "links": null, 34 | "shape": 6 35 | } 36 | ], 37 | "properties": { 38 | "Node name for S&R": "ShowText|pysssss" 39 | }, 40 | "widgets_values": [ 41 | "" 42 | ] 43 | }, 44 | { 45 | "id": 24, 46 | "type": "LoadImage", 47 | "pos": [ 48 | -1287.1475830078125, 49 | -968.3455810546875 50 | ], 51 | "size": [ 52 | 315, 53 | 314 54 | ], 55 | "flags": {}, 56 | "order": 0, 57 | "mode": 0, 58 | "inputs": [], 59 | "outputs": [ 60 | { 61 | "name": "IMAGE", 62 | "type": "IMAGE", 63 | "links": [ 64 | 37 65 | ] 66 | }, 67 | { 68 | "name": "MASK", 69 | "type": "MASK", 70 | "links": null 71 | } 72 | ], 73 | "properties": { 74 | "Node name for S&R": "LoadImage" 75 | }, 76 | "widgets_values": [ 77 | "1.jpg", 78 | "image" 79 | ] 80 | }, 81 | { 82 | "id": 22, 83 | "type": "LoadImage", 84 | "pos": [ 85 | -1285.79931640625, 86 | -597.0714111328125 87 | ], 88 | "size": [ 89 | 315, 90 | 314 91 | ], 92 | "flags": {}, 93 | "order": 1, 94 | "mode": 0, 95 | "inputs": [], 96 | "outputs": [ 97 | { 98 | "name": "IMAGE", 99 | "type": "IMAGE", 100 | "links": [ 101 | 35 102 | ] 103 | }, 104 | { 105 | "name": "MASK", 106 | "type": "MASK", 107 | "links": null 108 | } 109 | ], 110 | "properties": { 111 | "Node name for S&R": "LoadImage" 112 | }, 113 | "widgets_values": [ 114 | "2.jpg", 115 | "image" 116 | ] 117 | }, 118 | { 119 | "id": 23, 120 | "type": "LoadImage", 121 | "pos": [ 122 | -1278.814453125, 123 | -218.6844940185547 124 | ], 125 | "size": [ 126 | 315, 127 | 314 128 | ], 129 | "flags": {}, 130 | "order": 2, 131 | "mode": 0, 132 | "inputs": [], 133 | "outputs": [ 134 | { 135 | "name": "IMAGE", 136 | "type": "IMAGE", 137 | "links": [ 138 | 36 139 | ] 140 | }, 141 | { 142 | "name": "MASK", 143 | "type": "MASK", 144 | "links": null 145 | } 146 | ], 147 | "properties": { 148 | "Node name for S&R": "LoadImage" 149 | }, 150 | "widgets_values": [ 151 | "3.jpg", 152 | "image" 153 | ] 154 | }, 155 | { 156 | "id": 25, 157 | "type": "PreviewImage", 158 | "pos": [ 159 | -428.7579345703125, 160 | -751.380615234375 161 | ], 162 | "size": [ 163 | 420.0105285644531, 164 | 576.1502685546875 165 | ], 166 | "flags": {}, 167 | "order": 5, 168 | "mode": 0, 169 | "inputs": [ 170 | { 171 | "name": "images", 172 | "type": "IMAGE", 173 | "link": 42 174 | } 175 | ], 176 | "outputs": [], 177 | "properties": { 178 | "Node name for S&R": "PreviewImage" 179 | }, 180 | "widgets_values": [] 181 | }, 182 | { 183 | "id": 26, 184 | "type": "ShowText|pysssss", 185 | "pos": [ 186 | 29.915327072143555, 187 | -748.803955078125 188 | ], 189 | "size": [ 190 | 436.1102600097656, 191 | 570.7864379882812 192 | ], 193 | "flags": {}, 194 | "order": 6, 195 | "mode": 0, 196 | "inputs": [ 197 | { 198 | "name": "text", 199 | "type": "STRING", 200 | "link": 43, 201 | "widget": { 202 | "name": "text" 203 | } 204 | } 205 | ], 206 | "outputs": [ 207 | { 208 | "name": "STRING", 209 | "type": "STRING", 210 | "links": null, 211 | "shape": 6 212 | } 213 | ], 214 | "properties": { 215 | "Node name for S&R": "ShowText|pysssss" 216 | }, 217 | "widgets_values": [ 218 | "", 219 | "Error calling ChatGPT API: Error in streaming response: API Error 500: {\"error\":{\"message\":\"当前模型负载较高,请稍候重试,或者切换其他模型 (request id: B20250328235605730741257q8H1EMCx)\",\"type\":\"new_api_error\",\"param\":\"\",\"code\":null}}" 220 | ] 221 | }, 222 | { 223 | "id": 21, 224 | "type": "MultiImagesInput", 225 | "pos": [ 226 | -816.4220581054688, 227 | -958.3441772460938 228 | ], 229 | "size": [ 230 | 210, 231 | 122 232 | ], 233 | "flags": {}, 234 | "order": 3, 235 | "mode": 0, 236 | "inputs": [ 237 | { 238 | "name": "image_1", 239 | "type": "IMAGE", 240 | "link": 37, 241 | "shape": 7 242 | }, 243 | { 244 | "name": "image_2", 245 | "type": "IMAGE", 246 | "link": 35, 247 | "shape": 7 248 | }, 249 | { 250 | "name": "image_3", 251 | "type": "IMAGE", 252 | "link": null 253 | } 254 | ], 255 | "outputs": [ 256 | { 257 | "name": "images", 258 | "type": "IMAGE", 259 | "links": [ 260 | 41 261 | ], 262 | "slot_index": 0 263 | } 264 | ], 265 | "properties": {}, 266 | "widgets_values": [ 267 | 3, 268 | null 269 | ] 270 | }, 271 | { 272 | "id": 28, 273 | "type": "ComfyuiChatGPTApi", 274 | "pos": [ 275 | -889.3751831054688, 276 | -567.8804321289062 277 | ], 278 | "size": [ 279 | 400, 280 | 356 281 | ], 282 | "flags": {}, 283 | "order": 4, 284 | "mode": 0, 285 | "inputs": [ 286 | { 287 | "name": "files", 288 | "type": "FILES", 289 | "link": null, 290 | "shape": 7 291 | }, 292 | { 293 | "name": "images", 294 | "type": "IMAGE", 295 | "link": 41, 296 | "shape": 7 297 | } 298 | ], 299 | "outputs": [ 300 | { 301 | "name": "images", 302 | "type": "IMAGE", 303 | "links": [ 304 | 42 305 | ], 306 | "slot_index": 0 307 | }, 308 | { 309 | "name": "response", 310 | "type": "STRING", 311 | "links": [ 312 | 43 313 | ], 314 | "slot_index": 1 315 | }, 316 | { 317 | "name": "image_urls", 318 | "type": "STRING", 319 | "links": [ 320 | 44 321 | ], 322 | "slot_index": 2 323 | } 324 | ], 325 | "properties": { 326 | "Node name for S&R": "ComfyuiChatGPTApi" 327 | }, 328 | "widgets_values": [ 329 | "以图片image_3为背景,图片image_2的模特手拿着图片image_1的化妆瓶", 330 | "gpt-4o-all", 331 | "", 332 | 0.7, 333 | 4096, 334 | 1, 335 | 0, 336 | 0, 337 | -1, 338 | "randomize", 339 | 120 340 | ] 341 | } 342 | ], 343 | "links": [ 344 | [ 345 | 35, 346 | 22, 347 | 0, 348 | 21, 349 | 1, 350 | "IMAGE" 351 | ], 352 | [ 353 | 36, 354 | 23, 355 | 0, 356 | 21, 357 | 2, 358 | "IMAGE" 359 | ], 360 | [ 361 | 37, 362 | 24, 363 | 0, 364 | 21, 365 | 0, 366 | "IMAGE" 367 | ], 368 | [ 369 | 41, 370 | 21, 371 | 0, 372 | 28, 373 | 1, 374 | "IMAGE" 375 | ], 376 | [ 377 | 42, 378 | 28, 379 | 0, 380 | 25, 381 | 0, 382 | "IMAGE" 383 | ], 384 | [ 385 | 43, 386 | 28, 387 | 1, 388 | 26, 389 | 0, 390 | "STRING" 391 | ], 392 | [ 393 | 44, 394 | 28, 395 | 2, 396 | 17, 397 | 0, 398 | "STRING" 399 | ] 400 | ], 401 | "groups": [], 402 | "config": {}, 403 | "extra": { 404 | "ds": { 405 | "scale": 0.6830134553650711, 406 | "offset": [ 407 | 1500.863251417137, 408 | 1108.4863459186506 409 | ] 410 | } 411 | }, 412 | "version": 0.4 413 | } --------------------------------------------------------------------------------