From 43ce0f892d3523447fac12f718cd93df20172e8b Mon Sep 17 00:00:00 2001 From: charlyad142 Date: Fri, 18 Jul 2025 15:21:44 -0400 Subject: [PATCH] =?UTF-8?q?Actualizaci=C3=B3n=2020250718?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 160 +++++++++++++++++++++++++++++++++++++++++++--- bfl_nodes.py | 174 +++++++++++++++++++++++++++++++++++++++++++++++++++ bfl_utils.py | 123 ++++++++++++++++++++++++++++++++---- 3 files changed, 435 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index 943a1de..72476f2 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,34 @@ # ComfyUI BFL API Pro Nodes -Este nodo personalizado para ComfyUI proporciona integración con la API de BFL (Black Forest Labs) para mejorar y optimizar el procesamiento de imágenes. Permite utilizar Flux Pro, directamente dentro de ComfyUI, ofreciendo capacidades avanzadas de procesamiento de imágenes. +Este nodo personalizado para ComfyUI proporciona integración completa con la API de BFL (Black Forest Labs) para mejorar y optimizar el procesamiento de imágenes. Permite utilizar todos los modelos de Flux Pro, Flux Kontext y Flux Ultra directamente dentro de ComfyUI, ofreciendo capacidades avanzadas de procesamiento de imágenes. ## Características -- Integración con la API de BFL -- Procesamiento de imágenes optimizado -- Compatible con el flujo de trabajo de ComfyUI +- ✅ **Integración completa con la API de BFL** +- ✅ **Múltiples modelos soportados**: Flux Pro, Flux Kontext, Flux Ultra +- ✅ **ControlNet avanzado**: Canny, Depth +- ✅ **Inpainting y expansión de imágenes** +- ✅ **Sistema flexible de API keys** con prioridad automática +- ✅ **Procesamiento de imágenes optimizado** +- ✅ **Compatible con el flujo de trabajo de ComfyUI** +- ✅ **Manejo robusto de errores** + +## Nodos Disponibles + +### 🎨 Generación de Imágenes +- **BFL Image Generator (Pro 1.1/1.0)**: Generación básica de imágenes con soporte para versiones 1.0 y 1.1 +- **BFL Flux Ultra (Pro 1.1)**: Modelo Ultra para máxima calidad + +### 🎭 ControlNet +- **BFL Canny Control (Pro 1.0)**: Control de bordes con detección Canny +- **BFL Depth Control (Pro 1.0)**: Control de profundidad + +### 🔧 Edición y Manipulación +- **BFL Inpainting (Pro 1.0 Fill)**: Rellenado inteligente de áreas +- **BFL Image Expander (Pro 1.0)**: Expansión de imágenes con IA + +### 🌟 Modelos Especializados +- **BFL Flux Kontext (Pro/Max)**: Modelos Kontext Pro y Max para casos especiales ## Instalación @@ -17,10 +39,9 @@ git clone https://github.com/charlyad142/ComfyUI_bfl_api_pro_nodes.git ``` 3. Reinicia ComfyUI -## Configuración - -Para utilizar este nodo, necesitas configurar tu API key de BFL. Sigue estos pasos: +## Configuración de API Key +### Opción 1: Archivo config.ini (Recomendado) 1. Crea un archivo `config.ini` en la raíz del nodo 2. Agrega la siguiente configuración: @@ -29,15 +50,136 @@ Para utilizar este nodo, necesitas configurar tu API key de BFL. Sigue estos pas X_KEY = tu_api_key_aquí ``` +### Opción 2: Campo x_key en cada nodo +- Cada nodo tiene un campo `x_key` donde puedes ingresar tu API key directamente +- Útil para usar diferentes keys en diferentes nodos + +### Sistema de Prioridad de API Keys +El sistema busca la API key en el siguiente orden: +1. **Archivo config.ini** (prioridad más alta) +2. **Campo x_key del nodo** (si config.ini no existe o no tiene key) +3. **Error** (si no hay key en ningún lado) + ### Obtención de API Key 1. Regístrate en [BFL Platform](https://auth.bfl.ai/) 2. Ve a tu panel de control 3. En la sección de API Keys, genera una nueva key -4. Copia la key y pégala en tu archivo `config.ini` +4. Copia la key y pégala en tu archivo `config.ini` o en el campo `x_key` del nodo ## Uso +### Básico 1. En ComfyUI, busca los nodos "BFL" en el menú de nodos 2. Arrastra el nodo deseado a tu flujo de trabajo -3. Conecta los nodos según tus necesidades +3. Configura tu API key (en config.ini o en el campo x_key) +4. Conecta los nodos según tus necesidades + +### Ejemplos de Uso + +#### Generación de Imágenes +``` +BFL Image Generator → Prompt → Output +``` + +#### ControlNet con Canny +``` +Input Image → BFL Canny Control → Output +``` + +#### Inpainting +``` +Image + Mask → BFL Inpainting → Output +``` + +#### Expansión de Imágenes +``` +Image → BFL Image Expander → Output +``` + +## Parámetros Comunes + +### Campos Requeridos +- **prompt**: Descripción de la imagen a generar +- **x_key**: Tu API key de BFL +- **output_format**: Formato de salida (jpeg/png) +- **safety_tolerance**: Tolerancia de seguridad (0-6) + +### Campos Opcionales +- **seed**: Semilla para reproducibilidad (-1 para aleatorio) +- **steps**: Número de pasos de generación (15-50) +- **guidance**: Guía de generación (1.0-100.0) + +## Modelos Específicos + +### Flux Pro 1.1/1.0 +- **Versión 1.1**: Sin parámetros de steps/guidance +- **Versión 1.0**: Con parámetros de steps/guidance + +### Flux Ultra +- **Aspect Ratio**: Control de relación de aspecto +- **Raw Mode**: Modo raw para mayor control +- **Image Prompt**: Soporte para imágenes de referencia + +### Flux Kontext +- **Pro/Max**: Dos modelos especializados +- **Aspect Ratio**: Control de relación de aspecto + +### ControlNet +- **Canny**: Detección de bordes con umbrales configurables +- **Depth**: Control de profundidad + +## Solución de Problemas + +### Error de API Key +``` +"No se encontró una API key válida. Por favor, configure una API key en el nodo o en el archivo config.ini" +``` +**Solución**: Verifica que tu API key esté configurada correctamente en config.ini o en el campo x_key del nodo. + +### Error de Conexión +``` +"API Error 401: Unauthorized" +``` +**Solución**: Verifica que tu API key sea válida y tenga créditos disponibles. + +### Error de Formato +``` +"Invalid API response format" +``` +**Solución**: Verifica que todos los parámetros requeridos estén configurados correctamente. + +## Contribuir + +Las contribuciones son bienvenidas. Por favor: + +1. Fork el repositorio +2. Crea una rama para tu feature +3. Commit tus cambios +4. Push a la rama +5. Abre un Pull Request + +## Licencia + +Este proyecto está bajo la Licencia MIT. Ver el archivo `LICENSE` para más detalles. + +## Soporte + +Para soporte técnico o preguntas: +- Abre un issue en GitHub +- Consulta la documentación de BFL: [BFL Platform](https://auth.bfl.ai/) + +## Changelog + +### v2.0.0 +- ✅ Agregado soporte para Flux Ultra +- ✅ Agregado nodo de Depth Control +- ✅ Agregado nodo de Flux Kontext +- ✅ Sistema flexible de API keys con prioridad automática +- ✅ Mejorado el manejo de errores +- ✅ Eliminados campos de webhook innecesarios + +### v1.0.0 +- ✅ Nodos básicos de generación, inpainting y control +- ✅ Soporte para config.ini +- ✅ Integración con ComfyUI diff --git a/bfl_nodes.py b/bfl_nodes.py index fe6a55e..cd68b32 100644 --- a/bfl_nodes.py +++ b/bfl_nodes.py @@ -132,6 +132,7 @@ class BFL_CannyControl: "optional": { "preprocessed_image": ("IMAGE",), "seed": ("INT", {"default": -1}), + "prompt_upsampling": ("BOOLEAN", {"default": False}), } } @@ -153,6 +154,7 @@ class BFL_CannyControl: "output_format": kwargs['output_format'], "safety_tolerance": kwargs['safety_tolerance'], "seed": kwargs['seed'] if kwargs['seed'] != -1 else None, + "prompt_upsampling": kwargs.get('prompt_upsampling', False), } if kwargs.get('preprocessed_image'): @@ -230,11 +232,180 @@ class BFL_ImageExpander: print(f"BFL Expansion Error: {str(e)}") return (create_error_image(),) +class BFL_FluxKontext: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "prompt": ("STRING", {"multiline": True}), + "model": (["flux-kontext-pro", "flux-kontext-max"], {"default": "flux-kontext-pro"}), + "output_format": (["jpeg", "png"], {"default": "png"}), + "safety_tolerance": ("INT", {"default": 2, "min": 0, "max": 6}), + "x_key": ("STRING", {"default": ""}), + }, + "optional": { + "input_image": ("IMAGE",), + "seed": ("INT", {"default": -1}), + "aspect_ratio": ("STRING", {"default": ""}), + "prompt_upsampling": ("BOOLEAN", {"default": False}), + } + } + + RETURN_TYPES = ("IMAGE",) + FUNCTION = "generate" + CATEGORY = "BFL/Kontext" + + def generate(self, **kwargs): + try: + config = BFLConfigLoader() + + payload = { + "prompt": kwargs['prompt'], + "output_format": kwargs['output_format'], + "safety_tolerance": kwargs['safety_tolerance'], + "seed": kwargs['seed'] if kwargs['seed'] != -1 else None, + "prompt_upsampling": kwargs.get('prompt_upsampling', False), + } + + # Agregar campos opcionales solo si tienen valor + if kwargs.get('input_image') is not None: + payload['input_image'] = image_to_base64(kwargs['input_image']) + + if kwargs.get('aspect_ratio') and kwargs['aspect_ratio'].strip(): + payload['aspect_ratio'] = kwargs['aspect_ratio'] + + response = requests.post( + url=f"https://api.us1.bfl.ai/v1/{kwargs['model']}", + headers={"x-key": config.get_api_key(kwargs['x_key']), "Content-Type": "application/json"}, + json={k: v for k, v in payload.items() if v is not None}, + timeout=30 + ) + + return (handle_api_response(response, kwargs['output_format'], kwargs['x_key']),) + + except Exception as e: + print(f"BFL Flux Kontext Error: {str(e)}") + return (create_error_image(),) + +class BFL_DepthControl: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "prompt": ("STRING", {"multiline": True}), + "control_image": ("IMAGE",), + "steps": ("INT", {"default": 50, "min": 15, "max": 50}), + "guidance": ("FLOAT", {"default": 15.0, "min": 1.0, "max": 100.0}), + "output_format": (["jpeg", "png"], {"default": "jpeg"}), + "safety_tolerance": ("INT", {"default": 2, "min": 0, "max": 6}), + "x_key": ("STRING", {"default": ""}), + }, + "optional": { + "preprocessed_image": ("IMAGE",), + "seed": ("INT", {"default": -1}), + "prompt_upsampling": ("BOOLEAN", {"default": False}), + } + } + + RETURN_TYPES = ("IMAGE",) + FUNCTION = "generate" + CATEGORY = "BFL/ControlNet" + + def generate(self, **kwargs): + try: + config = BFLConfigLoader() + + payload = { + "prompt": kwargs['prompt'], + "control_image": image_to_base64(kwargs['control_image']), + "steps": kwargs['steps'], + "guidance": kwargs['guidance'], + "output_format": kwargs['output_format'], + "safety_tolerance": kwargs['safety_tolerance'], + "seed": kwargs['seed'] if kwargs['seed'] != -1 else None, + "prompt_upsampling": kwargs.get('prompt_upsampling', False), + } + + if kwargs.get('preprocessed_image'): + payload['preprocessed_image'] = image_to_base64(kwargs['preprocessed_image']) + + response = requests.post( + url="https://api.us1.bfl.ai/v1/flux-pro-1.0-depth", + headers={"x-key": config.get_api_key(kwargs['x_key']), "Content-Type": "application/json"}, + json=payload, + timeout=30 + ) + + return (handle_api_response(response, kwargs['output_format'], kwargs['x_key']),) + + except Exception as e: + print(f"BFL Depth Error: {str(e)}") + return (create_error_image(),) + +class BFL_FluxUltra: + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "prompt": ("STRING", {"multiline": True}), + "aspect_ratio": ("STRING", {"default": "16:9"}), + "output_format": (["jpeg", "png"], {"default": "jpeg"}), + "safety_tolerance": ("INT", {"default": 2, "min": 0, "max": 6}), + "x_key": ("STRING", {"default": ""}), + }, + "optional": { + "seed": ("INT", {"default": -1}), + "prompt_upsampling": ("BOOLEAN", {"default": False}), + "raw": ("BOOLEAN", {"default": False}), + "image_prompt": ("IMAGE",), + "image_prompt_strength": ("FLOAT", {"default": 0.1, "min": 0.0, "max": 1.0}), + } + } + + RETURN_TYPES = ("IMAGE",) + FUNCTION = "generate" + CATEGORY = "BFL/Ultra" + + def generate(self, **kwargs): + try: + config = BFLConfigLoader() + + payload = { + "prompt": kwargs['prompt'], + "aspect_ratio": kwargs['aspect_ratio'], + "output_format": kwargs['output_format'], + "safety_tolerance": kwargs['safety_tolerance'], + "seed": kwargs['seed'] if kwargs['seed'] != -1 else None, + "prompt_upsampling": kwargs.get('prompt_upsampling', False), + "raw": kwargs.get('raw', False), + } + + # Agregar image_prompt solo si se proporciona + if kwargs.get('image_prompt') is not None: + payload['image_prompt'] = image_to_base64(kwargs['image_prompt']) + payload['image_prompt_strength'] = kwargs.get('image_prompt_strength', 0.1) + + response = requests.post( + url="https://api.us1.bfl.ai/v1/flux-pro-1.1-ultra", + headers={"x-key": config.get_api_key(kwargs['x_key']), "Content-Type": "application/json"}, + json={k: v for k, v in payload.items() if v is not None}, + timeout=30 + ) + + return (handle_api_response(response, kwargs['output_format'], kwargs['x_key']),) + + except Exception as e: + print(f"BFL Flux Ultra Error: {str(e)}") + return (create_error_image(),) + NODE_CLASS_MAPPINGS = { "BFL Image Generator": BFL_ImageGenerator, "BFL Inpainting": BFL_Inpainting, "BFL Canny Control": BFL_CannyControl, "BFL Image Expander": BFL_ImageExpander, + "BFL Flux Kontext": BFL_FluxKontext, + "BFL Depth Control": BFL_DepthControl, + "BFL Flux Ultra": BFL_FluxUltra, } NODE_DISPLAY_NAME_MAPPINGS = { @@ -242,4 +413,7 @@ NODE_DISPLAY_NAME_MAPPINGS = { "BFL Inpainting": "BFL Inpainting (Pro 1.0 Fill)", "BFL Canny Control": "BFL Canny Control (Pro 1.0)", "BFL Image Expander": "BFL Image Expander (Pro 1.0)", + "BFL Flux Kontext": "BFL Flux Kontext (Pro/Max)", + "BFL Depth Control": "BFL Depth Control (Pro 1.0)", + "BFL Flux Ultra": "BFL Flux Ultra (Pro 1.1)", } \ No newline at end of file diff --git a/bfl_utils.py b/bfl_utils.py index 02eceda..efd692c 100644 --- a/bfl_utils.py +++ b/bfl_utils.py @@ -55,17 +55,53 @@ def mask_to_base64(mask_tensor, format='PNG'): return base64.b64encode(buffered.getvalue()).decode('utf-8') def handle_api_response(response, output_format, node_api_key=None): - if response.status_code != 200: - raise Exception(f"API Error {response.status_code}: {response.text}") + # Manejar diferentes códigos de estado HTTP + if response.status_code == 200: + try: + result = response.json() + + if 'id' not in result: + raise Exception("Invalid API response format: missing task ID") + + return poll_task_result(result['id'], output_format, node_api_key=node_api_key) + + except ValueError as e: + raise Exception(f"Invalid JSON response from API: {str(e)}") - result = response.json() + elif response.status_code == 422: + try: + error_data = response.json() + detail = error_data.get('detail', 'Unknown validation error') + if isinstance(detail, list) and len(detail) > 0: + error_msg = detail[0].get('msg', 'Validation error') + else: + error_msg = str(detail) + raise Exception(f"Validation Error: {error_msg}") + except ValueError: + raise Exception(f"Validation Error: {response.text}") - if 'id' not in result: - raise Exception("Invalid API response format") + elif response.status_code == 401: + raise Exception("Unauthorized: Please check your API key") - return poll_task_result(result['id'], output_format, node_api_key=node_api_key) + elif response.status_code == 403: + raise Exception("Forbidden: Insufficient permissions or quota exceeded") + + elif response.status_code == 429: + raise Exception("Rate limit exceeded: Please wait before making another request") + + elif response.status_code >= 500: + raise Exception(f"Server Error ({response.status_code}): Please try again later") + + else: + try: + error_data = response.json() + error_msg = error_data.get('error', error_data.get('message', response.text)) + except ValueError: + error_msg = response.text + + raise Exception(f"API Error {response.status_code}: {error_msg}") -def poll_task_result(task_id, output_format, max_attempts=15, node_api_key=None): +def poll_task_result(task_id, output_format, max_attempts=10, node_api_key=None): config = BFLConfigLoader() base_url = "https://api.us1.bfl.ai/v1/" @@ -77,11 +113,27 @@ def poll_task_result(task_id, output_format, max_attempts=15, node_api_key=None) timeout=30 ) + # Manejar errores HTTP + if response.status_code == 422: + error_data = response.json() + raise Exception(f"Validation Error: {error_data.get('detail', 'Unknown validation error')}") + response.raise_for_status() data = response.json() - if data['status'] == 'Ready': + # Verificar que tenemos los campos requeridos + if 'status' not in data: + raise Exception("Invalid API response: missing status field") + + status = data['status'] + + # Manejar diferentes estados según la documentación + if status == 'Ready': + if 'result' not in data or not data['result'] or 'sample' not in data['result']: + raise Exception("Task completed but no result available") + img_response = requests.get(data['result']['sample']) + img_response.raise_for_status() img = Image.open(io.BytesIO(img_response.content)) with io.BytesIO() as buffer: @@ -90,14 +142,59 @@ def poll_task_result(task_id, output_format, max_attempts=15, node_api_key=None) img_array = np.array(Image.open(buffer)).astype(np.float32) / 255.0 return torch.from_numpy(img_array)[None,] - time.sleep(2 ** attempt + 5) + elif status == 'Error': + error_msg = "Task failed" + if 'details' in data and data['details']: + error_msg = f"Task failed: {data['details']}" + raise Exception(error_msg) + + elif status == 'Task not found': + raise Exception(f"Task not found: {task_id}") + + elif status == 'Request Moderated': + raise Exception("Request was moderated by content filters") + + elif status == 'Content Moderated': + raise Exception("Generated content was moderated by safety filters") + + elif status == 'Pending': + # Tarea en progreso, continuar polling + progress = data.get('progress', 0) + print(f"Task {task_id} in progress: {progress}% (attempt {attempt + 1}/{max_attempts})") + + # Tiempo de espera más razonable: máximo 60 segundos por intento + wait_time = min(2 ** attempt + 5, 60) + time.sleep(wait_time) + continue + + else: + # Estado desconocido + print(f"Unknown status '{status}' for task {task_id}, retrying...") + wait_time = min(2 ** attempt + 5, 60) + time.sleep(wait_time) + continue + + except requests.exceptions.Timeout: + print(f"Timeout on attempt {attempt + 1}/{max_attempts} for task {task_id}") + if attempt == max_attempts - 1: + raise Exception(f"Request timeout after {max_attempts} attempts") + wait_time = min(2 ** attempt + 5, 60) + time.sleep(wait_time) + continue + + except requests.exceptions.RequestException as e: + print(f"Request error on attempt {attempt + 1}/{max_attempts}: {str(e)}") + if attempt == max_attempts - 1: + raise Exception(f"Request failed after {max_attempts} attempts: {str(e)}") + wait_time = min(2 ** attempt + 5, 60) + time.sleep(wait_time) + continue except Exception as e: - if attempt == max_attempts - 1: - raise - continue + # Para otros errores (parsing JSON, etc.), lanzar inmediatamente + raise Exception(f"Error processing task {task_id}: {str(e)}") - raise Exception("Max polling attempts reached") + raise Exception(f"Max polling attempts ({max_attempts}) reached for task {task_id}") def create_error_image(): blank = Image.new('RGB', (512, 512), color='red')