-
Notifications
You must be signed in to change notification settings - Fork 1
Add FLUX.2 API custom node for Floyo #1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
ritik-devsecops
wants to merge
1
commit into
main
Choose a base branch
from
codex/create-custom-node-for-flux2-pro
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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`. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,3 @@ | ||
| from .flux2_node import NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS | ||
|
|
||
| __all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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"} | ||
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The node inputs for
model,poll_interval, andmax_wait_secondsare ignored whenevercustom_nodes/floyo_flux2_api_node/config.iniexists, because_load_configalways replaces the runtime arguments with the config values. Since the repo ships with that file populated, selectingflux-2-flexor changing polling cadence in the UI has no effect unless users manually edit the config file, which defeats the documented purpose of those inputs. Please treat config entries as defaults and honor explicit node selections.Useful? React with 👍 / 👎.