增加火山引擎通用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