@@ -27,10 +27,12 @@ Repo layout (mirrors Floyo Seed API style):
|
||||
[API]
|
||||
BFL_API_KEY = <your_real_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=<your_real_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.
|
||||
|
||||
+11
@@ -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",
|
||||
]
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
[API]
|
||||
BFL_API_KEY = <your_bfl_api_key_here>
|
||||
BFL_BASE_URL = https://api.bfl.ai/v1/flux-2-pro
|
||||
BFL_FLEX_BASE_URL = https://api.bfl.ai/v1/flux-2-flex
|
||||
|
||||
@@ -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() == "<your_bfl_api_key_here>":
|
||||
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
|
||||
|
||||
@@ -0,0 +1,229 @@
|
||||
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": {
|
||||
"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."}),
|
||||
},
|
||||
}
|
||||
|
||||
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."}),
|
||||
"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."}),
|
||||
},
|
||||
}
|
||||
|
||||
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": "Floyo FLUX.2 Flex Text-to-Image",
|
||||
"Flux2FlexImageEdit": "Floyo FLUX.2 Flex Image Edit",
|
||||
}
|
||||
+3
-2
@@ -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:
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user