Buckets:
| 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): | |
| """火山引擎图像扩展节点""" | |
| 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): | |
| """火山引擎图像编辑节点""" | |
| 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): | |
| 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": ""}), | |
| "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" | |
| } | |
Xet Storage Details
- Size:
- 15.7 kB
- Xet hash:
- c0abb5a3e519db18232a3c4ea7bf4b37cdea0240e1f288bbfd61b5d4d838ad9e
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.