new file: README.md

new file:   __init__.py
	new file:   image2prompt_node.py
	new file:   requirements.txt
This commit is contained in:
dotaevo-linux-do
2025-11-11 14:18:37 +08:00
commit aa10c1fb99
4 changed files with 332 additions and 0 deletions
+80
View File
@@ -0,0 +1,80 @@
# ComfyUI NudeJS Image2Prompt 节点
这是一个 ComfyUI 自定义节点,可以将图片转换为文本提示。
## 功能特性
- **图片预处理**:自动调整图片尺寸(最大宽度 1000px,最大高度 800px)
- **Base64 编码**:将图片转换为 base64 格式
- **API 调用**:通过 POST 请求调用外部 API
- **智能重试**:支持配置重试次数,应对网络波动
- **双模式输出**:支持中性文本和提示词文本两种输出模式
## 安装方法
### 方法一:复制到 ComfyUI 目录
1. 将整个 `ComfyUI-NudeJS-Image2Prompt` 文件夹复制到 ComfyUI 的 `custom_nodes` 目录中:
```
ComfyUI/custom_nodes/ComfyUI-NudeJS-Image2Prompt/
```
2. 重启 ComfyUI
### 方法二:符号链接
在 Linux/macOS 系统中:
```bash
ln -s /path/to/ComfyUI-NudeJS-Image2Prompt /path/to/ComfyUI/custom_nodes/
```
## 使用方法
在 ComfyUI 工作流中添加 "NudeJS/Image2Prompt" 节点:
### 输入参数
- **images** (必需): 输入图片
- **APIUrl** (必需): API 接口地址,例如:`https://api.example.com`
- **APIKey** (必需): API 鉴权密钥
- **RetryCount** (可选): 请求失败后的重试次数,默认值:3
- **Neutral** (可选): 是否使用中性文本模式,默认值:true
### 输出
- **text**: 转换后的文本提示
## 工作流程示例
1. 添加图片加载节点(如 Load Image、Checkpoint Loader 等)
2. 添加 "Image2Prompt" 节点
3. 将图片连接到 Image2Prompt 节点的 "images" 输入
4. 配置 APIUrl 和 APIKey
5. 运行工作流
## 技术实现
- **图片处理**:使用 Pillow 进行图片缩放和格式转换
- **网络请求**:使用 requests 库,支持重试和超时
- **Base64 编码**:使用标准库 base64 进行编码
## 错误处理
- 网络请求失败时会自动重试(最多 RetryCount 次)
- 失败时会在控制台输出错误信息
- 失败时返回 `None`
## 依赖
- ComfyUI
- Python 3.7+
- requests
- Pillow (PIL)
## 许可证
MIT License
## 贡献
欢迎提交 Issue 和 Pull Request!
+3
View File
@@ -0,0 +1,3 @@
from .image2prompt_node import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"]
+247
View File
@@ -0,0 +1,247 @@
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 = {"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
# 注册节点
NODE_CLASS_MAPPINGS = {
"Image2Prompt": Image2Prompt,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"Image2Prompt": "Image2Prompt",
}
+2
View File
@@ -0,0 +1,2 @@
requests>=2.25.0
Pillow>=8.0.0