diff --git a/__init__.py b/__init__.py index 58830a5..3938695 100644 --- a/__init__.py +++ b/__init__.py @@ -2,6 +2,7 @@ from comfy_api.latest import ComfyExtension, io from typing_extensions import override from .gemini.gemini_image_node import * +from .grok.grok_image_node import * from .gemini.gemini_image_preset_node import * from .gemini.gemini_tts_node import * from .gemini.gemini_tts_multi_node import * @@ -27,7 +28,8 @@ class APIExtension(ComfyExtension): @override async def get_node_list(self) -> list[type[io.ComfyNode]]: return [ - GeminiImage, + GeminiImage, + GrokImage, GeminiTTS, GeminiTTSMulti, GeminiSTT, diff --git a/config.json.example b/config.json.example index f3eb877..cba946d 100644 --- a/config.json.example +++ b/config.json.example @@ -6,7 +6,10 @@ "api_key": "put your key here", "timeout": 120, "api_protocol": "openai-completions", - "models": ["gpt-4o", "gpt-5"] + "models": [ + "gpt-4o", + "gpt-5" + ] }, { "api-name": "OpenAI Responses", @@ -14,14 +17,29 @@ "api_key": "put your key here", "timeout": 120, "api_protocol": "openai-responses", - "models": ["gpt-5"] + "models": [ + "gpt-5" + ] } ], "skills": { - "paths": ["skills"], + "paths": [ + "skills" + ], "allow_call": false }, - "gemini-image":{ + "grok-image": { + "base_url": "https://api.x.ai/v1", + "api_key": "put your key here", + "timeout": 120, + "models": [ + "grok-imagine-image-2.0", + "grok-imagine-image-quality", + "grok-imagine-image-pro", + "grok-imagine-image" + ] + }, + "gemini-image": { "base_url": "https://generativelanguage.googleapis.com/v1beta/models", "api_key": "put your key here", "timeout": 120, @@ -31,7 +49,7 @@ "gemini-3.1-flash-image-preview" ] }, - "gemini-tts": { + "gemini-tts": { "base_url": "https://generativelanguage.googleapis.com/v1beta/models", "api_key": "put your key here", "timeout": 120, @@ -96,9 +114,9 @@ "FireRedTeam/FireRed-Image-Edit-1.1" ] }, - "proxy":{ + "proxy": { "enable": false, "http": "", "https": "http://127.0.0.1:7890" } -} +} \ No newline at end of file diff --git a/grok/grok_image_node.py b/grok/grok_image_node.py new file mode 100644 index 0000000..c34e53a --- /dev/null +++ b/grok/grok_image_node.py @@ -0,0 +1,525 @@ +import os +import json +import base64 +import requests +import torch +import numpy as np +from PIL import Image +from io import BytesIO +from typing import Optional, List, Dict, Any, Tuple +from comfy_api.latest import ComfyExtension, io +from ..utils.image_utils import tensor_to_base64_string +from ..utils.config_utils import get_config_section + + +DEFAULT_MODELS = [ + "grok-imagine-image-2.0", + "grok-imagine-image-quality", + "grok-imagine-image-pro", + "grok-imagine-image" +] + + +class GrokImage(io.ComfyNode): + """ + 这个节点使用 xAI Grok Image 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 DEFAULT_MODELS + + with open(config_path, 'r', encoding='utf-8') as f: + config = json.load(f) + + if 'grok-image' in config and 'models' in config['grok-image']: + models = config['grok-image']['models'] + if isinstance(models, list) and len(models) > 0: + return models + + return DEFAULT_MODELS + except Exception: + return DEFAULT_MODELS + + @classmethod + def _load_config_credentials(cls, config_options: Optional[dict] = 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', 120) + + # 如果覆盖配置中有有效的 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) + + # 检查是否存在 grok-image 配置段 + if 'grok-image' not in config: + raise ValueError("Missing 'grok-image' section in config file") + + grok_config = config['grok-image'] + + # 获取并验证 base_url (默认 https://api.x.ai/v1) + base_url = grok_config.get('base_url', 'https://api.x.ai/v1') + base_url = base_url.strip() if isinstance(base_url, str) else str(base_url).strip() + if not base_url: + base_url = "https://api.x.ai/v1" + + # 获取并验证 api_key + if 'api_key' not in grok_config: + raise ValueError("Missing 'api_key' in grok-image section") + api_key = grok_config['api_key'].strip() if isinstance(grok_config['api_key'], str) else str(grok_config['api_key']).strip() + if not api_key: + raise ValueError("api_key cannot be empty") + + # 获取 timeout 参数,默认值为 120 秒 + timeout = grok_config.get('timeout', 120) + if isinstance(timeout, str): + try: + timeout = int(timeout) + except ValueError: + timeout = 120 + + # 如果有配置覆盖,则使用覆盖的值(如果提供了) + 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 Exception as e: + raise ValueError(f"Config loading error: {str(e)}") + + @classmethod + def _get_proxy_config(cls, proxy_options: Optional[dict] = None) -> Optional[Dict[str, str]]: + """ + 从 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: + """ + 返回 GrokImage 节点 schema + """ + model_options = cls._load_models_from_config() + default_model = model_options[0] + + return io.Schema( + node_id="YCYY_Grok_Image_API", + display_name="Grok Image API", + category="YCYY/API/image", + inputs=[ + io.Image.Input( + id="images", + optional=True, + tooltip="Optional image(s) for image-to-image editing. Grok supports up to 3 reference images (1 for pro model)." + ), + 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="The text prompt used to generate or edit the image" + ), + io.Combo.Input( + id="model", + options=model_options, + default=default_model, + tooltip="Grok image model" + ), + io.Combo.Input( + id="aspect_ratio", + options=[ + "auto", + "1:1", + "2:3", + "3:2", + "3:4", + "4:3", + "9:16", + "16:9", + "9:19.5", + "19.5:9", + "9:20", + "20:9", + "1:2", + "2:1" + ], + default="auto", + tooltip="Aspect ratio of the output image. 'auto' matches input image in edit mode or generates 1:1." + ), + io.Combo.Input( + id="resolution", + options=[ + "1K", + "2K" + ], + default="1K", + tooltip="Resolution of the output image (1K or 2K)." + ), + io.Combo.Input( + id="quality", + options=[ + "default", + "medium", + "low" + ], + default="medium", + tooltip="Quality level, supported only by the grok-imagine-image-2.0 model." + ), + io.Int.Input( + id="number_of_images", + default=1, + min=1, + max=10, + step=1, + tooltip="Number of images to generate (1 to 10)." + ), + io.Int.Input( + id="seed", + min=0, + max=0xFFFFFFFFFFFFFFFF, + default=0, + control_after_generate=True, + tooltip="Random seed for generation." + ) + ], + outputs=[ + io.Image.Output(), + io.String.Output() + ], + description="This node uses the xAI Grok Image API to generate or edit images." + ) + + @classmethod + def execute( + cls, + prompt: str, + model: str, + aspect_ratio: str, + resolution: str, + quality: str, + number_of_images: int, + seed: int, + images: Optional[torch.Tensor] = None, + config_options: Optional[dict] = None, + proxy_options: Optional[dict] = None + ) -> io.NodeOutput: + # 加载配置和凭据,如果提供了 config_options 则使用覆盖配置 + base_url, api_key, timeout = cls._load_config_credentials(config_options) + # 获取代理配置,如果提供了 proxy_options 则使用覆盖配置 + proxies = cls._get_proxy_config(proxy_options) + + if not prompt or not prompt.strip(): + raise ValueError("prompt cannot be empty") + + clean_base_url = base_url.rstrip("/") + if clean_base_url.endswith("/images/generations") or clean_base_url.endswith("/images/edits"): + clean_base_url = clean_base_url.rsplit("/images", 1)[0] + elif clean_base_url.endswith("/images"): + clean_base_url = clean_base_url.rsplit("/images", 1)[0] + + gen_url = f"{clean_base_url}/images/generations" + edit_url = f"{clean_base_url}/images/edits" + + if images is not None: + return cls._edit_images( + api_url=edit_url, + api_key=api_key, + prompt=prompt, + model=model, + aspect_ratio=aspect_ratio, + resolution=resolution, + quality=quality, + number_of_images=number_of_images, + seed=seed, + images=images, + timeout=timeout, + proxies=proxies + ) + else: + return cls._generate_images( + api_url=gen_url, + api_key=api_key, + prompt=prompt, + model=model, + aspect_ratio=aspect_ratio, + resolution=resolution, + quality=quality, + number_of_images=number_of_images, + seed=seed, + timeout=timeout, + proxies=proxies + ) + + @classmethod + def _generate_images( + cls, + api_url: str, + api_key: str, + prompt: str, + model: str, + aspect_ratio: str, + resolution: str, + quality: str, + number_of_images: int, + seed: int, + timeout: int, + proxies: Optional[dict] = None + ) -> io.NodeOutput: + headers = { + "Authorization": f"Bearer {api_key}", + "Content-Type": "application/json" + } + payload = { + "model": model, + "prompt": prompt, + "n": number_of_images, + "seed": seed, + "response_format": "b64_json", + "resolution": resolution.lower() if resolution else "1k" + } + if aspect_ratio != "auto": + payload["aspect_ratio"] = aspect_ratio + if quality and quality != "default": + payload["quality"] = quality + + try: + resp = requests.post(api_url, headers=headers, json=payload, timeout=timeout, proxies=proxies) + return cls._parse_response(resp, model=model, timeout=timeout, proxies=proxies) + except Exception as e: + empty_image = cls._create_empty_image() + err_info = { + "success": False, + "message": f"The API request failed. Please check if the interface address and key are correct: {str(e)}" + } + return io.NodeOutput(empty_image, json.dumps(err_info, ensure_ascii=False)) + + @classmethod + def _edit_images( + cls, + api_url: str, + api_key: str, + prompt: str, + model: str, + aspect_ratio: str, + resolution: str, + quality: str, + number_of_images: int, + seed: int, + images: torch.Tensor, + timeout: int, + proxies: Optional[dict] = None + ) -> io.NodeOutput: + headers = { + "Authorization": f"Bearer {api_key}", + "Content-Type": "application/json" + } + + input_images = [] + total_imgs = images.shape[0] if len(images.shape) >= 4 else 1 + max_imgs = 1 if "pro" in model else 3 + num_to_take = min(total_imgs, max_imgs) + + for idx in range(num_to_take): + img_tensor = images[idx].unsqueeze(0) if len(images.shape) >= 4 else images.unsqueeze(0) + b64_str = tensor_to_base64_string(img_tensor, mime_type="image/png") + input_images.append({ + "url": f"data:image/png;base64,{b64_str}" + }) + + payload = { + "model": model, + "prompt": prompt, + "images": input_images, + "n": number_of_images, + "seed": seed, + "response_format": "b64_json", + "resolution": resolution.lower() if resolution else "1k" + } + if aspect_ratio != "auto": + payload["aspect_ratio"] = aspect_ratio + + try: + resp = requests.post(api_url, headers=headers, json=payload, timeout=timeout, proxies=proxies) + return cls._parse_response(resp, model=model, timeout=timeout, proxies=proxies) + except Exception as e: + empty_image = cls._create_empty_image() + err_info = { + "success": False, + "message": f"The API request failed. Please check if the interface address and key are correct: {str(e)}" + } + return io.NodeOutput(empty_image, json.dumps(err_info, ensure_ascii=False)) + + @classmethod + def _parse_response( + cls, + resp: requests.Response, + model: str = "", + timeout: int = 120, + proxies: Optional[dict] = None + ) -> io.NodeOutput: + # 检查 HTTP 状态码 + if resp.status_code != 200: + empty_image = cls._create_empty_image() + err_info = { + "success": False, + "message": f"API request returns an error. status_code: {resp.status_code}, error_reason: {resp.text}" + } + return io.NodeOutput(empty_image, json.dumps(err_info, ensure_ascii=False)) + + # 检查返回内容是否为空 + if not resp.text.strip(): + empty_image = cls._create_empty_image() + err_info = { + "success": False, + "message": "The API returns an empty content" + } + return io.NodeOutput(empty_image, json.dumps(err_info, ensure_ascii=False)) + + try: + data = resp.json() + except Exception as json_exception: + empty_image = cls._create_empty_image() + err_info = { + "success": False, + "message": f"The API returned a JSON parsing failure: {str(json_exception)}" + } + return io.NodeOutput(empty_image, json.dumps(err_info, ensure_ascii=False)) + + # 提取图像数据列表 + items = data.get("data", []) + if not items: + empty_image = cls._create_empty_image() + err_info = { + "success": False, + "message": "Image data not found in response", + "raw_response": data + } + return io.NodeOutput(empty_image, json.dumps(err_info, ensure_ascii=False)) + + image_tensors = [] + revised_prompts = [] + for item in items: + if not isinstance(item, dict): + continue + if item.get("revised_prompt"): + revised_prompts.append(item["revised_prompt"]) + + b64_json = item.get("b64_json") + image_url = item.get("url") + + if b64_json: + try: + image_bytes = base64.b64decode(b64_json) + pil_image = Image.open(BytesIO(image_bytes)).convert("RGB") + img_np = np.array(pil_image).astype(np.float32) / 255.0 + img_tensor = torch.from_numpy(img_np).unsqueeze(0) + image_tensors.append(img_tensor) + except Exception: + pass + elif image_url: + try: + img_resp = requests.get(image_url, timeout=timeout, proxies=proxies) + if img_resp.status_code == 200: + pil_image = Image.open(BytesIO(img_resp.content)).convert("RGB") + img_np = np.array(pil_image).astype(np.float32) / 255.0 + img_tensor = torch.from_numpy(img_np).unsqueeze(0) + image_tensors.append(img_tensor) + except Exception: + pass + + if not image_tensors: + empty_image = cls._create_empty_image() + err_info = { + "success": False, + "message": "Failed to decode or download any image from response", + "raw_response": data + } + return io.NodeOutput(empty_image, json.dumps(err_info, ensure_ascii=False)) + + if len(image_tensors) == 1: + final_tensor = image_tensors[0] + else: + final_tensor = torch.cat(image_tensors, dim=0) + + usage = data.get("usage", {}) + info = { + "success": True, + "model": model, + "created": data.get("created"), + "usage": usage, + "revised_prompts": revised_prompts + } + return io.NodeOutput(final_tensor, json.dumps(info, ensure_ascii=False, indent=2)) + + @classmethod + def _create_empty_image(cls) -> torch.Tensor: + """创建空图像""" + try: + return torch.zeros(1, 512, 512, 3, dtype=torch.float32) + except Exception: + return None diff --git a/locales/en/nodeDefs.json b/locales/en/nodeDefs.json index 9c7a91b..e38b0a9 100644 --- a/locales/en/nodeDefs.json +++ b/locales/en/nodeDefs.json @@ -3,44 +3,101 @@ "display_name": "OpenAI Text API", "description": "OpenAI and OpenAI-compatible text, image and video API.", "inputs": { - "api_name": {"name": "api_name"}, - "model": {"name": "model"}, - "system_prompt": {"name": "system_prompt"}, - "user_prompt": {"name": "user_prompt"}, - "persist_context": {"name": "persist_context", "tooltip": "Persist chat context between calls"}, - "clear_history": {"name": "clear_history", "tooltip": "Clear the current conversation history"}, - "stream": {"name": "stream", "tooltip": "If true, the model response is streamed to the client as it is generated using server-sent events (SSE)."}, - "images": {"name": "images", "tooltip": "Optional image input"}, - "videos": {"name": "videos", "tooltip": "Optional video input; unsupported APIs return an error"}, - "config_options": {"name": "config_options", "tooltip": "Configuration override"}, - "proxy_options": {"name": "proxy_options", "tooltip": "Proxy override"}, - "advanced_options": {"name": "advanced_options", "tooltip": "Advanced model options"}, - "skill_options": {"name": "skill_options", "tooltip": "Optional progressive Skill configuration from OpenAI Text Skill Options"} + "api_name": { + "name": "api_name" + }, + "model": { + "name": "model" + }, + "system_prompt": { + "name": "system_prompt" + }, + "user_prompt": { + "name": "user_prompt" + }, + "persist_context": { + "name": "persist_context", + "tooltip": "Persist chat context between calls" + }, + "clear_history": { + "name": "clear_history", + "tooltip": "Clear the current conversation history" + }, + "stream": { + "name": "stream", + "tooltip": "If true, the model response is streamed to the client as it is generated using server-sent events (SSE)." + }, + "images": { + "name": "images", + "tooltip": "Optional image input" + }, + "videos": { + "name": "videos", + "tooltip": "Optional video input; unsupported APIs return an error" + }, + "config_options": { + "name": "config_options", + "tooltip": "Configuration override" + }, + "proxy_options": { + "name": "proxy_options", + "tooltip": "Proxy override" + }, + "advanced_options": { + "name": "advanced_options", + "tooltip": "Advanced model options" + }, + "skill_options": { + "name": "skill_options", + "tooltip": "Optional progressive Skill configuration from OpenAI Text Skill Options" + } }, "outputs": { - "0": {"name": "Result"}, - "1": {"name": "Conversation", "tooltip": "Full redacted Pi Skill Agent ledger for Skill calls"}, - "2": {"name": "Skill Trace"} + "0": { + "name": "Result" + }, + "1": { + "name": "Conversation", + "tooltip": "Full redacted Pi Skill Agent ledger for Skill calls" + }, + "2": { + "name": "Skill Trace" + } } }, "YCYY_Preview_API_Result": { "display_name": "Preview API Result", "description": "Render text as a Markdown preview with copy support.", "inputs": { - "source": {"name": "source", "tooltip": "Markdown or plain text to preview"} + "source": { + "name": "source", + "tooltip": "Markdown or plain text to preview" + } }, "outputs": { - "0": {"name": "text"} + "0": { + "name": "text" + } } }, "YCYY_OpenAI_Text_Skill_Options": { "display_name": "OpenAI Text Skill Options", "description": "Load a local SKILL.md package for progressive use by OpenAI Text API.", "inputs": { - "skill_name": {"name": "skill_name", "tooltip": "Select a configured local Skill"}, - "description": {"name": "Skill description", "tooltip": "Read-only description of the selected Skill"} + "skill_name": { + "name": "skill_name", + "tooltip": "Select a configured local Skill" + }, + "description": { + "name": "Skill description", + "tooltip": "Read-only description of the selected Skill" + } }, - "outputs": {"0": {"name": "skill_options"}} + "outputs": { + "0": { + "name": "skill_options" + } + } }, "YCYY_OpenAI_Text_Advanced_Options": { "display_name": "OpenAI Text Advanced Options", @@ -51,7 +108,11 @@ "tooltip": "A JSON object containing protocol/API-specific request parameters. Example:\n{\n \"temperature\": 0.7,\n \"reasoning\": {\n \"effort\": \"medium\"\n }\n}" } }, - "outputs": {"0": {"name": "advanced_options"}} + "outputs": { + "0": { + "name": "advanced_options" + } + } }, "YCYY_Gemini_Image_API": { "display_name": "Gemini Image API", @@ -100,6 +161,60 @@ } } }, + "YCYY_Grok_Image_API": { + "display_name": "Grok Image API", + "description": "This node uses the xAI Grok Image API to generate or edit images.", + "inputs": { + "images": { + "name": "images", + "tooltip": "Optional reference image(s) for editing. Up to 3 images (1 for pro model)." + }, + "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": "The prompt used to generate or edit the image" + }, + "model": { + "name": "model", + "tooltip": "Grok image model" + }, + "aspect_ratio": { + "name": "aspect_ratio", + "tooltip": "Aspect ratio of the generated image. 'auto' matches input image in edit mode or generates 1:1." + }, + "resolution": { + "name": "resolution", + "tooltip": "Resolution of the output image (1K or 2K)." + }, + "quality": { + "name": "quality", + "tooltip": "Quality level, supported only by the grok-imagine-image-2.0 model." + }, + "number_of_images": { + "name": "number_of_images", + "tooltip": "Number of images to generate (1 to 10)." + }, + "seed": { + "name": "seed", + "tooltip": "Seed to use for generation." + } + }, + "outputs": { + "0": { + "name": "Image" + }, + "1": { + "name": "Info" + } + } + }, "YCYY_Ollama_VLM_API": { "display_name": "Ollama VLM API", "description": "This node uses the Ollama VLM model for image reasoning and analysis.", @@ -475,20 +590,49 @@ "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"}, - "negative_prompt": {"name": "negative_prompt", "tooltip": "Negative prompt"}, - "model": {"name": "model"}, - "width": {"name": "width"}, - "height": {"name": "height"}, - "steps": {"name": "steps"}, - "guidance": {"name": "guidance"} + "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" + }, + "negative_prompt": { + "name": "negative_prompt", + "tooltip": "Negative prompt" + }, + "model": { + "name": "model" + }, + "width": { + "name": "width" + }, + "height": { + "name": "height" + }, + "steps": { + "name": "steps" + }, + "guidance": { + "name": "guidance" + } }, "outputs": { - "0": {"name": "Image"}, - "1": {"name": "String"} + "0": { + "name": "Image" + }, + "1": { + "name": "String" + } } }, "YCYY_Image_Compare": { @@ -505,4 +649,4 @@ } } } -} +} \ No newline at end of file diff --git a/locales/zh/nodeDefs.json b/locales/zh/nodeDefs.json index 305bad0..ea9a79d 100644 --- a/locales/zh/nodeDefs.json +++ b/locales/zh/nodeDefs.json @@ -3,44 +3,101 @@ "display_name": "OpenAI 文本 API", "description": "支持 OpenAI 及兼容接口的文本、图像和视频请求。", "inputs": { - "api_name": {"name": "API 名称"}, - "model": {"name": "模型"}, - "system_prompt": {"name": "系统提示词"}, - "user_prompt": {"name": "用户提示词"}, - "persist_context": {"name": "保持上下文", "tooltip": "在多次调用之间保持对话上下文"}, - "clear_history": {"name": "清除历史", "tooltip": "清除当前会话历史"}, - "stream": {"name": "流式输出", "tooltip": "设为 true 时,模型响应会在生成过程中通过服务器发送事件(SSE)流式传输到客户端。"}, - "images": {"name": "图像", "tooltip": "可选图像输入"}, - "videos": {"name": "视频", "tooltip": "可选视频输入;不支持视频的接口会返回错误"}, - "config_options": {"name": "配置选项", "tooltip": "配置覆盖"}, - "proxy_options": {"name": "代理选项", "tooltip": "代理覆盖"}, - "advanced_options": {"name": "高级选项", "tooltip": "模型高级参数"}, - "skill_options": {"name": "Skill 选项", "tooltip": "来自 OpenAI 文本 Skill 选项节点的可选渐进式 Skill 配置"} + "api_name": { + "name": "API 名称" + }, + "model": { + "name": "模型" + }, + "system_prompt": { + "name": "系统提示词" + }, + "user_prompt": { + "name": "用户提示词" + }, + "persist_context": { + "name": "保持上下文", + "tooltip": "在多次调用之间保持对话上下文" + }, + "clear_history": { + "name": "清除历史", + "tooltip": "清除当前会话历史" + }, + "stream": { + "name": "流式输出", + "tooltip": "设为 true 时,模型响应会在生成过程中通过服务器发送事件(SSE)流式传输到客户端。" + }, + "images": { + "name": "图像", + "tooltip": "可选图像输入" + }, + "videos": { + "name": "视频", + "tooltip": "可选视频输入;不支持视频的接口会返回错误" + }, + "config_options": { + "name": "配置选项", + "tooltip": "配置覆盖" + }, + "proxy_options": { + "name": "代理选项", + "tooltip": "代理覆盖" + }, + "advanced_options": { + "name": "高级选项", + "tooltip": "模型高级参数" + }, + "skill_options": { + "name": "Skill 选项", + "tooltip": "来自 OpenAI 文本 Skill 选项节点的可选渐进式 Skill 配置" + } }, "outputs": { - "0": {"name": "结果"}, - "1": {"name": "对话", "tooltip": "Skill 调用时输出完整脱敏的 Pi Skill Agent ledger"}, - "2": {"name": "Skill 调用记录"} + "0": { + "name": "结果" + }, + "1": { + "name": "对话", + "tooltip": "Skill 调用时输出完整脱敏的 Pi Skill Agent ledger" + }, + "2": { + "name": "Skill 调用记录" + } } }, "YCYY_Preview_API_Result": { "display_name": "Preview API Result", "description": "将文本渲染为 Markdown 网页预览,并支持复制。", "inputs": { - "source": {"name": "文本", "tooltip": "要预览的 Markdown 或纯文本"} + "source": { + "name": "文本", + "tooltip": "要预览的 Markdown 或纯文本" + } }, "outputs": { - "0": {"name": "文本"} + "0": { + "name": "文本" + } } }, "YCYY_OpenAI_Text_Skill_Options": { "display_name": "OpenAI 文本 Skill 选项", "description": "加载本地 SKILL.md,并供 OpenAI 文本 API 渐进式读取引用资料。", "inputs": { - "skill_name": {"name": "Skill 名称", "tooltip": "选择配置中发现的本地 Skill"}, - "description": {"name": "Skill 描述", "tooltip": "当前所选 Skill 的只读描述"} + "skill_name": { + "name": "Skill 名称", + "tooltip": "选择配置中发现的本地 Skill" + }, + "description": { + "name": "Skill 描述", + "tooltip": "当前所选 Skill 的只读描述" + } }, - "outputs": {"0": {"name": "Skill 选项"}} + "outputs": { + "0": { + "name": "Skill 选项" + } + } }, "YCYY_OpenAI_Text_Advanced_Options": { "display_name": "OpenAI 文本高级选项(JSON)", @@ -51,7 +108,11 @@ "tooltip": "填写协议或 API 特有请求参数的 JSON 对象。示例:\n{\n \"temperature\": 0.7,\n \"reasoning\": {\n \"effort\": \"medium\"\n }\n}" } }, - "outputs": {"0": {"name": "高级选项"}} + "outputs": { + "0": { + "name": "高级选项" + } + } }, "YCYY_Gemini_Image_API": { "display_name": "Gemini 图像 API", @@ -61,11 +122,11 @@ "name": "图像", "tooltip": "可选的图像,用作模型的上下文" }, - "config_options":{ + "config_options": { "name": "配置选项", "tooltip": "可选配置覆盖选项,来自 YCYY API 配置选项" }, - "proxy_options":{ + "proxy_options": { "name": "代理选项", "tooltip": "可选代理覆盖选项,来自 YCYY API 代理选项" }, @@ -101,19 +162,73 @@ } } }, - "YCYY_Ollama_VLM_API":{ - "display_name": "Ollama 视觉 API", - "description": "这个节点使用Ollama VLM 模型进行图片推理分析", + "YCYY_Grok_Image_API": { + "display_name": "Grok 图像 API", + "description": "该节点使用 xAI Grok 图像 API 生成或编辑图像", "inputs": { - "images":{ + "images": { "name": "图像", - "tooltip": "用于分析的图像(支持多个)" + "tooltip": "可选的参考图像,用于图生图编辑。Grok 支持最多 3 张参考图片(pro 模型支持 1 张)。" }, - "config_options":{ + "config_options": { "name": "配置选项", "tooltip": "可选配置覆盖选项,来自 YCYY API 配置选项" }, - "proxy_options":{ + "proxy_options": { + "name": "代理选项", + "tooltip": "可选代理覆盖选项,来自 YCYY API 代理选项" + }, + "prompt": { + "name": "提示词", + "tooltip": "用于生成或编辑图像的提示词" + }, + "model": { + "name": "模型", + "tooltip": "Grok 图像模型" + }, + "aspect_ratio": { + "name": "图像宽高比", + "tooltip": "输出图像的宽高比。'auto' 在编辑模式下匹配输入图像,或生成 1:1 正方形。" + }, + "resolution": { + "name": "图像分辨率", + "tooltip": "控制输出图像的分辨率(1K 或 2K)。" + }, + "quality": { + "name": "质量", + "tooltip": "质量等级,仅 grok-imagine-image-2.0 模型支持。" + }, + "number_of_images": { + "name": "生成数量", + "tooltip": "要生成的图像数量(1 到 10)。" + }, + "seed": { + "name": "种子", + "tooltip": "用于生成噪声的随机种子。" + } + }, + "outputs": { + "0": { + "name": "图像" + }, + "1": { + "name": "信息" + } + } + }, + "YCYY_Ollama_VLM_API": { + "display_name": "Ollama 视觉 API", + "description": "这个节点使用Ollama VLM 模型进行图片推理分析", + "inputs": { + "images": { + "name": "图像", + "tooltip": "用于分析的图像(支持多个)" + }, + "config_options": { + "name": "配置选项", + "tooltip": "可选配置覆盖选项,来自 YCYY API 配置选项" + }, + "proxy_options": { "name": "代理选项", "tooltip": "可选代理覆盖选项,来自 YCYY API 代理选项" }, @@ -127,26 +242,26 @@ } } }, - "YCYY_Ollama_LLM_API":{ + "YCYY_Ollama_LLM_API": { "display_name": "Ollama 文本 API", "description": "该节点使用 Ollama 大语言模型进行对话", "inputs": { - "config_options":{ + "config_options": { "name": "配置选项", "tooltip": "可选配置覆盖选项,来自 YCYY API 配置选项" }, - "proxy_options":{ + "proxy_options": { "name": "代理选项", "tooltip": "可选代理覆盖选项,来自 YCYY API 代理选项" }, - "advanced_options":{ + "advanced_options": { "name": "高级选项", "tooltip": "模型的高级选项(可选),接受来自 Ollama 文本高级选项节点的输入。" }, - "system_prompt":{ + "system_prompt": { "name": "system_prompt" }, - "user_prompt":{ + "user_prompt": { "name": "user_prompt" }, "model": { @@ -172,17 +287,17 @@ } } }, - "YCYY_Ollama_LLM_Advanced_Options":{ + "YCYY_Ollama_LLM_Advanced_Options": { "display_name": "Ollama 文本高级选项", "description": "该节点为 ollama 大语言模型提供了高级选项。", - "inputs": { - "max_tokens":{ + "inputs": { + "max_tokens": { "name": "max_tokens" }, - "temperature":{ + "temperature": { "name": "temperature" }, - "top_p":{ + "top_p": { "name": "top_p" }, "enable_max_tokens": { @@ -261,19 +376,19 @@ } } }, - "YCYY_Gemini_Image_Preset":{ + "YCYY_Gemini_Image_Preset": { "display_name": "Gemini 图像预设", "description": "该节点为 Gemini 图像 API 提供预设值", "inputs": { - "preset":{ + "preset": { "name": "预设名称", "tooltip": "预设名称" }, - "description":{ + "description": { "name": "预设描述", "tooltip": "预设描述" }, - "prompt":{ + "prompt": { "name": "预设提示词", "tooltip": "预设提示词" } @@ -332,11 +447,11 @@ "name": "text", "tooltip": "需要转换为语音的文本" }, - "config_options":{ + "config_options": { "name": "配置选项", "tooltip": "可选配置覆盖选项,来自 YCYY API 配置选项" }, - "proxy_options":{ + "proxy_options": { "name": "代理选项", "tooltip": "可选代理覆盖选项,来自 YCYY API 代理选项" }, @@ -406,11 +521,11 @@ "name": "prompt", "tooltip": "用于指导转录的提示词。您可以要求特定的格式或指令。" }, - "config_options":{ + "config_options": { "name": "配置选项", "tooltip": "可选配置覆盖选项,来自 YCYY API 配置选项" }, - "proxy_options":{ + "proxy_options": { "name": "代理选项", "tooltip": "可选代理覆盖选项,来自 YCYY API 代理选项" }, @@ -431,11 +546,11 @@ "display_name": "魔搭图像API", "description": "该节点使用 ModelScope API 生成图像", "inputs": { - "config_options":{ + "config_options": { "name": "配置选项", "tooltip": "可选配置覆盖选项,来自 YCYY API 配置选项" }, - "proxy_options":{ + "proxy_options": { "name": "代理选项", "tooltip": "可选代理覆盖选项,来自 YCYY API 代理选项" }, @@ -476,20 +591,49 @@ "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": "提示词引导系数"} + "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": "提示词引导系数" + } }, "outputs": { - "0": {"name": "图像"}, - "1": {"name": "字符串"} + "0": { + "name": "图像" + }, + "1": { + "name": "字符串" + } } }, "YCYY_Image_Compare": { @@ -506,4 +650,4 @@ } } } -} +} \ No newline at end of file diff --git a/openai/__init__.py b/openai/__init__.py deleted file mode 100644 index 8b13789..0000000 --- a/openai/__init__.py +++ /dev/null @@ -1 +0,0 @@ -