增加火山引擎通用3.0文生图模型节点

This commit is contained in:
jueming
2025-06-24 10:32:25 +08:00
commit 2ebb082d7d
5 changed files with 405 additions and 0 deletions
+120
View File
@@ -0,0 +1,120 @@
# ComfyUI Volcengine SeeDream V3 Node
这是一个用于ComfyUI的自定义节点,可以调用火山引擎通用3.0文生图模型API来生成图像。
## 功能特性
- 调用火山引擎SeeDream V3文生图API
- 支持所有官方API参数
- 自动处理API签名验证
- 支持URL和Base64图像响应格式
- 完整的错误处理机制
## 安装
1. 将此项目克隆到ComfyUI的custom_nodes目录下:
```bash
cd ComfyUI/custom_nodes/
git clone [your-repo-url] ComfyUI-JM-Volcengine-API
```
2. 安装依赖:
```bash
cd ComfyUI-JM-Volcengine-API
pip install -r requirements.txt
```
3. 重启ComfyUI
## 使用方法
### 节点参数
#### 必需参数
- **access_key**: 火山引擎API访问密钥
- **secret_key**: 火山引擎API秘钥
- **prompt**: 图像生成提示词(支持中英文)
#### 可选参数
- **use_pre_llm**: 是否开启文本扩写(默认:False)
- **seed**: 随机种子,用于生成可重复的结果(默认:-1为随机)
- **guidance_scale**: 文本描述的影响程度(默认:2.5,范围:1.0-10.0)
- **aspect_ratio**: 图像宽高比,支持1.5K分辨率预设(默认:1:1)
- **return_url**: 是否返回图片URL链接(默认:True)
### 支持的图像分辨率 (1.5K)
节点支持以下预设的1.5K分辨率比例选项:
- **1:1** → 1536×1536 (正方形)
- **4:3** → 1472×1104 (传统照片比例)
- **3:2** → 1584×1056 (经典摄影比例)
- **16:9** → 1664×936 (宽屏比例)
- **9:16** → 936×1664 (竖屏比例,适合手机wallpaper)
- **21:9** → 2016×864 (超宽屏比例)
### 提示词优化技巧
1. **画面描述**: 使用连贯的自然语言描述画面内容(主体+行为+环境等)
2. **美学描述**: 用短词语描述画面美学(风格、色彩、光影、构图等)
3. **专业词汇**: 推荐使用英文描述专业词汇,获得更精准效果
4. **图像用途**: 说明图像用途和类型,如"PPT封面背景图"、"广告海报设计"等
5. **文字排版**: 将文字内容置于双引号""内,并描述文字的大小、字体、颜色、风格和位置
### 示例提示词
```
【风格:漫画风格】【镜头视角:特写】一个可爱的机器人在花园里浇花,阳光明媚,色彩鲜艳,卡通风格
```
### 参数说明对照表
| 节点参数名 | API参数名 | 说明 |
|-----------|-----------|------|
| access_key | - | 火山引擎API访问密钥 |
| secret_key | - | 火山引擎API密钥 |
| prompt | prompt | 图像生成提示词 |
| use_pre_llm | use_pre_llm | 文本扩写增强 |
| seed | seed | 随机种子 |
| guidance_scale | scale | 引导强度 |
| aspect_ratio | width/height | 自动设置宽高比 |
| return_url | return_url | 返回图片URL |
## 节点位置
在ComfyUI中,您可以在以下位置找到该节点:
**Add Node → JM-MiniMax-API/Video → volcengine-seedream-v3**
## API配置
1. 登录火山引擎控制台
2. 获取您的access_key和secret_key
3. 确保您的账户有足够的API调用额度
## 错误处理
节点包含完整的错误处理机制:
- API密钥验证
- 网络请求超时处理
- 图像下载失败处理
- API响应错误处理
如果生成失败,节点会返回一个空白图像并在控制台输出错误信息。
## 注意事项
- 图像链接有效期为24小时
- 建议使用1.3K-1.5K分辨率获得最佳画质
- 请合理使用API避免超出配额限制
- 网络环境需要能够访问火山引擎API服务
## 技术支持
如果您遇到任何问题,请检查:
1. API密钥是否正确
2. 网络连接是否正常
3. ComfyUI控制台的错误信息
4. 火山引擎API配额是否充足
## 许可证
本项目基于MIT许可证开源。
+11
View File
@@ -0,0 +1,11 @@
from .nodes.volcengine_seedream_v3 import VolcengineSeeDreamV3Node
NODE_CLASS_MAPPINGS = {
"volcengine-seedream-v3": VolcengineSeeDreamV3Node
}
NODE_DISPLAY_NAME_MAPPINGS = {
"volcengine-seedream-v3": "Volcengine SeeDream V3"
}
__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS']
+1
View File
@@ -0,0 +1 @@
# nodes/__init__.py
+271
View File
@@ -0,0 +1,271 @@
import json
import base64
import datetime
import hashlib
import hmac
import requests
import torch
import numpy as np
from PIL import Image
import io
import os
class VolcengineSeeDreamV3Node:
"""
ComfyUI custom node for Volcengine SeeDream V3 text-to-image API
"""
def __init__(self):
self.method = 'POST'
self.host = 'visual.volcengineapi.com'
self.region = 'cn-north-1'
self.endpoint = 'https://visual.volcengineapi.com'
self.service = 'cv'
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"access_key": ("STRING", {"default": "", "multiline": False}),
"secret_key": ("STRING", {"default": "", "multiline": False}),
"prompt": ("STRING", {"default": "", "multiline": True}),
},
"optional": {
"use_pre_llm": ("BOOLEAN", {"default": False}),
"seed": ("INT", {"default": -1, "min": -1, "max": 2147483647}),
"guidance_scale": ("FLOAT", {"default": 2.5, "min": 1.0, "max": 10.0, "step": 0.1}),
"aspect_ratio": (["1:1", "4:3", "3:2", "16:9", "9:16", "21:9"], {"default": "1:1"}),
"return_url": ("BOOLEAN", {"default": True}),
}
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("image",)
FUNCTION = "generate_image"
CATEGORY = "JM-MiniMax-API/Video"
def sign(self, key, msg):
"""Generate HMAC signature"""
return hmac.new(key, msg.encode('utf-8'), hashlib.sha256).digest()
def get_signature_key(self, key, date_stamp, region_name, service_name):
"""Generate signature key"""
k_date = self.sign(key.encode('utf-8'), date_stamp)
k_region = self.sign(k_date, region_name)
k_service = self.sign(k_region, service_name)
k_signing = self.sign(k_service, 'request')
return k_signing
def format_query(self, parameters):
"""Format query parameters"""
request_parameters_init = ''
for key in sorted(parameters):
request_parameters_init += key + '=' + parameters[key] + '&'
request_parameters = request_parameters_init[:-1]
return request_parameters
def sign_v4_request(self, access_key, secret_key, service, req_query, req_body):
"""Sign request using AWS V4 signature"""
if access_key is None or secret_key is None:
raise ValueError('Access key and secret key are required.')
t = datetime.datetime.utcnow()
current_date = t.strftime('%Y%m%dT%H%M%SZ')
datestamp = t.strftime('%Y%m%d')
canonical_uri = '/'
canonical_querystring = req_query
signed_headers = 'content-type;host;x-content-sha256;x-date'
payload_hash = hashlib.sha256(req_body.encode('utf-8')).hexdigest()
content_type = 'application/json'
canonical_headers = (
f'content-type:{content_type}\n'
f'host:{self.host}\n'
f'x-content-sha256:{payload_hash}\n'
f'x-date:{current_date}\n'
)
canonical_request = (
f'{self.method}\n{canonical_uri}\n{canonical_querystring}\n'
f'{canonical_headers}\n{signed_headers}\n{payload_hash}'
)
algorithm = 'HMAC-SHA256'
credential_scope = f'{datestamp}/{self.region}/{service}/request'
string_to_sign = (
f'{algorithm}\n{current_date}\n{credential_scope}\n'
f'{hashlib.sha256(canonical_request.encode("utf-8")).hexdigest()}'
)
signing_key = self.get_signature_key(secret_key, datestamp, self.region, service)
signature = hmac.new(signing_key, string_to_sign.encode('utf-8'), hashlib.sha256).hexdigest()
authorization_header = (
f'{algorithm} Credential={access_key}/{credential_scope}, '
f'SignedHeaders={signed_headers}, Signature={signature}'
)
headers = {
'X-Date': current_date,
'Authorization': authorization_header,
'X-Content-Sha256': payload_hash,
'Content-Type': content_type
}
return headers
def download_image_from_url(self, url):
"""Download image from URL and convert to tensor"""
try:
response = requests.get(url, timeout=30)
response.raise_for_status()
# Convert to PIL Image
image = Image.open(io.BytesIO(response.content))
# Convert to RGB if necessary
if image.mode != 'RGB':
image = image.convert('RGB')
# Convert to numpy array
image_array = np.array(image).astype(np.float32) / 255.0
# Convert to tensor with batch dimension
image_tensor = torch.from_numpy(image_array)[None,]
return image_tensor
except Exception as e:
raise Exception(f"Failed to download image from URL: {str(e)}")
def decode_base64_image(self, base64_string):
"""Decode base64 image string to tensor"""
try:
# Remove data URL prefix if present
if base64_string.startswith('data:image'):
base64_string = base64_string.split(',')[1]
# Decode base64
image_data = base64.b64decode(base64_string)
# Convert to PIL Image
image = Image.open(io.BytesIO(image_data))
# Convert to RGB if necessary
if image.mode != 'RGB':
image = image.convert('RGB')
# Convert to numpy array
image_array = np.array(image).astype(np.float32) / 255.0
# Convert to tensor with batch dimension
image_tensor = torch.from_numpy(image_array)[None,]
return image_tensor
except Exception as e:
raise Exception(f"Failed to decode base64 image: {str(e)}")
def get_resolution_from_aspect_ratio(self, aspect_ratio):
"""Get width and height from aspect ratio (1.5K resolution)"""
resolution_map = {
"1:1": (1536, 1536),
"4:3": (1472, 1104),
"3:2": (1584, 1056),
"16:9": (1664, 936),
"9:16": (936, 1664),
"21:9": (2016, 864)
}
return resolution_map.get(aspect_ratio, (1536, 1536))
def generate_image(self, access_key, secret_key, prompt, use_pre_llm=False,
seed=-1, guidance_scale=2.5, aspect_ratio="1:1", return_url=True):
"""
Generate image using Volcengine SeeDream V3 API
"""
try:
# Validate inputs
if not access_key or not secret_key:
raise ValueError("Access key and secret key are required")
if not prompt.strip():
raise ValueError("Prompt cannot be empty")
# Get resolution from aspect ratio
width, height = self.get_resolution_from_aspect_ratio(aspect_ratio)
# Prepare query parameters
query_params = {
'Action': 'CVProcess',
'Version': '2022-08-31',
}
formatted_query = self.format_query(query_params)
# Prepare body parameters
body_params = {
"req_key": "high_aes_general_v30l_zt2i",
"prompt": prompt,
"use_pre_llm": use_pre_llm,
"seed": seed,
"scale": guidance_scale,
"width": width,
"height": height,
"return_url": return_url
}
formatted_body = json.dumps(body_params)
# Sign the request
headers = self.sign_v4_request(access_key, secret_key, self.service,
formatted_query, formatted_body)
# Make the request
request_url = f"{self.endpoint}?{formatted_query}"
print(f"Making request to Volcengine SeeDream V3 API...")
print(f"Resolution: {aspect_ratio} ({width}x{height})")
print(f"Prompt: {prompt[:100]}...")
response = requests.post(request_url, headers=headers, data=formatted_body, timeout=60)
if response.status_code != 200:
raise Exception(f"API request failed with status {response.status_code}: {response.text}")
# Parse response
result = response.json()
if 'ResponseMetadata' in result and result['ResponseMetadata'].get('Error'):
error_info = result['ResponseMetadata']['Error']
raise Exception(f"API Error: {error_info.get('Message', 'Unknown error')}")
# Extract image data
if 'Result' not in result or 'data' not in result['Result']:
raise Exception("No image data in API response")
data = result['Result']['data']
# Handle URL or base64 response
if return_url and 'image_url' in data:
# Download image from URL
image_url = data['image_url']
print(f"Downloading image from URL: {image_url}")
image_tensor = self.download_image_from_url(image_url)
elif 'binary_data_base64' in data:
# Decode base64 image
print("Decoding base64 image data...")
base64_data = data['binary_data_base64'][0] if isinstance(data['binary_data_base64'], list) else data['binary_data_base64']
image_tensor = self.decode_base64_image(base64_data)
else:
raise Exception("No valid image data found in API response")
print("Image generated successfully!")
return (image_tensor,)
except Exception as e:
print(f"Error generating image: {str(e)}")
# Return a blank image in case of error
blank_image = torch.zeros((1, height, width, 3), dtype=torch.float32)
return (blank_image,)
+2
View File
@@ -0,0 +1,2 @@
requests>=2.28.0
Pillow>=9.0.0