add modelscop image
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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`。均可以通过前台节点配置参数实现覆盖配置文件参数的功能。
|
||||
|
||||
+6
-6
@@ -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,
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -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": "",
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user