Files
Hasasasa-ComfyUI_DashuaiTools/API_Nodes/api_caption.py
T
2025-12-04 00:29:06 +08:00

201 lines
8.2 KiB
Python

import base64
import io
import json
import time
from typing import Tuple
import numpy as np
import requests
from PIL import Image
from requests.adapters import HTTPAdapter
import torch
def tensor2pil(t_image: torch.Tensor) -> Image.Image:
if not isinstance(t_image, torch.Tensor):
raise TypeError("输入必须是 torch.Tensor")
arr = t_image.cpu().numpy()
if arr.ndim == 4:
arr = arr[0]
arr = np.clip(255.0 * arr.squeeze(), 0, 255).astype(np.uint8)
if arr.ndim == 3 and arr.shape[0] in (1, 3):
arr = np.transpose(arr, (1, 2, 0))
return Image.fromarray(arr)
class API_caption:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"api_type": (["硅基流动", "贞贞工坊", "OpenRouter", "其他"], {"default": "硅基流动"}),
"api_url": ("STRING", {"default": "<url>"}),
"API_Key": ("STRING", {"default": "<your_key>"}),
"model_name": ("STRING", {"default": "Qwen/Qwen3-VL-32B-Instruct"}),
"image": ("IMAGE",),
"prompt": ("STRING", {"default": "你是一位专业的AI图像生成提示词工程师。请详细描述这张图像的主体、前景、中景、背景、构图、视觉引导、色调、光影氛围等细节并创作出具有深度、氛围和艺术感的图像提示词。要求:中文提示词,不要出现对图像水印的描述,不要出现无关的文字和符号,不需要总结,限制在800字以内。", "multiline": True, "rows": 4}),
"output_language": (["中文", "英文"], {"default": "中文"}),
"temperature": ("FLOAT", {"default": 0.5, "min": 0.0, "max": 2.0, "step": 0.01}),
}
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("text",)
FUNCTION = "generate"
CATEGORY = "DaNodes/API"
def generate(self, api_type: str, API_Key: str, model_name: str, image: torch.Tensor, prompt: str,
output_language: str, temperature: float = 0.5, api_url: str = "") -> Tuple[str]:
try:
# 复用 HTTP 连接以减少握手开销
session = getattr(self, "_session", None)
if session is None:
session = requests.Session()
adapter = HTTPAdapter(pool_connections=4, pool_maxsize=4)
session.mount("https://", adapter)
session.mount("http://", adapter)
self._session = session
# 根据 provider 选择实际的 URL
provider_map = {
"OpenRouter": "https://openrouter.ai/api/v1/chat/completions",
"硅基流动": "https://api.siliconflow.cn/v1/chat/completions",
"贞贞工坊": "https://api.bltcy.ai/v1/chat/completions",
}
if api_type == "其他":
api_url = api_url or ""
else:
api_url = provider_map.get(api_type, api_url)
if not api_url:
return ("请求地址为空,请填写或选择有效的 API 类型",)
# image -> base64(保持原始尺寸,使用无损 PNG 编码)
pil = tensor2pil(image)
buf = io.BytesIO()
pil.save(buf, format="PNG")
img_b64 = base64.b64encode(buf.getvalue()).decode()
# 提示词
if output_language == "中文":
prompt_full = f"{prompt} 请用中文返回描述"
else:
prompt_full = f"{prompt} Please return the description in English."
# 按照示例将图片放在 messages[0].content 中
image_data_uri = f"data:image/png;base64,{img_b64}"
messages = [
{
"role": "user",
"content": [
{"type": "text", "text": prompt_full},
{"type": "image_url", "image_url": {"url": image_data_uri}},
],
}
]
payload = {"model": model_name, "stream": False, "messages": messages, "max_tokens": 400, "temperature": float(temperature)}
headers = {
"Authorization": f"Bearer {API_Key}",
"Content-Type": "application/json",
"Accept": "application/json",
"User-Agent": "ComfyUI-DaNodes/1.0",
}
# 固定使用硅基流动式 JSON 风格请求,遇到 429/5xx/读超时简单重试一次
r = None
last_err = None
for attempt in range(2):
try:
r = session.post(api_url, json=payload, headers=headers, timeout=90)
except Exception as e:
last_err = e
if attempt == 0:
time.sleep(1.0)
continue
return (f"API 请求失败(网络连接错误): {e}",)
if not r.ok and r.status_code in (429, 500, 502, 503, 504) and attempt == 0:
time.sleep(1.0)
continue
break
if r is None:
return (f"API 请求失败: {last_err}",)
if not r.ok:
body = None
try:
body = r.json()
body = json.dumps(body, ensure_ascii=False)
except Exception:
body = r.text
return (f"API 返回错误状态 {r.status_code}: {body}",)
try:
result = r.json()
except Exception as e:
return (f"无法解析 API 返回的 JSON: {e}; body: {r.text}",)
# 提取文本回复的帮助函数
def extract_text_from_content(content):
if isinstance(content, str):
return content
if isinstance(content, dict):
return content.get("text") or content.get("content") or str(content)
if isinstance(content, list):
parts = []
for block in content:
if not isinstance(block, dict):
parts.append(str(block))
continue
btype = block.get("type", "")
if btype in ("text", "paragraph"):
parts.append(block.get("text", ""))
elif btype == "image_url":
continue
else:
parts.append(block.get("text") or block.get("content") or "")
return " ".join(p for p in parts if p).strip()
return str(content)
caption = ""
if isinstance(result, dict):
choices = result.get("choices") or []
if choices:
first = choices[0]
if isinstance(first, dict):
msg = first.get("message") or first.get("delta") or None
if isinstance(msg, dict) and "content" in msg:
caption = extract_text_from_content(msg.get("content"))
else:
if "text" in first and isinstance(first.get("text"), str):
caption = first.get("text")
else:
caption = extract_text_from_content(first)
else:
caption = str(first)
else:
caption = result.get("caption") or result.get("text") or ""
else:
caption = str(result)
caption = caption.strip()
if caption == "":
try:
pretty = json.dumps(result, ensure_ascii=False)
except Exception:
pretty = str(result)
return (f"无法从 API 响应解析出文本。返回 JSON: {pretty}",)
return (caption,)
except Exception as e:
# 捕获整个函数体的未处理异常,避免 ComfyUI 无输出
return (f"节点内部异常: {e}",)
NODE_CLASS_MAPPINGS = {
"API_caption": API_caption
}
NODE_DISPLAY_NAME_MAPPINGS = {
"API_caption": "API_caption☀"
}