From a382e5759e06d11d57855da5bdcde6d40458fcd5 Mon Sep 17 00:00:00 2001 From: charlyad142 Date: Tue, 21 Oct 2025 15:56:23 -0400 Subject: [PATCH] first commit --- .gitignore | 55 +++++ README.md | 152 ++++++++++++ __init__.py | 3 + replicate_nodes.py | 597 +++++++++++++++++++++++++++++++++++++++++++++ requirements.txt | 5 + 5 files changed, 812 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 __init__.py create mode 100644 replicate_nodes.py create mode 100644 requirements.txt diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..f396140 --- /dev/null +++ b/.gitignore @@ -0,0 +1,55 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# Entornos virtuales +venv/ +env/ +ENV/ + +# IDEs y editores +.idea/ +.vscode/ +*.swp +*.swo +.DS_Store + +# Archivos de configuración local +.env +.env.local +config.json +config.ini + +# Archivos de caché y logs +*.log +.cache/ +.pytest_cache/ + +# Archivos temporales +*.tmp +*.temp +.coverage +htmlcov/ + +# Archivos específicos de ComfyUI +models/ +outputs/ +temp/ \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..805a5fd --- /dev/null +++ b/README.md @@ -0,0 +1,152 @@ +# ComfyUI Replicate Image Generation & Edit + +Este nodo de ComfyUI permite usar varios modelos de Replicate para generar y editar imágenes: + +- **Qwen Image Edit Plus (Multi-Image)**: Usa `qwen/qwen-image-edit-plus` para editar múltiples imágenes basándose en poses de referencia +- **Qwen Image Edit (Single Image)**: Usa `qwen/qwen-image-edit` para editar una sola imagen con instrucciones de texto +- **SeedEdit 3.0 (ByteDance)**: Usa `bytedance/seededit-3.0` para edición avanzada de imágenes con control de adherencia al prompt +- **Seedream 4.0 (ByteDance)**: Usa `bytedance/seedream-4` para generación de imágenes de alta calidad con múltiples resoluciones + +## Características + +### Qwen Image Edit Plus (Multi-Image) +- **Edición de múltiples imágenes**: Acepta 1-5 imágenes, la primera como referencia de pose +- **Edición basada en poses**: Las demás imágenes adoptan la pose de la primera imagen +- **Flexibilidad**: Puedes usar entre 1 y 5 imágenes según tus necesidades + +### Qwen Image Edit (Single Image) +- **Edición de imagen única**: Acepta una sola imagen para editar +- **Instrucciones de texto**: Usa prompts de texto para describir cómo editar la imagen +- **Simplicidad**: Ideal para ediciones directas con instrucciones claras + +### SeedEdit 3.0 (ByteDance) +- **Edición avanzada**: Modelo especializado de ByteDance para edición de imágenes +- **Control de adherencia**: Parámetro `guidance_scale` para controlar qué tan literal es el resultado +- **Alta calidad**: Optimizado para ediciones precisas y detalladas + +### Seedream 4.0 (ByteDance) +- **Generación de imágenes**: Modelo de ByteDance para generar imágenes desde texto +- **Múltiples resoluciones**: Soporte para 1K, 2K, 4K y dimensiones personalizadas +- **Generación secuencial**: Opción para generar múltiples imágenes relacionadas +- **Image-to-image**: Soporte para generación basada en imágenes de entrada + +### Características generales +- **Integración con Replicate API**: Utiliza la API oficial de Replicate +- **Configuración flexible**: Soporte para múltiples parámetros de configuración +- **Manejo de errores robusto**: Gestión completa de errores y timeouts + +## Instalación + +1. Coloca este nodo en la carpeta `custom_nodes` de ComfyUI +2. Instala las dependencias necesarias: + ```bash + pip install replicate requests pillow + ``` + +## Configuración + +### Opción 1: Variable de entorno (Recomendado) +```bash +export REPLICATE_API_TOKEN="tu_token_aqui" +``` + +### Opción 2: Archivo de configuración +Edita el archivo `config.ini` y reemplaza `tu_token_aqui` con tu token de API de Replicate. + +### Opción 3: Token en el nodo +Puedes ingresar el token directamente en el campo `api_token` del nodo. + +## Uso + +### Qwen Image Edit Plus (Multi-Image) +1. **images**: Una o más imágenes (mínimo 1, máximo 5). La primera imagen se usa como referencia de pose, las demás se editan. +2. **prompt**: Descripción de cómo quieres que se edite la imagen (ej: "The person in the second image adopts the pose from the first image") +3. **api_token**: Token de API de Replicate (opcional si está configurado en variables de entorno) + +### Qwen Image Edit (Single Image) +1. **image**: Una sola imagen para editar +2. **prompt**: Instrucciones de texto sobre cómo editar la imagen (ej: "Make the person wear a red shirt", "Change the background to a beach") +3. **api_token**: Token de API de Replicate (opcional si está configurado en variables de entorno) + +### SeedEdit 3.0 (ByteDance) +1. **image**: Una sola imagen para editar +2. **prompt**: Instrucciones de texto sobre cómo editar la imagen +3. **api_token**: Token de API de Replicate (opcional si está configurado en variables de entorno) + +### Seedream 4.0 (ByteDance) +1. **prompt**: Descripción de la imagen que quieres generar +2. **api_token**: Token de API de Replicate (opcional si está configurado en variables de entorno) +3. **image_input** (opcional): Imagen de referencia para generación image-to-image + +### Parámetros opcionales + +#### Para Qwen Image Edit Plus y Qwen Image Edit: +- **seed**: Semilla aleatoria para generación reproducible (-1 para aleatorio) +- **go_fast**: Ejecutar predicciones más rápidas con optimizaciones adicionales (por defecto: True) +- **aspect_ratio**: Proporción de aspecto para la imagen generada (por defecto: "match_input_image") +- **output_format**: Formato de salida (por defecto: "webp") +- **output_quality**: Calidad de salida de 0 a 100 (por defecto: 95) +- **disable_safety_checker**: Deshabilitar el verificador de seguridad (por defecto: False) + +#### Para SeedEdit 3.0: +- **seed**: Semilla aleatoria para generación reproducible (-1 para aleatorio) +- **guidance_scale**: Control de adherencia al prompt (1.0-10.0, por defecto: 5.5) + - Valores más altos = más literal al prompt + - Valores más bajos = más creativo/artístico + +#### Para Seedream 4.0: +- **size**: Resolución de la imagen (1K, 2K, 4K, custom) +- **width**: Ancho personalizado (1024-4096, solo cuando size='custom') +- **height**: Alto personalizado (1024-4096, solo cuando size='custom') +- **max_images**: Número máximo de imágenes a generar (1-15) +- **aspect_ratio**: Proporción de aspecto de la imagen +- **sequential_image_generation**: Modo de generación (disabled, auto) + +## Salidas + +- **image**: Imagen editada como tensor de ComfyUI +- **output_url**: URL de la imagen generada en Replicate + +## Ejemplos de uso + +### Qwen Image Edit Plus (Multi-Image) +1. Carga una o más imágenes en el campo `images` (mínimo 1, máximo 5) +2. La primera imagen se usa como referencia de pose +3. Las demás imágenes se editarán para adoptar la pose de la primera +4. Escribe un prompt descriptivo como "The person in the second image adopts the pose from the first image" +5. Configura tu token de API +6. Ejecuta el nodo + +### Qwen Image Edit (Single Image) +1. Carga una imagen en el campo `image` +2. Escribe instrucciones claras como "Make the person wear a red shirt" o "Change the background to a beach" +3. Configura tu token de API +4. Ejecuta el nodo + +### SeedEdit 3.0 (ByteDance) +1. Carga una imagen en el campo `image` +2. Escribe instrucciones detalladas sobre cómo editar la imagen +3. Ajusta el `guidance_scale` según necesites más o menos adherencia al prompt +4. Configura tu token de API +5. Ejecuta el nodo + +### Seedream 4.0 (ByteDance) +1. Escribe un prompt descriptivo de la imagen que quieres generar +2. Selecciona la resolución (1K, 2K, 4K o custom) +3. Si usas 'custom', configura width y height +4. Opcionalmente conecta una imagen de referencia en `image_input` +5. Configura tu token de API +6. Ejecuta el nodo + +## Obtener token de API + +1. Ve a [https://replicate.com/account/api-tokens](https://replicate.com/account/api-tokens) +2. Crea una cuenta o inicia sesión +3. Genera un nuevo token de API +4. Copia el token y configúralo según una de las opciones anteriores + +## Notas + +- El nodo convierte las imágenes de entrada a formato base64 para enviarlas a la API +- La imagen de salida se descarga automáticamente y se convierte al formato de tensor de ComfyUI +- El nodo incluye manejo de errores completo y mensajes informativos diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..19c7ed4 --- /dev/null +++ b/__init__.py @@ -0,0 +1,3 @@ +from .replicate_nodes import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS + +__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS'] \ No newline at end of file diff --git a/replicate_nodes.py b/replicate_nodes.py new file mode 100644 index 0000000..c0c24b3 --- /dev/null +++ b/replicate_nodes.py @@ -0,0 +1,597 @@ +import requests +import torch +import replicate +import os +import numpy as np +from PIL import Image +import io +import base64 + +def create_error_image(): + """Crear una imagen de error roja""" + blank = Image.new('RGB', (512, 512), color='red') + blank.putpixel((256, 256), (255, 0, 0)) + tensor = torch.from_numpy(np.array(blank).astype(np.float32) / 255.0) + return tensor[None,] + +class ReplicateQwenImageEdit: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "images": ("IMAGE",), + "prompt": ("STRING", {"multiline": True, "default": ""}), + "api_token": ("STRING", {"default": ""}), + }, + "optional": { + "seed": ("INT", {"default": -1}), + "go_fast": ("BOOLEAN", {"default": True}), + "aspect_ratio": (["match_input_image", "1:1", "16:9", "9:16", "4:3", "3:4"], {"default": "match_input_image"}), + "output_format": (["webp", "jpg", "png"], {"default": "webp"}), + "output_quality": ("INT", {"default": 95, "min": 0, "max": 100}), + "disable_safety_checker": ("BOOLEAN", {"default": False}), + } + } + + RETURN_TYPES = ("IMAGE", "STRING") + RETURN_NAMES = ("image", "output_url") + FUNCTION = "generate" + CATEGORY = "Replicate/Qwen" + + def generate(self, **kwargs): + try: + # Obtener el token de API + api_token = self.get_api_token(kwargs['api_token']) + + # Configurar el cliente de Replicate + client = replicate.Client(api_token=api_token) + + # Procesar las imágenes (puede ser 1-5 imágenes) + images_tensor = kwargs['images'] + + # Verificar que tenemos al menos 1 imagen + if images_tensor is None or len(images_tensor) == 0: + raise Exception("Se requiere al menos 1 imagen") + + # Verificar que no excedemos el máximo de 5 imágenes + if len(images_tensor) > 5: + raise Exception("Máximo 5 imágenes permitidas") + + # Convertir todas las imágenes a URLs + image_urls = [] + for i in range(len(images_tensor)): + image_url = self.image_to_url(images_tensor[i]) + image_urls.append(image_url) + + # Preparar los parámetros de entrada + input_params = { + "image": image_urls, + "prompt": kwargs['prompt'], + "go_fast": kwargs.get('go_fast', True), + "aspect_ratio": kwargs.get('aspect_ratio', "match_input_image"), + "output_format": kwargs.get('output_format', "webp"), + "output_quality": kwargs.get('output_quality', 95), + "disable_safety_checker": kwargs.get('disable_safety_checker', False), + } + + # Agregar seed solo si no es -1 + if kwargs.get('seed', -1) != -1: + input_params["seed"] = kwargs['seed'] + + # Ejecutar el modelo + output = client.run( + "qwen/qwen-image-edit-plus", + input=input_params + ) + + # Procesar la salida + if output and len(output) > 0: + # Obtener la URL de la imagen generada + # La salida de Replicate ya es una URL de cadena directamente + output_url = str(output[0]) + + # Descargar y convertir la imagen + image_tensor = self.download_and_convert_image(output_url) + + return (image_tensor, output_url) + else: + raise Exception("No se recibió ninguna imagen de la API") + + except Exception as e: + print(f"Error en Replicate Qwen Image Edit: {str(e)}") + return (create_error_image(), f"Error: {str(e)}") + + def get_api_token(self, node_token): + """Obtener el token de API desde el nodo o variables de entorno""" + if node_token and node_token.strip(): + return node_token.strip() + + # Intentar obtener desde variable de entorno + env_token = os.getenv('REPLICATE_API_TOKEN') + if env_token: + return env_token + + raise ValueError("No se encontró un token de API válido. Configure REPLICATE_API_TOKEN en las variables de entorno o en el nodo.") + + def image_to_url(self, image_tensor): + """Convertir tensor de imagen a URL temporal o base64""" + try: + # Convertir tensor a PIL Image + if hasattr(image_tensor, 'numpy'): + image_array = (image_tensor.numpy().squeeze() * 255).astype(np.uint8) + else: + # Si ya es un array de numpy + image_array = (image_tensor.squeeze() * 255).astype(np.uint8) + + if len(image_array.shape) == 3: + image = Image.fromarray(image_array) + else: + image = Image.fromarray(image_array, mode='L') + + # Convertir a base64 data URL + buffered = io.BytesIO() + image.save(buffered, format='PNG') + img_base64 = base64.b64encode(buffered.getvalue()).decode('utf-8') + + return f"data:image/png;base64,{img_base64}" + + except Exception as e: + print(f"Error convirtiendo imagen: {str(e)}") + print(f"Tipo de tensor: {type(image_tensor)}") + print(f"Forma del tensor: {image_tensor.shape if hasattr(image_tensor, 'shape') else 'No shape'}") + raise Exception(f"Error procesando imagen: {str(e)}") + + def download_and_convert_image(self, image_url): + """Descargar imagen desde URL y convertir a tensor""" + try: + # Descargar la imagen + response = requests.get(image_url, timeout=30) + response.raise_for_status() + + # Convertir a PIL Image + image = Image.open(io.BytesIO(response.content)) + + # Convertir a RGB si es necesario + if image.mode != 'RGB': + image = image.convert('RGB') + + # Convertir a tensor de ComfyUI + img_array = np.array(image).astype(np.float32) / 255.0 + return torch.from_numpy(img_array)[None,] + + except Exception as e: + print(f"Error descargando imagen: {str(e)}") + raise Exception(f"Error descargando imagen: {str(e)}") + +class ReplicateQwenImageEditSingle: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "image": ("IMAGE",), + "prompt": ("STRING", {"multiline": True, "default": ""}), + "api_token": ("STRING", {"default": ""}), + }, + "optional": { + "seed": ("INT", {"default": -1}), + "go_fast": ("BOOLEAN", {"default": True}), + "aspect_ratio": (["match_input_image", "1:1", "16:9", "9:16", "4:3", "3:4"], {"default": "match_input_image"}), + "output_format": (["webp", "jpg", "png"], {"default": "webp"}), + "output_quality": ("INT", {"default": 95, "min": 0, "max": 100}), + "disable_safety_checker": ("BOOLEAN", {"default": False}), + } + } + + RETURN_TYPES = ("IMAGE", "STRING") + RETURN_NAMES = ("image", "output_url") + FUNCTION = "generate" + CATEGORY = "Replicate/Qwen" + + def generate(self, **kwargs): + try: + # Obtener el token de API + api_token = self.get_api_token(kwargs['api_token']) + + # Configurar el cliente de Replicate + client = replicate.Client(api_token=api_token) + + # Procesar la imagen (solo una imagen) + image_tensor = kwargs['image'] + + # Verificar que tenemos una imagen + if image_tensor is None or len(image_tensor) == 0: + raise Exception("Se requiere al menos 1 imagen") + + # Convertir la imagen a URL + image_url = self.image_to_url(image_tensor[0]) # Tomar la primera imagen + + # Preparar los parámetros de entrada + input_params = { + "image": image_url, + "prompt": kwargs['prompt'], + "go_fast": kwargs.get('go_fast', True), + "aspect_ratio": kwargs.get('aspect_ratio', "match_input_image"), + "output_format": kwargs.get('output_format', "webp"), + "output_quality": kwargs.get('output_quality', 95), + "disable_safety_checker": kwargs.get('disable_safety_checker', False), + } + + # Agregar seed solo si no es -1 + if kwargs.get('seed', -1) != -1: + input_params["seed"] = kwargs['seed'] + + # Ejecutar el modelo + output = client.run( + "qwen/qwen-image-edit", + input=input_params + ) + + # Procesar la salida + if output and len(output) > 0: + # Obtener la URL de la imagen generada + output_url = str(output[0]) + + # Descargar y convertir la imagen + image_tensor = self.download_and_convert_image(output_url) + + return (image_tensor, output_url) + else: + raise Exception("No se recibió ninguna imagen de la API") + + except Exception as e: + print(f"Error en Replicate Qwen Image Edit Single: {str(e)}") + return (create_error_image(), f"Error: {str(e)}") + + def get_api_token(self, node_token): + """Obtener el token de API desde el nodo o variables de entorno""" + if node_token and node_token.strip(): + return node_token.strip() + + # Intentar obtener desde variable de entorno + env_token = os.getenv('REPLICATE_API_TOKEN') + if env_token: + return env_token + + raise ValueError("No se encontró un token de API válido. Configure REPLICATE_API_TOKEN en las variables de entorno o en el nodo.") + + def image_to_url(self, image_tensor): + """Convertir tensor de imagen a URL temporal o base64""" + try: + # Convertir tensor a PIL Image + if hasattr(image_tensor, 'numpy'): + image_array = (image_tensor.numpy().squeeze() * 255).astype(np.uint8) + else: + # Si ya es un array de numpy + image_array = (image_tensor.squeeze() * 255).astype(np.uint8) + + if len(image_array.shape) == 3: + image = Image.fromarray(image_array) + else: + image = Image.fromarray(image_array, mode='L') + + # Convertir a base64 data URL + buffered = io.BytesIO() + image.save(buffered, format='PNG') + img_base64 = base64.b64encode(buffered.getvalue()).decode('utf-8') + + return f"data:image/png;base64,{img_base64}" + + except Exception as e: + print(f"Error convirtiendo imagen: {str(e)}") + print(f"Tipo de tensor: {type(image_tensor)}") + print(f"Forma del tensor: {image_tensor.shape if hasattr(image_tensor, 'shape') else 'No shape'}") + raise Exception(f"Error procesando imagen: {str(e)}") + + def download_and_convert_image(self, image_url): + """Descargar imagen desde URL y convertir a tensor""" + try: + # Descargar la imagen + response = requests.get(image_url, timeout=30) + response.raise_for_status() + + # Convertir a PIL Image + image = Image.open(io.BytesIO(response.content)) + + # Convertir a RGB si es necesario + if image.mode != 'RGB': + image = image.convert('RGB') + + # Convertir a tensor de ComfyUI + img_array = np.array(image).astype(np.float32) / 255.0 + return torch.from_numpy(img_array)[None,] + + except Exception as e: + print(f"Error descargando imagen: {str(e)}") + raise Exception(f"Error descargando imagen: {str(e)}") + +class ReplicateSeedEdit: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "image": ("IMAGE",), + "prompt": ("STRING", {"multiline": True, "default": ""}), + "api_token": ("STRING", {"default": ""}), + }, + "optional": { + "seed": ("INT", {"default": -1}), + "guidance_scale": ("FLOAT", {"default": 5.5, "min": 1.0, "max": 10.0}), + } + } + + RETURN_TYPES = ("IMAGE", "STRING") + RETURN_NAMES = ("image", "output_url") + FUNCTION = "generate" + CATEGORY = "Replicate/SeedEdit" + + def generate(self, **kwargs): + try: + # Obtener el token de API + api_token = self.get_api_token(kwargs['api_token']) + + # Configurar el cliente de Replicate + client = replicate.Client(api_token=api_token) + + # Procesar la imagen (solo una imagen) + image_tensor = kwargs['image'] + + # Verificar que tenemos una imagen + if image_tensor is None or len(image_tensor) == 0: + raise Exception("Se requiere al menos 1 imagen") + + # Convertir la imagen a URL + image_url = self.image_to_url(image_tensor[0]) # Tomar la primera imagen + + # Preparar los parámetros de entrada + input_params = { + "image": image_url, + "prompt": kwargs['prompt'], + "guidance_scale": kwargs.get('guidance_scale', 5.5), + } + + # Agregar seed solo si no es -1 + if kwargs.get('seed', -1) != -1: + input_params["seed"] = kwargs['seed'] + + # Ejecutar el modelo + output = client.run( + "bytedance/seededit-3.0", + input=input_params + ) + + # Procesar la salida + if output: + # Obtener la URL de la imagen generada + # Para SeedEdit, la salida es directamente una URL de cadena + output_url = str(output) + + # Descargar y convertir la imagen + image_tensor = self.download_and_convert_image(output_url) + + return (image_tensor, output_url) + else: + raise Exception("No se recibió ninguna imagen de la API") + + except Exception as e: + print(f"Error en Replicate SeedEdit: {str(e)}") + return (create_error_image(), f"Error: {str(e)}") + + def get_api_token(self, node_token): + """Obtener el token de API desde el nodo o variables de entorno""" + if node_token and node_token.strip(): + return node_token.strip() + + # Intentar obtener desde variable de entorno + env_token = os.getenv('REPLICATE_API_TOKEN') + if env_token: + return env_token + + raise ValueError("No se encontró un token de API válido. Configure REPLICATE_API_TOKEN en las variables de entorno o en el nodo.") + + def image_to_url(self, image_tensor): + """Convertir tensor de imagen a URL temporal o base64""" + try: + # Convertir tensor a PIL Image + if hasattr(image_tensor, 'numpy'): + image_array = (image_tensor.numpy().squeeze() * 255).astype(np.uint8) + else: + # Si ya es un array de numpy + image_array = (image_tensor.squeeze() * 255).astype(np.uint8) + + if len(image_array.shape) == 3: + image = Image.fromarray(image_array) + else: + image = Image.fromarray(image_array, mode='L') + + # Convertir a base64 data URL + buffered = io.BytesIO() + image.save(buffered, format='PNG') + img_base64 = base64.b64encode(buffered.getvalue()).decode('utf-8') + + return f"data:image/png;base64,{img_base64}" + + except Exception as e: + print(f"Error convirtiendo imagen: {str(e)}") + print(f"Tipo de tensor: {type(image_tensor)}") + print(f"Forma del tensor: {image_tensor.shape if hasattr(image_tensor, 'shape') else 'No shape'}") + raise Exception(f"Error procesando imagen: {str(e)}") + + def download_and_convert_image(self, image_url): + """Descargar imagen desde URL y convertir a tensor""" + try: + # Descargar la imagen + response = requests.get(image_url, timeout=30) + response.raise_for_status() + + # Convertir a PIL Image + image = Image.open(io.BytesIO(response.content)) + + # Convertir a RGB si es necesario + if image.mode != 'RGB': + image = image.convert('RGB') + + # Convertir a tensor de ComfyUI + img_array = np.array(image).astype(np.float32) / 255.0 + return torch.from_numpy(img_array)[None,] + + except Exception as e: + print(f"Error descargando imagen: {str(e)}") + raise Exception(f"Error descargando imagen: {str(e)}") + +class ReplicateSeedream4: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "prompt": ("STRING", {"multiline": True, "default": ""}), + "api_token": ("STRING", {"default": ""}), + }, + "optional": { + "image_input": ("IMAGE",), + "size": (["1K", "2K", "4K", "custom"], {"default": "2K"}), + "width": ("INT", {"default": 2048, "min": 1024, "max": 4096}), + "height": ("INT", {"default": 2048, "min": 1024, "max": 4096}), + "max_images": ("INT", {"default": 1, "min": 1, "max": 15}), + "aspect_ratio": (["match_input_image", "1:1", "16:9", "9:16", "4:3", "3:4"], {"default": "match_input_image"}), + "sequential_image_generation": (["disabled", "auto"], {"default": "disabled"}), + } + } + + RETURN_TYPES = ("IMAGE", "STRING") + RETURN_NAMES = ("image", "output_url") + FUNCTION = "generate" + CATEGORY = "Replicate/Seedream" + + def generate(self, **kwargs): + try: + # Obtener el token de API + api_token = self.get_api_token(kwargs['api_token']) + + # Configurar el cliente de Replicate + client = replicate.Client(api_token=api_token) + + # Preparar los parámetros de entrada + input_params = { + "prompt": kwargs['prompt'], + "size": kwargs.get('size', "2K"), + "max_images": kwargs.get('max_images', 1), + "aspect_ratio": kwargs.get('aspect_ratio', "match_input_image"), + "sequential_image_generation": kwargs.get('sequential_image_generation', "disabled"), + } + + # Agregar dimensiones personalizadas solo si size es 'custom' + if kwargs.get('size') == 'custom': + input_params['width'] = kwargs.get('width', 2048) + input_params['height'] = kwargs.get('height', 2048) + + # Procesar imagen de entrada si se proporciona + if 'image_input' in kwargs and kwargs['image_input'] is not None: + image_tensor = kwargs['image_input'] + + # Verificar que tenemos una imagen + if image_tensor is not None and len(image_tensor) > 0: + # Convertir todas las imágenes a URLs + image_urls = [] + for i in range(len(image_tensor)): + image_url = self.image_to_url(image_tensor[i]) + image_urls.append(image_url) + + input_params['image_input'] = image_urls + + # Ejecutar el modelo + output = client.run( + "bytedance/seedream-4", + input=input_params + ) + + # Procesar la salida + if output and len(output) > 0: + # Obtener la URL de la imagen generada (tomar la primera si hay múltiples) + output_url = str(output[0]) + + # Descargar y convertir la imagen + image_tensor = self.download_and_convert_image(output_url) + + return (image_tensor, output_url) + else: + raise Exception("No se recibió ninguna imagen de la API") + + except Exception as e: + print(f"Error en Replicate Seedream4: {str(e)}") + return (create_error_image(), f"Error: {str(e)}") + + def get_api_token(self, node_token): + """Obtener el token de API desde el nodo o variables de entorno""" + if node_token and node_token.strip(): + return node_token.strip() + + # Intentar obtener desde variable de entorno + env_token = os.getenv('REPLICATE_API_TOKEN') + if env_token: + return env_token + + raise ValueError("No se encontró un token de API válido. Configure REPLICATE_API_TOKEN en las variables de entorno o en el nodo.") + + def image_to_url(self, image_tensor): + """Convertir tensor de imagen a URL temporal o base64""" + try: + # Convertir tensor a PIL Image + if hasattr(image_tensor, 'numpy'): + image_array = (image_tensor.numpy().squeeze() * 255).astype(np.uint8) + else: + # Si ya es un array de numpy + image_array = (image_tensor.squeeze() * 255).astype(np.uint8) + + if len(image_array.shape) == 3: + image = Image.fromarray(image_array) + else: + image = Image.fromarray(image_array, mode='L') + + # Convertir a base64 data URL + buffered = io.BytesIO() + image.save(buffered, format='PNG') + img_base64 = base64.b64encode(buffered.getvalue()).decode('utf-8') + + return f"data:image/png;base64,{img_base64}" + + except Exception as e: + print(f"Error convirtiendo imagen: {str(e)}") + print(f"Tipo de tensor: {type(image_tensor)}") + print(f"Forma del tensor: {image_tensor.shape if hasattr(image_tensor, 'shape') else 'No shape'}") + raise Exception(f"Error procesando imagen: {str(e)}") + + def download_and_convert_image(self, image_url): + """Descargar imagen desde URL y convertir a tensor""" + try: + # Descargar la imagen + response = requests.get(image_url, timeout=30) + response.raise_for_status() + + # Convertir a PIL Image + image = Image.open(io.BytesIO(response.content)) + + # Convertir a RGB si es necesario + if image.mode != 'RGB': + image = image.convert('RGB') + + # Convertir a tensor de ComfyUI + img_array = np.array(image).astype(np.float32) / 255.0 + return torch.from_numpy(img_array)[None,] + + except Exception as e: + print(f"Error descargando imagen: {str(e)}") + raise Exception(f"Error descargando imagen: {str(e)}") + +NODE_CLASS_MAPPINGS = { + "Replicate Qwen Image Edit Plus": ReplicateQwenImageEdit, + "Replicate Qwen Image Edit": ReplicateQwenImageEditSingle, + "Replicate SeedEdit": ReplicateSeedEdit, + "Replicate Seedream4": ReplicateSeedream4, +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "Replicate Qwen Image Edit Plus": "Qwen Image Edit Plus (Multi-Image)", + "Replicate Qwen Image Edit": "Qwen Image Edit (Single Image)", + "Replicate SeedEdit": "SeedEdit 3.0 (ByteDance)", + "Replicate Seedream4": "Seedream 4.0 (ByteDance)", +} \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..679ac49 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +replicate>=0.25.0 +requests>=2.25.0 +Pillow>=8.0.0 +torch>=1.9.0 +numpy>=1.21.0