From 631446e0c7168b9d818f1df3ed10e05b762c1440 Mon Sep 17 00:00:00 2001 From: RITIK Date: Thu, 27 Nov 2025 03:42:05 +0530 Subject: [PATCH 1/5] Add FLUX.2 flex nodes (text/edit) and flex config --- README.md | 17 +++ __init__.py | 11 ++ config.ini | 1 + nodes/flux2_config.py | 18 ++++ nodes/flux2_flex_node.py | 227 +++++++++++++++++++++++++++++++++++++++ nodes/flux2_utils.py | 6 +- 6 files changed, 277 insertions(+), 3 deletions(-) create mode 100644 nodes/flux2_flex_node.py diff --git a/README.md b/README.md index 4c1a9c2..7995410 100644 --- a/README.md +++ b/README.md @@ -27,10 +27,12 @@ Repo layout (mirrors Floyo Seed API style): [API] BFL_API_KEY = BFL_BASE_URL = https://api.bfl.ai/v1/flux-2-pro + BFL_FLEX_BASE_URL = https://api.bfl.ai/v1/flux-2-flex ``` ```bash export BFL_API_KEY= export BFL_BASE_URL=https://api.bfl.ai/v1/flux-2-pro + export BFL_FLEX_BASE_URL=https://api.bfl.ai/v1/flux-2-flex ``` 4. Restart ComfyUI. @@ -45,6 +47,21 @@ Repo layout (mirrors Floyo Seed API style): - **Inputs (optional):** `input_image_2` … `input_image_8` (IMAGE tensors, keep total refs <= 8). `width`, `height` (0 = match input), `seed` (`-1` = random), `safety_tolerance`, `output_format` - **Output:** `image` (IMAGE tensor ready for Save Image) +### FLUX.2 [flex] Text-to-Image (`Flux2FlexTextToImage`) +- **Inputs (required):** `prompt`, `width`, `height`, `guidance` (1.5–10), `steps` (1–50) +- **Inputs (optional):** `seed` (`-1` = random), `safety_tolerance`, `output_format` +- **Output:** `image` (IMAGE tensor) + +### FLUX.2 [flex] Image Edit (`Flux2FlexImageEdit`) +- **Inputs (required):** `prompt`, `input_image` (IMAGE tensor) +- **Inputs (optional):** `input_image_2` … `input_image_10` (IMAGE tensors, keep total refs <= 10), `width`, `height`, `seed`, `safety_tolerance`, `output_format`, `guidance`, `steps` +- **Output:** `image` (IMAGE tensor) + +### FLUX.2 [pro] Image Edit (`Flux2ProImageEdit`) +- **Inputs (required):** `prompt`, `input_image` (IMAGE tensor) +- **Inputs (optional):** `input_image_2` … `input_image_8` (IMAGE tensors, keep total refs <= 8). `width`, `height` (0 = match input), `seed` (`-1` = random), `safety_tolerance`, `output_format` +- **Output:** `image` (IMAGE tensor ready for Save Image) + ## Usage Notes - The returned URL expires quickly; run your Floyo download/output node immediately after this node. - API statuses handled: `Ready`, `Pending`, and error/moderation states. Failures return an error string in the node output. diff --git a/__init__.py b/__init__.py index 37f8129..e0d556b 100644 --- a/__init__.py +++ b/__init__.py @@ -4,10 +4,21 @@ from .nodes.flux2_node import ( # noqa: F401 NODE_CLASS_MAPPINGS, NODE_DISPLAY_NAME_MAPPINGS, ) +from .nodes.flux2_flex_node import ( # noqa: F401 + Flux2FlexTextToImage, + Flux2FlexImageEdit, + NODE_CLASS_MAPPINGS as FLEX_NODE_CLASS_MAPPINGS, + NODE_DISPLAY_NAME_MAPPINGS as FLEX_NODE_DISPLAY_NAME_MAPPINGS, +) + +NODE_CLASS_MAPPINGS.update(FLEX_NODE_CLASS_MAPPINGS) +NODE_DISPLAY_NAME_MAPPINGS.update(FLEX_NODE_DISPLAY_NAME_MAPPINGS) __all__ = [ "Flux2ProTextToImage", "Flux2ProImageEdit", + "Flux2FlexTextToImage", + "Flux2FlexImageEdit", "NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS", ] diff --git a/config.ini b/config.ini index 142f370..07cc517 100644 --- a/config.ini +++ b/config.ini @@ -1,3 +1,4 @@ [API] BFL_API_KEY = BFL_BASE_URL = https://api.bfl.ai/v1/flux-2-pro +BFL_FLEX_BASE_URL = https://api.bfl.ai/v1/flux-2-flex diff --git a/nodes/flux2_config.py b/nodes/flux2_config.py index 675bedd..b2f697f 100644 --- a/nodes/flux2_config.py +++ b/nodes/flux2_config.py @@ -25,6 +25,8 @@ class Flux2Config: key_from_file = config.get("API", "BFL_API_KEY", fallback=None) base_from_env = os.environ.get("BFL_BASE_URL") base_from_file = config.get("API", "BFL_BASE_URL", fallback=None) + flex_base_from_env = os.environ.get("BFL_FLEX_BASE_URL") + flex_base_from_file = config.get("API", "BFL_FLEX_BASE_URL", fallback=None) if key_from_env: self._key = key_from_env @@ -49,6 +51,18 @@ class Flux2Config: self._base_url = default_base print(f"BFL_BASE_URL not set; using default {default_base}") + default_flex_base = "https://api.bfl.ai/v1/flux-2-flex" + if flex_base_from_env: + self._flex_base_url = flex_base_from_env + print(f"BFL_FLEX_BASE_URL loaded from environment: {self._flex_base_url}") + elif flex_base_from_file: + self._flex_base_url = flex_base_from_file + os.environ.setdefault("BFL_FLEX_BASE_URL", self._flex_base_url) + print(f"BFL_FLEX_BASE_URL loaded from config.ini: {self._flex_base_url}") + else: + self._flex_base_url = default_flex_base + print(f"BFL_FLEX_BASE_URL not set; using default {default_flex_base}") + if self._key and self._key.strip() == "": print("WARNING: BFL_API_KEY is still the placeholder value. Replace it with a real key from https://api.bfl.ai.") @@ -59,3 +73,7 @@ class Flux2Config: def get_base_url(self): """Return the configured base URL for FLUX.2 API.""" return self._base_url + + def get_flex_base_url(self): + """Return the configured base URL for FLUX.2 flex API.""" + return self._flex_base_url diff --git a/nodes/flux2_flex_node.py b/nodes/flux2_flex_node.py new file mode 100644 index 0000000..d1104f9 --- /dev/null +++ b/nodes/flux2_flex_node.py @@ -0,0 +1,227 @@ +from typing import List, Optional + +from .flux2_utils import ( + Flux2API, + download_image_to_tensor, + image_tensor_to_base64, + merge_reference_images, +) +from .flux2_utils import _blank_image_tensor as _blank_image +from .flux2_config import Flux2Config + + +def _validate_resolution(width: int, height: int) -> Optional[str]: + """Validate resolution constraints for FLUX.2 flex.""" + for dim_name, dim in (("width", width), ("height", height)): + if dim <= 0: + continue # 0 means "use default/match input" for edits; for T2I user must set + if dim % 16 != 0: + return f"{dim_name} must be a multiple of 16 (got {dim})." + if dim < 64 or dim > 2048: + return f"{dim_name} must be between 64 and 2048 pixels (got {dim})." + return None + + +class Flux2FlexTextToImage: + """ + FLUX.2 [flex] text-to-image node for Floyo. + + Adds guidance/steps controls; outputs IMAGE tensor. + """ + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "prompt": ("STRING", {"multiline": True, "default": "", "tooltip": "Describe what to generate."}), + "width": ("INT", {"default": 1024, "min": 64, "max": 2048, "step": 16, "tooltip": "Output width (multiple of 16, 64-2048)."}), + "height": ("INT", {"default": 1024, "min": 64, "max": 2048, "step": 16, "tooltip": "Output height (multiple of 16, 64-2048)."}), + "guidance": ("FLOAT", {"default": 4.5, "min": 1.5, "max": 10.0, "step": 0.1, "tooltip": "Prompt adherence (1.5-10)."}), + "steps": ("INT", {"default": 50, "min": 1, "max": 50, "step": 1, "tooltip": "Inference steps (1-50)."}), + }, + "optional": { + "seed": ("INT", {"default": -1, "min": -1, "max": 0xFFFFFFFF, "tooltip": "-1 = random. Any other integer is reproducible."}), + "safety_tolerance": ("INT", {"default": 2, "min": 0, "max": 6, "tooltip": "Moderation level 0 (strict) to 6 (permissive)."}), + "output_format": (["jpeg", "png"], {"default": "jpeg", "tooltip": "Output format."}), + }, + } + + RETURN_TYPES = ("IMAGE",) + RETURN_NAMES = ("image",) + FUNCTION = "generate" + CATEGORY = "Floyo/Flux2 Flex" + + def generate( + self, + prompt: str, + width: int, + height: int, + guidance: float, + steps: int, + seed: int = -1, + safety_tolerance: int = 2, + output_format: str = "jpeg", + ): + try: + resolution_error = _validate_resolution(width, height) + if resolution_error: + return (f"Error: {resolution_error}",) + + cfg = Flux2Config() + client = Flux2API(base_url=cfg.get_flex_base_url()) + payload = { + "prompt": prompt, + "width": width if width > 0 else None, + "height": height if height > 0 else None, + "seed": None if seed is None or seed < 0 else seed, + "safety_tolerance": safety_tolerance, + "output_format": output_format, + "guidance": guidance, + "steps": steps, + } + + run_result = client.run(payload) + image_url = run_result.get("sample") + if not image_url: + print("Error: FLUX.2 flex response did not include an image URL.") + return (_blank_image(),) + + image_tensor = download_image_to_tensor(image_url) + return (image_tensor,) + except Exception as exc: # noqa: BLE001 + print(f"Error generating with FLUX.2 flex: {exc}") + return (_blank_image(),) + + +class Flux2FlexImageEdit: + """ + FLUX.2 [flex] image editing node for Floyo. + + Supports up to 10 references, guidance, steps; outputs IMAGE tensor. + """ + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "prompt": ("STRING", {"multiline": True, "default": "", "tooltip": "Describe the edit you want."}), + "input_image": ("IMAGE", {"tooltip": "Base image."}), + }, + "optional": { + "input_image_2": ("IMAGE", {"tooltip": "Optional reference image #2."}), + "input_image_3": ("IMAGE", {"tooltip": "Optional reference image #3."}), + "input_image_4": ("IMAGE", {"tooltip": "Optional reference image #4."}), + "input_image_5": ("IMAGE", {"tooltip": "Optional reference image #5."}), + "input_image_6": ("IMAGE", {"tooltip": "Optional reference image #6."}), + "input_image_7": ("IMAGE", {"tooltip": "Optional reference image #7."}), + "input_image_8": ("IMAGE", {"tooltip": "Optional reference image #8."}), + "input_image_9": ("IMAGE", {"tooltip": "Optional reference image #9."}), + "input_image_10": ("IMAGE", {"tooltip": "Optional reference image #10."}), + "width": ("INT", {"default": 1024, "min": 0, "max": 2048, "step": 16, "tooltip": "Override width (0 = keep). Multiple of 16."}), + "height": ("INT", {"default": 1024, "min": 0, "max": 2048, "step": 16, "tooltip": "Override height (0 = keep). Multiple of 16."}), + "seed": ("INT", {"default": -1, "min": -1, "max": 0xFFFFFFFF, "tooltip": "-1 = random. Any other integer is reproducible."}), + "safety_tolerance": ("INT", {"default": 2, "min": 0, "max": 6, "tooltip": "Moderation level 0 (strict) to 6 (permissive)."}), + "output_format": (["jpeg", "png"], {"default": "jpeg", "tooltip": "Output format."}), + "guidance": ("FLOAT", {"default": 4.5, "min": 1.5, "max": 10.0, "step": 0.1, "tooltip": "Prompt adherence (1.5-10)."}), + "steps": ("INT", {"default": 50, "min": 1, "max": 50, "step": 1, "tooltip": "Inference steps (1-50)."}), + }, + } + + RETURN_TYPES = ("IMAGE",) + RETURN_NAMES = ("image",) + FUNCTION = "edit" + CATEGORY = "Floyo/Flux2 Flex" + + def edit( + self, + prompt: str, + input_image, + input_image_2=None, + input_image_3=None, + input_image_4=None, + input_image_5=None, + input_image_6=None, + input_image_7=None, + input_image_8=None, + input_image_9=None, + input_image_10=None, + width: int = 1024, + height: int = 1024, + seed: int = -1, + safety_tolerance: int = 2, + output_format: str = "jpeg", + guidance: float = 4.5, + steps: int = 50, + ): + try: + resolution_error = _validate_resolution(width, height) + if resolution_error: + return (f"Error: {resolution_error}",) + + def resolve_image(source_tensor) -> Optional[str]: + if source_tensor is None: + return None + try: + return image_tensor_to_base64(source_tensor, format="PNG") + except Exception as exc: + print(f"Warning: failed to convert image tensor to base64: {exc}") + return None + + base_image_value = resolve_image(input_image) + if not base_image_value: + return ("Error: Provide a base image.",) + + refs = [ + input_image_2, + input_image_3, + input_image_4, + input_image_5, + input_image_6, + input_image_7, + input_image_8, + input_image_9, + input_image_10, + ] + ref_values: List[str] = [] + for tensor_val in refs: + resolved = resolve_image(tensor_val) + ref_values.append(resolved if resolved else "") + + reference_payload = merge_reference_images(base_image_value, ref_values) + + cfg = Flux2Config() + client = Flux2API(base_url=cfg.get_flex_base_url()) + payload = { + "prompt": prompt, + **reference_payload, + "width": width if width > 0 else None, + "height": height if height > 0 else None, + "seed": None if seed is None or seed < 0 else seed, + "safety_tolerance": safety_tolerance, + "output_format": output_format, + "guidance": guidance, + "steps": steps, + } + + run_result = client.run(payload) + image_url = run_result.get("sample") + if not image_url: + print("Error: FLUX.2 flex response did not include an image URL.") + return (_blank_image(),) + + image_tensor = download_image_to_tensor(image_url) + return (image_tensor,) + except Exception as exc: # noqa: BLE001 + print(f"Error editing with FLUX.2 flex: {exc}") + return (_blank_image(),) + + +NODE_CLASS_MAPPINGS = { + "Flux2FlexTextToImage": Flux2FlexTextToImage, + "Flux2FlexImageEdit": Flux2FlexImageEdit, +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "Flux2FlexTextToImage": "FLUX.2-Flex Text-to-Image", + "Flux2FlexImageEdit": "FLUX.2-Flex Image Edit", +} diff --git a/nodes/flux2_utils.py b/nodes/flux2_utils.py index b964253..19a3a7e 100644 --- a/nodes/flux2_utils.py +++ b/nodes/flux2_utils.py @@ -16,9 +16,9 @@ class Flux2APIError(Exception): class Flux2API: - """Minimal client for the Black Forest Labs FLUX.2 [pro] endpoint.""" + """Minimal client for the Black Forest Labs FLUX.2 endpoint.""" - def __init__(self, timeout: int = 600, poll_interval: float = 1.0): + def __init__(self, base_url: str, timeout: int = 600, poll_interval: float = 1.0): self.timeout = timeout self.poll_interval = poll_interval @@ -28,7 +28,7 @@ class Flux2API: raise Flux2APIError( "BFL_API_KEY is not set. Define it in config.ini or export BFL_API_KEY before running ComfyUI." ) - self.base_url = cfg.get_base_url() + self.base_url = base_url self.headers = { "accept": "application/json", From cf64782577a694be7d9ec662fee233f0549aa72a Mon Sep 17 00:00:00 2001 From: RITIK Date: Thu, 27 Nov 2025 04:03:13 +0530 Subject: [PATCH 2/5] Reorder flex inputs; update flex display names --- nodes/flux2_flex_node.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/nodes/flux2_flex_node.py b/nodes/flux2_flex_node.py index d1104f9..11a2ec4 100644 --- a/nodes/flux2_flex_node.py +++ b/nodes/flux2_flex_node.py @@ -40,6 +40,8 @@ class Flux2FlexTextToImage: "steps": ("INT", {"default": 50, "min": 1, "max": 50, "step": 1, "tooltip": "Inference steps (1-50)."}), }, "optional": { + "guidance": ("FLOAT", {"default": 4.5, "min": 1.5, "max": 10.0, "step": 0.1, "tooltip": "Prompt adherence (1.5-10)."}), + "steps": ("INT", {"default": 50, "min": 1, "max": 50, "step": 1, "tooltip": "Inference steps (1-50)."}), "seed": ("INT", {"default": -1, "min": -1, "max": 0xFFFFFFFF, "tooltip": "-1 = random. Any other integer is reproducible."}), "safety_tolerance": ("INT", {"default": 2, "min": 0, "max": 6, "tooltip": "Moderation level 0 (strict) to 6 (permissive)."}), "output_format": (["jpeg", "png"], {"default": "jpeg", "tooltip": "Output format."}), @@ -119,11 +121,11 @@ class Flux2FlexImageEdit: "input_image_10": ("IMAGE", {"tooltip": "Optional reference image #10."}), "width": ("INT", {"default": 1024, "min": 0, "max": 2048, "step": 16, "tooltip": "Override width (0 = keep). Multiple of 16."}), "height": ("INT", {"default": 1024, "min": 0, "max": 2048, "step": 16, "tooltip": "Override height (0 = keep). Multiple of 16."}), + "guidance": ("FLOAT", {"default": 4.5, "min": 1.5, "max": 10.0, "step": 0.1, "tooltip": "Prompt adherence (1.5-10)."}), + "steps": ("INT", {"default": 50, "min": 1, "max": 50, "step": 1, "tooltip": "Inference steps (1-50)."}), "seed": ("INT", {"default": -1, "min": -1, "max": 0xFFFFFFFF, "tooltip": "-1 = random. Any other integer is reproducible."}), "safety_tolerance": ("INT", {"default": 2, "min": 0, "max": 6, "tooltip": "Moderation level 0 (strict) to 6 (permissive)."}), "output_format": (["jpeg", "png"], {"default": "jpeg", "tooltip": "Output format."}), - "guidance": ("FLOAT", {"default": 4.5, "min": 1.5, "max": 10.0, "step": 0.1, "tooltip": "Prompt adherence (1.5-10)."}), - "steps": ("INT", {"default": 50, "min": 1, "max": 50, "step": 1, "tooltip": "Inference steps (1-50)."}), }, } @@ -222,6 +224,6 @@ NODE_CLASS_MAPPINGS = { } NODE_DISPLAY_NAME_MAPPINGS = { - "Flux2FlexTextToImage": "FLUX.2-Flex Text-to-Image", - "Flux2FlexImageEdit": "FLUX.2-Flex Image Edit", + "Flux2FlexTextToImage": "Floyo FLUX.2 Flex Text-to-Image", + "Flux2FlexImageEdit": "Floyo FLUX.2 Flex Image Edit", } From f50ce0270dfdb678c2430e56b35d567811b28f8a Mon Sep 17 00:00:00 2001 From: RITIK Date: Thu, 27 Nov 2025 04:14:23 +0530 Subject: [PATCH 3/5] Rename flex nodes and align edit input order --- nodes/flux2_flex_node.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nodes/flux2_flex_node.py b/nodes/flux2_flex_node.py index 11a2ec4..6546e22 100644 --- a/nodes/flux2_flex_node.py +++ b/nodes/flux2_flex_node.py @@ -224,6 +224,6 @@ NODE_CLASS_MAPPINGS = { } NODE_DISPLAY_NAME_MAPPINGS = { - "Flux2FlexTextToImage": "Floyo FLUX.2 Flex Text-to-Image", - "Flux2FlexImageEdit": "Floyo FLUX.2 Flex Image Edit", + "Flux2FlexTextToImage": "Floyo Flex Text-to-Image", + "Flux2FlexImageEdit": "Floyo Flex Image Edit", } From 0217d42997c88a59b1ba4b0fb568046ad0762504 Mon Sep 17 00:00:00 2001 From: RITIK Date: Thu, 27 Nov 2025 04:18:46 +0530 Subject: [PATCH 4/5] Set flex node display names to FLUX.2 naming --- nodes/flux2_flex_node.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nodes/flux2_flex_node.py b/nodes/flux2_flex_node.py index 6546e22..11a2ec4 100644 --- a/nodes/flux2_flex_node.py +++ b/nodes/flux2_flex_node.py @@ -224,6 +224,6 @@ NODE_CLASS_MAPPINGS = { } NODE_DISPLAY_NAME_MAPPINGS = { - "Flux2FlexTextToImage": "Floyo Flex Text-to-Image", - "Flux2FlexImageEdit": "Floyo Flex Image Edit", + "Flux2FlexTextToImage": "Floyo FLUX.2 Flex Text-to-Image", + "Flux2FlexImageEdit": "Floyo FLUX.2 Flex Image Edit", } From 4d2100c6389cf434031ba36cb237ca9b57ec20b2 Mon Sep 17 00:00:00 2001 From: RITIK Date: Thu, 27 Nov 2025 04:48:40 +0530 Subject: [PATCH 5/5] Fix pro nodes to pass base_url when creating Flux2API --- nodes/flux2_node.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/nodes/flux2_node.py b/nodes/flux2_node.py index 248f0a6..16ef769 100644 --- a/nodes/flux2_node.py +++ b/nodes/flux2_node.py @@ -7,6 +7,7 @@ from .flux2_utils import ( merge_reference_images, ) from .flux2_utils import _blank_image_tensor as _blank_image +from .flux2_config import Flux2Config def _validate_resolution(width: int, height: int) -> Optional[str]: @@ -62,7 +63,7 @@ class Flux2ProTextToImage: if resolution_error: return (f"Error: {resolution_error}",) - client = Flux2API() + client = Flux2API(base_url=Flux2Config().get_base_url()) payload = { "prompt": prompt, "width": width if width > 0 else None, @@ -183,7 +184,7 @@ class Flux2ProImageEdit: "output_format": output_format, } - client = Flux2API() + client = Flux2API(base_url=Flux2Config().get_base_url()) run_result = client.run(payload) image_url = run_result.get("sample") if not image_url: