Files
jinchanz-ComfyUI-ADIC/module/node.py
T
2025-06-27 01:52:48 +08:00

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 图片缩放"
}