Files

433 lines
15 KiB
Python

import torch
import numpy as np
import base64
import io
from PIL import Image
import os
import requests
from typing import List, Optional
import folder_paths
import logging
import yaml
from .utils import tensor2pil, pil2tensor
config_dir = os.path.join(folder_paths.base_path, "config")
if not os.path.exists(config_dir):
os.makedirs(config_dir)
logger = logging.getLogger(__name__)
class BaseNode:
def save_config(self, config, config_filename):
config_path = os.path.join(config_dir, config_filename)
with open(config_path, 'w', encoding='utf-8') as f:
yaml.dump(config, f, indent=4)
def load_config(self, config_filename):
config_path = os.path.join(config_dir, config_filename)
try:
with open(config_path, 'r', encoding='utf-8') as f:
config = yaml.load(f, Loader=yaml.FullLoader)
return config
except FileNotFoundError:
return {}
def pil_to_base64(self, pil_image):
buffer = io.BytesIO()
pil_image.save(buffer, format='PNG')
img_data = buffer.getvalue()
return base64.b64encode(img_data).decode('utf-8')
def encode_image_to_base64(self, image_path):
with open(image_path, 'rb') as f:
img_data = f.read()
return base64.b64encode(img_data).decode('utf-8')
def download_image(self, image_url, target_image_name, project_path, timeout=120):
response = requests.get(image_url, timeout=timeout)
response.raise_for_status()
output_dir = os.path.join(folder_paths.get_output_directory(), project_path)
if not os.path.exists(output_dir):
os.makedirs(output_dir)
image_path = os.path.join(output_dir, target_image_name)
with open(image_path, 'wb') as f:
f.write(response.content)
return image_path
class VolcanoOfficialNode(BaseNode):
def __init__(self):
from volcengine.visual.VisualService import VisualService
self.visual_service = VisualService()
def save_config(self, config):
super().save_config(config, 'volcano_config.yml')
def load_config(self):
return super().load_config('volcano_config.yml')
def setup_credentials(self, ak, sk):
"""设置API凭证"""
if ak and sk:
self.save_config({"ak": ak, "sk": sk})
else:
config = self.load_config()
ak = config.get("ak")
sk = config.get("sk")
if not ak or not sk:
raise Exception("volcano engine ak or sk not found")
self.visual_service.set_ak(ak)
self.visual_service.set_sk(sk)
return ak, sk
def request_volcano_api(self, access_key, secret_key, form_data):
"""通用的火山引擎API请求方法"""
# 设置凭证
self.setup_credentials(access_key, secret_key)
# 发送请求,如果失败则重试一次
max_retries = 2
last_exception = None
for attempt in range(max_retries):
try:
resp = self.visual_service.cv_process(form_data)
# 检查响应状态
if resp.get('code') == 50429:
if attempt < max_retries - 1:
logger.warning(f"volcano api error, retry... ({attempt + 1}/{max_retries})")
continue
else:
raise Exception(f"volcano api error, retry failed: {resp}")
# 解码返回的图像
img_base64 = resp['data']['binary_data_base64'][0]
img_data = base64.b64decode(img_base64)
result_image = Image.open(io.BytesIO(img_data))
# 删除图片base64,方便print
resp['data']['binary_data_base64'][0] = ""
logger.debug(f"volcano api response: {resp}")
return result_image, resp
except Exception as e:
last_exception = e
error_msg = str(e)
# 检查是否超qps
if ('"code":50429' in error_msg):
if attempt < max_retries - 1:
logger.warning(f"volcano api error, retry... ({attempt + 1}/{max_retries}): {error_msg}")
continue
raise e
raise last_exception
class ArkBaseNode(BaseNode):
def save_config(self, config):
super().save_config(config, 'ark_config.yml')
def load_config(self):
return super().load_config('ark_config.yml')
def calculate_image_size(self, level, aspect_ratio):
size_2k = {
"1:1": (2048, 2048),
"4:3": (2304, 1728),
"3:4": (1728, 2304),
"16:9": (2560, 1440),
"9:16": (1440, 2560),
"3:2": (2496, 1664),
"2:3": (1664, 2496),
"21:9": (3024, 1296)
}
if aspect_ratio not in size_2k:
raise Exception(f"不支持的宽高比: {aspect_ratio}")
width_2k, height_2k = size_2k[aspect_ratio]
if level == "1k":
width = width_2k // 2
height = height_2k // 2
elif level == "2k":
width = width_2k
height = height_2k
elif level == "4k":
width = width_2k * 2
height = height_2k * 2
else:
raise Exception(f"不支持的等级: {level}")
return f"{width}x{height}"
def setup_credentials(self, api_key, api_url=None, model=None):
config = self.load_config()
if api_key:
config = {"api_key": api_key}
if api_url:
config["api_url"] = api_url
if model:
config["model"] = model
self.save_config(config)
else:
api_key = config.get("api_key")
if not api_key:
raise Exception("ark api key not found")
api_url = config.get("api_url", "https://ark.ap-southeast.bytepluses.com/api/v3/images/generations") if api_url is None else api_url
model = config.get("model", "seedream-4-0-250828") if model is None else model
return api_key, api_url, model
def request_ark_api(self, prompt, image_paths=None, max_images=1, image_size="1440x2560",
api_key=None, api_url=None, model=None, timeout=None):
api_key, api_url, model = self.setup_credentials(api_key, api_url, model)
images = []
if image_paths and len(image_paths) > 0:
for image_path in image_paths:
if image_path.startswith("http://") or image_path.startswith("https://"):
images.append(image_path)
else:
images.append(f"data:image/png;base64,{self.encode_image_to_base64(image_path)}")
request_data = {
"model": model,
"prompt": prompt,
"image": images,
"sequential_image_generation": "auto",
"sequential_image_generation_options": {
"max_images": max_images
},
"response_format": "url",
"size": image_size,
"stream": False,
"watermark": False
}
headers = {
"Content-Type": "application/json",
"Authorization": f"Bearer {api_key}"
}
logger.info(f"ark请求数据 image_size: {image_size}, prompt: {prompt}")
response = requests.post(api_url, headers=headers, json=request_data, timeout=timeout)
response.raise_for_status()
result_data = response.json()
logger.info(f"ark请求成功,生成{len(result_data.get('data', []))}张图片")
image_paths = []
for i, image_data in enumerate(result_data.get('data', [])):
image_url = image_data.get('url')
if image_url:
image_path = self.download_image(image_url, f"ark_result_{i}.png", "ark", timeout)
image_paths.append(image_path)
return image_paths
class VolcanoOutpaintingNode(VolcanoOfficialNode):
"""火山引擎图像扩展节点"""
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"image": ("IMAGE",),
"mask": ("MASK",),
"access_key": ("STRING", {"default": ""}),
"secret_key": ("STRING", {"default": ""}),
"seed": ("INT", {"default": 42, "min": 0, "max": 999999})
},
"optional": {
"version": ("STRING", {"default": "i2i_outpainting", "tooltip": "this value is the reqKey of the volcano api."}),
}
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("result_image",)
FUNCTION = "process_outpainting"
CATEGORY = "image/volcano"
def process_outpainting(self, image, mask, access_key, secret_key, seed, version="i2i_outpainting"):
try:
# 使用节点库的转换函数
pil_image = tensor2pil(image)
pil_mask = tensor2pil(mask)
# 确保mask是灰度图像
if pil_mask.mode != 'L':
pil_mask = pil_mask.convert('L')
# 转换为base64
image_base64 = self.pil_to_base64(pil_image)
mask_base64 = self.pil_to_base64(pil_mask)
# 构建请求参数
form_data = {
"req_key": version,
"binary_data_base64": [image_base64, mask_base64],
"top": 0,
"left": 0,
"bottom": 0,
"right": 0,
"seed": seed
}
# 调用基类的通用API请求方法
result_image, _ = self.request_volcano_api(access_key, secret_key, form_data)
# 使用节点库的转换函数转换结果为tensor
result_tensor = pil2tensor(result_image)
return (result_tensor,)
except Exception as e:
logger.exception(e)
raise e
class VolcanoImageEditNode(VolcanoOfficialNode):
"""火山引擎图像编辑节点"""
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"image": ("IMAGE",),
"prompt": ("STRING", {"multiline": True, "default": ""}),
"access_key": ("STRING", {"default": ""}),
"secret_key": ("STRING", {"default": ""}),
"seed": ("INT", {"default": 42, "min": 0, "max": 0xffffffffffffffff}),
"scale": ("FLOAT", {"default": 0.5, "min": 0.1, "max": 1.0, "step": 0.01})
},
"optional": {
"version": (["byteedit_v2.0","seededit_v3.0"], {"default": "seededit_v3.0"}),
}
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("edited_image",)
FUNCTION = "process_image_edit"
CATEGORY = "image/volcano"
def process_image_edit(self, image, prompt, access_key, secret_key, seed, scale=0.5, version="seededit_v3.0"):
try:
# 使用节点库的转换函数
pil_image = tensor2pil(image)
# 转换为base64
image_base64 = self.pil_to_base64(pil_image)
binary_data = [image_base64]
# 构建请求参数
form_data = {
"req_key": version,
"binary_data_base64": binary_data,
"prompt": prompt,
"seed": seed,
"scale": scale
}
# 调用基类的通用API请求方法
result_image, _ = self.request_volcano_api(access_key, secret_key, form_data)
# 使用节点库的转换函数转换结果为tensor
result_tensor = pil2tensor(result_image)
return (result_tensor,)
except Exception as e:
logger.exception(e)
raise e
class VolcanoArkImageEditNode(ArkBaseNode):
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"image": ("IMAGE",),
"prompt": ("STRING", {"multiline": True, "default": ""}),
"level": (["1k", "2k", "4k"], {"default": "2k"}),
"aspect_ratio": (["1:1", "4:3", "3:4", "16:9", "9:16", "3:2", "2:3", "21:9"], {"default": "9:16"}),
"max_images": ("INT", {"default": 1, "min": 1, "max": 10}),
"timeout": ("INT", {"default": 120, "min": 10, "max": 600})
},
"optional": {
"image2": ("IMAGE",),
"image3": ("IMAGE",),
"api_key": ("STRING", {"default": ""}),
"api_url": ("STRING", {"default": "", "multiline": True}),
"model": ("STRING", {"default": ""})
}
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("edited_image",)
FUNCTION = "process_image_edit"
CATEGORY = "image/ark"
def process_image_edit(self, image, prompt, level, aspect_ratio, max_images, timeout,
image2=None, image3=None, api_key="", api_url="", model=""):
pil_image = tensor2pil(image)
temp_dir = os.path.join(folder_paths.get_temp_directory(), "ark")
if not os.path.exists(temp_dir):
os.makedirs(temp_dir)
temp_image_path = os.path.join(temp_dir, "input_image.png")
pil_image.save(temp_image_path)
image_paths = [temp_image_path]
if image2 is not None:
pil_image2 = tensor2pil(image2)
temp_image_path2 = os.path.join(temp_dir, "input_image2.png")
pil_image2.save(temp_image_path2)
image_paths.append(temp_image_path2)
if image3 is not None:
pil_image3 = tensor2pil(image3)
temp_image_path3 = os.path.join(temp_dir, "input_image3.png")
pil_image3.save(temp_image_path3)
image_paths.append(temp_image_path3)
image_size = self.calculate_image_size(level, aspect_ratio)
image_paths_result = self.request_ark_api(
prompt=prompt,
image_paths=image_paths,
max_images=max_images,
image_size=image_size,
api_key=api_key if api_key else None,
api_url=api_url if api_url else None,
model=model if model else None,
timeout=timeout
)
if not image_paths_result or len(image_paths_result) == 0:
raise Exception("ark api returned no images")
result_image = Image.open(image_paths_result[0])
result_tensor = pil2tensor(result_image)
return (result_tensor,)
# 节点映射
NODE_CLASS_MAPPINGS = {
"VolcanoOutpaintingNode": VolcanoOutpaintingNode,
"VolcanoImageEditNode": VolcanoImageEditNode,
"VolcanoArkImageEditNode": VolcanoArkImageEditNode
}
NODE_DISPLAY_NAME_MAPPINGS = {
"VolcanoOutpaintingNode": "volcano outpainting",
"VolcanoImageEditNode": "volcano image edit",
"VolcanoArkImageEditNode": "volcano ark image edit"
}