增加火山引擎通用3.0文生图模型节点
This commit is contained in:
@@ -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
@@ -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']
|
||||
@@ -0,0 +1 @@
|
||||
# nodes/__init__.py
|
||||
@@ -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,)
|
||||
@@ -0,0 +1,2 @@
|
||||
requests>=2.28.0
|
||||
Pillow>=9.0.0
|
||||
Reference in New Issue
Block a user