Files
2025-12-07 01:39:42 +08:00

376 lines
11 KiB
Python

import base64
import io
from typing import Dict, Any, List, Tuple, Union
import torch
from PIL import Image
import requests
from requests.adapters import HTTPAdapter
from requests.packages.urllib3.util.retry import Retry
class Image2Prompt:
"""
ComfyUI 自定义节点:将图片转换为文本提示
"""
@classmethod
def INPUT_TYPES(cls) -> Dict[str, Any]:
return {
"required": {
"images": ("IMAGE",),
"APIUrl": ("STRING", {"default": "", "multiline": False}),
"APIKey": ("STRING", {"default": "", "multiline": False}),
"RetryCount": ("INT", {"default": 3, "min": 0, "max": 100}),
"Neutral": ("BOOLEAN", {"default": True}),
}
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("text",)
FUNCTION = "execute"
CATEGORY = "NudeJS"
def __init__(self):
self.session = requests.Session()
# 配置重试策略
retry_strategy = Retry(
total=3,
backoff_factor=1,
status_forcelist=[429, 500, 502, 503, 504],
)
adapter = HTTPAdapter(max_retries=retry_strategy)
self.session.mount("http://", adapter)
self.session.mount("https://", adapter)
def execute(
self,
images: torch.Tensor,
APIUrl: str,
APIKey: str,
RetryCount: int,
Neutral: bool,
) -> Tuple[Union[str, None]]:
"""
执行图片转提示词的主逻辑
Args:
images: 输入的图片张量
APIUrl: API 接口地址
APIKey: API 密钥
RetryCount: 重试次数
Neutral: 是否使用中性文本
Returns:
转换后的文本提示
"""
if len(images) == 0:
return (None,)
# 获取第一张图片(ComfyUI 通常批量处理,这里取第一张)
img_tensor = images[0]
# 转换为 PIL Image
pil_image = self._tensor_to_pil(img_tensor)
# 检查并缩放图片
resized_image = self._resize_image(pil_image)
# 转换为 base64
img_base64 = self._image_to_base64(resized_image)
# 发送请求获取提示词
prompt_text = self._call_api(
APIUrl=APIUrl,
APIKey=APIKey,
imgBase64=img_base64,
retry_count=RetryCount,
neutral=Neutral,
)
return (prompt_text,)
def _tensor_to_pil(self, img_tensor: torch.Tensor) -> Image.Image:
"""
将 ComfyUI 的张量转换为 PIL Image
Args:
img_tensor: ComfyUI 中的图片张量 (B, H, W, C) 或 (H, W, C)
Returns:
PIL Image
"""
# 确保张量在 CPU 上
if img_tensor.is_cuda:
img_tensor = img_tensor.cpu()
# 处理不同的张量形状
if len(img_tensor.shape) == 4:
# 批量图片,取第一张
img_tensor = img_tensor[0]
elif len(img_tensor.shape) == 3:
# 单张图片
pass
else:
raise ValueError(f"Unexpected tensor shape: {img_tensor.shape}")
# 转换为 PIL Image
# ComfyUI 的图片张量通常值范围是 [0, 1],形状是 [H, W, C]
if img_tensor.shape[-1] in [3, 4]: # RGB 或 RGBA
array = (img_tensor.numpy() * 255).astype("uint8")
pil_image = Image.fromarray(array, mode="RGB")
else:
raise ValueError(f"Unexpected number of channels: {img_tensor.shape[-1]}")
return pil_image
def _resize_image(self, image: Image.Image) -> Image.Image:
"""
缩放图片,确保宽度不超过 1000,高度不超过 800
Args:
image: 输入的 PIL 图片
Returns:
缩放后的图片
"""
width, height = image.size
new_width = width
new_height = height
# 检查宽度
if width > 1000:
new_width = 1000
new_height = int((height * 1000) / width)
# 检查高度
if new_height > 800:
new_height = 800
new_width = int((new_width * 800) / new_height)
# 如果需要缩放,则调整大小
if new_width != width or new_height != height:
image = image.resize((new_width, new_height), Image.Resampling.LANCZOS)
return image
def _image_to_base64(self, image: Image.Image) -> str:
"""
将 PIL 图片转换为 base64 字符串(不包含 data:image/png;base64, 前缀)
Args:
image: 输入的 PIL 图片
Returns:
base64 编码的字符串
"""
# 转换为 RGB 模式(如果需要)
if image.mode != "RGB":
image = image.convert("RGB")
# 保存到内存中的字节流
buffer = io.BytesIO()
image.save(buffer, format="PNG")
img_bytes = buffer.getvalue()
# 编码为 base64
img_base64 = base64.b64encode(img_bytes).decode("utf-8")
return img_base64
def _call_api(
self,
APIUrl: str,
APIKey: str,
imgBase64: str,
retry_count: int,
neutral: bool,
) -> Union[str, None]:
"""
调用 API 接口获取提示词
Args:
APIUrl: API 地址
APIKey: API 密钥
imgBase64: base64 编码的图片
retry_count: 最大重试次数
neutral: 是否使用中性文本
Returns:
提示词文本,失败返回 None
"""
url = f"{APIUrl}/generate"
headers = {"authorization": APIKey}
data = { "type": "img2img", "base64": imgBase64 }
max_retries = max(0, retry_count)
for attempt in range(max_retries + 1):
try:
response = self.session.post(url, json=data, headers=headers, timeout=30)
response.raise_for_status()
result = response.json()
if result.get("code") == 200:
if neutral:
return result.get("neutralText")
else:
return result.get("promptText")
else:
# API 返回错误码,记录日志
print(f"API request failed with code: {result.get('code')}, message: {result.get('message', 'unknown error')}")
except requests.exceptions.RequestException as e:
print(f"Attempt {attempt + 1} failed: {str(e)}")
except Exception as e:
print(f"Unexpected error on attempt {attempt + 1}: {str(e)}")
# 如果不是最后一次尝试,等待一下再重试
if attempt < max_retries:
import time
time.sleep(1)
# 所有重试都失败了
print(f"Failed after {max_retries} retries")
return None
class Text2Prompt:
"""
ComfyUI 自定义节点:将文本描述转换为图片提示
"""
@classmethod
def INPUT_TYPES(cls) -> Dict[str, Any]:
return {
"required": {
"imgDesc": ("STRING", {"default": "", "multiline": True}),
"APIUrl": ("STRING", {"default": "", "multiline": False}),
"APIKey": ("STRING", {"default": "", "multiline": False}),
"RetryCount": ("INT", {"default": 3, "min": 0, "max": 100}),
"Neutral": ("BOOLEAN", {"default": True}),
}
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("text",)
FUNCTION = "execute"
CATEGORY = "NudeJS"
def __init__(self):
self.session = requests.Session()
# 配置重试策略
retry_strategy = Retry(
total=3,
backoff_factor=1,
status_forcelist=[429, 500, 502, 503, 504],
)
adapter = HTTPAdapter(max_retries=retry_strategy)
self.session.mount("http://", adapter)
self.session.mount("https://", adapter)
def execute(
self,
imgDesc: str,
APIUrl: str,
APIKey: str,
RetryCount: int,
Neutral: bool,
) -> Tuple[Union[str, None]]:
"""
执行文本转提示词的主逻辑
Args:
imgDesc: 图片的文本描述
APIUrl: API 接口地址
APIKey: API 密钥
RetryCount: 重试次数
Neutral: 是否使用中性文本
Returns:
转换后的文本提示
"""
# 发送请求获取提示词
prompt_text = self._call_api(
APIUrl=APIUrl,
APIKey=APIKey,
imgDesc=imgDesc,
retry_count=RetryCount,
neutral=Neutral,
)
return (prompt_text,)
def _call_api(
self,
APIUrl: str,
APIKey: str,
imgDesc: str,
retry_count: int,
neutral: bool,
) -> Union[str, None]:
"""
调用 API 接口获取提示词
Args:
APIUrl: API 地址
APIKey: API 密钥
imgDesc: 图片的文本描述
retry_count: 最大重试次数
neutral: 是否使用中性文本
Returns:
提示词文本,失败返回 None
"""
url = f"{APIUrl}/generate"
headers = {"authorization": APIKey}
data = { "type": "text2img", "desc": imgDesc }
max_retries = max(0, retry_count)
for attempt in range(max_retries + 1):
try:
response = self.session.post(url, json=data, headers=headers, timeout=30)
response.raise_for_status()
result = response.json()
if result.get("code") == 200:
if neutral:
return result.get("neutralText")
else:
return result.get("promptText")
else:
# API 返回错误码,记录日志
print(f"API request failed with code: {result.get('code')}, message: {result.get('message', 'unknown error')}")
except requests.exceptions.RequestException as e:
print(f"Attempt {attempt + 1} failed: {str(e)}")
except Exception as e:
print(f"Unexpected error on attempt {attempt + 1}: {str(e)}")
# 如果不是最后一次尝试,等待一下再重试
if attempt < max_retries:
import time
time.sleep(1)
# 所有重试都失败了
print(f"Failed after {max_retries} retries")
return None
# 注册节点
NODE_CLASS_MAPPINGS = {
"Image2Prompt": Image2Prompt,
"Text2Prompt": Text2Prompt,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"Image2Prompt": "Image2Prompt",
"Text2Prompt": "Text2Prompt",
}