Add package init for Floyo FLUX.2 node
This commit is contained in:
@@ -1 +1,79 @@
|
||||
# ComfyUI-Floyo-Flux2-API-node
|
||||
# Floyo FLUX.2 API Custom Node
|
||||
|
||||
A Floyo-ready ComfyUI custom node for the Black Forest Labs FLUX.2 API. The node mirrors Floyo's URL-first I/O pattern, supports both **flux-2-pro** and **flux-2-flex** variants, and polls the API's `polling_url` until an image URL is returned.
|
||||
|
||||
## Features
|
||||
- URL/Base64 inputs for the base image plus up to eight additional reference images.
|
||||
- Model selector for `flux-2-pro` (fast, production) or `flux-2-flex` (adjustable steps & guidance).
|
||||
- Optional sizing, seed, safety tolerance, and output format controls.
|
||||
- Flex-only parameters: guidance and steps.
|
||||
- Configurable polling interval and timeout aligned with BFL recommendations.
|
||||
|
||||
## Installation
|
||||
1. Copy the `custom_nodes/floyo_flux2_api_node` folder into your ComfyUI `custom_nodes` directory (keep the `__init__.py` + `flux2_node.py` pairing intact so the node is auto-discovered).
|
||||
2. Ensure `requests` is available in your Python environment.
|
||||
3. Provide your Black Forest Labs API key via either:
|
||||
- Environment: `export BFL_API_KEY=your_key`
|
||||
- Config file: update `custom_nodes/floyo_flux2_api_node/config.ini` (`[auth] api_key`)
|
||||
|
||||
## Configuration
|
||||
`config.ini` lets you set defaults without changing the node code:
|
||||
|
||||
```ini
|
||||
[auth]
|
||||
api_key = YOUR_BFL_API_KEY
|
||||
|
||||
[api]
|
||||
base_url = https://api.bfl.ai/v1
|
||||
model = flux-2-pro
|
||||
poll_interval = 0.5
|
||||
max_wait_seconds = 120
|
||||
```
|
||||
|
||||
> The node falls back to `BFL_API_KEY` if it is set. The placeholder value must be replaced.
|
||||
|
||||
## Node Inputs
|
||||
- **prompt** (string, required): Edit or generation prompt.
|
||||
- **input_image** (string, required): URL or base64 image to edit; also works for text-to-image prompts.
|
||||
- **input_image_2 ... input_image_9** (string, optional): Additional reference images.
|
||||
- **model** (`flux-2-pro` \| `flux-2-flex`): Selects the target endpoint.
|
||||
- **width / height** (int, optional): Multiples of 16; defaults to matching input.
|
||||
- **seed** (int, optional): `-1` for random.
|
||||
- **safety_tolerance** (0-6): Moderation strictness.
|
||||
- **output_format** (`jpeg` \| `png`).
|
||||
- **guidance**, **steps**: Only applied when model = `flux-2-flex`.
|
||||
- **poll_interval**, **max_wait_seconds**: Override the default polling cadence.
|
||||
|
||||
## Output
|
||||
- **image_url**: The signed URL returned by the FLUX.2 API (`result.sample`). Download and re-serve the image within 10 minutes per BFL delivery rules.
|
||||
|
||||
## Workflow Tips
|
||||
- Keep inputs as URLs to align with Floyo's storage and queueing behavior.
|
||||
- Multi-reference editing: supply additional `input_image_*` fields to mix styles and content.
|
||||
- For flex runs, tweak `guidance` (1.5-10) and `steps` (<=50) for quality/performance tradeoffs.
|
||||
- Respect BFL rate limits and poll using the provided `polling_url` rather than a hardcoded endpoint.
|
||||
|
||||
## Example (Python)
|
||||
```python
|
||||
import os
|
||||
import requests
|
||||
|
||||
api_key = os.environ["BFL_API_KEY"]
|
||||
resp = requests.post(
|
||||
"https://api.bfl.ai/v1/flux-2-pro",
|
||||
headers={"x-key": api_key, "accept": "application/json"},
|
||||
json={
|
||||
"prompt": "Cinematic city sunset, 85mm lens",
|
||||
"input_image": "https://example.com/source.jpg",
|
||||
"output_format": "jpeg",
|
||||
},
|
||||
).json()
|
||||
|
||||
polling_url = resp["polling_url"]
|
||||
result = requests.get(polling_url, headers={"x-key": api_key}).json()
|
||||
print(result)
|
||||
```
|
||||
|
||||
## Notes
|
||||
- Delivery URLs expire after ~10 minutes and are not CORS-enabled; download and serve from your own storage.
|
||||
- For regional routing, adjust `base_url` to `https://api.eu.bfl.ai/v1` or `https://api.us.bfl.ai/v1` in `config.ini`.
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
from .flux2_node import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS
|
||||
|
||||
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"]
|
||||
@@ -0,0 +1,8 @@
|
||||
[auth]
|
||||
api_key = YOUR_BFL_API_KEY
|
||||
|
||||
[api]
|
||||
base_url = https://api.bfl.ai/v1
|
||||
model = flux-2-pro
|
||||
poll_interval = 0.5
|
||||
max_wait_seconds = 120
|
||||
@@ -0,0 +1,181 @@
|
||||
import configparser
|
||||
import os
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict
|
||||
|
||||
import requests
|
||||
|
||||
CONFIG_PATH = Path(__file__).parent / "config.ini"
|
||||
DEFAULT_BASE_URL = "https://api.bfl.ai/v1"
|
||||
DEFAULT_MODEL = "flux-2-pro"
|
||||
|
||||
|
||||
class FloyoFlux2APINode:
|
||||
"""
|
||||
ComfyUI custom node that calls Black Forest Labs FLUX.2 API endpoints via URLs.
|
||||
|
||||
Inputs are URL/base64 strings to align with Floyo's storage model. The node polls
|
||||
the `polling_url` returned by the API until the result is ready or a timeout occurs.
|
||||
"""
|
||||
|
||||
RETURN_TYPES = ("STRING",)
|
||||
RETURN_NAMES = ("image_url",)
|
||||
FUNCTION = "generate"
|
||||
CATEGORY = "Floyo/API"
|
||||
|
||||
@classmethod
|
||||
def INPUT_TYPES(cls):
|
||||
required = {
|
||||
"prompt": ("STRING", {"multiline": True, "default": "Describe the image you want"}),
|
||||
"input_image": ("STRING", {"default": "https://example.com/your-image.jpg"}),
|
||||
}
|
||||
|
||||
optional_images = {
|
||||
f"input_image_{i}": ("STRING", {"default": ""}) for i in range(2, 10)
|
||||
}
|
||||
|
||||
optional = {
|
||||
**optional_images,
|
||||
"model": ("STRING", {"default": DEFAULT_MODEL, "choices": ["flux-2-pro", "flux-2-flex"]}),
|
||||
"width": ("INT", {"default": 0, "min": 0, "max": 4096, "step": 16}),
|
||||
"height": ("INT", {"default": 0, "min": 0, "max": 4096, "step": 16}),
|
||||
"seed": ("INT", {"default": -1, "min": -1, "max": 2**31 - 1}),
|
||||
"safety_tolerance": ("INT", {"default": 2, "min": 0, "max": 6}),
|
||||
"output_format": ("STRING", {"default": "jpeg", "choices": ["jpeg", "png"]}),
|
||||
"guidance": ("FLOAT", {"default": 4.5, "min": 1.5, "max": 10.0, "step": 0.1}),
|
||||
"steps": ("INT", {"default": 50, "min": 1, "max": 50}),
|
||||
"poll_interval": ("FLOAT", {"default": 0.5, "min": 0.1, "max": 5.0, "step": 0.1}),
|
||||
"max_wait_seconds": ("INT", {"default": 120, "min": 5, "max": 600}),
|
||||
}
|
||||
|
||||
return {"required": required, "optional": optional}
|
||||
|
||||
@classmethod
|
||||
def IS_CHANGED(cls, **kwargs):
|
||||
return time.time()
|
||||
|
||||
def generate(
|
||||
self,
|
||||
prompt: str,
|
||||
input_image: str,
|
||||
model: str = DEFAULT_MODEL,
|
||||
width: int = 0,
|
||||
height: int = 0,
|
||||
seed: int = -1,
|
||||
safety_tolerance: int = 2,
|
||||
output_format: str = "jpeg",
|
||||
guidance: float = 4.5,
|
||||
steps: int = 50,
|
||||
poll_interval: float = 0.5,
|
||||
max_wait_seconds: int = 120,
|
||||
**kwargs: Any,
|
||||
):
|
||||
api_key, base_url, model, poll_interval, max_wait_seconds = self._load_config(
|
||||
model, poll_interval, max_wait_seconds
|
||||
)
|
||||
|
||||
payload: Dict[str, Any] = {
|
||||
"prompt": prompt,
|
||||
"input_image": input_image,
|
||||
"safety_tolerance": safety_tolerance,
|
||||
"output_format": output_format,
|
||||
}
|
||||
|
||||
for key, value in kwargs.items():
|
||||
if key.startswith("input_image_") and isinstance(value, str) and value.strip():
|
||||
payload[key] = value
|
||||
|
||||
if width > 0:
|
||||
payload["width"] = width
|
||||
if height > 0:
|
||||
payload["height"] = height
|
||||
if seed >= 0:
|
||||
payload["seed"] = seed
|
||||
|
||||
if model == "flux-2-flex":
|
||||
payload["guidance"] = guidance
|
||||
payload["steps"] = steps
|
||||
|
||||
endpoint = f"{base_url.rstrip('/')}/flux-2-{model.split('-')[-1]}"
|
||||
|
||||
response = requests.post(
|
||||
endpoint,
|
||||
headers={
|
||||
"accept": "application/json",
|
||||
"Content-Type": "application/json",
|
||||
"x-key": api_key,
|
||||
},
|
||||
json=payload,
|
||||
timeout=30,
|
||||
)
|
||||
response.raise_for_status()
|
||||
data = response.json()
|
||||
|
||||
polling_url = data.get("polling_url")
|
||||
if not polling_url:
|
||||
raise RuntimeError("API response missing polling_url.")
|
||||
|
||||
image_url = self._poll_for_result(
|
||||
polling_url=polling_url,
|
||||
api_key=api_key,
|
||||
poll_interval=poll_interval,
|
||||
max_wait_seconds=max_wait_seconds,
|
||||
)
|
||||
|
||||
return (image_url,)
|
||||
|
||||
def _load_config(self, model: str, poll_interval: float, max_wait_seconds: int):
|
||||
config = configparser.ConfigParser()
|
||||
if CONFIG_PATH.exists():
|
||||
config.read(CONFIG_PATH)
|
||||
|
||||
api_key = os.getenv("BFL_API_KEY") or config.get("auth", "api_key", fallback=None)
|
||||
if not api_key or "YOUR_BFL_API_KEY" in api_key:
|
||||
raise RuntimeError("BFL API key not configured. Set BFL_API_KEY env or update config.ini.")
|
||||
|
||||
base_url = config.get("api", "base_url", fallback=DEFAULT_BASE_URL)
|
||||
configured_model = config.get("api", "model", fallback=model)
|
||||
effective_model = configured_model if configured_model in {"flux-2-pro", "flux-2-flex"} else model
|
||||
|
||||
configured_interval = config.getfloat("api", "poll_interval", fallback=poll_interval)
|
||||
configured_timeout = config.getint("api", "max_wait_seconds", fallback=max_wait_seconds)
|
||||
|
||||
return api_key, base_url, effective_model, configured_interval, configured_timeout
|
||||
|
||||
def _poll_for_result(
|
||||
self,
|
||||
polling_url: str,
|
||||
api_key: str,
|
||||
poll_interval: float,
|
||||
max_wait_seconds: int,
|
||||
) -> str:
|
||||
start_time = time.time()
|
||||
while True:
|
||||
result = requests.get(
|
||||
polling_url,
|
||||
headers={"accept": "application/json", "x-key": api_key},
|
||||
timeout=30,
|
||||
)
|
||||
result.raise_for_status()
|
||||
payload = result.json()
|
||||
status = payload.get("status")
|
||||
|
||||
if status == "Ready":
|
||||
result_obj = payload.get("result", {})
|
||||
image_url = result_obj.get("sample")
|
||||
if not image_url:
|
||||
raise RuntimeError("Result missing sample URL.")
|
||||
return image_url
|
||||
|
||||
if status in {"Error", "Failed", "Content Moderated", "Request Moderated"}:
|
||||
raise RuntimeError(f"Generation failed with status: {status} - {payload}")
|
||||
|
||||
if time.time() - start_time > max_wait_seconds:
|
||||
raise TimeoutError("Polling timed out before result was ready.")
|
||||
|
||||
time.sleep(poll_interval)
|
||||
|
||||
|
||||
NODE_CLASS_MAPPINGS = {"FloyoFlux2APINode": FloyoFlux2APINode}
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {"FloyoFlux2APINode": "Floyo FLUX.2 API"}
|
||||
Reference in New Issue
Block a user