merge modelscope image and edit node
This commit is contained in:
@@ -23,7 +23,9 @@ git clone https://github.com/ycyy/ComfyUI-YCYY-API.git
|
||||
|
||||
### modelscope-image
|
||||
|
||||
The ModelScope image generation interface only requires you to fill in the corresponding `api_key`. Other parameters remain unchanged.
|
||||
`modelscope-image` uses an array to configure multiple ModelScope image API names. Image generation and editing share one node: leave `image` disconnected to generate an image, or connect it to edit an image. We recommend separate generation and editing `api-name` entries, each with the appropriate models. Changing `api-name` updates the node's model list.
|
||||
|
||||
Legacy single-object `modelscope-image` configurations remain supported and use `default` as the api name. Move the former `modelscope-image-edit` settings into a separate entry in the `modelscope-image` array. For the official endpoint, normally only the corresponding `api_key` needs to be changed from the example.
|
||||
|
||||
### openai-text
|
||||
|
||||
|
||||
+3
-1
@@ -23,7 +23,9 @@ git clone https://github.com/ycyy/ComfyUI-YCYY-API.git
|
||||
|
||||
### modelscope-image
|
||||
|
||||
魔搭图片生成接口只需要填写对应的 `api_key` 其他参数保持不变即可
|
||||
`modelscope-image` 使用数组配置多个魔搭图片 API name。图片生成和图片编辑共用一个节点:不连接 `image` 时生成图片,连接 `image` 时编辑图片。建议分别创建生成和编辑两个 `api-name`,并在各自的 `models` 中配置对应类型的模型;切换 `api-name` 后节点会同步更新模型列表。
|
||||
|
||||
旧版单对象 `modelscope-image` 配置仍可使用,其 api name 显示为 `default`。原 `modelscope-image-edit` 配置需要迁移为 `modelscope-image` 数组中的独立配置项。通常只需要填写对应的 `api_key`,其他官方接口参数保持示例值即可。
|
||||
|
||||
### openai-text
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ from .ollama.ollama_vlm_node import *
|
||||
from .ollama.ollama_llm_node import *
|
||||
from .options.ollama_llm_advanced_options_node import *
|
||||
from .modelscope.modelscope_image_node import *
|
||||
from .modelscope.modelscope_image_edit_node import *
|
||||
from .options.config_options_node import *
|
||||
from .options.gemini_speaker_options_node import *
|
||||
from .options.gemini_batch_speakers_options_node import *
|
||||
@@ -37,7 +36,6 @@ class APIExtension(ComfyExtension):
|
||||
OllamaLLM,
|
||||
OllamaVLM,
|
||||
ModelScopeImage,
|
||||
ModelScopeImageEdit,
|
||||
ConfigOptions,
|
||||
ProxyOptions,
|
||||
GeminiImagePreset,
|
||||
|
||||
+27
-23
@@ -109,31 +109,35 @@
|
||||
"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-2512",
|
||||
"ideogram-ai/ideogram-4-fp8",
|
||||
"krea/Krea-2-Turbo"
|
||||
]
|
||||
},
|
||||
"modelscope-image-edit": {
|
||||
"base_url": "https://api-inference.modelscope.cn",
|
||||
"api_key": "put your token here",
|
||||
"timeout": 300,
|
||||
"models": [
|
||||
"Qwen/Qwen-Image-Edit-2511",
|
||||
"black-forest-labs/FLUX.2-klein-9B",
|
||||
"FireRedTeam/FireRed-Image-Edit-1.1"
|
||||
]
|
||||
},
|
||||
"modelscope-image": [
|
||||
{
|
||||
"api-name": "ModelScope Image Generation",
|
||||
"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-2512",
|
||||
"ideogram-ai/ideogram-4-fp8",
|
||||
"krea/Krea-2-Turbo"
|
||||
]
|
||||
},
|
||||
{
|
||||
"api-name": "ModelScope Image Editing",
|
||||
"base_url": "https://api-inference.modelscope.cn",
|
||||
"api_key": "put your token here",
|
||||
"timeout": 300,
|
||||
"models": [
|
||||
"Qwen/Qwen-Image-Edit-2511",
|
||||
"black-forest-labs/FLUX.2-klein-9B",
|
||||
"FireRedTeam/FireRed-Image-Edit-1.1"
|
||||
]
|
||||
}
|
||||
],
|
||||
"proxy": {
|
||||
"enable": false,
|
||||
"http": "",
|
||||
"https": "http://127.0.0.1:7890"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+11
-52
@@ -609,7 +609,7 @@
|
||||
},
|
||||
"YCYY_ModelScope_Image_API": {
|
||||
"display_name": "ModelScope Image API",
|
||||
"description": "This node uses the ModelScope API to generate images.",
|
||||
"description": "Generate or edit images through the ModelScope API. Connecting an image enables edit mode.",
|
||||
"inputs": {
|
||||
"config_options": {
|
||||
"name": "config_options",
|
||||
@@ -621,61 +621,16 @@
|
||||
},
|
||||
"prompt": {
|
||||
"name": "prompt",
|
||||
"tooltip": "Image generation positive prompt"
|
||||
},
|
||||
"negative_prompt": {
|
||||
"name": "negative_prompt",
|
||||
"tooltip": "Image generation negative prompt"
|
||||
},
|
||||
"model": {
|
||||
"name": "model"
|
||||
},
|
||||
"width": {
|
||||
"name": "width"
|
||||
},
|
||||
"height": {
|
||||
"name": "height"
|
||||
},
|
||||
"steps": {
|
||||
"name": "steps"
|
||||
},
|
||||
"guidance": {
|
||||
"name": "guidance"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"name": "Image"
|
||||
},
|
||||
"1": {
|
||||
"name": "String"
|
||||
}
|
||||
}
|
||||
},
|
||||
"YCYY_ModelScope_Image_Edit_API": {
|
||||
"display_name": "ModelScope Image Edit API",
|
||||
"description": "This node uses the ModelScope API to edit an input image.",
|
||||
"inputs": {
|
||||
"image": {
|
||||
"name": "image",
|
||||
"tooltip": "Input image to edit"
|
||||
},
|
||||
"config_options": {
|
||||
"name": "config_options",
|
||||
"tooltip": "Optional configuration override from YCYY API Config Options"
|
||||
},
|
||||
"proxy_options": {
|
||||
"name": "proxy_options",
|
||||
"tooltip": "Optional proxy configuration override from YCYY API Proxy Options"
|
||||
},
|
||||
"prompt": {
|
||||
"name": "prompt",
|
||||
"tooltip": "Image editing instruction"
|
||||
"tooltip": "Prompt used to generate or edit the image"
|
||||
},
|
||||
"negative_prompt": {
|
||||
"name": "negative_prompt",
|
||||
"tooltip": "Negative prompt"
|
||||
},
|
||||
"api_name": {
|
||||
"name": "api name",
|
||||
"tooltip": "Select a ModelScope image API name"
|
||||
},
|
||||
"model": {
|
||||
"name": "model"
|
||||
},
|
||||
@@ -690,6 +645,10 @@
|
||||
},
|
||||
"guidance": {
|
||||
"name": "guidance"
|
||||
},
|
||||
"image": {
|
||||
"name": "image",
|
||||
"tooltip": "Optional source image. Connect it to edit an image; leave it disconnected to generate one"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
@@ -715,4 +674,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -610,7 +610,7 @@
|
||||
},
|
||||
"YCYY_ModelScope_Image_API": {
|
||||
"display_name": "魔搭图像API",
|
||||
"description": "该节点使用 ModelScope API 生成图像",
|
||||
"description": "通过 ModelScope API 生成或编辑图像;连接图像后启用编辑模式",
|
||||
"inputs": {
|
||||
"config_options": {
|
||||
"name": "配置选项",
|
||||
@@ -622,11 +622,15 @@
|
||||
},
|
||||
"prompt": {
|
||||
"name": "prompt",
|
||||
"tooltip": "图像生成正向提示"
|
||||
"tooltip": "用于生成或编辑图像的提示词"
|
||||
},
|
||||
"negative_prompt": {
|
||||
"name": "negative_prompt",
|
||||
"tooltip": "图像生成负向提示"
|
||||
"tooltip": "负向提示词"
|
||||
},
|
||||
"api_name": {
|
||||
"name": "api name",
|
||||
"tooltip": "选择 ModelScope 图像 API 名称"
|
||||
},
|
||||
"model": {
|
||||
"name": "模型"
|
||||
@@ -642,55 +646,10 @@
|
||||
},
|
||||
"guidance": {
|
||||
"name": "提示词引导系数"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
"0": {
|
||||
"name": "图像"
|
||||
},
|
||||
"1": {
|
||||
"name": "字符串"
|
||||
}
|
||||
}
|
||||
},
|
||||
"YCYY_ModelScope_Image_Edit_API": {
|
||||
"display_name": "魔搭图像编辑API",
|
||||
"description": "该节点使用 ModelScope API 编辑输入图像",
|
||||
"inputs": {
|
||||
"image": {
|
||||
"name": "输入图像",
|
||||
"tooltip": "要编辑的输入图像"
|
||||
},
|
||||
"config_options": {
|
||||
"name": "配置选项",
|
||||
"tooltip": "可选配置覆盖选项,来自 YCYY API 配置选项"
|
||||
},
|
||||
"proxy_options": {
|
||||
"name": "代理选项",
|
||||
"tooltip": "可选代理覆盖选项,来自 YCYY API 代理选项"
|
||||
},
|
||||
"prompt": {
|
||||
"name": "prompt",
|
||||
"tooltip": "图像编辑指令"
|
||||
},
|
||||
"negative_prompt": {
|
||||
"name": "negative_prompt",
|
||||
"tooltip": "负向提示"
|
||||
},
|
||||
"model": {
|
||||
"name": "模型"
|
||||
},
|
||||
"width": {
|
||||
"name": "宽度"
|
||||
},
|
||||
"height": {
|
||||
"name": "高度"
|
||||
},
|
||||
"steps": {
|
||||
"name": "采样步数"
|
||||
},
|
||||
"guidance": {
|
||||
"name": "提示词引导系数"
|
||||
"tooltip": "可选源图像;连接后编辑图像,不连接时生成图像"
|
||||
}
|
||||
},
|
||||
"outputs": {
|
||||
@@ -716,4 +675,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,260 +0,0 @@
|
||||
import json
|
||||
import os
|
||||
import time
|
||||
from io import BytesIO
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
|
||||
import requests
|
||||
import torch
|
||||
from PIL import Image
|
||||
from comfy_api.latest import io
|
||||
|
||||
from ..utils.config_utils import get_config_section
|
||||
from ..utils.image_utils import pil_to_tensor, tensor_to_base64_string
|
||||
|
||||
|
||||
class ModelScopeImageEdit(io.ComfyNode):
|
||||
"""Edit one or more input images with a ModelScope image model."""
|
||||
|
||||
_CONFIG_SECTION = "modelscope-image-edit"
|
||||
_DEFAULT_MODEL = "Qwen/Qwen-Image-Edit-2511"
|
||||
|
||||
@classmethod
|
||||
def _load_models_from_config(cls) -> List[str]:
|
||||
try:
|
||||
config_path = os.path.join(os.path.dirname(__file__), "..", "config.json")
|
||||
with open(config_path, "r", encoding="utf-8") as config_file:
|
||||
models = json.load(config_file).get(cls._CONFIG_SECTION, {}).get("models")
|
||||
if isinstance(models, list) and models:
|
||||
return models
|
||||
except (OSError, ValueError, TypeError):
|
||||
pass
|
||||
return [cls._DEFAULT_MODEL]
|
||||
|
||||
@classmethod
|
||||
def _load_config_credentials(cls, config_options=None) -> Tuple[str, str, int]:
|
||||
"""Load edit endpoint credentials, allowing Config Options overrides."""
|
||||
config = get_config_section(cls._CONFIG_SECTION)
|
||||
if not config:
|
||||
raise ValueError(f"Missing '{cls._CONFIG_SECTION}' section in config file")
|
||||
|
||||
config_options = config_options or {}
|
||||
base_url = str(config_options.get("base_url") or config.get("base_url") or "").strip()
|
||||
api_key = str(config_options.get("api_key") or config.get("api_key") or "").strip()
|
||||
timeout = config_options.get("timeout") or config.get("timeout", 300)
|
||||
try:
|
||||
timeout = int(timeout)
|
||||
except (TypeError, ValueError):
|
||||
timeout = 300
|
||||
if not base_url:
|
||||
raise ValueError(f"Missing 'base_url' in {cls._CONFIG_SECTION} section")
|
||||
if not api_key:
|
||||
raise ValueError(f"Missing 'api_key' in {cls._CONFIG_SECTION} section")
|
||||
return base_url.rstrip("/"), api_key, timeout
|
||||
|
||||
@classmethod
|
||||
def _get_proxy_config(cls, proxy_options=None) -> Optional[Dict[str, str]]:
|
||||
if proxy_options is not None:
|
||||
if not proxy_options.get("enable", False):
|
||||
return None
|
||||
proxies = {
|
||||
key: proxy_options[key].strip()
|
||||
for key in ("http", "https")
|
||||
if isinstance(proxy_options.get(key), str) and proxy_options[key].strip()
|
||||
}
|
||||
return proxies or None
|
||||
|
||||
try:
|
||||
proxy_config = get_config_section("proxy") or {}
|
||||
if not proxy_config.get("enable", False):
|
||||
return None
|
||||
proxies = {
|
||||
key: proxy_config[key]
|
||||
for key in ("http", "https")
|
||||
if proxy_config.get(key)
|
||||
}
|
||||
return proxies or None
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
@classmethod
|
||||
def define_schema(cls) -> io.Schema:
|
||||
model_options = cls._load_models_from_config()
|
||||
return io.Schema(
|
||||
node_id="YCYY_ModelScope_Image_Edit_API",
|
||||
display_name="ModelScope Image Edit API",
|
||||
category="YCYY/API/image",
|
||||
inputs=[
|
||||
io.Image.Input(
|
||||
id="image",
|
||||
tooltip="Input image to edit",
|
||||
),
|
||||
io.AnyType.Input(
|
||||
id="config_options",
|
||||
optional=True,
|
||||
tooltip="Optional configuration override from YCYY API Config Options",
|
||||
),
|
||||
io.AnyType.Input(
|
||||
id="proxy_options",
|
||||
optional=True,
|
||||
tooltip="Optional proxy configuration override from YCYY API Proxy Options",
|
||||
),
|
||||
io.String.Input(id="prompt", multiline=True, tooltip="Image editing instruction"),
|
||||
io.String.Input(id="negative_prompt", multiline=True, tooltip="Negative prompt"),
|
||||
io.Combo.Input(
|
||||
id="model",
|
||||
options=model_options,
|
||||
default=model_options[0],
|
||||
tooltip="Select ModelScope image editing 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.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 edit an input image.",
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def execute(
|
||||
cls,
|
||||
image,
|
||||
prompt,
|
||||
negative_prompt,
|
||||
model,
|
||||
width,
|
||||
height,
|
||||
steps,
|
||||
guidance,
|
||||
seed,
|
||||
config_options=None,
|
||||
proxy_options=None,
|
||||
) -> io.NodeOutput:
|
||||
if image is None:
|
||||
raise ValueError("image cannot be empty")
|
||||
if not prompt or not prompt.strip():
|
||||
raise ValueError("prompt cannot be empty")
|
||||
base_url, api_key, timeout = cls._load_config_credentials(config_options)
|
||||
return cls._edit_images(
|
||||
base_url,
|
||||
api_key,
|
||||
image,
|
||||
prompt,
|
||||
negative_prompt,
|
||||
model,
|
||||
width,
|
||||
height,
|
||||
steps,
|
||||
guidance,
|
||||
seed,
|
||||
timeout,
|
||||
cls._get_proxy_config(proxy_options),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def _edit_images(
|
||||
cls,
|
||||
base_url,
|
||||
api_key,
|
||||
image,
|
||||
prompt,
|
||||
negative_prompt,
|
||||
model,
|
||||
width,
|
||||
height,
|
||||
steps,
|
||||
guidance,
|
||||
seed,
|
||||
timeout,
|
||||
proxies,
|
||||
) -> io.NodeOutput:
|
||||
api_url = f"{base_url}/v1/images/generations"
|
||||
headers = {
|
||||
"Authorization": f"Bearer {api_key}",
|
||||
"Content-Type": "application/json",
|
||||
"X-ModelScope-Async-Mode": "true",
|
||||
}
|
||||
# ModelScope accepts the source image as an OpenAI-compatible data URL.
|
||||
image_data = tensor_to_base64_string(image[0].unsqueeze(0) if image.ndim == 4 else image)
|
||||
payload = {
|
||||
"model": model,
|
||||
"prompt": prompt,
|
||||
"image_url": f"data:image/png;base64,{image_data}",
|
||||
"size": f"{width}x{height}",
|
||||
"steps": steps,
|
||||
"guidance": guidance,
|
||||
"seed": seed,
|
||||
}
|
||||
if negative_prompt:
|
||||
payload["negative_prompt"] = negative_prompt
|
||||
|
||||
try:
|
||||
response = requests.post(
|
||||
api_url,
|
||||
headers=headers,
|
||||
json=payload,
|
||||
timeout=timeout,
|
||||
proxies=proxies,
|
||||
)
|
||||
if response.status_code != 200:
|
||||
raise RuntimeError(f"HTTP {response.status_code}: {response.text}")
|
||||
task_id = response.json().get("task_id")
|
||||
if not task_id:
|
||||
raise RuntimeError("ModelScope response did not contain task_id")
|
||||
|
||||
output_image_url, task_data = cls._wait_for_task(
|
||||
base_url, api_key, task_id, timeout, proxies
|
||||
)
|
||||
output_response = requests.get(output_image_url, timeout=timeout, proxies=proxies)
|
||||
output_response.raise_for_status()
|
||||
result_image = Image.open(BytesIO(output_response.content)).convert("RGB")
|
||||
return io.NodeOutput(
|
||||
pil_to_tensor(result_image),
|
||||
json.dumps(task_data, ensure_ascii=False),
|
||||
)
|
||||
except Exception as error:
|
||||
raise RuntimeError(
|
||||
json.dumps(
|
||||
{"success": False, "message": f"ModelScope image edit failed: {error}"},
|
||||
ensure_ascii=False,
|
||||
)
|
||||
) from error
|
||||
|
||||
@classmethod
|
||||
def _wait_for_task(cls, base_url, api_key, task_id, timeout, proxies):
|
||||
deadline = time.monotonic() + timeout
|
||||
while time.monotonic() < deadline:
|
||||
response = requests.get(
|
||||
f"{base_url}/v1/tasks/{task_id}",
|
||||
headers={
|
||||
"Authorization": f"Bearer {api_key}",
|
||||
"X-ModelScope-Task-Type": "image_generation",
|
||||
},
|
||||
timeout=timeout,
|
||||
proxies=proxies,
|
||||
)
|
||||
if response.status_code != 200:
|
||||
raise RuntimeError(f"Task query HTTP {response.status_code}: {response.text}")
|
||||
data = response.json()
|
||||
status = data.get("task_status")
|
||||
if status == "SUCCEED":
|
||||
output_images = data.get("output_images") or []
|
||||
if output_images:
|
||||
return output_images[0], data
|
||||
raise RuntimeError("Task succeeded without output image")
|
||||
if status == "FAILED":
|
||||
raise RuntimeError(data.get("message") or "Image editing task failed")
|
||||
time.sleep(min(5, max(0, deadline - time.monotonic())))
|
||||
raise TimeoutError("Timed out waiting for ModelScope image editing task")
|
||||
|
||||
@classmethod
|
||||
def _create_empty_image(cls):
|
||||
return torch.zeros(1, 512, 512, 3, dtype=torch.float32)
|
||||
+308
-256
@@ -1,334 +1,386 @@
|
||||
import os
|
||||
import json
|
||||
import time
|
||||
from io import BytesIO
|
||||
from typing import Dict, List, Optional, Tuple
|
||||
|
||||
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
|
||||
from comfy_api.latest import io
|
||||
|
||||
try:
|
||||
from ..utils.config_utils import (
|
||||
DEFAULT_MODELSCOPE_IMAGE_MODELS,
|
||||
get_config_section,
|
||||
get_modelscope_image_api_config,
|
||||
get_modelscope_image_apis,
|
||||
)
|
||||
from ..utils.image_utils import pil_to_tensor, tensor_to_base64_string
|
||||
except (ImportError, ValueError):
|
||||
from utils.config_utils import (
|
||||
DEFAULT_MODELSCOPE_IMAGE_MODELS,
|
||||
get_config_section,
|
||||
get_modelscope_image_api_config,
|
||||
get_modelscope_image_apis,
|
||||
)
|
||||
from utils.image_utils import pil_to_tensor, tensor_to_base64_string
|
||||
|
||||
|
||||
try:
|
||||
from aiohttp import web
|
||||
from server import PromptServer
|
||||
|
||||
@PromptServer.instance.routes.get("/ycyy/modelscope-image/apis/all")
|
||||
async def get_all_modelscope_image_apis(request):
|
||||
try:
|
||||
return web.json_response([
|
||||
{"api-name": item["api-name"], "models": item["models"]}
|
||||
for item in get_modelscope_image_apis()
|
||||
])
|
||||
except Exception as exc:
|
||||
return web.json_response({"error": str(exc)}, status=500)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
DEFAULT_MODELS = list(DEFAULT_MODELSCOPE_IMAGE_MODELS)
|
||||
|
||||
|
||||
class ModelScopeImage(io.ComfyNode):
|
||||
"""
|
||||
这个节点使用 ModelScope API 生成图像
|
||||
"""
|
||||
"""Generate or edit an image through the asynchronous ModelScope API."""
|
||||
|
||||
@classmethod
|
||||
def _load_models_from_config(cls) -> List[str]:
|
||||
"""
|
||||
从 config.json 中加载模型列表
|
||||
如果获取不到,返回默认模型列表
|
||||
"""
|
||||
def _load_models_from_config(cls, api_name: Optional[str] = None) -> List[str]:
|
||||
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"]
|
||||
apis = get_modelscope_image_apis()
|
||||
if api_name:
|
||||
for item in apis:
|
||||
if item["api-name"] == api_name:
|
||||
return item.get("models") or list(DEFAULT_MODELS)
|
||||
models = list(dict.fromkeys(
|
||||
model for item in apis for model in item.get("models", [])
|
||||
))
|
||||
return models or list(DEFAULT_MODELS)
|
||||
except Exception:
|
||||
return ["Tongyi-MAI/Z-Image-Turbo"]
|
||||
return list(DEFAULT_MODELS)
|
||||
|
||||
@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)
|
||||
def _load_config_credentials(
|
||||
cls,
|
||||
api_name: Optional[str] = None,
|
||||
config_options: Optional[dict] = None,
|
||||
) -> Tuple[str, str, int]:
|
||||
try:
|
||||
api_config = get_modelscope_image_api_config(api_name)
|
||||
except Exception:
|
||||
apis = get_modelscope_image_apis()
|
||||
api_config = apis[0] if apis else {
|
||||
"base_url": "https://api-inference.modelscope.cn",
|
||||
"api_key": "",
|
||||
"timeout": 300,
|
||||
}
|
||||
|
||||
# 如果覆盖配置中有有效的 base_url 和 api_key,则直接返回
|
||||
if base_url and api_key:
|
||||
return base_url, api_key, timeout
|
||||
base_url = str(
|
||||
api_config.get("base_url") or "https://api-inference.modelscope.cn"
|
||||
).strip()
|
||||
api_key = str(api_config.get("api_key") or "").strip()
|
||||
timeout = api_config.get("timeout", 300)
|
||||
|
||||
# 否则从配置文件加载
|
||||
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}")
|
||||
config_options = config_options or {}
|
||||
override_base_url = str(config_options.get("base_url") or "").strip()
|
||||
override_api_key = str(config_options.get("api_key") or "").strip()
|
||||
if override_base_url:
|
||||
base_url = override_base_url
|
||||
if override_api_key:
|
||||
api_key = override_api_key
|
||||
if config_options.get("timeout"):
|
||||
timeout = config_options["timeout"]
|
||||
|
||||
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)}")
|
||||
timeout = int(timeout)
|
||||
except (TypeError, ValueError):
|
||||
timeout = 300
|
||||
if timeout <= 0:
|
||||
timeout = 300
|
||||
if not base_url:
|
||||
raise ValueError("ModelScope base_url cannot be empty")
|
||||
if not api_key:
|
||||
raise ValueError(
|
||||
"ModelScope API key not found. Please provide an api_key in "
|
||||
"config.json ('modelscope-image') or via API Config Options."
|
||||
)
|
||||
return base_url, api_key, timeout
|
||||
|
||||
@classmethod
|
||||
def _get_proxy_config(cls, proxy_options=None) -> Optional[Dict]:
|
||||
"""
|
||||
从 config.json 中获取代理配置,如果提供了 proxy_options 则优先使用
|
||||
返回 proxies 字典或 None
|
||||
"""
|
||||
# 如果提供了代理覆盖配置
|
||||
def _get_proxy_config(
|
||||
cls, proxy_options: Optional[dict] = None
|
||||
) -> Optional[Dict[str, str]]:
|
||||
if proxy_options is not None:
|
||||
if not proxy_options.get('enable', False):
|
||||
if not proxy_options.get("enable", False):
|
||||
return None
|
||||
proxies = {
|
||||
key: proxy_options[key].strip()
|
||||
for key in ("http", "https")
|
||||
if isinstance(proxy_options.get(key), str)
|
||||
and proxy_options[key].strip()
|
||||
}
|
||||
return proxies or 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):
|
||||
proxy_config = get_config_section("proxy") or {}
|
||||
if 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
|
||||
proxies = {
|
||||
key: proxy_config[key]
|
||||
for key in ("http", "https")
|
||||
if proxy_config.get(key)
|
||||
}
|
||||
return proxies or 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]
|
||||
try:
|
||||
apis = get_modelscope_image_apis()
|
||||
api_names = [item["api-name"] for item in apis]
|
||||
models = list(dict.fromkeys(
|
||||
model for item in apis for model in item.get("models", [])
|
||||
))
|
||||
except Exception:
|
||||
api_names = ["default"]
|
||||
models = list(DEFAULT_MODELS)
|
||||
if not api_names:
|
||||
api_names = ["default"]
|
||||
if not models:
|
||||
models = list(DEFAULT_MODELS)
|
||||
|
||||
return io.Schema(
|
||||
node_id="YCYY_ModelScope_Image_API",
|
||||
display_name="ModelScope Image API",
|
||||
category="YCYY/API/image",
|
||||
inputs=[
|
||||
io.AnyType.Input(
|
||||
id="config_options",
|
||||
optional=True,
|
||||
tooltip="Optional configuration override from YCYY API Config Options"
|
||||
),
|
||||
io.AnyType.Input(
|
||||
id="proxy_options",
|
||||
optional=True,
|
||||
tooltip="Optional proxy configuration override from YCYY API Proxy Options"
|
||||
),
|
||||
io.String.Input(
|
||||
id="prompt",
|
||||
multiline=True,
|
||||
tooltip="Image generation positive prompt"
|
||||
tooltip="Prompt used to generate or edit the image.",
|
||||
),
|
||||
io.String.Input(
|
||||
id="negative_prompt",
|
||||
multiline=True,
|
||||
tooltip="Image generation negative prompt"
|
||||
tooltip="Negative prompt.",
|
||||
),
|
||||
io.Combo.Input(
|
||||
id="api_name",
|
||||
options=api_names,
|
||||
default=api_names[0],
|
||||
tooltip="Select a ModelScope image API name.",
|
||||
),
|
||||
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
|
||||
options=models,
|
||||
default=models[0],
|
||||
tooltip="Select a model from the chosen API name.",
|
||||
),
|
||||
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
|
||||
id="guidance", min=1.5, max=20, default=3.5, step=0.1
|
||||
),
|
||||
io.Int.Input(
|
||||
id="seed",
|
||||
min=0,
|
||||
max=2147483647,
|
||||
default=0,
|
||||
control_after_generate=True
|
||||
)
|
||||
control_after_generate=True,
|
||||
),
|
||||
io.Image.Input(
|
||||
id="image",
|
||||
optional=True,
|
||||
tooltip="Optional source image. Connect it to edit an image; leave it disconnected to generate one.",
|
||||
),
|
||||
io.AnyType.Input(
|
||||
id="config_options",
|
||||
optional=True,
|
||||
tooltip="Optional configuration override from YCYY API Config Options.",
|
||||
),
|
||||
io.AnyType.Input(
|
||||
id="proxy_options",
|
||||
optional=True,
|
||||
tooltip="Optional proxy configuration override from YCYY API Proxy Options.",
|
||||
),
|
||||
],
|
||||
outputs=[
|
||||
io.Image.Output(),
|
||||
io.String.Output()
|
||||
],
|
||||
description="This node uses the ModelScope API to generate images."
|
||||
outputs=[io.Image.Output(), io.String.Output()],
|
||||
description="Generate or edit images through the ModelScope API. Connecting an image enables edit mode.",
|
||||
)
|
||||
|
||||
@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)
|
||||
def execute(
|
||||
cls,
|
||||
prompt,
|
||||
negative_prompt,
|
||||
model,
|
||||
width,
|
||||
height,
|
||||
steps,
|
||||
guidance,
|
||||
seed,
|
||||
api_name=None,
|
||||
image=None,
|
||||
config_options=None,
|
||||
proxy_options=None,
|
||||
) -> io.NodeOutput:
|
||||
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)
|
||||
raise ValueError("prompt cannot be empty")
|
||||
|
||||
base_url, api_key, timeout = cls._load_config_credentials(
|
||||
api_name=api_name, config_options=config_options
|
||||
)
|
||||
return cls._request_image(
|
||||
base_url=base_url,
|
||||
api_key=api_key,
|
||||
prompt=prompt,
|
||||
negative_prompt=negative_prompt,
|
||||
model=model,
|
||||
width=width,
|
||||
height=height,
|
||||
steps=steps,
|
||||
guidance=guidance,
|
||||
seed=seed,
|
||||
image=image,
|
||||
timeout=timeout,
|
||||
proxies=cls._get_proxy_config(proxy_options),
|
||||
)
|
||||
|
||||
@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"
|
||||
# 构建请求头
|
||||
def _request_image(
|
||||
cls,
|
||||
base_url,
|
||||
api_key,
|
||||
prompt,
|
||||
negative_prompt,
|
||||
model,
|
||||
width,
|
||||
height,
|
||||
steps,
|
||||
guidance,
|
||||
seed,
|
||||
image,
|
||||
timeout,
|
||||
proxies,
|
||||
) -> io.NodeOutput:
|
||||
service_root = cls._normalize_base_url(base_url)
|
||||
api_url = f"{service_root}/v1/images/generations"
|
||||
headers = {
|
||||
"Authorization": f"Bearer {api_key}",
|
||||
"Content-Type": "application/json",
|
||||
"X-ModelScope-Async-Mode": "true"
|
||||
"X-ModelScope-Async-Mode": "true",
|
||||
}
|
||||
# 构建请求体
|
||||
mode = "edit" if image is not None else "generation"
|
||||
payload = {
|
||||
"model": model,
|
||||
"prompt": prompt,
|
||||
"size": f"{width}x{height}",
|
||||
"steps": steps,
|
||||
"guidance": guidance,
|
||||
"seed": seed
|
||||
"seed": seed,
|
||||
}
|
||||
if negative_prompt is not None and negative_prompt:
|
||||
if negative_prompt:
|
||||
payload["negative_prompt"] = negative_prompt
|
||||
if image is not None:
|
||||
image_data = tensor_to_base64_string(image)
|
||||
payload["image_url"] = f"data:image/png;base64,{image_data}"
|
||||
|
||||
try:
|
||||
response = requests.post(
|
||||
api_url,
|
||||
headers=headers,
|
||||
data=json.dumps(payload, ensure_ascii=False).encode('utf-8'),
|
||||
timeout=timeout,
|
||||
proxies=proxies
|
||||
api_url,
|
||||
headers=headers,
|
||||
json=payload,
|
||||
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({
|
||||
raise RuntimeError(f"HTTP {response.status_code}: {response.text}")
|
||||
task_id = response.json().get("task_id")
|
||||
if not task_id:
|
||||
raise RuntimeError("ModelScope response did not contain task_id")
|
||||
|
||||
output_image_url, task_data = cls._wait_for_task(
|
||||
service_root, api_key, task_id, timeout, proxies
|
||||
)
|
||||
output_response = requests.get(
|
||||
output_image_url, timeout=timeout, proxies=proxies
|
||||
)
|
||||
output_response.raise_for_status()
|
||||
result_image = Image.open(BytesIO(output_response.content)).convert("RGB")
|
||||
result_info = {
|
||||
"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)
|
||||
"message": f"Image {mode} success.",
|
||||
"mode": mode,
|
||||
"model": model,
|
||||
"task_id": task_id,
|
||||
"image_url": output_image_url,
|
||||
"task": task_data,
|
||||
}
|
||||
return io.NodeOutput(
|
||||
pil_to_tensor(result_image),
|
||||
json.dumps(result_info, ensure_ascii=False),
|
||||
)
|
||||
except Exception as error:
|
||||
raise RuntimeError(
|
||||
json.dumps(
|
||||
{
|
||||
"success": False,
|
||||
"mode": mode,
|
||||
"message": f"ModelScope image {mode} failed: {error}",
|
||||
},
|
||||
ensure_ascii=False,
|
||||
)
|
||||
) from error
|
||||
|
||||
@classmethod
|
||||
def _wait_for_task(cls, service_root, api_key, task_id, timeout, proxies):
|
||||
deadline = time.monotonic() + timeout
|
||||
while time.monotonic() < deadline:
|
||||
response = requests.get(
|
||||
f"{service_root}/v1/tasks/{task_id}",
|
||||
headers={
|
||||
"Authorization": f"Bearer {api_key}",
|
||||
"X-ModelScope-Task-Type": "image_generation",
|
||||
},
|
||||
timeout=timeout,
|
||||
proxies=proxies,
|
||||
)
|
||||
if response.status_code != 200:
|
||||
raise RuntimeError(
|
||||
f"Task query HTTP {response.status_code}: {response.text}"
|
||||
)
|
||||
data = response.json()
|
||||
status = data.get("task_status")
|
||||
if status == "SUCCEED":
|
||||
output_images = data.get("output_images") or []
|
||||
if output_images:
|
||||
return output_images[0], data
|
||||
raise RuntimeError("Task succeeded without output image")
|
||||
if status == "FAILED":
|
||||
raise RuntimeError(data.get("message") or "Image task failed")
|
||||
remaining = deadline - time.monotonic()
|
||||
if remaining > 0:
|
||||
time.sleep(min(5, remaining))
|
||||
raise TimeoutError("Timed out waiting for ModelScope image task")
|
||||
|
||||
@classmethod
|
||||
def _normalize_base_url(cls, base_url: str) -> str:
|
||||
clean_url = base_url.strip().rstrip("/")
|
||||
for suffix in ("/v1/images/generations", "/v1/images", "/v1"):
|
||||
if clean_url.endswith(suffix):
|
||||
clean_url = clean_url[:-len(suffix)]
|
||||
break
|
||||
if not clean_url:
|
||||
raise ValueError("ModelScope base_url cannot be empty")
|
||||
return clean_url
|
||||
|
||||
# 创建空图像
|
||||
@classmethod
|
||||
def _create_empty_image(cls):
|
||||
try:
|
||||
return torch.zeros(1, 512, 512, 3, dtype=torch.float32)
|
||||
except Exception as e:
|
||||
return None
|
||||
return torch.zeros(1, 512, 512, 3, dtype=torch.float32)
|
||||
|
||||
@@ -330,3 +330,106 @@ def get_openai_image_api_config(api_name=None, section_key="openai-image"):
|
||||
raise ValueError(f"Unknown API name: {api_name}")
|
||||
|
||||
|
||||
DEFAULT_MODELSCOPE_IMAGE_MODELS = [
|
||||
"Tongyi-MAI/Z-Image-Turbo",
|
||||
"black-forest-labs/FLUX.1-Krea-dev",
|
||||
"Qwen/Qwen-Image-2512",
|
||||
"ideogram-ai/ideogram-4-fp8",
|
||||
"krea/Krea-2-Turbo",
|
||||
"Qwen/Qwen-Image-Edit-2511",
|
||||
"black-forest-labs/FLUX.2-klein-9B",
|
||||
"FireRedTeam/FireRed-Image-Edit-1.1",
|
||||
]
|
||||
|
||||
|
||||
def get_modelscope_image_apis(section_key="modelscope-image"):
|
||||
"""Return normalized ModelScope image API configurations.
|
||||
|
||||
``modelscope-image`` is normally an array. A legacy single mapping is also
|
||||
accepted so existing generation configurations keep working after the
|
||||
generation and editing nodes are merged.
|
||||
"""
|
||||
raw = get_config_section(section_key)
|
||||
if raw is None:
|
||||
return [{
|
||||
"api-name": "default",
|
||||
"base_url": "https://api-inference.modelscope.cn",
|
||||
"api_key": "",
|
||||
"timeout": 300,
|
||||
"models": list(DEFAULT_MODELSCOPE_IMAGE_MODELS),
|
||||
}]
|
||||
if isinstance(raw, dict):
|
||||
raw_items = [raw]
|
||||
elif isinstance(raw, list):
|
||||
raw_items = raw
|
||||
else:
|
||||
raise ValueError(f"{section_key} must be an object or array")
|
||||
if not raw_items:
|
||||
raise ValueError(f"{section_key} cannot be empty")
|
||||
|
||||
result = []
|
||||
names = set()
|
||||
for index, item in enumerate(raw_items):
|
||||
if not isinstance(item, dict):
|
||||
raise ValueError(f"{section_key}[{index}] must be an object")
|
||||
|
||||
name = item.get("api-name") or item.get("api_name")
|
||||
if not name:
|
||||
if len(raw_items) == 1:
|
||||
name = "default"
|
||||
else:
|
||||
raise ValueError(f"{section_key}[{index}] missing 'api-name'")
|
||||
name = str(name).strip()
|
||||
if not name:
|
||||
raise ValueError(f"{section_key}[{index}] api-name cannot be empty")
|
||||
if name in names:
|
||||
raise ValueError(f"Duplicate api-name: {name}")
|
||||
names.add(name)
|
||||
|
||||
base_url = str(
|
||||
item.get("base_url", "") or "https://api-inference.modelscope.cn"
|
||||
).strip()
|
||||
if not base_url:
|
||||
base_url = "https://api-inference.modelscope.cn"
|
||||
|
||||
models = item.get("models")
|
||||
if not isinstance(models, list) or not models:
|
||||
models = list(DEFAULT_MODELSCOPE_IMAGE_MODELS)
|
||||
else:
|
||||
models = [str(model).strip() for model in models if str(model).strip()]
|
||||
if not models:
|
||||
models = list(DEFAULT_MODELSCOPE_IMAGE_MODELS)
|
||||
|
||||
timeout = item.get("timeout", 300)
|
||||
try:
|
||||
timeout = int(timeout)
|
||||
except (TypeError, ValueError):
|
||||
timeout = 300
|
||||
if timeout <= 0:
|
||||
timeout = 300
|
||||
|
||||
result.append({
|
||||
"api-name": name,
|
||||
"base_url": base_url,
|
||||
"api_key": str(item.get("api_key", "") or "").strip(),
|
||||
"timeout": timeout,
|
||||
"models": models,
|
||||
})
|
||||
return result
|
||||
|
||||
|
||||
def get_modelscope_image_api_names(section_key="modelscope-image"):
|
||||
return [item["api-name"] for item in get_modelscope_image_apis(section_key)]
|
||||
|
||||
|
||||
def get_modelscope_image_api_config(api_name=None, section_key="modelscope-image"):
|
||||
apis = get_modelscope_image_apis(section_key)
|
||||
if not apis:
|
||||
raise ValueError(f"No configured APIs found in {section_key}")
|
||||
if not api_name:
|
||||
return apis[0]
|
||||
for item in apis:
|
||||
if item["api-name"] == api_name:
|
||||
return item
|
||||
raise ValueError(f"Unknown API name: {api_name}")
|
||||
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import { app } from "../../scripts/app.js";
|
||||
import { api } from "../../scripts/api.js";
|
||||
|
||||
const NODE_CLASS = "YCYY_ModelScope_Image_API";
|
||||
let apiMap = new Map();
|
||||
|
||||
async function loadApis() {
|
||||
try {
|
||||
const response = await api.fetchApi("/ycyy/modelscope-image/apis/all");
|
||||
if (!response.ok) throw new Error(`HTTP ${response.status}`);
|
||||
const data = await response.json();
|
||||
apiMap = new Map((Array.isArray(data) ? data : []).map(item => [item["api-name"], item]));
|
||||
} catch (error) {
|
||||
console.error("[YCYY] Failed to load ModelScope Image API list:", error);
|
||||
}
|
||||
}
|
||||
|
||||
function applyModels(node, apiName, keepModel = false) {
|
||||
const selected = apiMap.get(apiName);
|
||||
const modelWidget = node.widgets?.find(widget => widget.name === "model");
|
||||
if (!selected || !modelWidget) return;
|
||||
const models = Array.isArray(selected.models) ? selected.models : [];
|
||||
modelWidget.options.values = models;
|
||||
if (!keepModel || !models.includes(modelWidget.value)) modelWidget.value = models[0] ?? "";
|
||||
app.canvas?.draw(true, true);
|
||||
}
|
||||
|
||||
app.registerExtension({
|
||||
name: "YCYY.ModelScope.Image",
|
||||
async setup() { await loadApis(); },
|
||||
async beforeRegisterNodeDef(nodeType) {
|
||||
if (nodeType.comfyClass !== NODE_CLASS) return;
|
||||
const originalCreated = nodeType.prototype.onNodeCreated;
|
||||
nodeType.prototype.onNodeCreated = function () {
|
||||
const result = originalCreated?.apply(this, arguments);
|
||||
const apiWidget = this.widgets?.find(widget => widget.name === "api_name");
|
||||
if (apiWidget) {
|
||||
const originalCallback = apiWidget.callback;
|
||||
apiWidget.callback = value => {
|
||||
applyModels(this, value);
|
||||
originalCallback?.call(this, value);
|
||||
};
|
||||
setTimeout(() => applyModels(this, apiWidget.value, true), 0);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
const originalConfigure = nodeType.prototype.onConfigure;
|
||||
nodeType.prototype.onConfigure = function () {
|
||||
const result = originalConfigure?.apply(this, arguments);
|
||||
const apiWidget = this.widgets?.find(widget => widget.name === "api_name");
|
||||
if (apiWidget && apiMap.size && !apiMap.has(apiWidget.value)) {
|
||||
const fallback = apiMap.keys().next().value;
|
||||
console.warn(`[YCYY] ModelScope Image API "${apiWidget.value}" no longer exists; using "${fallback}"`);
|
||||
apiWidget.value = fallback;
|
||||
}
|
||||
if (apiWidget) applyModels(this, apiWidget.value, true);
|
||||
return result;
|
||||
};
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user