diff --git a/README.md b/README.md index 11f5858..fdd276a 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,30 @@ # ComfyUI-YCYY-API +[**English**](README.md) | [**中文**](README_zh_CN.md) + Freely call APIs in ComfyUI +## Instructions for use +* In ComfyUI, go to your custom_nodes folder, open a terminal and run the following command: +``` +git clone https://github.com/ycyy/ComfyUI-YCYY-API.git +``` +* Copy the `config.json.example` file and rename it to `config.json`. Then modify the corresponding `base_url` and `api_key`. + +* Start `ComfyUI` and find the `YCYY/API` node directory to start your experience. + +## Configuration file description + +### gemini-image + +`base_url` supports both official and third-party compatible interfaces, which can be configured according to actual conditions. The path ends with `v1beta/models`. `models` supports `gemini-2.5-flash-image` and `gemini-3-pro-image-preview` models. If using a third-party interface, modify the model name according to actual conditions. + +### ollama-vlm and ollama-llm +`base_url` supports both local and `ollama` cloud service interfaces, which can be configured according to actual conditions. The interface is in `OpenAI` compatible format. The models in the configuration file are those supported by the official cloud service and can be modified as needed. If calling a local interface without configuring `api_key`, this option can be left blank. + +### modelscope-image + +The ModelScope image generation interface only requires you to fill in the corresponding `api_key`. Other parameters remain unchanged. + +## Advanced usage instructions + +API nodes support `Config Options` and `Proxy Options`. Both can be used to override configuration file parameters by configuring parameters through the front-end node. diff --git a/README_zh_CN.md b/README_zh_CN.md new file mode 100644 index 0000000..e3a7159 --- /dev/null +++ b/README_zh_CN.md @@ -0,0 +1,35 @@ +# ComfyUI-YCYY-API +[**English**](README.md) | [**中文**](README_zh_CN.md) + +在 ComfyUI 中自由的调用 API + +## 使用说明 +* 在 ComfyUI 中进入你的 custom_nodes 文件夹,打开终端并运行以下命令: +``` +git clone https://github.com/ycyy/ComfyUI-YCYY-API.git +``` +* 复制 `config.json.example` 文件并重命名为 `config.json`。然后修改对应的 `base_url` 和 `api_key` + +* 启动 `ComfyUI` 找到 `YCYY/API` 节点目录开始体验吧 + +## 配置文件说明 + +### gemini-image + +`base_url` 同时支持官方和第三方兼容接口根据实际情况配置,路径以 `v1beta/models` 结尾。`models` 支持 `gemini-2.5-flash-image`、`gemini-3-pro-image-preview` 模型。如果使用第三方接口,模型名称根据实际情况修改。 + +### ollama-vlm 和 ollama-llm +`base_url` 同时支持本地和 `ollama` 云服务接口,根据实际情况配置,接口为 `OpenAI` 兼容格式。配置文件的模型为官方云服务支持的模型,可以根据需要进行修改。如果调用本地接口没有配置 `api_key` 则可以不填写该选项。 + +### modelscope-image + +魔搭图片生成接口只需要填写对应的 `api_key` 其他参数保持不变即可 + +### proxy + +`proxy` 支持配置http代理,适用于特殊网络环境 + +## 高级使用说明 + +API节点支持 `Config Options` 和 `Proxy Options`。均可以通过前台节点配置参数实现覆盖配置文件参数的功能。 + diff --git a/__init__.py b/__init__.py index ace33d9..1cbf115 100644 --- a/__init__.py +++ b/__init__.py @@ -1,11 +1,11 @@ from .gemini.gemini_image_node import * from .gemini.gemini_image_preset_node import * -from .options.gemini_image_config_options_node import * -from .options.proxy_options_node import * from .ollama.ollama_vlm_node import * from .ollama.ollama_llm_node import * from .options.ollama_llm_advanced_options_node import * -from .options.ollama_config_options_node import * +from .modelscope.modelscope_image_node import * +from .options.config_options_node import * +from .options.proxy_options_node import * from typing_extensions import override class APIExtension(ComfyExtension): @@ -14,12 +14,12 @@ class APIExtension(ComfyExtension): return [ GeminiImage, GeminiImagePreset, - GeminiImageConfigOptions, - ProxyConfigOptions, OllamaVLM, OllamaLLM, OllamaLLMAdvanceOptions, - OllamaConfigOptions + ModelScopeImage, + ConfigOptions, + ProxyOptions, ] diff --git a/config.json.example b/config.json.example index 62aefbc..6a44a8d 100644 --- a/config.json.example +++ b/config.json.example @@ -30,6 +30,16 @@ "gpt-oss:120b" ] }, + "modelscope-image": { + "base_url": "https://api-inference.modelscope.cn", + "api_key": "put your token here", + "timeout": 300, + "models": [ + "Tongyi-MAI/Z-Image-Turbo", + "black-forest-labs/FLUX.1-Krea-dev", + "Qwen/Qwen-Image" + ] + }, "proxy":{ "enable": false, "http": "", diff --git a/gemini/__init__.py b/gemini/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/modelscope/modelscope_image_node.py b/modelscope/modelscope_image_node.py new file mode 100644 index 0000000..0ae828b --- /dev/null +++ b/modelscope/modelscope_image_node.py @@ -0,0 +1,334 @@ +import os +import json +import time +import requests +import torch +import numpy as np +from PIL import Image +from io import BytesIO +from typing import Optional, Dict, List, Tuple +from comfy_api.latest import ComfyExtension, io +from ..utils.config_utils import get_config_section +from ..utils.image_utils import pil_to_tensor + +class ModelScopeImage(io.ComfyNode): + """ + 这个节点使用 ModelScope API 生成图像 + """ + @classmethod + def _load_models_from_config(cls) -> List[str]: + """ + 从 config.json 中加载模型列表 + 如果获取不到,返回默认模型列表 + """ + try: + config_path = os.path.join(os.path.dirname(__file__), '..', "config.json") + if not os.path.exists(config_path): + return ["Tongyi-MAI/Z-Image-Turbo"] + + with open(config_path, 'r', encoding='utf-8') as f: + config = json.load(f) + + if 'modelscope-image' in config and 'models' in config['modelscope-image']: + models = config['modelscope-image']['models'] + if isinstance(models, list) and len(models) > 0: + return models + + return ["Tongyi-MAI/Z-Image-Turbo"] + except Exception: + return ["Tongyi-MAI/Z-Image-Turbo"] + + @classmethod + def _load_config_credentials(cls, config_options=None) -> Tuple[str, str, int]: + """ + 从 config.json 中加载并验证 API 凭据,如果提供了 config_options 则优先使用 + 返回 (base_url, api_key, timeout) 元组 + """ + # 如果提供了配置覆盖,则使用覆盖配置 + if config_options is not None: + base_url = config_options.get('base_url', '').strip() + api_key = config_options.get('api_key', '').strip() + timeout = config_options.get('timeout', 300) + + # 如果覆盖配置中有有效的 base_url 和 api_key,则直接返回 + if base_url and api_key: + return base_url, api_key, timeout + + # 否则从配置文件加载 + config_path = os.path.join(os.path.dirname(__file__), '..', "config.json") + + # 检查配置文件是否存在 + if not os.path.exists(config_path): + raise FileNotFoundError(f"Config file not found: {config_path}") + + try: + with open(config_path, 'r', encoding='utf-8') as f: + config = json.load(f) + + # 检查是否存在 modelscope 配置段 + if 'modelscope-image' not in config: + raise ValueError("Missing 'modelscope-image' section in config file") + + modelscope_image_config = config['modelscope-image'] + + # 获取并验证 base_url + if 'base_url' not in modelscope_image_config: + raise ValueError("Missing 'base_url' in modelscope-image section") + base_url = modelscope_image_config['base_url'].strip() if isinstance(modelscope_image_config['base_url'], str) else str(modelscope_image_config['base_url']).strip() + if not base_url: + raise ValueError("base_url cannot be empty") + + # 获取并验证 api_key + if 'api_key' not in modelscope_image_config: + raise ValueError("Missing 'api_key' in modelscope section") + api_key = modelscope_image_config['api_key'].strip() if isinstance(modelscope_image_config['api_key'], str) else str(modelscope_image_config['api_key']).strip() + if not api_key: + raise ValueError("api_key cannot be empty") + + # 获取 timeout 参数,默认值为 300 秒 + timeout = modelscope_image_config.get('timeout', 300) + if isinstance(timeout, str): + try: + timeout = int(timeout) + except ValueError: + timeout = 300 + + # 如果有配置覆盖,则使用覆盖的值(如果提供了) + if config_options is not None: + if config_options.get('base_url', '').strip(): + base_url = config_options['base_url'].strip() + if config_options.get('api_key', '').strip(): + api_key = config_options['api_key'].strip() + if config_options.get('timeout'): + timeout = config_options['timeout'] + + return base_url, api_key, timeout + + except FileNotFoundError: + raise + except ValueError: + raise + except json.JSONDecodeError as e: + raise ValueError(f"Invalid JSON in config file: {str(e)}") + except Exception as e: + raise ValueError(f"Config loading error: {str(e)}") + + @classmethod + def _get_proxy_config(cls, proxy_options=None) -> Optional[Dict]: + """ + 从 config.json 中获取代理配置,如果提供了 proxy_options 则优先使用 + 返回 proxies 字典或 None + """ + # 如果提供了代理覆盖配置 + if proxy_options is not None: + if not proxy_options.get('enable', False): + return None + + proxies = {} + if proxy_options.get('http', '').strip(): + proxies['http'] = proxy_options['http'].strip() + if proxy_options.get('https', '').strip(): + proxies['https'] = proxy_options['https'].strip() + + return proxies if proxies else None + + # 否则从配置文件加载 + try: + proxy_config = get_config_section('proxy') + if not proxy_config or not proxy_config.get('enable', False): + return None + + proxies = {} + if proxy_config.get('http'): + proxies['http'] = proxy_config['http'] + if proxy_config.get('https'): + proxies['https'] = proxy_config['https'] + + return proxies if proxies else None + except Exception: + return None + + @classmethod + def define_schema(cls) -> io.Schema: + """ + 返回一个包含该节点所有信息的模式(schema) + """ + # 从配置文件加载模型列表 + model_options = cls._load_models_from_config() + default_model = model_options[0] + + return io.Schema( + node_id="YCYY_ModelScope_Image_API", + display_name="ModelScope Image API", + category="YCYY/API/image", + inputs=[ + io.String.Input( + id="prompt", + multiline=True, + tooltip="Image generation positive prompt" + ), + io.String.Input( + id="negative_prompt", + multiline=True, + tooltip="Image generation negative prompt" + ), + io.Combo.Input( + id="model", + options=model_options, + default=default_model, + tooltip="Select ModelScope image generation model" + ), + io.Int.Input( + id="width", + min=64, + max=2048, + default=1024, + step=8 + ), + io.Int.Input( + id="height", + min=64, + max=2048, + default=1024, + step=8 + ), + io.Int.Input( + id="steps", + min=1, + max=100, + default=30, + step=1 + ), + io.Float.Input( + id="guidance", + min=1.5, + max=20, + default=3.5, + step=0.1 + ), + io.AnyType.Input( + id="config_options", + optional=True, + tooltip="Optional configuration override" + ), + io.AnyType.Input( + id="proxy_options", + optional=True, + tooltip="Optional proxy configuration override" + ), + io.Int.Input( + id="seed", + min=0, + max=2147483647, + default=0, + control_after_generate=True + ) + ], + outputs=[ + io.Image.Output(), + io.String.Output() + ], + description="This node uses the ModelScope API to generate images." + ) + + @classmethod + def execute(cls, prompt, negative_prompt, model, width, height, steps, guidance, seed, config_options=None, proxy_options=None) -> io.NodeOutput: + """ + 节点执行入口 + """ + base_url, api_key, timeout = cls._load_config_credentials(config_options) + proxies = cls._get_proxy_config(proxy_options) + if not prompt or not prompt.strip(): + raise Exception("prompt cannot be empty") + return cls._generate_images(base_url,api_key,prompt,negative_prompt,model,width,height, steps, guidance, seed,timeout,proxies) + @classmethod + def _generate_images(cls,base_url,api_key,prompt,negative_prompt,model,width,height, steps, guidance, seed,timeout,proxies)-> io.NodeOutput: + # 构建返回参数 + result_image = cls._create_empty_image() + result_message = json.dumps({ + "success": False, + "message": "API request returns an error" + }) + output_image_url = None + # 构建请求 URL + api_url = f"{base_url}/v1/images/generations" + # 构建请求头 + headers = { + "Authorization": f"Bearer {api_key}", + "Content-Type": "application/json", + "X-ModelScope-Async-Mode": "true" + } + # 构建请求体 + payload = { + "model": model, + "prompt": prompt, + "size": f"{width}x{height}", + "steps": steps, + "guidance": guidance, + "seed": seed + } + if negative_prompt is not None and negative_prompt: + payload["negative_prompt"] = negative_prompt + try: + response = requests.post( + api_url, + headers=headers, + data=json.dumps(payload, ensure_ascii=False).encode('utf-8'), + timeout=timeout, + proxies=proxies + ) + if response.status_code != 200: + result_message = json.dumps({ + "success": False, + "message": f"API request returns an error.status_code:{response.status_code}.error_reason:{response.text}" + }) + raise Exception(result_message) + task_id = response.json()["task_id"] + if task_id is not None and task_id: + while True: + result = requests.get( + f"{base_url}/v1/tasks/{task_id}", + headers={ + 'Authorization': f'Bearer {api_key}', + 'X-ModelScope-Task-Type': 'image_generation' + }, + timeout=timeout + ) + if result.status_code != 200: + result_message = json.dumps({ + "success": False, + "message": f"API request returns an error.status_code:{result.status_code}.error_reason:{result.text}" + }) + raise Exception(result_message) + data = result.json() + if data["task_status"] == "SUCCEED": + output_image_url = data["output_images"][0] + break + elif data["task_status"] == "FAILED": + result_message = json.dumps({ + "success": False, + "message": "Image generation failed." + }) + break + time.sleep(5) + output_image_response = requests.get(output_image_url, timeout=timeout) + pil_image = Image.open(BytesIO(output_image_response.content)) + if pil_image.mode != 'RGB': + pil_image = pil_image.convert('RGB') + result_image = pil_to_tensor(pil_image) + result_message = json.dumps({ + "success": True, + "message": "Image generation success.", + "image_url": output_image_url + }) + return io.NodeOutput(result_image,result_message) + except Exception as e: + raise Exception(result_message) + + # 创建空图像 + @classmethod + def _create_empty_image(cls): + try: + return torch.zeros(1, 512, 512, 3, dtype=torch.float32) + except Exception as e: + return None \ No newline at end of file diff --git a/options/ollama_config_options_node.py b/options/config_options_node.py similarity index 71% rename from options/ollama_config_options_node.py rename to options/config_options_node.py index 698f629..0f4fd8c 100644 --- a/options/ollama_config_options_node.py +++ b/options/config_options_node.py @@ -1,28 +1,27 @@ from comfy_api.latest import ComfyExtension, io -class OllamaConfigOptions(io.ComfyNode): +class ConfigOptions(io.ComfyNode): """ - 这个节点用于覆盖 Ollama API 的配置参数(base_url、api_key、timeout) + 这个节点用于覆盖 API 的配置参数(base_url、api_key、timeout) """ @classmethod def define_schema(cls) -> io.Schema: return io.Schema( - node_id="YCYY_Ollama_Config_Options", - display_name="Ollama Config Options", + node_id="YCYY_Config_Options", + display_name="Config Options", category="YCYY/API/utils", inputs=[ io.String.Input( id="base_url", - default="http://localhost:11434/api/chat", multiline=True, - tooltip="Override the base URL for Ollama API" + tooltip="Override the API base URL" ), io.String.Input( id="api_key", default="", multiline=True, - tooltip="Override the API key for Ollama API (optional)" + tooltip="Override the API key" ), io.Int.Input( id="timeout", @@ -36,10 +35,10 @@ class OllamaConfigOptions(io.ComfyNode): io.AnyType.Output( id="config_options", display_name="config_options", - tooltip="Configuration override for Ollama API" + tooltip="Override configuration options" ) ], - description="This node provides configuration override options for Ollama API (base_url, api_key, timeout)." + description="This node provides configuration override options (base_url, api_key, timeout)." ) @classmethod diff --git a/options/gemini_image_config_options_node.py b/options/gemini_image_config_options_node.py deleted file mode 100644 index 475bba9..0000000 --- a/options/gemini_image_config_options_node.py +++ /dev/null @@ -1,57 +0,0 @@ -from comfy_api.latest import ComfyExtension, io - -class GeminiImageConfigOptions(io.ComfyNode): - """ - 这个节点用于覆盖 Gemini Image API 的配置参数(base_url、api_key、timeout) - """ - - @classmethod - def define_schema(cls) -> io.Schema: - return io.Schema( - node_id="YCYY_Gemini_Image_Config_Options", - display_name="Gemini Image Config Options", - category="YCYY/API/utils", - inputs=[ - io.String.Input( - id="base_url", - default="https://generativelanguage.googleapis.com/v1beta/models", - multiline=True, - tooltip="Override the base URL for Gemini Image API" - ), - io.String.Input( - id="api_key", - default="", - multiline=True, - tooltip="Override the API key for Gemini Image API" - ), - io.Int.Input( - id="timeout", - default=120, - min=1, - max=600, - tooltip="Override the request timeout in seconds" - ) - ], - outputs=[ - io.AnyType.Output( - id="config_options", - display_name="config_options", - tooltip="Configuration override for Gemini Image API" - ) - ], - description="This node provides configuration override options for Gemini Image API (base_url, api_key, timeout)." - ) - - @classmethod - def execute(cls, base_url, api_key, timeout) -> io.NodeOutput: - # 验证和清理输入 - base_url = base_url.strip() if base_url else "" - api_key = api_key.strip() if api_key else "" - - config_options = { - "base_url": base_url, - "api_key": api_key, - "timeout": timeout - } - - return io.NodeOutput(config_options) \ No newline at end of file diff --git a/options/proxy_options_node.py b/options/proxy_options_node.py index 6973801..48cd3ea 100644 --- a/options/proxy_options_node.py +++ b/options/proxy_options_node.py @@ -1,6 +1,6 @@ from comfy_api.latest import ComfyExtension, io -class ProxyConfigOptions(io.ComfyNode): +class ProxyOptions(io.ComfyNode): """ 这个节点用于覆盖代理配置参数(enable、http、https) """ @@ -8,8 +8,8 @@ class ProxyConfigOptions(io.ComfyNode): @classmethod def define_schema(cls) -> io.Schema: return io.Schema( - node_id="YCYY_Proxy_Config_Options", - display_name="Proxy Config Options", + node_id="YCYY_Proxy_Options", + display_name="Proxy Options", category="YCYY/API/utils", inputs=[ io.Boolean.Input(