diff --git a/__init__.py b/__init__.py index a3dbb38..3e19a12 100644 --- a/__init__.py +++ b/__init__.py @@ -6,6 +6,7 @@ from .nodes4tuzi import ( LoadVideoFromUrlVHS, LoadVideoFromUrlComfyIO, ) +from .nodes4hypr import HyprLab_Image_API_Node # Exporting the node classes for ComfyUI to discover NODE_CLASS_MAPPINGS = { @@ -31,6 +32,7 @@ NODE_CLASS_MAPPINGS = { "reg_tuzi_chat_response": RegTuziChatResponse, "load_video_from_url": LoadVideoFromUrlVHS, "load_video_from_url_comfy_core": LoadVideoFromUrlComfyIO, + "hyprlab_image_api_node": HyprLab_Image_API_Node, } NODE_DISPLAY_NAME_MAPPINGS = { @@ -56,4 +58,5 @@ NODE_DISPLAY_NAME_MAPPINGS = { "BillBum_Modified_LLM_ForceStream_Mode": "LLM StreamResponse Node (Old)", "BillBum_Modified_GPTImage1_API_Node": "Custom GPTImage1 API Node", "BillBum_Modified_Flux_API_with_imgInput": "Custom Flux API Node", + "hyprlab_image_api_node": "HyprLab ImageGen API Node", } \ No newline at end of file diff --git a/billbum_modified.py b/billbum_modified.py index faf8f7c..6a7a45b 100644 --- a/billbum_modified.py +++ b/billbum_modified.py @@ -161,7 +161,7 @@ class Input_Text: def INPUT_TYPES(s): return { "required": { - "text": ("STRING", {"multiline": True},), + "text": ("STRING", {"dynamicPrompts": True, "multiline": True},), }, } diff --git a/nodes4hypr.py b/nodes4hypr.py new file mode 100644 index 0000000..afa9a0b --- /dev/null +++ b/nodes4hypr.py @@ -0,0 +1,141 @@ +import torch +import numpy as np +from PIL import Image +import io +import base64 +import requests +import random +import tenacity +import math +from comfy.utils import common_upscale + + +## ====== Utility Functions ====== +def tensor2pil(image): + return Image.fromarray(np.clip(255. * image.cpu().numpy().squeeze(), 0, 255).astype(np.uint8)) + +def pil2tensor(image): + return torch.from_numpy(np.array(image).astype(np.float32) / 255.0).unsqueeze(0) + +def downscale_input(image): + + samples = image.movedim(-1,1) + total = int(1536 * 1024) + scale_by = math.sqrt(total / (samples.shape[3] * samples.shape[2])) + if scale_by >= 1: + return image + width = round(samples.shape[3] * scale_by) + height = round(samples.shape[2] * scale_by) + s = common_upscale(samples, width, height, "lanczos", "disabled") + s = s.movedim(1,-1) + return s + + +## ====== HyprLab API Nodes ====== +class HyprLab_Image_API_Node: + def __init__(self): + pass + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "prompt": ("STRING", {"forceInput": True, "dynamicPrompts": True, "tooltip": "The main text prompt"}), + "seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}), + "model": ("STRING", {"default": "nano-banana-pro"}), + "api_url": ("STRING", {"multiline": False, "default": "https://api.hyprlab.io/v1/images/generations"}), + "api_key": ("STRING", {"multiline": False, "default": "YOUR_API_KEY_HERE"}), + "resolution": (["1K", "2K", "4K"], {"default": "1K"}), + "aspect_ratio": ([ + "match_input_image", "1:1", "9:16", "16:9", "3:4", + "4:3", "3:2", "2:3", "5:4", "4:5", "21:9" + ], {"default": "1:1"}), + }, + "optional": { + "image_input": ("IMAGE", {"default": None, "tooltip": "Optional input images to guide generation"}), + } + } + + + RETURN_TYPES = ("IMAGE",) + RETURN_NAMES = ("IMAGE",) + FUNCTION = "generate_image" + CATEGORY = "BillBum_API/Image_API" + + + @staticmethod + def _encode_images_to_base64(images): + if images is None: + return [] + if images.dim() == 3: + images = images.unsqueeze(0) + + images = downscale_input(images) + encoded_images = [] + for idx in range(images.shape[0]): + tensor_image = images[idx].clamp(0.0, 1.0) + + pil_image = tensor2pil(tensor_image) + buffer = io.BytesIO() + pil_image.save(buffer, format="PNG") + encoded = base64.b64encode(buffer.getvalue()).decode("utf-8") + + encoded_images.append(f"data:image/png;base64,{encoded}") + return encoded_images + + @tenacity.retry(wait=tenacity.wait_exponential(multiplier=1.25, min=5, max=30), stop=tenacity.stop_after_attempt(3)) + def generate_image(self, prompt, seed, model, api_url, api_key, resolution, aspect_ratio, image_input=None): + + headers = { + "Content-Type": "application/json", + "Authorization": f"Bearer {api_key}" + } + + payload = { + "model": model, + "prompt": prompt, + "resolution": resolution, + "aspect_ratio": aspect_ratio, + "output_format": "png", + "response_format": "b64_json", + "seed": seed + } + + if image_input is not None: + encoded_imgs = self._encode_images_to_base64(image_input) + if encoded_imgs: + payload["image_input"] = encoded_imgs + + try: + response = requests.post(api_url, headers=headers, json=payload, timeout=60) + response.raise_for_status() + response_data = response.json() + + images_output = [] + + data_list = response_data.get("data", []) + if not data_list and "b64_json" in response_data: + data_list = [response_data] + + for item in data_list: + b64_str = item.get("b64_json") + if b64_str: + img_data = base64.b64decode(b64_str) + img = Image.open(io.BytesIO(img_data)) + + if img.mode != "RGBA": + img = img.convert("RGBA") + + images_output.append(pil2tensor(img)) + + if not images_output: + print(f"API Response: {response_data}") + raise ValueError("API did not return any valid images.") + + return (torch.cat(images_output, dim=0),) + + except Exception as e: + if isinstance(e, requests.exceptions.RequestException) and e.response is not None: + print(f"API Error Response: {e.response.text}") + raise ValueError(f"HyprBanana API Error: {e}") + diff --git a/pyproject.toml b/pyproject.toml index d6eee93..346df8a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "comfyui_billbum_api_nodes" description = "API call node for Third-party platforms both official and local. Support VLMs LLMs Dalle3 Flux-Pro(Support kontext, banana etc... now!! and new Support gpt-image-1!!). And some little tools: img to b64 url, b64 url to img, b64 url to b64 data, reg text to word and ',' only, etc." -version = "1.1.6" +version = "1.1.7" license = {file = "LICENSE"} dependencies = ["tenacity", "openai", "pillow", "requests", "torch", "numpy", "tiktoken"]