Actualización 20250718

This commit is contained in:
charlyad142
2025-07-18 15:21:44 -04:00
parent bcb11f62ac
commit 43ce0f892d
3 changed files with 435 additions and 22 deletions
+151 -9
View File
@@ -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
+174
View File
@@ -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)",
}
+110 -13
View File
@@ -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')