├── .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 | 
21 |
22 | 
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 | 
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 | 
49 |
50 | 
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 | 
69 |
70 | 
71 |
72 | 
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 | 
88 |
89 |
90 | 
91 |
92 |
93 |
94 | # 🥵 Comfly的QQ群 / my wechat
95 |
96 | 
97 |
98 | 
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 | }
--------------------------------------------------------------------------------