first commit
This commit is contained in:
+55
@@ -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/
|
||||
@@ -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
|
||||
@@ -0,0 +1,3 @@
|
||||
from .replicate_nodes import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS
|
||||
|
||||
__all__ = ['NODE_CLASS_MAPPINGS', 'NODE_DISPLAY_NAME_MAPPINGS']
|
||||
@@ -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)",
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
replicate>=0.25.0
|
||||
requests>=2.25.0
|
||||
Pillow>=8.0.0
|
||||
torch>=1.9.0
|
||||
numpy>=1.21.0
|
||||
Reference in New Issue
Block a user