├── requirements.txt ├── .gitattributes ├── image ├── 视觉社.png └── 标记与吉卜力.jpg ├── workflow └── ComfyUI-GPT-API.png ├── __init__.py ├── pyproject.toml ├── .gitignore ├── .github └── workflows │ └── publish.yml ├── README.md ├── README_EN.md └── GPT_API_nodes.py /requirements.txt: -------------------------------------------------------------------------------- 1 | Pillow>=10.1.0 2 | requests 3 | numpy 4 | torch -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /image/视觉社.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CY-CHENYUE/ComfyUI-GPT-API/HEAD/image/视觉社.png -------------------------------------------------------------------------------- /image/标记与吉卜力.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CY-CHENYUE/ComfyUI-GPT-API/HEAD/image/标记与吉卜力.jpg -------------------------------------------------------------------------------- /workflow/ComfyUI-GPT-API.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CY-CHENYUE/ComfyUI-GPT-API/HEAD/workflow/ComfyUI-GPT-API.png -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | ComfyUI-GPT-API节点 3 | 通过API调用GPT生成图像 4 | """ 5 | 6 | from .GPT_API_nodes import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS 7 | 8 | __all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS'] -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "gpt-api" 3 | description = "A custom node for ComfyUI to integrate GPT API." 4 | version = "1.0.0" 5 | license = {file = "LICENSE"} 6 | dependencies = ["Pillow>=10.1.0", "requests", "numpy", "torch"] 7 | 8 | [project.urls] 9 | Repository = "https://github.com/CY-CHENYUE/ComfyUI-GPT-API" 10 | # Used by Comfy Registry https://comfyregistry.org 11 | 12 | [tool.comfy] 13 | PublisherId = "" 14 | DisplayName = "ComfyUI-GPT-API" 15 | Icon = "" 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Python相关文件 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | *.so 6 | .Python 7 | env/ 8 | build/ 9 | develop-eggs/ 10 | dist/ 11 | downloads/ 12 | eggs/ 13 | .eggs/ 14 | lib/ 15 | lib64/ 16 | parts/ 17 | sdist/ 18 | var/ 19 | *.egg-info/ 20 | .installed.cfg 21 | *.egg 22 | 23 | # 虚拟环境 24 | venv/ 25 | ENV/ 26 | env/ 27 | 28 | # IDE相关文件 29 | .idea/ 30 | .vscode/ 31 | *.swp 32 | *.swo 33 | 34 | # 系统文件 35 | .DS_Store 36 | Thumbs.db 37 | 38 | # 日志文件 39 | *.log 40 | 41 | # 本地配置文件(根据需要添加) 42 | config.local.py 43 | secrets.json 44 | 45 | # 大型数据文件(根据需要添加) 46 | *.csv 47 | *.dat 48 | *.pkl 49 | 50 | # API密钥和配置文件 51 | config.ini 52 | .env 53 | gpt_api_config.json 54 | *_key.txt 55 | 56 | # 日志和缓存 57 | *.log 58 | .cache/ 59 | logs/ 60 | 61 | # 测试文件 62 | test_*.py 63 | 64 | # 文档文件夹 65 | doc/ 66 | docs/ 67 | documentation/ -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish to Comfy registry 2 | on: 3 | workflow_dispatch: 4 | push: 5 | branches: 6 | - main 7 | - master 8 | paths: 9 | - "pyproject.toml" 10 | 11 | permissions: 12 | issues: write 13 | 14 | jobs: 15 | publish-node: 16 | name: Publish Custom Node to registry 17 | runs-on: ubuntu-latest 18 | if: ${{ github.repository_owner == 'CY-CHENYUE' }} 19 | steps: 20 | - name: Check out code 21 | uses: actions/checkout@v4 22 | with: 23 | submodules: true 24 | - name: Publish Custom Node 25 | uses: Comfy-Org/publish-node-action@v1 26 | with: 27 | ## Add your own personal access token to your Github Repository secrets and reference it here. 28 | personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }} 29 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ComfyUI-GPT-API 2 | 3 | [English](README_EN.md) | 中文 4 | 5 | 通过API直接调用GPT系列模型的ComfyUI扩展节点集合。该项目提供了一系列自定义节点(计划) 6 | 7 | ## 安装方法 8 | 9 | 1. 下载或克隆这个仓库到ComfyUI的`custom_nodes`目录中: 10 | ``` 11 | cd ComfyUI/custom_nodes 12 | git clone https://github.com/CY-CHENYUE/ComfyUI-GPT-API 13 | ``` 14 | 15 | 2. 安装依赖项: 16 | ``` 17 | cd ComfyUI-GPT-API 18 | pip install -r requirements.txt 19 | ``` 20 | 21 | 3. 重启ComfyUI,节点将自动加载 22 | 23 | ## 节点使用指南 24 | 25 | ### GPT4o Image Generation (图像生成节点) 26 | 27 | #### 功能简介 28 | 29 | 通过API直接调用GPT-4o生成图像,可使用参考图像引导生成过程。支持图像处理质量控制和随机种子设置。 30 | 31 | #### 使用方法 32 | 33 | ![alt text](image/视觉社.png) 34 | 35 | ![alt text](workflow/ComfyUI-GPT-API.png) 36 | 37 | 38 | 39 | 1. 从节点浏览器中找到"GPT4o Image Generation"节点并添加到工作流中 40 | 2. 输入您的GPT API密钥、自定义API地址和模型名称(只需首次设置,将自动保存) 41 | 3. 编写图像生成提示词 42 | 4. 连接一个或多个参考图像到images输入(必需) 43 | 5. 设置随机种子和图像处理质量参数 44 | 6. 运行工作流,节点将调用API生成图像并返回 45 | 46 | #### 节点参数说明 47 | 48 | ##### 必需参数 49 | - **prompt**: 图像生成的提示词,描述您想要生成的图像内容 50 | - **api_key**: OpenAI API密钥(首次设置后会自动保存) 51 | - **model**: 使用的模型名称(如"gpt-4o-all") 52 | - **api_url**: API请求地址 53 | - **images**: 用作参考的图像输入(支持多张图像) 54 | - **seed**: 随机种子值 55 | 56 | ##### 可选参数 57 | - **max_image_size**: 图像最大尺寸(默认1024像素) 58 | - **image_quality**: JPEG压缩质量(50-100) 59 | - **image_detail**: 图像处理质量,影响token消耗和处理细节 60 | - **auto**: 自动选择处理质量(默认) 61 | - **high**: 高品质处理,提供更多图像细节,但消耗更多token 62 | - **low**: 低品质处理,token消耗少,适合简单任务 63 | 64 | 65 | ## 注意事项 66 | 67 | - 请确保您有有效的GPT API密钥 68 | - API调用可能需要一定的时间,请耐心等待 69 | - 图像生成受到API服务提供商的限制和规则约束 70 | - 种子值设为0时系统会随机生成一个有效种子 71 | - 所有配置(API密钥、URL、模型)将保存到节点本地目录,下次使用时自动加载 72 | - 用户输入的参数总是优先于保存的配置 73 | 74 | ## 疑难解答 75 | 76 | 如果遇到安装或运行问题: 77 | 78 | 1. 确保已安装所有依赖项 79 | 2. 检查API密钥是否有效 80 | 3. 验证API URL是否正确 81 | 4. 确认使用的模型名称是服务商支持的 82 | 5. 检查网络连接是否正常 83 | 6. 查看节点返回的"API Respond"信息,获取详细错误信息 84 | 85 | ## Contact Me 86 | 87 | - X (Twitter): [@cychenyue](https://x.com/cychenyue) 88 | - TikTok: [@cychenyue](https://www.tiktok.com/@cychenyue) 89 | - YouTube: [@CY-CHENYUE](https://www.youtube.com/@CY-CHENYUE) 90 | - BiliBili: [@CY-CHENYUE](https://space.bilibili.com/402808950) 91 | - 小红书: [@CY-CHENYUE](https://www.xiaohongshu.com/user/profile/6360e61f000000001f01bda0) -------------------------------------------------------------------------------- /README_EN.md: -------------------------------------------------------------------------------- 1 | # ComfyUI-GPT-API 2 | 3 | English | [中文](README.md) 4 | 5 | A collection of ComfyUI extension nodes that directly call GPT models via API. This project provides a series of custom nodes (planned) for accessing various GPT capabilities in ComfyUI workflows. 6 | 7 | ## Installation 8 | 9 | 1. Download or clone this repository into the `custom_nodes` directory of ComfyUI: 10 | ``` 11 | cd ComfyUI/custom_nodes 12 | git clone https://github.com/CY-CHENYUE/ComfyUI-GPT-API 13 | ``` 14 | 15 | 2. Install dependencies: 16 | ``` 17 | cd ComfyUI-GPT-API 18 | pip install -r requirements.txt 19 | ``` 20 | 21 | 3. Restart ComfyUI, and the nodes will be automatically loaded 22 | 23 | ## Node Usage Guide 24 | 25 | ### GPT4o Image Generation 26 | 27 | #### Features 28 | 29 | Directly call GPT-4o via API to generate images, with the ability to use reference images to guide the generation process. Supports image quality control and random seed settings. 30 | 31 | #### How to Use 32 | 33 | ![alt text](image/视觉社.png) 34 | 35 | ![workflow example](workflow/ComfyUI-GPT-API.png) 36 | 37 | 38 | 39 | 1. Find the "GPT4o Image Generation" node in the node browser and add it to your workflow 40 | 2. Enter your GPT API key, custom API address, and model name (these only need to be set once and will be automatically saved) 41 | 3. Write your image generation prompt 42 | 4. Connect one or more reference images to the images input (required) 43 | 5. Set the random seed and image processing quality parameters 44 | 6. Run the workflow, and the node will call the API to generate an image and return it 45 | 46 | #### Node Parameters 47 | 48 | ##### Required Parameters 49 | - **prompt**: The image generation prompt that describes the image you want to create 50 | - **api_key**: Your OpenAI API key (will be saved automatically after first use) 51 | - **model**: The model name to use (e.g., "gpt-4o-all") 52 | - **api_url**: The API endpoint URL 53 | - **images**: Reference image input (supports multiple images) 54 | - **seed**: Random seed value 55 | 56 | ##### Optional Parameters 57 | - **max_image_size**: Maximum image dimension (default 1024 pixels) 58 | - **image_quality**: JPEG compression quality (50-100) 59 | - **image_detail**: Image processing quality, affects token usage and detail level 60 | - **auto**: Automatically select processing quality (default) 61 | - **high**: High-quality processing with more image details, but consumes more tokens 62 | - **low**: Low-quality processing, uses fewer tokens, suitable for simple tasks 63 | 64 | ## Notes 65 | 66 | - Please ensure you have a valid GPT API key 67 | - API calls may take some time, please be patient 68 | - Image generation is subject to the limitations and rules of the API service provider 69 | - When the seed value is set to 0, the system will generate a random valid seed 70 | - All configurations (API key, URL, model) will be saved to the node's local directory and automatically loaded next time 71 | - User-provided parameters always take precedence over saved configurations 72 | 73 | ## Troubleshooting 74 | 75 | If you encounter installation or operation issues: 76 | 77 | 1. Make sure all dependencies are installed 78 | 2. Check if your API key is valid 79 | 3. Verify that the API URL is correct 80 | 4. Confirm that the model name is supported by the service provider 81 | 5. Check your network connection 82 | 6. Review the "API Respond" information returned by the node for detailed error messages 83 | 84 | ## Contact Me 85 | 86 | - X (Twitter): [@cychenyue](https://x.com/cychenyue) 87 | - TikTok: [@cychenyue](https://www.tiktok.com/@cychenyue) 88 | - YouTube: [@CY-CHENYUE](https://www.youtube.com/@CY-CHENYUE) 89 | - BiliBili: [@CY-CHENYUE](https://space.bilibili.com/402808950) 90 | - Xiaohongshu: [@CY-CHENYUE](https://www.xiaohongshu.com/user/profile/6360e61f000000001f01bda0) -------------------------------------------------------------------------------- /GPT_API_nodes.py: -------------------------------------------------------------------------------- 1 | import os 2 | import base64 3 | import io 4 | import json 5 | import torch 6 | import numpy as np 7 | from PIL import Image 8 | import requests 9 | from io import BytesIO 10 | import traceback 11 | import random 12 | import time 13 | 14 | class GPTImageGenerator: 15 | @classmethod 16 | def INPUT_TYPES(cls): 17 | # 创建临时实例以加载配置 18 | temp_instance = cls() 19 | config = temp_instance.load_config() 20 | 21 | return { 22 | "required": { 23 | "prompt": ("STRING", {"multiline": True}), 24 | "api_key": ("STRING", {"default": "", "multiline": False}), 25 | "model": ("STRING", {"default": config["model"], "multiline": False}), 26 | "api_url": ("STRING", {"default": config["api_url"], "multiline": False}), 27 | "images": ("IMAGE",), 28 | "seed": ("INT", {"default": 66666666, "min": 0, "max": 4294967295}), 29 | }, 30 | "optional": { 31 | "max_image_size": ("INT", {"default": 1024, "min": 256, "max": 2048}), 32 | "image_quality": ("INT", {"default": 85, "min": 50, "max": 100}), 33 | "image_detail": (["auto", "high", "low"], {"default": "auto"}), 34 | } 35 | } 36 | 37 | RETURN_TYPES = ("IMAGE", "STRING") 38 | RETURN_NAMES = ("image", "API Respond") 39 | FUNCTION = "generate_image" 40 | CATEGORY = "GPT-API" 41 | 42 | def __init__(self): 43 | """初始化日志系统和API密钥存储""" 44 | self.log_messages = [] # 全局日志消息存储 45 | # 获取节点所在目录 46 | self.node_dir = os.path.dirname(os.path.abspath(__file__)) 47 | self.config_file = os.path.join(self.node_dir, "gpt_api_config.json") 48 | 49 | def log(self, message): 50 | """全局日志函数:记录到日志列表""" 51 | if hasattr(self, 'log_messages'): 52 | self.log_messages.append(message) 53 | return message 54 | 55 | def save_config(self, api_key, api_url, model): 56 | """保存API配置到文件""" 57 | if not api_key or len(api_key) < 10: 58 | return False 59 | 60 | try: 61 | config = { 62 | "api_key": api_key, 63 | "api_url": api_url, 64 | "model": model, 65 | "saved_time": time.strftime("%Y-%m-%d %H:%M:%S") 66 | } 67 | with open(self.config_file, "w") as f: 68 | json.dump(config, f) 69 | self.log("已保存API配置到节点目录") 70 | return True 71 | except Exception as e: 72 | self.log(f"保存API配置失败: {e}") 73 | return False 74 | 75 | def load_config(self): 76 | """从文件加载API配置""" 77 | default_config = { 78 | "api_key": "", 79 | "api_url": "API URL", 80 | "model": "MODEL NAME" 81 | } 82 | 83 | if not os.path.exists(self.config_file): 84 | return default_config 85 | 86 | try: 87 | with open(self.config_file, "r") as f: 88 | config = json.load(f) 89 | 90 | # 验证必要字段 91 | if "api_key" not in config or len(config["api_key"]) < 10: 92 | self.log("已保存的API密钥无效") 93 | return default_config 94 | 95 | self.log("成功加载已保存的API配置") 96 | return config 97 | except Exception as e: 98 | self.log(f"加载API配置失败: {e}") 99 | return default_config 100 | 101 | def get_api_key(self, user_input_key, user_api_url, user_model): 102 | """获取API密钥和配置,优先使用用户输入的值""" 103 | # 如果用户输入了有效的密钥,使用并保存所有配置 104 | if user_input_key and len(user_input_key) > 10: 105 | self.log("使用用户输入的API配置") 106 | # 保存到文件中 107 | self.save_config(user_input_key, user_api_url, user_model) 108 | return user_input_key 109 | 110 | # 如果用户没有输入,尝试从文件读取 111 | config = self.load_config() 112 | if config["api_key"] and len(config["api_key"]) > 10: 113 | self.log("使用已保存的API配置") 114 | return config["api_key"] 115 | 116 | # 如果都没有,返回空字符串 117 | self.log("警告: 未提供有效的API密钥") 118 | return "" 119 | 120 | def get_saved_api_url(self): 121 | """获取保存的API URL""" 122 | config = self.load_config() 123 | return config["api_url"] 124 | 125 | def get_saved_model(self): 126 | """获取保存的模型名称""" 127 | config = self.load_config() 128 | return config["model"] 129 | 130 | def generate_empty_image(self, width=512, height=512): 131 | """生成标准格式的空白RGB图像张量""" 132 | empty_image = np.ones((height, width, 3), dtype=np.float32) * 0.2 133 | tensor = torch.from_numpy(empty_image).unsqueeze(0) # [1, H, W, 3] 134 | 135 | self.log(f"创建ComfyUI兼容的空白图像: 形状={tensor.shape}, 类型={tensor.dtype}") 136 | return tensor 137 | 138 | def encode_images_to_base64(self, image_tensor, max_dimension=1024, quality=85): 139 | """将ComfyUI的图像张量(单张或多张)转换为base64编码的列表,并进行压缩处理""" 140 | try: 141 | # 确定图像数量 142 | batch_size = image_tensor.shape[0] 143 | self.log(f"检测到 {batch_size} 张参考图像") 144 | 145 | base64_images = [] 146 | 147 | # 逐一处理每张图像 148 | for i in range(batch_size): 149 | # 获取单张图像 150 | input_image = image_tensor[i].cpu().numpy() 151 | 152 | # 转换为PIL图像 153 | input_image = (input_image * 255).astype(np.uint8) 154 | pil_image = Image.fromarray(input_image) 155 | 156 | original_width, original_height = pil_image.width, pil_image.height 157 | self.log(f"参考图像 {i+1} 原始尺寸: {original_width}x{original_height}") 158 | 159 | # 检查是否需要调整大小 160 | if original_width > max_dimension or original_height > max_dimension: 161 | # 计算等比例缩放 162 | if original_width > original_height: 163 | new_width = max_dimension 164 | new_height = int(original_height * (max_dimension / original_width)) 165 | else: 166 | new_height = max_dimension 167 | new_width = int(original_width * (max_dimension / original_height)) 168 | 169 | # 调整图像大小 170 | pil_image = pil_image.resize((new_width, new_height), Image.LANCZOS) 171 | self.log(f"参考图像 {i+1} 已调整尺寸: {new_width}x{new_height}") 172 | 173 | self.log(f"参考图像 {i+1} 处理成功,尺寸: {pil_image.width}x{pil_image.height}") 174 | 175 | # 转换为base64,使用JPEG格式和压缩 176 | buffered = BytesIO() 177 | pil_image.save(buffered, format="JPEG", quality=quality) 178 | img_str = base64.b64encode(buffered.getvalue()).decode('utf-8') 179 | img_size = len(img_str) / 1024 # 大小(KB) 180 | 181 | self.log(f"参考图像 {i+1} 编码大小: {img_size:.2f} KB") 182 | base64_images.append(img_str) 183 | 184 | return base64_images 185 | except Exception as e: 186 | self.log(f"图像转base64编码失败: {str(e)}") 187 | return None 188 | 189 | def generate_image(self, prompt, api_key, model, api_url, images, seed, max_image_size=1024, image_quality=85, image_detail="auto"): 190 | """生成图像 - 支持参考图片(单张或多张),可设置随机种子和图像处理质量""" 191 | response_text = "" 192 | 193 | # 重置日志消息 194 | self.log_messages = [] 195 | 196 | try: 197 | # 直接使用节点传入的种子值,ComfyUI已经处理了随机种子生成 198 | self.log(f"使用种子值: {seed}") 199 | 200 | # 设置随机种子,确保潜在的随机行为是可重现的 201 | torch.manual_seed(seed) 202 | np.random.seed(seed) 203 | random.seed(seed) 204 | 205 | # 获取API密钥和配置 206 | config = self.load_config() 207 | 208 | # 判断是否需要保存配置(API key、URL或model中任何一个与已保存的不同) 209 | should_save_config = False 210 | has_valid_api_key = api_key and len(api_key) > 10 211 | 212 | # 使用API密钥(优先使用用户输入,否则使用保存的) 213 | if has_valid_api_key: 214 | actual_api_key = api_key 215 | # 如果API密钥变化,需要保存配置 216 | if actual_api_key != config["api_key"]: 217 | should_save_config = True 218 | else: 219 | actual_api_key = config["api_key"] 220 | 221 | # 使用API URL(检查是否有变化,避免空值覆盖已保存的值) 222 | if api_url and api_url.strip(): # 确保不是空字符串 223 | actual_api_url = api_url 224 | if actual_api_url != config["api_url"]: 225 | should_save_config = True 226 | else: 227 | actual_api_url = config["api_url"] 228 | 229 | # 使用模型(检查是否有变化,避免空值覆盖已保存的值) 230 | if model and model.strip(): # 确保不是空字符串 231 | actual_model = model 232 | if actual_model != config["model"]: 233 | should_save_config = True 234 | else: 235 | actual_model = config["model"] 236 | 237 | # 如果需要保存并且有有效的API密钥,保存所有配置 238 | if should_save_config and actual_api_key: 239 | self.log("检测到配置变化,正在保存新配置...") 240 | self.save_config(actual_api_key, actual_api_url, actual_model) 241 | 242 | # 记录使用的配置 243 | if actual_api_key: 244 | if should_save_config: 245 | self.log("使用并保存了新的API配置") 246 | else: 247 | self.log("使用现有API配置") 248 | 249 | if not actual_api_key: 250 | error_message = "错误: 未提供有效的API密钥。请在节点中输入API密钥或确保已保存密钥。" 251 | self.log(error_message) 252 | full_text = "## 错误\n" + error_message + "\n\n## 使用说明\n1. 在节点中输入您的GPT API密钥\n2. 密钥将自动保存到节点目录,下次可以不必输入" 253 | return (self.generate_empty_image(512, 512), full_text) # 返回空白图像 254 | 255 | # 准备请求头 256 | headers = { 257 | "Content-Type": "application/json", 258 | "Authorization": f"Bearer {actual_api_key}" 259 | } 260 | 261 | # 构建请求内容 262 | content_items = [] 263 | 264 | # 添加提示文本(在用户提示词前加上指示生成图片的引导语) 265 | image_generation_prompt = "Create image"+prompt 266 | content_items.append({ 267 | "type": "text", 268 | "text": image_generation_prompt 269 | }) 270 | 271 | # 添加图像(现在是必需的,可以是多张) 272 | base64_images = self.encode_images_to_base64(images, max_dimension=max_image_size, quality=image_quality) 273 | if not base64_images or len(base64_images) == 0: 274 | error_message = "错误: 无法编码输入图像。请检查图像是否有效。" 275 | self.log(error_message) 276 | return (self.generate_empty_image(512, 512), error_message) # 返回空白图像 277 | 278 | # 添加所有图像到请求中 279 | for i, img_base64 in enumerate(base64_images): 280 | self.log(f"将图像 {i+1} 添加到请求中,图像处理质量: {image_detail}") 281 | content_items.append({ 282 | "type": "image_url", 283 | "image_url": { 284 | "url": f"data:image/jpeg;base64,{img_base64}", 285 | "detail": image_detail 286 | } 287 | }) 288 | 289 | self.log(f"成功添加 {len(base64_images)} 张图像到请求中") 290 | 291 | # 准备请求数据 - 使用chat completions格式 292 | payload = { 293 | "model": actual_model, 294 | "messages": [ 295 | { 296 | "role": "user", 297 | "content": content_items 298 | } 299 | ], 300 | "seed": seed # 在API请求中添加种子参数 301 | } 302 | 303 | # 记录请求信息 304 | self.log(f"请求GPT API,模型: {actual_model},包含 {len(base64_images)} 张图像,种子: {seed}") 305 | self.log(f"图像处理质量: {image_detail}") 306 | 307 | # 提供token估算信息 308 | token_estimation = 0 309 | if image_detail == "low": 310 | token_estimation = 85 * len(base64_images) 311 | self.log(f"图像token估算: 每张图像85个token (低质量),总计约 {token_estimation} 个token") 312 | elif image_detail == "high": 313 | # 高质量模式下的token估算比较复杂,这里给出一个粗略估计 314 | token_estimation = len(base64_images) * 765 # 假设每张图像为1024x1024,消耗约765个token 315 | self.log(f"图像token估算: 每张图像约765个token (高质量,基于1024x1024图像),总计约 {token_estimation} 个token") 316 | else: # auto 317 | token_estimation = len(base64_images) * 425 # 介于高低质量之间 318 | self.log(f"图像token估算: 每张图像约425个token (自动质量),总计约 {token_estimation} 个token") 319 | 320 | # 发送API请求 321 | self.log(f"发送API请求到: {actual_api_url}") 322 | 323 | response = requests.post(actual_api_url, headers=headers, json=payload) 324 | 325 | # 记录API响应 326 | status_code = response.status_code 327 | self.log(f"API响应状态码: {status_code}") 328 | 329 | if status_code != 200: 330 | error_msg = f"API请求失败,状态码: {status_code},响应: {response.text}" 331 | self.log(error_msg) 332 | 333 | # 构建错误返回文本 334 | full_error_text = f"## 错误\n{error_msg}\n\n## 请求信息\n模型: {actual_model}\n提示词: {prompt}\n种子: {seed}\n图像处理质量: {image_detail}\n估计token消耗: {token_estimation}\n\n## 处理日志\n" + "\n".join(self.log_messages) 335 | 336 | return (self.generate_empty_image(512, 512), full_error_text) # 返回空白图像 337 | 338 | # 解析响应 339 | response_json = response.json() 340 | 341 | # 保存响应文本用于返回 342 | response_text = json.dumps(response_json, ensure_ascii=False, indent=2) 343 | self.log("API响应接收成功,正在处理...") 344 | 345 | # 提取响应内容 346 | try: 347 | if "choices" in response_json and len(response_json["choices"]) > 0: 348 | message = response_json["choices"][0]["message"] 349 | if "content" in message: 350 | # 获取GPT回复的文本内容 351 | text_content = message["content"] 352 | self.log(f"提取到GPT响应文本: {text_content}") 353 | 354 | # 检查是否包含图像数据 (URL 或 Base64) 355 | image_tensor = None 356 | result_text = "" 357 | 358 | # --- 1. 尝试从文本中提取所有可能的图像URL --- 359 | import re 360 | 361 | # 查找Markdown格式图像链接: ![...](URL) 362 | markdown_pattern = r'!\[.*?\]\((https?://[^)\s]+)\)' 363 | 364 | # 查找链接中包含常见图片域名的URL (filesystem.site特别处理) 365 | domain_pattern = r'(https?://(?:filesystem\.site|cdn|img|image)[^)\s\"\'<>]+)' 366 | 367 | # 查找以常见图片格式结尾的URL 368 | format_pattern = r'(https?://[^)\s\"\'<>]+\.(?:png|jpg|jpeg|gif|webp))' 369 | 370 | # 查找openai视频URL (特殊处理) 371 | openai_pattern = r'(https?://videos\.openai\.com[^)\s\"\'<>]+)' 372 | 373 | # 收集所有找到的URL 374 | all_urls = [] 375 | 376 | # 提取Markdown图片链接 (这是最可能的正确格式) 377 | markdown_urls = re.findall(markdown_pattern, text_content) 378 | if markdown_urls: 379 | self.log(f"找到 {len(markdown_urls)} 个Markdown图片链接") 380 | all_urls.extend(markdown_urls) 381 | 382 | # 提取图片域名URL (备选方案1) 383 | domain_urls = re.findall(domain_pattern, text_content) 384 | if domain_urls: 385 | self.log(f"找到 {len(domain_urls)} 个图片域名URL") 386 | all_urls.extend(domain_urls) 387 | 388 | # 提取图片格式URL (备选方案2) 389 | format_urls = re.findall(format_pattern, text_content) 390 | if format_urls: 391 | self.log(f"找到 {len(format_urls)} 个图片格式URL") 392 | all_urls.extend(format_urls) 393 | 394 | # 提取OpenAI视频URL (备选方案3) 395 | openai_urls = re.findall(openai_pattern, text_content) 396 | if openai_urls: 397 | self.log(f"找到 {len(openai_urls)} 个OpenAI视频URL") 398 | all_urls.extend(openai_urls) 399 | 400 | # 去重并保持顺序 (优先使用Markdown链接中的URL) 401 | unique_urls = [] 402 | seen = set() 403 | for url in all_urls: 404 | # 清理URL (移除可能的引号或括号) 405 | clean_url = url.strip('"\'()<>') 406 | if clean_url not in seen: 407 | seen.add(clean_url) 408 | unique_urls.append(clean_url) 409 | 410 | self.log(f"共找到 {len(unique_urls)} 个唯一图像URL") 411 | for i, url in enumerate(unique_urls): 412 | self.log(f"URL {i+1}: {url}") 413 | 414 | # --- 2. 尝试从URL加载图像 --- 415 | image_processed_successfully = False 416 | 417 | for url in unique_urls: 418 | self.log(f"尝试从URL加载图像: {url}") 419 | try: 420 | # 添加超时和流式传输以提高稳健性 421 | img_response = requests.get(url, timeout=15, stream=True) 422 | img_response.raise_for_status() # 检查HTTP错误 423 | 424 | # 从URL加载图像 425 | buffer = BytesIO(img_response.content) 426 | pil_image = Image.open(buffer) 427 | 428 | # 确保是RGB模式 429 | if pil_image.mode != 'RGB': 430 | pil_image = pil_image.convert('RGB') 431 | 432 | self.log(f"成功从URL加载图像: {url} ({pil_image.width}x{pil_image.height})") 433 | 434 | # 转换为ComfyUI格式 435 | img_array = np.array(pil_image).astype(np.float32) / 255.0 436 | image_tensor = torch.from_numpy(img_array).unsqueeze(0) 437 | 438 | # 构建返回文本 439 | result_text = f"## GPT响应\n\n{text_content}\n\n" 440 | result_text += f"\n## 处理信息\n已从URL加载图像: {url}" 441 | result_text += f"\n种子: {seed}" 442 | result_text += f"\n图像处理质量: {image_detail}" 443 | result_text += f"\n估计token消耗: {token_estimation}" 444 | result_text += f"\n\n## 处理日志\n" + "\n".join(self.log_messages) 445 | 446 | image_processed_successfully = True 447 | break # 成功找到并处理了一个URL,跳出循环 448 | 449 | except requests.exceptions.RequestException as req_err: 450 | self.log(f" -> 从URL下载失败 (网络错误): {req_err}") 451 | except Image.UnidentifiedImageError: 452 | self.log(f" -> 从URL下载的数据无法识别为图像") 453 | except Exception as e: 454 | self.log(f" -> 从URL加载或处理时发生未知错误: {e}") 455 | traceback.print_exc() # 打印更详细的错误堆栈 456 | 457 | # --- 3. 如果所有URL都失败,尝试Base64 --- 458 | if not image_processed_successfully: 459 | base64_pattern = r'data:image\/[^;]+;base64,([a-zA-Z0-9+/=]+)' 460 | base64_matches = re.findall(base64_pattern, text_content) 461 | 462 | if base64_matches: 463 | self.log("尝试从Base64加载图像...") 464 | try: 465 | img_data = base64.b64decode(base64_matches[0]) 466 | buffer = BytesIO(img_data) 467 | pil_image = Image.open(buffer) 468 | 469 | if pil_image.mode != 'RGB': 470 | pil_image = pil_image.convert('RGB') 471 | 472 | self.log(f"成功从Base64加载图像: {pil_image.width}x{pil_image.height}") 473 | 474 | img_array = np.array(pil_image).astype(np.float32) / 255.0 475 | image_tensor = torch.from_numpy(img_array).unsqueeze(0) 476 | 477 | result_text = f"## GPT响应\n\n{text_content}\n\n" 478 | result_text += f"\n## 处理信息\n已从Base64数据加载图像" 479 | result_text += f"\n种子: {seed}" 480 | result_text += f"\n图像处理质量: {image_detail}" 481 | result_text += f"\n估计token消耗: {token_estimation}" 482 | result_text += f"\n\n## 处理日志\n" + "\n".join(self.log_messages) 483 | 484 | image_processed_successfully = True 485 | 486 | except Exception as e: 487 | self.log(f"从Base64加载图像失败: {e}") 488 | 489 | # --- 4. 如果URL和Base64都失败,返回空白图像和警告 --- 490 | if not image_processed_successfully: 491 | self.log("在响应中未能成功处理图像数据,返回空白图像") 492 | 493 | # 创建空白图像 494 | image_tensor = self.generate_empty_image(512, 512) 495 | 496 | # 构建返回文本 497 | result_text = f"## GPT响应\n\n{text_content}\n\n" 498 | result_text += f"\n## 请求信息\n模型: {actual_model}\n提示词: {prompt}\n种子: {seed}" 499 | result_text += f"\n图像处理质量: {image_detail}" 500 | result_text += f"\n估计token消耗: {token_estimation}" 501 | result_text += f"\n\n## 警告\n未能从API响应中成功加载图像。" 502 | result_text += f"\n\n## 处理日志\n" + "\n".join(self.log_messages) 503 | 504 | # 返回结果 505 | return (image_tensor, result_text) 506 | 507 | except Exception as e: 508 | error_message = f"处理API响应时出错: {str(e)}" 509 | self.log(error_message) 510 | traceback.print_exc() 511 | 512 | # 如果无法提取有效内容,返回原始响应 513 | self.log("无法从响应中提取有效的图像或文本内容") 514 | full_text = f"## API响应\n" + response_text + f"\n\n## 请求信息\n模型: {actual_model}\n提示词: {prompt}\n种子: {seed}\n图像处理质量: {image_detail}\n估计token消耗: {token_estimation}\n\n## 处理日志\n" + "\n".join(self.log_messages) 515 | return (self.generate_empty_image(512, 512), full_text) # 返回空白图像 516 | 517 | except Exception as e: 518 | error_message = f"处理过程中出错: {str(e)}" 519 | self.log(f"GPT API调用错误: {str(e)}") 520 | traceback.print_exc() 521 | 522 | # 合并日志和错误信息 523 | full_text = f"## 错误\n" + error_message + f"\n\n## 请求信息\n模型: {actual_model if 'actual_model' in locals() else '未知'}\n提示词: {prompt}\n种子: {seed}\n图像处理质量: {image_detail}\n估计token消耗: {token_estimation if 'token_estimation' in locals() else '未知'}\n\n## 处理日志\n" + "\n".join(self.log_messages) 524 | return (self.generate_empty_image(512, 512), full_text) # 返回空白图像 525 | 526 | # 注册节点 527 | NODE_CLASS_MAPPINGS = { 528 | "GPT-ImageGenerator": GPTImageGenerator 529 | } 530 | 531 | NODE_DISPLAY_NAME_MAPPINGS = { 532 | "GPT-ImageGenerator": "GPT4o Image Generation" 533 | } --------------------------------------------------------------------------------