1278 lines
49 KiB
Python
1278 lines
49 KiB
Python
import json
|
|
import requests
|
|
from typing import List, Dict, Any
|
|
import torch
|
|
import numpy as np
|
|
from PIL import Image
|
|
import io
|
|
from urllib.parse import urlparse
|
|
|
|
|
|
|
|
class ADIC_COMMON_API:
|
|
"""ADIC Common API"""
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {
|
|
"params": ("STRING", {"forceInput": True}),
|
|
"app_name": ("STRING", {"default": "NHCI"}),
|
|
"api_key": ("STRING", {"default": ""}),
|
|
"api_endpoint": ("STRING", {"default": "/open/api/v1/ai/imageTranslate"}),
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = ("STRING",)
|
|
RETURN_NAMES = ("response",)
|
|
|
|
FUNCTION = "translate"
|
|
|
|
OUTPUT_NODE = True
|
|
|
|
CATEGORY = "Malette"
|
|
|
|
def translate(self, params, app_name, api_key, api_endpoint):
|
|
try:
|
|
# 验证必填参数
|
|
if not api_key or api_key.strip() == "":
|
|
raise ValueError("API密钥不能为空")
|
|
|
|
if not api_endpoint or api_endpoint.strip() == "":
|
|
raise ValueError("API端点不能为空")
|
|
|
|
# 解析params参数
|
|
try:
|
|
if isinstance(params, str):
|
|
request_body = json.loads(params)
|
|
else:
|
|
request_body = params
|
|
|
|
|
|
except json.JSONDecodeError as e:
|
|
raise ValueError(f"params参数JSON解析失败: {str(e)}")
|
|
|
|
# 设置请求头
|
|
headers = {
|
|
"Content-Type": "application/json",
|
|
"Accept": "*/*",
|
|
"Authorization": f"Bearer {api_key.strip()}",
|
|
"X-App-Name": app_name.strip()
|
|
}
|
|
|
|
print(f"[ADIC_COMMON_API] 请求参数: {json.dumps(request_body, ensure_ascii=False)}")
|
|
|
|
# 发送POST请求
|
|
response = requests.post(
|
|
api_endpoint.strip(),
|
|
headers=headers,
|
|
json=request_body,
|
|
timeout=600,
|
|
verify=False # 忽略SSL证书验证
|
|
)
|
|
|
|
# 检查响应状态
|
|
response.raise_for_status()
|
|
|
|
# 解析响应
|
|
response_data = response.json()
|
|
|
|
# 检查业务状态
|
|
if not response_data.get("success", False):
|
|
error_msg = response_data.get("message", "API调用失败")
|
|
print(f"[ADIC_COMMON_API] 业务错误: {error_msg}")
|
|
return (json.dumps({
|
|
"error": error_msg,
|
|
"code": response_data.get("code", -1),
|
|
"debugInfo": response_data.get("debugInfo")
|
|
}, ensure_ascii=False, indent=2),)
|
|
|
|
# 返回成功结果
|
|
result_json = json.dumps(response_data, ensure_ascii=False, indent=2)
|
|
print(f"[ADIC_COMMON_API] 请求结果: {result_json}")
|
|
|
|
return (result_json,)
|
|
|
|
except requests.exceptions.RequestException as e:
|
|
error_msg = f"网络请求失败: {str(e)}"
|
|
print(f"[ADIC_COMMON_API] {error_msg}")
|
|
return (json.dumps({"error": error_msg}, ensure_ascii=False),)
|
|
|
|
except json.JSONDecodeError as e:
|
|
error_msg = f"JSON 解析失败: {str(e)}"
|
|
print(f"[ADIC_COMMON_API] {error_msg}")
|
|
return (json.dumps({"error": error_msg}, ensure_ascii=False),)
|
|
|
|
except Exception as e:
|
|
error_msg = f"未知错误: {str(e)}"
|
|
print(f"[ADIC_COMMON_API] {error_msg}")
|
|
return (json.dumps({"error": error_msg}, ensure_ascii=False),)
|
|
|
|
|
|
class ImageTranslateAPI:
|
|
"""阿里云图片翻译API节点"""
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {
|
|
"params": ("STRING", {"forceInput": True}),
|
|
"app_name": ("STRING", {"default": "NHCI"}),
|
|
"api_key": ("STRING", {"default": ""}),
|
|
"api_endpoint": ("STRING", {"default": "/open/api/v1/ai/imageTranslate"}),
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = ("STRING",)
|
|
RETURN_NAMES = ("response",)
|
|
|
|
FUNCTION = "translate"
|
|
|
|
OUTPUT_NODE = True
|
|
|
|
CATEGORY = "Malette"
|
|
|
|
def translate(self, params, app_name, api_key, api_endpoint):
|
|
try:
|
|
# 验证必填参数
|
|
if not api_key or api_key.strip() == "":
|
|
raise ValueError("API密钥不能为空")
|
|
|
|
if not api_endpoint or api_endpoint.strip() == "":
|
|
raise ValueError("API端点不能为空")
|
|
|
|
# 解析params参数
|
|
try:
|
|
if isinstance(params, str):
|
|
request_body = json.loads(params)
|
|
else:
|
|
request_body = params
|
|
|
|
except json.JSONDecodeError as e:
|
|
raise ValueError(f"params参数JSON解析失败: {str(e)}")
|
|
|
|
# 设置请求头
|
|
headers = {
|
|
"Content-Type": "application/json",
|
|
"Accept": "*/*",
|
|
"Authorization": f"Bearer {api_key.strip()}",
|
|
"X-App-Name": app_name.strip()
|
|
}
|
|
|
|
print(f"[ImageTranslateAPI] 请求参数: {json.dumps(request_body, ensure_ascii=False)}")
|
|
|
|
# 发送POST请求
|
|
response = requests.post(
|
|
api_endpoint.strip(),
|
|
headers=headers,
|
|
json=request_body,
|
|
timeout=600,
|
|
verify=False # 忽略SSL证书验证
|
|
)
|
|
|
|
# 检查响应状态
|
|
response.raise_for_status()
|
|
|
|
# 解析响应
|
|
response_data = response.json()
|
|
|
|
print(f"[ImageTranslateAPI] 响应数据: {response_data}")
|
|
|
|
# 检查业务状态
|
|
if not response_data.get("success", False):
|
|
error_msg = response_data.get("message", "API调用失败")
|
|
print(f"[ImageTranslateAPI] 业务错误: {error_msg}")
|
|
return (json.dumps({
|
|
"error": error_msg,
|
|
"code": response_data.get("code", -1),
|
|
"debugInfo": response_data.get("debugInfo")
|
|
}, ensure_ascii=False, indent=2),)
|
|
|
|
# 返回成功结果
|
|
result_json = json.dumps(response_data, ensure_ascii=False, indent=2)
|
|
print(f"[ImageTranslateAPI] 翻译完成,处理了 {len(response_data.get('data', []))} 张图片")
|
|
print(f"[ImageTranslateAPI] 翻译结果: {result_json}")
|
|
|
|
return (result_json,)
|
|
|
|
except requests.exceptions.RequestException as e:
|
|
error_msg = f"网络请求失败: {str(e)}"
|
|
print(f"[ImageTranslateAPI] {error_msg}")
|
|
return (json.dumps({"error": error_msg}, ensure_ascii=False),)
|
|
|
|
except json.JSONDecodeError as e:
|
|
error_msg = f"JSON 解析失败: {str(e)}"
|
|
print(f"[ImageTranslateAPI] {error_msg}")
|
|
return (json.dumps({"error": error_msg}, ensure_ascii=False),)
|
|
|
|
except Exception as e:
|
|
error_msg = f"未知错误: {str(e)}"
|
|
print(f"[ImageTranslateAPI] {error_msg}")
|
|
return (json.dumps({"error": error_msg}, ensure_ascii=False),)
|
|
|
|
|
|
class ImageTranslateParamsBuilder:
|
|
"""图片翻译参数构建器节点"""
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {
|
|
"url1": ("STRING", {"default": ""}),
|
|
},
|
|
"optional": {
|
|
"source_language1": (["", "zh", "en", "tr"], {"default": ""}),
|
|
"target_language1": (["", "es", "fr", "pt", "ko", "en"], {"default": "en"}),
|
|
"url2": ("STRING", {"default": ""}),
|
|
"source_language2": (["", "zh", "en", "tr"], {"default": ""}),
|
|
"target_language2": (["", "es", "fr", "pt", "ko", "en"], {"default": "en"}),
|
|
"url3": ("STRING", {"default": ""}),
|
|
"source_language3": (["", "zh", "en", "tr"], {"default": ""}),
|
|
"target_language3": (["", "es", "fr", "pt", "ko", "en"], {"default": "en"}),
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = ("STRING",)
|
|
RETURN_NAMES = ("params_json",)
|
|
|
|
FUNCTION = "build"
|
|
|
|
OUTPUT_NODE = False
|
|
|
|
CATEGORY = "Malette"
|
|
|
|
def build(self, url1, source_language1="", target_language1="en",
|
|
url2="", source_language2="", target_language2="en",
|
|
url3="", source_language3="", target_language3="en"):
|
|
"""构建符合API要求的params JSON字符串"""
|
|
params_array = []
|
|
|
|
# 处理所有URL和对应的语言设置
|
|
url_configs = [
|
|
(url1, source_language1, target_language1),
|
|
(url2, source_language2, target_language2),
|
|
(url3, source_language3, target_language3),
|
|
]
|
|
|
|
for url, source_lang, target_lang in url_configs:
|
|
url = url.strip()
|
|
if not url:
|
|
continue
|
|
|
|
# 验证URL格式
|
|
if not (url.startswith('http://') or url.startswith('https://')):
|
|
print(f"[ImageTranslateParamsBuilder] 警告: URL格式不正确: {url}")
|
|
continue
|
|
|
|
param = {"url": url}
|
|
|
|
# 添加源语言(如果指定了)
|
|
if source_lang and source_lang.strip():
|
|
param["sourceLanguage"] = source_lang.strip()
|
|
|
|
# 添加目标语言(如果指定了)
|
|
if target_lang and target_lang.strip():
|
|
param["targetLanguage"] = target_lang.strip()
|
|
|
|
params_array.append(param)
|
|
|
|
if not params_array:
|
|
print("[ImageTranslateParamsBuilder] 警告: 没有有效的URL")
|
|
return (json.dumps({"params": []}),)
|
|
|
|
result = {"params": params_array}
|
|
result_json = json.dumps(result, ensure_ascii=False, indent=2)
|
|
print(f"[ImageTranslateParamsBuilder] 构建了 {len(params_array)} 个翻译参数")
|
|
|
|
return (result_json,)
|
|
|
|
|
|
class ImageTranslateResultExtractor:
|
|
"""图片翻译结果提取器节点"""
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {
|
|
"api_response": ("STRING", {"forceInput": True}),
|
|
},
|
|
"optional": {
|
|
"extract_type": (["all", "result_urls", "result_jsons", "source_info"], {"default": "result_urls"}),
|
|
"index": ("INT", {"default": 0, "min": 0, "max": 100}),
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = ("STRING",)
|
|
RETURN_NAMES = ("extracted_data",)
|
|
|
|
FUNCTION = "extract"
|
|
|
|
OUTPUT_NODE = False
|
|
|
|
CATEGORY = "Malette"
|
|
|
|
def extract(self, api_response, extract_type="result_urls", index=0):
|
|
"""从API响应中提取特定数据"""
|
|
try:
|
|
# 解析API响应
|
|
response_data = json.loads(api_response)
|
|
|
|
# 检查是否有错误
|
|
if "error" in response_data:
|
|
return (api_response,)
|
|
|
|
# 获取数据列表
|
|
data_list = response_data.get("data", [])
|
|
if not data_list:
|
|
return (json.dumps([], ensure_ascii=False),)
|
|
|
|
if extract_type == "all":
|
|
# 返回完整的数据列表
|
|
result = json.dumps(data_list, ensure_ascii=False, indent=2)
|
|
|
|
elif extract_type == "result_urls":
|
|
# 提取所有结果图片URL
|
|
urls = [item.get("resultUrl", "") for item in data_list if item.get("resultUrl")]
|
|
result = json.dumps(urls, ensure_ascii=False)
|
|
|
|
elif extract_type == "result_jsons":
|
|
# 提取所有结果JSON
|
|
jsons = [item.get("resultJson", "") for item in data_list if item.get("resultJson")]
|
|
result = json.dumps(jsons, ensure_ascii=False)
|
|
|
|
elif extract_type == "source_info":
|
|
# 提取源信息(URL和语言)
|
|
source_info = []
|
|
for item in data_list:
|
|
info = {
|
|
"url": item.get("url", ""),
|
|
"sourceLanguage": item.get("sourceLanguage", ""),
|
|
"targetLanguage": item.get("targetLanguage", "")
|
|
}
|
|
source_info.append(info)
|
|
result = json.dumps(source_info, ensure_ascii=False, indent=2)
|
|
|
|
else:
|
|
result = json.dumps(data_list, ensure_ascii=False, indent=2)
|
|
|
|
print(f"[ImageTranslateResultExtractor] 提取类型: {extract_type}, 数据数量: {len(data_list)}")
|
|
return (result,)
|
|
|
|
except json.JSONDecodeError as e:
|
|
error_msg = f"JSON 解析失败: {str(e)}"
|
|
print(f"[ImageTranslateResultExtractor] {error_msg}")
|
|
return (json.dumps({"error": error_msg}, ensure_ascii=False),)
|
|
|
|
except Exception as e:
|
|
error_msg = f"提取过程出错: {str(e)}"
|
|
print(f"[ImageTranslateResultExtractor] {error_msg}")
|
|
return (json.dumps({"error": error_msg}, ensure_ascii=False),)
|
|
|
|
|
|
class LoadImagesFromUrls:
|
|
"""从URL列表加载图片节点"""
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {
|
|
"urls": ("STRING", {"forceInput": True, "multiline": True}),
|
|
},
|
|
"optional": {
|
|
"input_format": (["json_array", "newline_separated", "auto"], {"default": "auto"}),
|
|
"max_images": ("INT", {"default": 10, "min": 1, "max": 50}),
|
|
"timeout": ("INT", {"default": 30, "min": 5, "max": 300}),
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = ("IMAGE", "STRING", "INT")
|
|
RETURN_NAMES = ("images", "loaded_urls", "count")
|
|
|
|
OUTPUT_IS_LIST = (True, False, False)
|
|
|
|
FUNCTION = "load_images"
|
|
|
|
OUTPUT_NODE = False
|
|
|
|
CATEGORY = "Malette"
|
|
|
|
def load_images(self, urls, input_format="auto", max_images=10, timeout=30):
|
|
"""从URL列表加载图片"""
|
|
try:
|
|
# 解析URL列表
|
|
url_list = self._parse_urls(urls, input_format)
|
|
|
|
if not url_list:
|
|
print("[LoadImagesFromUrls] 警告: 没有找到有效的URL")
|
|
# 返回空图片数据
|
|
empty_image = torch.zeros((1, 64, 64, 3), dtype=torch.float32)
|
|
return (empty_image, json.dumps([]), 0)
|
|
|
|
# 限制图片数量
|
|
url_list = url_list[:max_images]
|
|
|
|
loaded_images = []
|
|
loaded_urls = []
|
|
|
|
print(f"[LoadImagesFromUrls] 开始加载 {len(url_list)} 张图片")
|
|
|
|
for i, url in enumerate(url_list):
|
|
try:
|
|
print(f"[LoadImagesFromUrls] 正在加载第 {i+1}/{len(url_list)} 张图片: {url}")
|
|
|
|
# 验证URL格式
|
|
if not self._is_valid_url(url):
|
|
print(f"[LoadImagesFromUrls] 跳过无效URL: {url}")
|
|
continue
|
|
|
|
# 下载图片
|
|
response = requests.get(
|
|
url,
|
|
timeout=timeout,
|
|
headers={
|
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36'
|
|
},
|
|
verify=False
|
|
)
|
|
response.raise_for_status()
|
|
|
|
# 检查内容类型
|
|
content_type = response.headers.get('content-type', '')
|
|
if not content_type.startswith('image/'):
|
|
print(f"[LoadImagesFromUrls] 跳过非图片内容: {url}, content-type: {content_type}")
|
|
continue
|
|
|
|
# 将响应内容转换为PIL图片
|
|
image = Image.open(io.BytesIO(response.content))
|
|
|
|
# 确保图片是RGB模式
|
|
if image.mode != 'RGB':
|
|
image = image.convert('RGB')
|
|
|
|
# 转换为numpy数组
|
|
image_np = np.array(image).astype(np.float32) / 255.0
|
|
|
|
# 转换为torch张量 (H, W, C)
|
|
image_tensor = torch.from_numpy(image_np)
|
|
|
|
loaded_images.append(image_tensor)
|
|
loaded_urls.append(url)
|
|
|
|
print(f"[LoadImagesFromUrls] 成功加载图片: {image.size}, 模式: {image.mode}")
|
|
|
|
except requests.exceptions.RequestException as e:
|
|
print(f"[LoadImagesFromUrls] 下载图片失败 {url}: {str(e)}")
|
|
continue
|
|
except Exception as e:
|
|
print(f"[LoadImagesFromUrls] 处理图片失败 {url}: {str(e)}")
|
|
continue
|
|
|
|
if not loaded_images:
|
|
print("[LoadImagesFromUrls] 警告: 没有成功加载任何图片")
|
|
# 返回空图片列表
|
|
empty_image = torch.zeros((64, 64, 3), dtype=torch.float32)
|
|
return ([empty_image], json.dumps([]), 0)
|
|
|
|
# 保持原始尺寸,将每张图片作为独立的张量返回
|
|
# 为每张图片添加batch维度,但保持原始尺寸
|
|
output_images = []
|
|
for i, img_tensor in enumerate(loaded_images):
|
|
# 图片格式为 (H, W, C),添加batch维度变为 (1, H, W, C)
|
|
batched_img = img_tensor.unsqueeze(0)
|
|
output_images.append(batched_img)
|
|
print(f" 图片 {i+1}: {img_tensor.shape[1]}x{img_tensor.shape[0]} (WxH)")
|
|
|
|
print(f"[LoadImagesFromUrls] 成功加载 {len(loaded_images)} 张图片,保持原始尺寸")
|
|
|
|
return (output_images, json.dumps(loaded_urls, ensure_ascii=False), len(loaded_images))
|
|
|
|
except Exception as e:
|
|
error_msg = f"加载图片过程出错: {str(e)}"
|
|
print(f"[LoadImagesFromUrls] {error_msg}")
|
|
# 返回错误时的默认数据
|
|
empty_image = torch.zeros((1, 64, 64, 3), dtype=torch.float32)
|
|
return ([empty_image], json.dumps({"error": error_msg}, ensure_ascii=False), 0)
|
|
|
|
def _parse_urls(self, urls_input, input_format):
|
|
"""解析URL输入"""
|
|
urls_input = urls_input.strip()
|
|
if not urls_input:
|
|
return []
|
|
|
|
url_list = []
|
|
|
|
if input_format == "auto":
|
|
# 自动检测格式
|
|
try:
|
|
# 尝试解析为JSON
|
|
parsed = json.loads(urls_input)
|
|
if isinstance(parsed, list):
|
|
url_list = [str(url).strip() for url in parsed if str(url).strip()]
|
|
print("[LoadImagesFromUrls] 检测到JSON数组格式")
|
|
else:
|
|
raise ValueError("JSON不是数组格式")
|
|
except:
|
|
# 解析为换行分隔
|
|
url_list = [line.strip() for line in urls_input.split('\n') if line.strip()]
|
|
print("[LoadImagesFromUrls] 检测到换行分隔格式")
|
|
|
|
elif input_format == "json_array":
|
|
try:
|
|
parsed = json.loads(urls_input)
|
|
if isinstance(parsed, list):
|
|
url_list = [str(url).strip() for url in parsed if str(url).strip()]
|
|
else:
|
|
raise ValueError("JSON格式错误:不是数组")
|
|
except Exception as e:
|
|
print(f"[LoadImagesFromUrls] JSON解析失败: {str(e)}")
|
|
return []
|
|
|
|
elif input_format == "newline_separated":
|
|
url_list = [line.strip() for line in urls_input.split('\n') if line.strip()]
|
|
|
|
# 过滤有效的URL
|
|
valid_urls = [url for url in url_list if self._is_valid_url(url)]
|
|
|
|
print(f"[LoadImagesFromUrls] 解析得到 {len(valid_urls)} 个有效URL")
|
|
return valid_urls
|
|
|
|
def _is_valid_url(self, url):
|
|
"""验证URL格式"""
|
|
try:
|
|
result = urlparse(url)
|
|
return all([result.scheme in ['http', 'https'], result.netloc])
|
|
except:
|
|
return False
|
|
|
|
|
|
class PythonCodeExecutor:
|
|
"""Python代码执行器节点"""
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {
|
|
"code": ("STRING", {"multiline": True, "default": """# 在这里编写Python代码
|
|
# 可用变量:
|
|
# - input1, input2, input3: 输入数据
|
|
# - json, re: 预导入的模块
|
|
#
|
|
# 示例:将换行分割的字符串转换为JSON数组
|
|
# lines = input1.strip().split('\\n')
|
|
# result = [line.strip() for line in lines if line.strip()]
|
|
# output = json.dumps(result, ensure_ascii=False)
|
|
|
|
# 请将最终结果赋值给 'output' 变量
|
|
output = "请在上方编写代码"
|
|
"""}),
|
|
},
|
|
"optional": {
|
|
"input1": ("STRING", {"default": "", "forceInput": True}),
|
|
"input2": ("STRING", {"default": "", "forceInput": True}),
|
|
"input3": ("STRING", {"default": "", "forceInput": True}),
|
|
"safe_mode": ("BOOLEAN", {"default": True}),
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = ("STRING", "STRING")
|
|
RETURN_NAMES = ("output", "logs")
|
|
|
|
FUNCTION = "execute_code"
|
|
|
|
OUTPUT_NODE = False
|
|
|
|
CATEGORY = "Malette"
|
|
|
|
def execute_code(self, code, input1="", input2="", input3="", safe_mode=True):
|
|
"""执行Python代码"""
|
|
import re
|
|
import math
|
|
import random
|
|
from datetime import datetime, timedelta
|
|
|
|
try:
|
|
# 准备执行环境
|
|
local_vars = {
|
|
'input1': input1,
|
|
'input2': input2,
|
|
'input3': input3,
|
|
'json': json,
|
|
're': re,
|
|
'math': math,
|
|
'random': random,
|
|
'datetime': datetime,
|
|
'timedelta': timedelta,
|
|
'len': len,
|
|
'str': str,
|
|
'int': int,
|
|
'float': float,
|
|
'list': list,
|
|
'dict': dict,
|
|
'tuple': tuple,
|
|
'set': set,
|
|
'range': range,
|
|
'enumerate': enumerate,
|
|
'zip': zip,
|
|
'map': map,
|
|
'filter': filter,
|
|
'sorted': sorted,
|
|
'max': max,
|
|
'min': min,
|
|
'sum': sum,
|
|
'any': any,
|
|
'all': all,
|
|
'print': print,
|
|
'output': None
|
|
}
|
|
|
|
# 安全模式检查
|
|
if safe_mode:
|
|
dangerous_keywords = [
|
|
'import os', 'import sys', 'import subprocess', 'import shutil',
|
|
'__import__', 'eval', 'exec', 'compile', 'open', 'file',
|
|
'input', 'raw_input', 'globals', 'locals', 'vars', 'dir',
|
|
'getattr', 'setattr', 'delattr', 'hasattr'
|
|
]
|
|
|
|
code_lower = code.lower()
|
|
for keyword in dangerous_keywords:
|
|
if keyword in code_lower:
|
|
return (json.dumps({"error": f"安全模式禁止使用: {keyword}"}, ensure_ascii=False),
|
|
f"[PythonCodeExecutor] 安全检查失败: 发现禁用关键词 '{keyword}'")
|
|
|
|
# 捕获print输出
|
|
import io
|
|
import sys
|
|
old_stdout = sys.stdout
|
|
sys.stdout = captured_output = io.StringIO()
|
|
|
|
logs = []
|
|
logs.append(f"[PythonCodeExecutor] 开始执行代码,安全模式: {'开启' if safe_mode else '关闭'}")
|
|
logs.append(f"[PythonCodeExecutor] 输入数据:")
|
|
logs.append(f" input1: {repr(input1[:100])}{'...' if len(input1) > 100 else ''}")
|
|
logs.append(f" input2: {repr(input2[:100])}{'...' if len(input2) > 100 else ''}")
|
|
logs.append(f" input3: {repr(input3[:100])}{'...' if len(input3) > 100 else ''}")
|
|
|
|
# 执行用户代码
|
|
exec(code, {"__builtins__": {}}, local_vars)
|
|
|
|
# 恢复stdout
|
|
sys.stdout = old_stdout
|
|
captured_print = captured_output.getvalue()
|
|
|
|
# 获取输出结果
|
|
result = local_vars.get('output', None)
|
|
|
|
if result is None:
|
|
logs.append("[PythonCodeExecutor] 警告: 代码未设置output变量")
|
|
output_str = json.dumps({"warning": "代码未设置output变量"}, ensure_ascii=False)
|
|
else:
|
|
# 将结果转换为字符串
|
|
if isinstance(result, str):
|
|
output_str = result
|
|
else:
|
|
try:
|
|
output_str = json.dumps(result, ensure_ascii=False, indent=2)
|
|
except:
|
|
output_str = str(result)
|
|
|
|
logs.append(f"[PythonCodeExecutor] 代码执行成功")
|
|
logs.append(f"[PythonCodeExecutor] 输出类型: {type(result).__name__}")
|
|
logs.append(f"[PythonCodeExecutor] 输出长度: {len(output_str)} 字符")
|
|
|
|
# 添加print输出到日志
|
|
if captured_print:
|
|
logs.append("[PythonCodeExecutor] Print输出:")
|
|
for line in captured_print.strip().split('\n'):
|
|
logs.append(f" {line}")
|
|
|
|
log_output = '\n'.join(logs)
|
|
|
|
return (output_str, log_output)
|
|
|
|
except SyntaxError as e:
|
|
error_msg = f"语法错误: {str(e)}"
|
|
log_output = f"[PythonCodeExecutor] {error_msg}\n在第 {e.lineno} 行: {e.text}"
|
|
return (json.dumps({"error": error_msg}, ensure_ascii=False), log_output)
|
|
|
|
except Exception as e:
|
|
error_msg = f"执行错误: {str(e)}"
|
|
log_output = f"[PythonCodeExecutor] {error_msg}"
|
|
return (json.dumps({"error": error_msg}, ensure_ascii=False), log_output)
|
|
|
|
finally:
|
|
# 确保恢复stdout
|
|
if 'old_stdout' in locals():
|
|
sys.stdout = old_stdout
|
|
|
|
|
|
class StringToJsonArray:
|
|
"""字符串转JSON数组节点(快捷版本)"""
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {
|
|
"text": ("STRING", {"forceInput": True, "multiline": True}),
|
|
},
|
|
"optional": {
|
|
"separator": (["newline", "comma", "semicolon", "pipe", "tab"], {"default": "newline"}),
|
|
"remove_empty": ("BOOLEAN", {"default": True}),
|
|
"trim_whitespace": ("BOOLEAN", {"default": True}),
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = ("STRING",)
|
|
RETURN_NAMES = ("json_array",)
|
|
|
|
FUNCTION = "convert"
|
|
|
|
OUTPUT_NODE = False
|
|
|
|
CATEGORY = "Malette"
|
|
|
|
def convert(self, text, separator="newline", remove_empty=True, trim_whitespace=True):
|
|
"""将字符串转换为JSON数组"""
|
|
try:
|
|
if not text:
|
|
return (json.dumps([], ensure_ascii=False),)
|
|
|
|
# 选择分隔符
|
|
separators = {
|
|
"newline": "\n",
|
|
"comma": ",",
|
|
"semicolon": ";",
|
|
"pipe": "|",
|
|
"tab": "\t"
|
|
}
|
|
|
|
sep = separators.get(separator, "\n")
|
|
|
|
# 分割字符串
|
|
items = text.split(sep)
|
|
|
|
# 处理选项
|
|
if trim_whitespace:
|
|
items = [item.strip() for item in items]
|
|
|
|
if remove_empty:
|
|
items = [item for item in items if item]
|
|
|
|
# 转换为JSON
|
|
result = json.dumps(items, ensure_ascii=False, indent=2)
|
|
|
|
print(f"[StringToJsonArray] 转换了 {len(items)} 个项目,分隔符: {separator}")
|
|
|
|
return (result,)
|
|
|
|
except Exception as e:
|
|
error_msg = f"转换失败: {str(e)}"
|
|
print(f"[StringToJsonArray] {error_msg}")
|
|
return (json.dumps({"error": error_msg}, ensure_ascii=False),)
|
|
|
|
|
|
class MarketImageGenerateWithPolling:
|
|
"""营销图生图任务创建与轮询节点"""
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {
|
|
"generate_params": ("STRING", {"forceInput": True, "multiline": True}),
|
|
"app_name": ("STRING", {"default": "NHCI"}),
|
|
"api_key": ("STRING", {"default": ""}),
|
|
"user_id": ("STRING", {"default": ""}),
|
|
"base_url": ("STRING", {"default": "https://pre-zhimei.alibabadesign.com"}),
|
|
},
|
|
"optional": {
|
|
"poll_interval": ("INT", {"default": 5, "min": 1, "max": 60}),
|
|
"max_poll_time": ("INT", {"default": 300, "min": 30, "max": 1800}),
|
|
"auto_start_polling": ("BOOLEAN", {"default": True}),
|
|
}
|
|
}
|
|
|
|
RETURN_TYPES = ("STRING", "STRING", "STRING")
|
|
RETURN_NAMES = ("result", "task_id", "status")
|
|
|
|
FUNCTION = "execute_task"
|
|
|
|
OUTPUT_NODE = True
|
|
|
|
CATEGORY = "Malette"
|
|
|
|
def execute_task(self, generate_params, app_name, api_key, user_id, base_url,
|
|
poll_interval=5, max_poll_time=300, auto_start_polling=True):
|
|
"""执行营销图生图任务并轮询结果"""
|
|
import time
|
|
|
|
try:
|
|
# 验证必填参数
|
|
if not api_key or api_key.strip() == "":
|
|
raise ValueError("API密钥不能为空")
|
|
|
|
if not user_id or user_id.strip() == "":
|
|
raise ValueError("用户ID不能为空")
|
|
|
|
# 解析生成参数
|
|
try:
|
|
if isinstance(generate_params, str):
|
|
request_body = json.loads(generate_params)
|
|
else:
|
|
request_body = generate_params
|
|
except json.JSONDecodeError as e:
|
|
raise ValueError(f"生成参数JSON解析失败: {str(e)}")
|
|
|
|
# 设置请求头
|
|
headers = {
|
|
"Content-Type": "application/json",
|
|
"Accept": "*/*",
|
|
"Authorization": f"Bearer {api_key.strip()}",
|
|
"X-App-Name": app_name.strip()
|
|
}
|
|
|
|
# 步骤1: 创建营销图生图任务
|
|
create_url = f"{base_url.rstrip('/')}/open/api/v1/ai/marketImageGenerate"
|
|
|
|
print(f"[MarketImageGenerateWithPolling] 创建任务...")
|
|
print(f"[MarketImageGenerateWithPolling] 请求URL: {create_url}")
|
|
print(f"[MarketImageGenerateWithPolling] 请求参数: {json.dumps(request_body, ensure_ascii=False)}")
|
|
|
|
# 发送创建任务请求
|
|
response = requests.post(
|
|
create_url,
|
|
headers=headers,
|
|
json=request_body,
|
|
timeout=60,
|
|
verify=False
|
|
)
|
|
|
|
response.raise_for_status()
|
|
create_response = response.json()
|
|
|
|
print(f"[MarketImageGenerateWithPolling] 创建任务响应: {json.dumps(create_response, ensure_ascii=False)}")
|
|
|
|
# 检查创建任务是否成功
|
|
if not create_response.get("success", False):
|
|
error_msg = create_response.get("message", "创建任务失败")
|
|
return (
|
|
json.dumps({"error": error_msg, "code": create_response.get("code", -1)}, ensure_ascii=False),
|
|
"",
|
|
"failed"
|
|
)
|
|
|
|
# 获取任务ID
|
|
task_id = create_response.get("data")
|
|
if not task_id:
|
|
return (
|
|
json.dumps({"error": "未获取到任务ID"}, ensure_ascii=False),
|
|
"",
|
|
"failed"
|
|
)
|
|
|
|
task_id_str = str(task_id)
|
|
print(f"[MarketImageGenerateWithPolling] 任务创建成功,任务ID: {task_id_str}")
|
|
|
|
# 如果不自动轮询,直接返回任务ID
|
|
if not auto_start_polling:
|
|
return (
|
|
json.dumps({"task_id": task_id_str, "message": "任务已创建,请手动查询结果"}, ensure_ascii=False),
|
|
task_id_str,
|
|
"created"
|
|
)
|
|
|
|
# 步骤2: 轮询查询任务结果
|
|
query_url = f"{base_url.rstrip('/')}/open/api/v1/ai/getMainTask"
|
|
|
|
# 构建查询参数
|
|
query_params = {
|
|
"appKey": api_key.strip(),
|
|
"appName": app_name.strip(),
|
|
"id": task_id,
|
|
"userId": user_id.strip()
|
|
}
|
|
|
|
print(f"[MarketImageGenerateWithPolling] 开始轮询任务结果...")
|
|
print(f"[MarketImageGenerateWithPolling] 轮询间隔: {poll_interval}秒, 最大轮询时间: {max_poll_time}秒")
|
|
|
|
start_time = time.time()
|
|
poll_count = 0
|
|
|
|
while time.time() - start_time < max_poll_time:
|
|
poll_count += 1
|
|
print(f"[MarketImageGenerateWithPolling] 第 {poll_count} 次查询任务状态...")
|
|
|
|
try:
|
|
# 查询任务状态
|
|
query_response = requests.post(
|
|
query_url,
|
|
headers=headers,
|
|
json=query_params,
|
|
timeout=30,
|
|
verify=False
|
|
)
|
|
|
|
query_response.raise_for_status()
|
|
query_data = query_response.json()
|
|
|
|
print(f"[MarketImageGenerateWithPolling] 查询响应: {json.dumps(query_data, ensure_ascii=False)}")
|
|
|
|
# 检查查询是否成功
|
|
if not query_data.get("success", False):
|
|
error_msg = query_data.get("message", "查询任务失败")
|
|
print(f"[MarketImageGenerateWithPolling] 查询失败: {error_msg}")
|
|
time.sleep(poll_interval)
|
|
continue
|
|
|
|
# 获取任务数据
|
|
task_data = query_data.get("data")
|
|
if not task_data:
|
|
print("[MarketImageGenerateWithPolling] 未获取到任务数据")
|
|
time.sleep(poll_interval)
|
|
continue
|
|
|
|
# 检查任务状态
|
|
task_status = task_data.get("status", "").upper()
|
|
print(f"[MarketImageGenerateWithPolling] 任务状态: {task_status}")
|
|
|
|
# 任务完成状态判断
|
|
if task_status in ["SUCCESS", "COMPLETED", "FINISHED"]:
|
|
print(f"[MarketImageGenerateWithPolling] 任务完成!耗时 {time.time() - start_time:.1f} 秒")
|
|
result_json = json.dumps(query_data, ensure_ascii=False, indent=2)
|
|
return (result_json, task_id_str, "completed")
|
|
|
|
elif task_status in ["FAILED", "ERROR", "CANCELLED"]:
|
|
print(f"[MarketImageGenerateWithPolling] 任务失败,状态: {task_status}")
|
|
error_result = {
|
|
"error": f"任务失败,状态: {task_status}",
|
|
"task_data": task_data
|
|
}
|
|
return (json.dumps(error_result, ensure_ascii=False, indent=2), task_id_str, "failed")
|
|
|
|
else:
|
|
# 任务仍在进行中
|
|
print(f"[MarketImageGenerateWithPolling] 任务进行中,状态: {task_status},等待 {poll_interval} 秒后重试...")
|
|
time.sleep(poll_interval)
|
|
continue
|
|
|
|
except requests.exceptions.RequestException as e:
|
|
print(f"[MarketImageGenerateWithPolling] 查询请求失败: {str(e)}")
|
|
time.sleep(poll_interval)
|
|
continue
|
|
|
|
except Exception as e:
|
|
print(f"[MarketImageGenerateWithPolling] 查询过程出错: {str(e)}")
|
|
time.sleep(poll_interval)
|
|
continue
|
|
|
|
# 轮询超时
|
|
timeout_result = {
|
|
"error": f"轮询超时({max_poll_time}秒),任务可能仍在进行中",
|
|
"task_id": task_id_str,
|
|
"poll_count": poll_count,
|
|
"elapsed_time": time.time() - start_time
|
|
}
|
|
|
|
return (json.dumps(timeout_result, ensure_ascii=False, indent=2), task_id_str, "timeout")
|
|
|
|
except requests.exceptions.RequestException as e:
|
|
error_msg = f"网络请求失败: {str(e)}"
|
|
print(f"[MarketImageGenerateWithPolling] {error_msg}")
|
|
return (json.dumps({"error": error_msg}, ensure_ascii=False), "", "error")
|
|
|
|
except json.JSONDecodeError as e:
|
|
error_msg = f"JSON 解析失败: {str(e)}"
|
|
print(f"[MarketImageGenerateWithPolling] {error_msg}")
|
|
return (json.dumps({"error": error_msg}, ensure_ascii=False), "", "error")
|
|
|
|
except Exception as e:
|
|
error_msg = f"未知错误: {str(e)}"
|
|
print(f"[MarketImageGenerateWithPolling] {error_msg}")
|
|
return (json.dumps({"error": error_msg}, ensure_ascii=False), "", "error")
|
|
|
|
class ImageStitch:
|
|
"""Upstreamed from https://github.com/kijai/ComfyUI-KJNodes"""
|
|
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {
|
|
"required": {
|
|
"image1": ("IMAGE",),
|
|
"direction": (["right", "down", "left", "up"], {"default": "right"}),
|
|
"match_image_size": ("BOOLEAN", {"default": True}),
|
|
"spacing_width": (
|
|
"INT",
|
|
{"default": 0, "min": 0, "max": 1024, "step": 2},
|
|
),
|
|
"spacing_color": (
|
|
["white", "black", "red", "green", "blue"],
|
|
{"default": "white"},
|
|
),
|
|
},
|
|
"optional": {
|
|
"image2": ("IMAGE",),
|
|
},
|
|
}
|
|
|
|
RETURN_TYPES = ("IMAGE",)
|
|
FUNCTION = "stitch"
|
|
CATEGORY = "image/transform"
|
|
DESCRIPTION = """
|
|
Stitches image2 to image1 in the specified direction.
|
|
If image2 is not provided, returns image1 unchanged.
|
|
Optional spacing can be added between images.
|
|
"""
|
|
|
|
def stitch(
|
|
self,
|
|
image1,
|
|
direction,
|
|
match_image_size,
|
|
spacing_width,
|
|
spacing_color,
|
|
image2=None,
|
|
):
|
|
if image2 is None:
|
|
return (image1,)
|
|
|
|
# Handle batch size differences
|
|
if image1.shape[0] != image2.shape[0]:
|
|
max_batch = max(image1.shape[0], image2.shape[0])
|
|
if image1.shape[0] < max_batch:
|
|
image1 = torch.cat(
|
|
[image1, image1[-1:].repeat(max_batch - image1.shape[0], 1, 1, 1)]
|
|
)
|
|
if image2.shape[0] < max_batch:
|
|
image2 = torch.cat(
|
|
[image2, image2[-1:].repeat(max_batch - image2.shape[0], 1, 1, 1)]
|
|
)
|
|
|
|
# Match image sizes if requested
|
|
if match_image_size:
|
|
h1, w1 = image1.shape[1:3]
|
|
h2, w2 = image2.shape[1:3]
|
|
aspect_ratio = w2 / h2
|
|
|
|
if direction in ["left", "right"]:
|
|
target_h, target_w = h1, int(h1 * aspect_ratio)
|
|
else: # up, down
|
|
target_w, target_h = w1, int(w1 / aspect_ratio)
|
|
|
|
image2 = comfy.utils.common_upscale(
|
|
image2.movedim(-1, 1), target_w, target_h, "lanczos", "disabled"
|
|
).movedim(1, -1)
|
|
|
|
color_map = {
|
|
"white": 1.0,
|
|
"black": 0.0,
|
|
"red": (1.0, 0.0, 0.0),
|
|
"green": (0.0, 1.0, 0.0),
|
|
"blue": (0.0, 0.0, 1.0),
|
|
}
|
|
|
|
color_val = color_map[spacing_color]
|
|
|
|
# When not matching sizes, pad to align non-concat dimensions
|
|
if not match_image_size:
|
|
h1, w1 = image1.shape[1:3]
|
|
h2, w2 = image2.shape[1:3]
|
|
pad_value = 0.0
|
|
if not isinstance(color_val, tuple):
|
|
pad_value = color_val
|
|
|
|
if direction in ["left", "right"]:
|
|
# For horizontal concat, pad heights to match
|
|
if h1 != h2:
|
|
target_h = max(h1, h2)
|
|
if h1 < target_h:
|
|
pad_h = target_h - h1
|
|
pad_top, pad_bottom = pad_h // 2, pad_h - pad_h // 2
|
|
image1 = torch.nn.functional.pad(image1, (0, 0, 0, 0, pad_top, pad_bottom), mode='constant', value=pad_value)
|
|
if h2 < target_h:
|
|
pad_h = target_h - h2
|
|
pad_top, pad_bottom = pad_h // 2, pad_h - pad_h // 2
|
|
image2 = torch.nn.functional.pad(image2, (0, 0, 0, 0, pad_top, pad_bottom), mode='constant', value=pad_value)
|
|
else: # up, down
|
|
# For vertical concat, pad widths to match
|
|
if w1 != w2:
|
|
target_w = max(w1, w2)
|
|
if w1 < target_w:
|
|
pad_w = target_w - w1
|
|
pad_left, pad_right = pad_w // 2, pad_w - pad_w // 2
|
|
image1 = torch.nn.functional.pad(image1, (0, 0, pad_left, pad_right), mode='constant', value=pad_value)
|
|
if w2 < target_w:
|
|
pad_w = target_w - w2
|
|
pad_left, pad_right = pad_w // 2, pad_w - pad_w // 2
|
|
image2 = torch.nn.functional.pad(image2, (0, 0, pad_left, pad_right), mode='constant', value=pad_value)
|
|
|
|
# Ensure same number of channels
|
|
if image1.shape[-1] != image2.shape[-1]:
|
|
max_channels = max(image1.shape[-1], image2.shape[-1])
|
|
if image1.shape[-1] < max_channels:
|
|
image1 = torch.cat(
|
|
[
|
|
image1,
|
|
torch.ones(
|
|
*image1.shape[:-1],
|
|
max_channels - image1.shape[-1],
|
|
device=image1.device,
|
|
),
|
|
],
|
|
dim=-1,
|
|
)
|
|
if image2.shape[-1] < max_channels:
|
|
image2 = torch.cat(
|
|
[
|
|
image2,
|
|
torch.ones(
|
|
*image2.shape[:-1],
|
|
max_channels - image2.shape[-1],
|
|
device=image2.device,
|
|
),
|
|
],
|
|
dim=-1,
|
|
)
|
|
|
|
# Add spacing if specified
|
|
if spacing_width > 0:
|
|
spacing_width = spacing_width + (spacing_width % 2) # Ensure even
|
|
|
|
if direction in ["left", "right"]:
|
|
spacing_shape = (
|
|
image1.shape[0],
|
|
max(image1.shape[1], image2.shape[1]),
|
|
spacing_width,
|
|
image1.shape[-1],
|
|
)
|
|
else:
|
|
spacing_shape = (
|
|
image1.shape[0],
|
|
spacing_width,
|
|
max(image1.shape[2], image2.shape[2]),
|
|
image1.shape[-1],
|
|
)
|
|
|
|
spacing = torch.full(spacing_shape, 0.0, device=image1.device)
|
|
if isinstance(color_val, tuple):
|
|
for i, c in enumerate(color_val):
|
|
if i < spacing.shape[-1]:
|
|
spacing[..., i] = c
|
|
if spacing.shape[-1] == 4: # Add alpha
|
|
spacing[..., 3] = 1.0
|
|
else:
|
|
spacing[..., : min(3, spacing.shape[-1])] = color_val
|
|
if spacing.shape[-1] == 4:
|
|
spacing[..., 3] = 1.0
|
|
|
|
# Concatenate images
|
|
images = [image2, image1] if direction in ["left", "up"] else [image1, image2]
|
|
if spacing_width > 0:
|
|
images.insert(1, spacing)
|
|
|
|
concat_dim = 2 if direction in ["left", "right"] else 1
|
|
return (torch.cat(images, dim=concat_dim),)
|
|
|
|
def conditioning_set_values(conditioning, values={}):
|
|
c = []
|
|
for t in conditioning:
|
|
n = [t[0], t[1].copy()]
|
|
for k in values:
|
|
n[1][k] = values[k]
|
|
c.append(n)
|
|
|
|
return c
|
|
|
|
|
|
class ReferenceLatent:
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {"required": {"conditioning": ("CONDITIONING", ),
|
|
},
|
|
"optional": {"latent": ("LATENT", ),}
|
|
}
|
|
|
|
RETURN_TYPES = ("CONDITIONING",)
|
|
FUNCTION = "append"
|
|
|
|
CATEGORY = "advanced/conditioning/edit_models"
|
|
DESCRIPTION = "This node sets the guiding latent for an edit model. If the model supports it you can chain multiple to set multiple reference images."
|
|
|
|
def append(self, conditioning, latent=None):
|
|
if latent is not None:
|
|
conditioning = conditioning_set_values(conditioning, {"reference_latents": [latent["samples"]]}, append=True)
|
|
return (conditioning, )
|
|
|
|
|
|
|
|
|
|
PREFERED_KONTEXT_RESOLUTIONS = [
|
|
(672, 1568),
|
|
(688, 1504),
|
|
(720, 1456),
|
|
(752, 1392),
|
|
(800, 1328),
|
|
(832, 1248),
|
|
(880, 1184),
|
|
(944, 1104),
|
|
(1024, 1024),
|
|
(1104, 944),
|
|
(1184, 880),
|
|
(1248, 832),
|
|
(1328, 800),
|
|
(1392, 752),
|
|
(1456, 720),
|
|
(1504, 688),
|
|
(1568, 672),
|
|
]
|
|
|
|
import comfy.utils
|
|
|
|
class FluxKontextImageScale:
|
|
@classmethod
|
|
def INPUT_TYPES(s):
|
|
return {"required": {"image": ("IMAGE", ),
|
|
},
|
|
}
|
|
|
|
RETURN_TYPES = ("IMAGE",)
|
|
FUNCTION = "scale"
|
|
|
|
CATEGORY = "advanced/conditioning/flux"
|
|
DESCRIPTION = "This node resizes the image to one that is more optimal for flux kontext."
|
|
|
|
def scale(self, image):
|
|
width = image.shape[2]
|
|
height = image.shape[1]
|
|
aspect_ratio = width / height
|
|
_, width, height = min((abs(aspect_ratio - w / h), w, h) for w, h in PREFERED_KONTEXT_RESOLUTIONS)
|
|
image = comfy.utils.common_upscale(image.movedim(-1, 1), width, height, "lanczos", "center").movedim(1, -1)
|
|
return (image, )
|
|
|
|
|
|
# 节点映射
|
|
NODE_CLASS_MAPPINGS = {
|
|
"ImageTranslateAPI": ImageTranslateAPI,
|
|
"ImageTranslateParamsBuilder": ImageTranslateParamsBuilder,
|
|
"ImageTranslateResultExtractor": ImageTranslateResultExtractor,
|
|
"ADIC_COMMON_API": ADIC_COMMON_API,
|
|
"LoadImagesFromUrls": LoadImagesFromUrls,
|
|
"PythonCodeExecutor": PythonCodeExecutor,
|
|
"StringToJsonArray": StringToJsonArray,
|
|
"MarketImageGenerateWithPolling": MarketImageGenerateWithPolling,
|
|
"ImageStitch": ImageStitch,
|
|
"ReferenceLatent": ReferenceLatent,
|
|
"FluxKontextImageScale": FluxKontextImageScale
|
|
}
|
|
|
|
NODE_DISPLAY_NAME_MAPPINGS = {
|
|
"ImageTranslateAPI": "图片翻译 API",
|
|
"ImageTranslateParamsBuilder": "图片翻译参数构建器",
|
|
"ImageTranslateResultExtractor": "图片翻译结果提取器",
|
|
"ADIC_COMMON_API": "ADIC Common API",
|
|
"LoadImagesFromUrls": "从URL列表加载图片",
|
|
"PythonCodeExecutor": "Python代码执行器",
|
|
"StringToJsonArray": "字符串转JSON数组",
|
|
"MarketImageGenerateWithPolling": "营销图生图任务(带轮询)",
|
|
"ImageStitch": "图片拼接",
|
|
"ReferenceLatent": "参考潜变量",
|
|
"FluxKontextImageScale": "Flux Kontext 图片缩放"
|
|
} |