diff --git a/__init__.py b/__init__.py index 029ce9e..4b9aae9 100644 --- a/__init__.py +++ b/__init__.py @@ -416,6 +416,11 @@ try: NODE_CLASS_MAPPINGS as TEXTBOX_MAPPINGS from .nodes.tools.textbox_node import \ NODE_DISPLAY_NAME_MAPPINGS as TEXTBOX_DISPLAY_MAPPINGS + # API图像生成器节点 + from .nodes.tools.api_image_generator import \ + NODE_CLASS_MAPPINGS as API_IMAGE_GENERATOR_MAPPINGS + from .nodes.tools.api_image_generator import \ + NODE_DISPLAY_NAME_MAPPINGS as API_IMAGE_GENERATOR_DISPLAY_MAPPINGS except ImportError as e: print(f"导入错误: {e}") @@ -438,6 +443,8 @@ except ImportError as e: TEXT_CONCATENATE_DISPLAY_MAPPINGS = {} MATH_EXPRESSION_MAPPINGS = {} MATH_EXPRESSION_DISPLAY_MAPPINGS = {} + API_IMAGE_GENERATOR_MAPPINGS = {} + API_IMAGE_GENERATOR_DISPLAY_MAPPINGS = {} # 尝试导入其他可能有依赖的节点 try: @@ -670,6 +677,7 @@ except ImportError: LAZY_SWITCH_MAPPINGS = {} LAZY_SWITCH_DISPLAY = {} + # 合并所有节点映射 NODE_CLASS_MAPPINGS = {} NODE_CLASS_MAPPINGS.update(EMPTY_UNIT_MAPPINGS) @@ -700,6 +708,7 @@ NODE_CLASS_MAPPINGS.update(AUDIO_CROP_MAPPINGS) NODE_CLASS_MAPPINGS.update(TEXTBOX_MAPPINGS) NODE_CLASS_MAPPINGS.update(TEXT_CONCATENATE_MAPPINGS) NODE_CLASS_MAPPINGS.update(MATH_EXPRESSION_MAPPINGS) +NODE_CLASS_MAPPINGS.update(API_IMAGE_GENERATOR_MAPPINGS) NODE_CLASS_MAPPINGS.update(THINK_REMOVER_MAPPINGS) NODE_CLASS_MAPPINGS.update(LORA_INFO_MAPPINGS) NODE_CLASS_MAPPINGS.update(KONTEXT_PRESETS_MAPPINGS) @@ -737,6 +746,7 @@ NODE_DISPLAY_NAME_MAPPINGS.update(MASK_DISPLAY_MAPPINGS) NODE_DISPLAY_NAME_MAPPINGS.update(TEXTBOX_DISPLAY_MAPPINGS) NODE_DISPLAY_NAME_MAPPINGS.update(TEXT_CONCATENATE_DISPLAY_MAPPINGS) NODE_DISPLAY_NAME_MAPPINGS.update(MATH_EXPRESSION_DISPLAY_MAPPINGS) +NODE_DISPLAY_NAME_MAPPINGS.update(API_IMAGE_GENERATOR_DISPLAY_MAPPINGS) NODE_DISPLAY_NAME_MAPPINGS.update(THINK_REMOVER_DISPLAY_MAPPINGS) NODE_DISPLAY_NAME_MAPPINGS.update(LORA_INFO_DISPLAY_MAPPINGS) NODE_DISPLAY_NAME_MAPPINGS.update(KONTEXT_PRESETS_DISPLAY_MAPPINGS) @@ -781,6 +791,7 @@ NODE_CATEGORIES = { "LoadKontextPresets_UTK", "ColorToMask_UTK", "LazySwitchKJ_UTK", + "APIImageGenerator_UTK", ] } diff --git a/nodes/tools/api_image_generator.py b/nodes/tools/api_image_generator.py new file mode 100644 index 0000000..210a0ef --- /dev/null +++ b/nodes/tools/api_image_generator.py @@ -0,0 +1,482 @@ +""" +API图像生成器节点 +~~~~~~~~~~~~~~~~ + +通用API图像生成服务节点,支持多种运营商API接口。 +提供统一的接口来调用不同的图像生成API服务。 + +:copyright: (c) 2024 by May +:license: MIT, see LICENSE for more details. +""" + +import json +import base64 +import io +import requests +from PIL import Image +import numpy as np + +# 条件导入torch,避免在没有torch的环境中导入失败 +try: + import torch + TORCH_AVAILABLE = True +except ImportError: + TORCH_AVAILABLE = False + # 创建一个简单的torch替代类 + class MockTorch: + @staticmethod + def from_numpy(array): + return array + + @staticmethod + def permute(tensor, *dims): + return tensor + + @staticmethod + def unsqueeze(tensor, dim): + return tensor + + torch = MockTorch() + + +class APIImageGenerator_UTK: + """ + 通用API图像生成器节点 + + 支持多种运营商API接口,提供统一的图像生成服务。 + 包含运营商选择、API密钥管理、参数配置等功能。 + """ + + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "provider": (["placeholder", "jimeng4"], { + "tooltip": "选择API运营商", + "default": "placeholder" + }), + "api_key": ("STRING", { + "default": "", + "multiline": False, + "tooltip": "API密钥" + }), + "prompt": ("STRING", { + "default": "Generate a beautiful image", + "multiline": True, + "tooltip": "图像生成提示词" + }), + "negative_prompt": ("STRING", { + "default": "", + "multiline": True, + "tooltip": "负面提示词" + }), + "width": ("INT", { + "default": 1024, + "min": 256, + "max": 2048, + "step": 64, + "tooltip": "生成图像宽度" + }), + "height": ("INT", { + "default": 1024, + "min": 256, + "max": 2048, + "step": 64, + "tooltip": "生成图像高度" + }), + "steps": ("INT", { + "default": 20, + "min": 1, + "max": 100, + "tooltip": "生成步数" + }), + "cfg_scale": ("FLOAT", { + "default": 7.0, + "min": 1.0, + "max": 20.0, + "step": 0.1, + "tooltip": "CFG引导强度" + }), + "seed": ("INT", { + "default": -1, + "min": -1, + "max": 2147483647, + "tooltip": "随机种子(-1为随机)" + }), + "scheduler": (["DDIM", "DDPM", "DPM++ 2M", "DPM++ 2M Karras", "DPM++ SDE", "DPM++ SDE Karras"], { + "default": "DDIM", + "tooltip": "调度器类型" + }), + "model": (["placeholder", "jimeng4-general", "jimeng4-portrait"], { + "default": "placeholder", + "tooltip": "选择模型(即梦4.0: general=通用模型, portrait=人像模型)" + }), + }, + "optional": { + "image": ("IMAGE", { + "tooltip": "输入图像(仅图生图或编辑模型时需要)" + }), + "controlnet_image": ("IMAGE", { + "tooltip": "ControlNet输入图像(可选)" + }), + "controlnet_type": (["none", "canny", "depth", "pose", "openpose"], { + "default": "none", + "tooltip": "ControlNet类型" + }), + "controlnet_strength": ("FLOAT", { + "default": 1.0, + "min": 0.0, + "max": 2.0, + "step": 0.1, + "tooltip": "ControlNet强度" + }), + } + } + + RETURN_TYPES = ("IMAGE", "STRING") + RETURN_NAMES = ("image", "api_url") + FUNCTION = "generate_image" + CATEGORY = "UniversalToolkit/Tools" + + DESCRIPTION = """ + 通用API图像生成器节点,支持多种运营商API接口。 + + 功能特性: + - **多运营商支持**: 支持多种API服务提供商 + - **即梦4.0集成**: 已集成火山引擎即梦4.0图像生成API + - **统一接口**: 提供标准化的参数配置 + - **灵活配置**: 支持完整的生成参数调整 + - **可选图像输入**: 支持文生图和图生图两种模式 + - **ControlNet支持**: 可选的控制网络输入 + - **URL输出**: 返回API调用地址用于调试 + + 支持的运营商: + - **即梦4.0**: 火山引擎图像生成服务,支持通用和人像模型 + - **占位符**: 用于测试和演示的模拟API + + 使用说明: + 1. 选择API运营商并填入对应的API密钥 + 2. 输入生成提示词和参数 + 3. 可选连接输入图像(仅图生图或编辑模型时需要) + 4. 可选添加ControlNet控制图像 + 5. 执行生成获取结果图像和API调用地址 + + 注意事项: + - 请确保API密钥有效且有足够额度 + - 即梦4.0需要有效的火山引擎API密钥 + - 不同运营商的参数范围可能不同 + - 生成时间取决于API服务商的响应速度 + - 输入图像仅在需要图生图或编辑功能时连接 + """ + + def generate_image(self, provider, api_key, prompt, negative_prompt, + width, height, steps, cfg_scale, seed, scheduler, model, + image=None, controlnet_image=None, controlnet_type="none", controlnet_strength=1.0): + """ + 执行API图像生成 + + Args: + provider: API运营商 + api_key: API密钥 + prompt: 生成提示词 + negative_prompt: 负面提示词 + width: 图像宽度 + height: 图像高度 + steps: 生成步数 + cfg_scale: CFG引导强度 + seed: 随机种子 + scheduler: 调度器类型 + model: 模型选择 + image: 输入图像(可选,仅图生图或编辑模型时需要) + controlnet_image: ControlNet输入图像 + controlnet_type: ControlNet类型 + controlnet_strength: ControlNet强度 + + Returns: + Tuple[torch.Tensor, str]: 生成的图像和API调用URL + """ + + try: + # 转换输入图像为PIL格式(如果提供了图像) + input_image = None + if image is not None: + if isinstance(image, torch.Tensor): + # 处理批次图像,取第一张 + if image.dim() == 4: + image = image[0] + + # 转换为numpy数组 + if image.shape[0] == 3: # CHW格式 + image_np = image.permute(1, 2, 0).cpu().numpy() + else: # HWC格式 + image_np = image.cpu().numpy() + + # 归一化到0-255范围 + if image_np.max() <= 1.0: + image_np = (image_np * 255).astype(np.uint8) + else: + image_np = image_np.astype(np.uint8) + + input_image = Image.fromarray(image_np) + else: + input_image = image + + # 根据运营商调用不同的API + if provider == "placeholder": + # 占位符实现 + result_image, api_url = self._placeholder_api( + input_image, prompt, negative_prompt, width, height, + steps, cfg_scale, seed, scheduler, model, + controlnet_image, controlnet_type, controlnet_strength + ) + elif provider == "jimeng4": + # 即梦4.0 API调用 + result_image, api_url = self._call_jimeng4_api( + api_key, input_image, prompt, negative_prompt, + width, height, steps, cfg_scale, seed, scheduler, model, + controlnet_image, controlnet_type, controlnet_strength + ) + else: + # 其他运营商API调用 + result_image, api_url = self._call_api( + provider, api_key, input_image, prompt, negative_prompt, + width, height, steps, cfg_scale, seed, scheduler, model, + controlnet_image, controlnet_type, controlnet_strength + ) + + # 转换结果为ComfyUI格式 + if isinstance(result_image, Image.Image): + # 转换为RGB模式 + if result_image.mode != 'RGB': + result_image = result_image.convert('RGB') + + # 转换为numpy数组 + result_np = np.array(result_image).astype(np.float32) / 255.0 + + # 转换为torch tensor (HWC -> CHW) + result_tensor = torch.from_numpy(result_np).permute(2, 0, 1) + + # 添加批次维度 + result_tensor = result_tensor.unsqueeze(0) + else: + result_tensor = result_image + + return (result_tensor, api_url) + + except Exception as e: + print(f"API图像生成错误: {str(e)}") + # 返回原图像作为fallback + if isinstance(image, torch.Tensor): + return (image, f"错误: {str(e)}") + else: + # 转换PIL图像为tensor + if image.mode != 'RGB': + image = image.convert('RGB') + result_np = np.array(image).astype(np.float32) / 255.0 + result_tensor = torch.from_numpy(result_np).permute(2, 0, 1).unsqueeze(0) + return (result_tensor, f"错误: {str(e)}") + + def _placeholder_api(self, input_image, prompt, negative_prompt, width, height, + steps, cfg_scale, seed, scheduler, model, + controlnet_image=None, controlnet_type="none", controlnet_strength=1.0): + """ + 占位符API实现,用于测试和演示 + """ + if input_image is not None: + # 如果有输入图像,调整尺寸 + result_image = input_image.resize((width, height), Image.Resampling.LANCZOS) + else: + # 如果没有输入图像,生成一个简单的彩色图像作为占位符 + result_image = Image.new('RGB', (width, height), color=(128, 128, 128)) + + # 构造API URL(模拟) + api_url = f"https://api.placeholder.com/v1/images/generations" + + return result_image, api_url + + def _call_jimeng4_api(self, api_key, input_image, prompt, negative_prompt, + width, height, steps, cfg_scale, seed, scheduler, model, + controlnet_image=None, controlnet_type="none", controlnet_strength=1.0): + """ + 调用即梦4.0 API服务 + + 基于火山引擎即梦4.0图像生成API文档实现 + """ + try: + # 即梦4.0 API端点 + api_url = "https://ark.cn-beijing.volces.com/api/v3/seedream-4.0" + + # 构造请求数据 + request_data = { + "prompt": prompt, + "size": f"{width}x{height}", + "response_format": "url", + "model": model, + "n": 1, # 生成图像数量 + } + + # 添加负面提示词 + if negative_prompt: + request_data["negative_prompt"] = negative_prompt + + # 添加随机种子 + if seed != -1: + request_data["seed"] = seed + + # 添加CFG引导强度 + if cfg_scale != 7.0: + request_data["guidance_scale"] = cfg_scale + + # 添加步数 + if steps != 20: + request_data["num_inference_steps"] = steps + + # 如果有输入图像,处理图生图 + if input_image is not None: + # 将图像转换为base64 + buffer = io.BytesIO() + input_image.save(buffer, format='PNG') + image_base64 = base64.b64encode(buffer.getvalue()).decode() + request_data["image"] = image_base64 + request_data["strength"] = 0.8 # 默认强度 + + # 请求头 + headers = { + "Authorization": f"Bearer {api_key}", + "Content-Type": "application/json" + } + + # 发送请求 + print(f"调用即梦4.0 API: {api_url}") + print(f"请求数据: {json.dumps(request_data, indent=2, ensure_ascii=False)}") + + response = requests.post(api_url, headers=headers, json=request_data, timeout=60) + + if response.status_code == 200: + result_data = response.json() + + # 获取生成的图像URL + if "data" in result_data and len(result_data["data"]) > 0: + image_url = result_data["data"][0]["url"] + + # 下载图像 + image_response = requests.get(image_url, timeout=30) + if image_response.status_code == 200: + image_bytes = io.BytesIO(image_response.content) + result_image = Image.open(image_bytes) + result_image = result_image.convert('RGB') + + return result_image, api_url + else: + raise Exception(f"下载图像失败: {image_response.status_code}") + else: + raise Exception("API响应中未找到图像数据") + else: + error_msg = f"API调用失败: {response.status_code}" + try: + error_data = response.json() + if "error" in error_data: + error_msg += f" - {error_data['error']}" + except: + error_msg += f" - {response.text}" + raise Exception(error_msg) + + except Exception as e: + print(f"即梦4.0 API调用错误: {str(e)}") + # 返回占位符图像 + if input_image is not None: + result_image = input_image + else: + result_image = Image.new('RGB', (width, height), color=(128, 128, 128)) + return result_image, f"错误: {str(e)}" + + def _call_api(self, provider, api_key, input_image, prompt, negative_prompt, + width, height, steps, cfg_scale, seed, scheduler, model, + controlnet_image=None, controlnet_type="none", controlnet_strength=1.0): + """ + 调用实际的API服务 + + 这里可以根据不同的provider实现不同的API调用逻辑 + 后续添加具体API时会扩展此方法 + """ + + # 构造API请求数据 + api_data = { + "prompt": prompt, + "negative_prompt": negative_prompt, + "width": width, + "height": height, + "steps": steps, + "cfg_scale": cfg_scale, + "seed": seed if seed != -1 else None, + "scheduler": scheduler, + "model": model, + } + + # 如果提供了输入图像,转换为base64编码 + if input_image is not None: + buffer = io.BytesIO() + input_image.save(buffer, format='PNG') + image_base64 = base64.b64encode(buffer.getvalue()).decode() + api_data["input_image"] = image_base64 + + # 添加ControlNet参数 + if controlnet_image is not None and controlnet_type != "none": + # 处理ControlNet图像 + if isinstance(controlnet_image, torch.Tensor): + if controlnet_image.dim() == 4: + controlnet_image = controlnet_image[0] + + if controlnet_image.shape[0] == 3: + controlnet_np = controlnet_image.permute(1, 2, 0).cpu().numpy() + else: + controlnet_np = controlnet_image.cpu().numpy() + + if controlnet_np.max() <= 1.0: + controlnet_np = (controlnet_np * 255).astype(np.uint8) + else: + controlnet_np = controlnet_np.astype(np.uint8) + + controlnet_pil = Image.fromarray(controlnet_np) + else: + controlnet_pil = controlnet_image + + # 转换为base64 + controlnet_buffer = io.BytesIO() + controlnet_pil.save(controlnet_buffer, format='PNG') + controlnet_base64 = base64.b64encode(controlnet_buffer.getvalue()).decode() + + api_data.update({ + "controlnet_type": controlnet_type, + "controlnet_strength": controlnet_strength, + "controlnet_image": controlnet_base64, + }) + + # 构造API URL + api_url = f"https://api.{provider}.com/v1/images/generations" + + # 这里应该发送实际的HTTP请求 + # 目前返回占位符图像 + print(f"模拟调用API: {provider}") + print(f"API数据: {json.dumps(api_data, indent=2, ensure_ascii=False)}") + + # 生成占位符图像 + if input_image is not None: + result_image = input_image + else: + result_image = Image.new('RGB', (width, height), color=(128, 128, 128)) + + return result_image, api_url + + +# 节点注册 +NODE_CLASS_MAPPINGS = { + "APIImageGenerator_UTK": APIImageGenerator_UTK, +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "APIImageGenerator_UTK": "API Image Generator (UTK)", +} diff --git a/pyproject.toml b/pyproject.toml index fb31f47..dee8d84 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,10 +4,18 @@ description = "A comprehensive toolkit based on ComfyUI, providing image, mask, version = "1.3.7" license = {file = "LICENSE"} dependencies = [ - "torch", - "numpy", - "Pillow", - "opencv-python" + "torch>=1.9.0", + "numpy>=1.21.0", + "Pillow>=8.0.0", + "opencv-python>=4.5.0", + "librosa>=0.8.0", + "torchaudio>=1.9.0", + "soundfile>=0.10.0", + "scipy>=1.7.0", + "color-matcher", + "requests>=2.25.0", + "aiohttp>=3.8.0", + "tqdm>=4.60.0" ] requires-python = ">=3.8" classifiers = [ @@ -24,7 +32,7 @@ Documentation = "https://github.com/whmc76/ComfyUI-UniversalToolkit/wiki" [tool.comfy] PublisherId = "whmc76" DisplayName = "ComfyUI-UniversalToolkit" -Icon = "" -Banner = "" +Icon = "https://raw.githubusercontent.com/whmc76/ComfyUI-UniversalToolkit/main/assets/icon.png" +Banner = "https://raw.githubusercontent.com/whmc76/ComfyUI-UniversalToolkit/main/assets/banner.png" requires-comfyui = ">=1.0.0" includes = []