diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..80b23cb --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,21 @@ +# Changelog + +All notable changes to this project from v1.1.0 onward are documented in this file. Earlier history lives in `git log`. + +## [1.1.0] — 2026-05-25 + +### Added + +| Node / Feature | Endpoint | Notes | +|---|---|---| +| Flux Erase (BFL) | `POST /v1/flux-tools/erase-v1` | Object removal via base64 image + binary mask. White pixels in the mask are erased; black pixels are kept. Knobs: `dilate_pixels` (0–25, default 10), `safety_tolerance` (0–5), `output_format` (png / jpeg), `seed`, `webhook_url`, `webhook_secret`. | +| Flux Outpaint (BFL) | `POST /v1/flux-tools/outpainting-v1` | Image extension to a target canvas. Knobs: `width` / `height` (≥64, step 32), `center_reference` (toggle), `reference_offset_x` / `reference_offset_y`, optional `prompt`, `auto_crop`, `output_format` (png / jpeg). | +| `image_format` on Image to Base64 (BFL) | — | New optional dropdown: `jpeg` (default, backward-compatible) or `png` (lossless, recommended for masks fed into Flux Erase / Flux Pro Fill). | + +### Fixed + +| Issue | Detail | +|---|---| +| Workflow crash on non-multiple-of-32 width/height | `BaseFlux.generate_image` now catches `check_multiple_of_32`'s `ValueError` inside its try/except and returns a blank image, matching the rest of the graceful-failure path. | +| `FluxErase` `image` / `mask` defaulted to `None` | Defaults are now `""` — prevents JSON-null serialization when the socket is left unwired. Consistent with `FluxOutpaint`'s `input_image` default. | +| Whitespace-only prompts on Flux Outpaint | Prompts that contain only whitespace are no longer forwarded — `if prompt and prompt.strip():` strips the no-op case. | diff --git a/README.md b/README.md index 3cfb57b..e274628 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,8 @@ You can either use `config.ini` for a global API key, or connect a **Flux Config | Flux Dev (BFL) | Text-to-image with Flux Dev | | Flux Pro Fill (BFL) | Inpainting / outpainting | | Flux Pro Expand (BFL) | Outpainting with directional padding | +| Flux Erase (BFL) | Object removal via binary mask (`flux-tools/erase-v1`) | +| Flux Outpaint (BFL) | Image extension via target canvas + reference offsets (`flux-tools/outpainting-v1`) | | Flux Kontext Pro (BFL) | Image editing with context (up to 4 images) | | Flux Kontext Max (BFL) | Image editing with context, max quality | | Flux 2 Max (BFL) | Flux 2 Max generation | @@ -75,12 +77,16 @@ You can either use `config.ini` for a global API key, or connect a **Flux Config ### Utils | Node | Description | |---|---| -| Image to Base64 (BFL) | Convert a ComfyUI IMAGE to a base64 data URI for use as image input | +| Image to Base64 (BFL) | Convert a ComfyUI IMAGE to base64 — choose `jpeg` (default) or `png` (lossless, recommended for masks) | ## Workflow Example workflows are available in the `workflows` folder. +## Changelog + +See [CHANGELOG.md](CHANGELOG.md). + ## Contributors - [@pleberer](https://github.com/pleberer) diff --git a/__init__.py b/__init__.py index ce625a5..70b9c2a 100644 --- a/__init__.py +++ b/__init__.py @@ -1,11 +1,12 @@ -import importlib.util import importlib +import importlib.util node_list = [ -"api_node", -"finetune", -"config_node", -"utils", + "api_node", + "finetune", + "flux_tools", + "config_node", + "utils", ] NODE_CLASS_MAPPINGS = {} @@ -20,4 +21,4 @@ for module_name in node_list: WEB_DIRECTORY = "./web" -__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"] \ No newline at end of file +__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"] diff --git a/nodes/base.py b/nodes/base.py index 52785d5..b964e33 100644 --- a/nodes/base.py +++ b/nodes/base.py @@ -1,16 +1,17 @@ -import requests -from PIL import Image import io -import numpy as np -import torch -import os import time -from .status import Status -from .config import config_loader + +import numpy as np +import requests +import torch +from PIL import Image + from .config_node import get_config_loader +from .status import Status REQUEST_TIMEOUT = 300 # seconds for connect + read + class BaseFlux: RETURN_TYPES = ("IMAGE",) FUNCTION = "generate_image" @@ -18,7 +19,7 @@ class BaseFlux: def process_result(self, result, output_format="jpeg"): try: - sample_url = result['result']['sample'] + sample_url = result["result"]["sample"] img_response = requests.get(sample_url) img = Image.open(io.BytesIO(img_response.content)) @@ -38,7 +39,7 @@ class BaseFlux: return self.create_blank_image() def create_blank_image(self): - blank_img = Image.new('RGB', (512, 512), color='black') + blank_img = Image.new("RGB", (512, 512), color="black") img_array = np.array(blank_img).astype(np.float32) / 255.0 img_tensor = torch.from_numpy(img_array)[None,] return (img_tensor,) @@ -90,7 +91,10 @@ class BaseFlux: print(f"[BFL] Poll response: {result_response.status_code}") if result_response.status_code != 200: - print(f"[BFL] HTTP error on attempt {attempt}/{max_attempts}: {result_response.status_code}, {result_response.text}") + print( + f"[BFL] HTTP error on attempt {attempt}/{max_attempts}: " + f"{result_response.status_code}, {result_response.text}" + ) attempt += 1 if attempt <= max_attempts: time.sleep(5) @@ -129,22 +133,28 @@ class BaseFlux: time.sleep(5) elapsed = time.time() - start_time - print(f"[BFL] All {max_attempts} attempts exhausted for task {task_id} after {elapsed:.1f}s — returning blank image.") + print( + f"[BFL] All {max_attempts} attempts exhausted for task {task_id} " + f"after {elapsed:.1f}s — returning blank image." + ) return self.create_blank_image() def generate_image(self, url_path, arguments, config_override=None): - if "width" in arguments and "height" in arguments: - self.check_multiple_of_32(arguments["width"], arguments["height"]) - try: + if "width" in arguments and "height" in arguments: + self.check_multiple_of_32(arguments["width"], arguments["height"]) + task_id = self.post_request(url_path, arguments, config_override) if task_id: print(f"Task ID '{task_id}'") - return self.get_result(task_id, output_format=arguments.get("output_format", "jpeg"), config_override=config_override) + return self.get_result( + task_id, output_format=arguments.get("output_format", "jpeg"), config_override=config_override + ) return self.create_blank_image() except Exception as e: print(f"Error generating image: {str(e)}") return self.create_blank_image() + class BaseFinetuneFlux(BaseFlux): - CATEGORY = "BFL/Finetune" + CATEGORY = "BFL/Finetune" diff --git a/nodes/flux_tools.py b/nodes/flux_tools.py new file mode 100644 index 0000000..4586d2a --- /dev/null +++ b/nodes/flux_tools.py @@ -0,0 +1,240 @@ +from .base import BaseFlux + + +class FluxErase(BaseFlux): + CATEGORY = "BFL" + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "image": ( + "STRING", + {"default": "", "tooltip": "Input image (base64-encoded string)."}, + ), + "mask": ( + "STRING", + { + "default": "", + "tooltip": ( + "Black/white mask (base64-encoded string). White (255) = remove, black (0) = keep. " + "Must match the input image dimensions." + ), + }, + ), + "dilate_pixels": ( + "INT", + { + "default": 10, + "min": 0, + "max": 25, + "tooltip": ( + "Pixels to dilate the mask before removal. Range 0-25, default 10. " + "Helps the model fully cover object edges." + ), + }, + ), + "safety_tolerance": ( + "INT", + { + "default": 2, + "min": 0, + "max": 5, + "tooltip": ( + "Tolerance level for input and output moderation. " + "Between 0 and 5, 0 being most strict, 5 being least strict." + ), + }, + ), + "output_format": ( + ["png", "jpeg"], + {"default": "png", "tooltip": "png (default) or jpeg."}, + ), + }, + "optional": { + "seed": ( + "INT", + {"default": -1, "tooltip": "Optional seed for reproducibility. -1 = random."}, + ), + "webhook_url": ( + "STRING", + {"default": "", "tooltip": "URL to receive webhook notifications."}, + ), + "webhook_secret": ( + "STRING", + {"default": "", "tooltip": "Optional secret for webhook signature verification."}, + ), + "config": ( + "BFL_CONFIG", + {"tooltip": "Optional Flux Config (BFL) override for x-key, base URL, and region."}, + ), + }, + } + + def generate_image( + self, + image, + mask, + dilate_pixels, + safety_tolerance, + output_format, + seed=-1, + webhook_url="", + webhook_secret="", + config=None, + ): + arguments = { + "image": image, + "mask": mask, + "dilate_pixels": dilate_pixels, + "safety_tolerance": safety_tolerance, + "output_format": output_format, + } + if seed != -1: + arguments["seed"] = seed + if webhook_url: + arguments["webhook_url"] = webhook_url + if webhook_secret: + arguments["webhook_secret"] = webhook_secret + return super().generate_image("flux-tools/erase-v1", arguments, config) + + +class FluxOutpaint(BaseFlux): + CATEGORY = "BFL" + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "input_image": ( + "STRING", + {"default": "", "tooltip": "Reference image to expand (base64-encoded string)."}, + ), + "width": ( + "INT", + { + "default": 1024, + "min": 64, + "max": 4096, + "step": 32, + "tooltip": "Target canvas width in pixels (>=64).", + }, + ), + "height": ( + "INT", + { + "default": 1024, + "min": 64, + "max": 4096, + "step": 32, + "tooltip": "Target canvas height in pixels (>=64).", + }, + ), + "output_format": ( + ["png", "jpeg"], + {"default": "png", "tooltip": "png (default) or jpeg."}, + ), + }, + "optional": { + "prompt": ( + "STRING", + { + "default": "", + "multiline": True, + "tooltip": ( + "Experimental: optional text guidance for the outpainted region. " + "The model may not strictly follow this prompt; the visual content of the input image " + "is the primary signal. Leave unset for default behavior." + ), + }, + ), + "center_reference": ( + "BOOLEAN", + { + "default": True, + "tooltip": ( + "When True (default), the server centers the reference image on the canvas " + "(reference_offset_x/y are omitted from the request). " + "When False, the reference_offset_x/y values below are sent explicitly." + ), + }, + ), + "reference_offset_x": ( + "INT", + { + "default": 0, + "min": -8192, + "max": 8192, + "tooltip": ( + "Left offset (px) of the reference image's top-left corner on the canvas. " + "Negative values allowed. None = center horizontally." + ), + }, + ), + "reference_offset_y": ( + "INT", + { + "default": 0, + "min": -8192, + "max": 8192, + "tooltip": ( + "Top offset (px) of the reference image's top-left corner on the canvas. " + "Negative values allowed. None = center vertically." + ), + }, + ), + "auto_crop": ( + "BOOLEAN", + { + "default": False, + "tooltip": ( + "If true, crop the reference image to the canvas bounds when it extends beyond the edges. " + "Defaults to false (out-of-bounds placements return 422)." + ), + }, + ), + "config": ( + "BFL_CONFIG", + {"tooltip": "Optional Flux Config (BFL) override for x-key, base URL, and region."}, + ), + }, + } + + def generate_image( + self, + input_image, + width, + height, + output_format, + prompt="", + center_reference=True, + reference_offset_x=0, + reference_offset_y=0, + auto_crop=False, + config=None, + ): + arguments = { + "input_image": input_image, + "width": width, + "height": height, + "output_format": output_format, + } + if not center_reference: + arguments["reference_offset_x"] = reference_offset_x + arguments["reference_offset_y"] = reference_offset_y + if prompt and prompt.strip(): + arguments["prompt"] = prompt + if auto_crop: + arguments["auto_crop"] = auto_crop + return super().generate_image("flux-tools/outpainting-v1", arguments, config) + + +NODE_CLASS_MAPPINGS = { + "FluxErase_BFL": FluxErase, + "FluxOutpaint_BFL": FluxOutpaint, +} + +NODE_DISPLAY_NAME_MAPPINGS = { + "FluxErase_BFL": "Flux Erase (BFL)", + "FluxOutpaint_BFL": "Flux Outpaint (BFL)", +} diff --git a/nodes/utils.py b/nodes/utils.py index 07a0783..6d1b595 100644 --- a/nodes/utils.py +++ b/nodes/utils.py @@ -1,5 +1,6 @@ -import io import base64 +import io + import numpy as np from PIL import Image @@ -10,31 +11,30 @@ class ImageToBase64: return { "required": { "image": ("IMAGE",), - } + }, + "optional": { + "image_format": (["jpeg", "png"], {"default": "jpeg"}), + }, } RETURN_TYPES = ("STRING",) FUNCTION = "convert" CATEGORY = "BFL/Utils" - def convert(self, image): + def convert(self, image, image_format="jpeg"): img_array = (image[0].numpy() * 255).astype(np.uint8) pil_image = Image.fromarray(img_array) pil_image = pil_image.convert("RGB") buffer = io.BytesIO() - pil_image.save(buffer, format="JPEG") + pil_image.save(buffer, format=image_format.upper()) b64 = base64.b64encode(buffer.getvalue()).decode("utf-8") return (b64,) -NODE_CLASS_MAPPINGS = { - "ImageToBase64_BFL": ImageToBase64 -} +NODE_CLASS_MAPPINGS = {"ImageToBase64_BFL": ImageToBase64} -NODE_DISPLAY_NAME_MAPPINGS = { - "ImageToBase64_BFL": "Image to Base64 (BFL)" -} +NODE_DISPLAY_NAME_MAPPINGS = {"ImageToBase64_BFL": "Image to Base64 (BFL)"} diff --git a/pyproject.toml b/pyproject.toml index 8340a8b..8a64b07 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "comfyui-flux-bfl-api" description = "Custom nodes for integrating Flux models with the BFL API." -version = "1.0.13" +version = "1.1.0" license = {file = "LICENSE"} dependencies = ["torch"] diff --git a/workflows/BFL-API-tools.json b/workflows/BFL-API-tools.json new file mode 100644 index 0000000..4371a25 --- /dev/null +++ b/workflows/BFL-API-tools.json @@ -0,0 +1 @@ +{"id":"8470242f-20f2-4d98-8010-cd028da90cf0","revision":0,"last_node_id":35,"last_link_id":40,"nodes":[{"id":31,"type":"LoadImage","pos":[1032.6198623827217,519.9128686614227],"size":[282.798828125,314.00000000000006],"flags":{},"order":0,"mode":0,"inputs":[{"localized_name":"image","name":"image","type":"COMBO","widget":{"name":"image"},"link":null},{"localized_name":"choose file to upload","name":"upload","type":"IMAGEUPLOAD","widget":{"name":"upload"},"link":null}],"outputs":[{"localized_name":"IMAGE","name":"IMAGE","type":"IMAGE","links":[35]},{"localized_name":"MASK","name":"MASK","type":"MASK","links":[36]}],"properties":{"cnr_id":"comfy-core","ver":"0.22.0","Node name for S&R":"LoadImage"},"widgets_values":["Istanbul_asv2020-02_img53_Maiden's_Tower.jpg","image"]},{"id":21,"type":"ImageToBase64_BFL","pos":[1327.2305751742256,526.7113559014133],"size":[270,58],"flags":{},"order":3,"mode":0,"inputs":[{"localized_name":"image","name":"image","type":"IMAGE","link":35},{"localized_name":"image_format","name":"image_format","shape":7,"type":"COMBO","widget":{"name":"image_format"},"link":null}],"outputs":[{"localized_name":"STRING","name":"STRING","type":"STRING","links":[30]}],"properties":{"aux_id":"gelasdev/ComfyUI-FLUX-BFL-API","ver":"03014500d07fc4415a061e55d970f27eb396037d","Node name for S&R":"ImageToBase64_BFL","cnr_id":"comfyui-flux-bfl-api"},"widgets_values":["jpeg"]},{"id":23,"type":"MaskToImage","pos":[1331.8410061407271,630.2607299463089],"size":[193.2779296875,26],"flags":{},"order":4,"mode":0,"inputs":[{"localized_name":"mask","name":"mask","type":"MASK","link":36}],"outputs":[{"localized_name":"IMAGE","name":"IMAGE","type":"IMAGE","links":[23]}],"properties":{"cnr_id":"comfy-core","ver":"0.14.1","Node name for S&R":"MaskToImage"},"widgets_values":[]},{"id":22,"type":"ImageToBase64_BFL","pos":[1329.1165194041637,710.2162183822143],"size":[270,58],"flags":{},"order":6,"mode":0,"inputs":[{"localized_name":"image","name":"image","type":"IMAGE","link":23},{"localized_name":"image_format","name":"image_format","shape":7,"type":"COMBO","widget":{"name":"image_format"},"link":null}],"outputs":[{"localized_name":"STRING","name":"STRING","type":"STRING","links":[31]}],"properties":{"aux_id":"gelasdev/ComfyUI-FLUX-BFL-API","ver":"03014500d07fc4415a061e55d970f27eb396037d","Node name for S&R":"ImageToBase64_BFL","cnr_id":"comfyui-flux-bfl-api"},"widgets_values":["jpeg"]},{"id":26,"type":"FluxErase_BFL","pos":[1620.5267779807286,529.1220828121636],"size":[270,250],"flags":{},"order":8,"mode":0,"inputs":[{"localized_name":"config","name":"config","shape":7,"type":"BFL_CONFIG","link":29},{"localized_name":"image","name":"image","type":"STRING","widget":{"name":"image"},"link":30},{"localized_name":"mask","name":"mask","type":"STRING","widget":{"name":"mask"},"link":31},{"localized_name":"dilate_pixels","name":"dilate_pixels","type":"INT","widget":{"name":"dilate_pixels"},"link":null},{"localized_name":"safety_tolerance","name":"safety_tolerance","type":"INT","widget":{"name":"safety_tolerance"},"link":null},{"localized_name":"output_format","name":"output_format","type":"COMBO","widget":{"name":"output_format"},"link":null},{"localized_name":"seed","name":"seed","shape":7,"type":"INT","widget":{"name":"seed"},"link":null},{"localized_name":"webhook_url","name":"webhook_url","shape":7,"type":"STRING","widget":{"name":"webhook_url"},"link":null},{"localized_name":"webhook_secret","name":"webhook_secret","shape":7,"type":"STRING","widget":{"name":"webhook_secret"},"link":null}],"outputs":[{"localized_name":"IMAGE","name":"IMAGE","type":"IMAGE","links":[34]}],"properties":{"aux_id":"gelasdev/ComfyUI-FLUX-BFL-API","ver":"ba3a4964dbfd53b34c5fe441dc21cfc6475400e3","Node name for S&R":"FluxErase_BFL","cnr_id":"comfyui-flux-bfl-api"},"widgets_values":["","",10,2,"png",398,"randomize","",""]},{"id":30,"type":"PreviewImage","pos":[1907.8232032441945,525.9240532566708],"size":[140,246.00000000000006],"flags":{},"order":10,"mode":0,"inputs":[{"localized_name":"images","name":"images","type":"IMAGE","link":34}],"outputs":[],"properties":{"cnr_id":"comfy-core","ver":"0.22.0","Node name for S&R":"PreviewImage"},"widgets_values":[]},{"id":33,"type":"LoadImage","pos":[1033.1079416303592,942.0385377778939],"size":[282.798828125,314.0000000000001],"flags":{},"order":2,"mode":0,"inputs":[{"localized_name":"image","name":"image","type":"COMBO","widget":{"name":"image"},"link":null},{"localized_name":"choose file to upload","name":"upload","type":"IMAGEUPLOAD","widget":{"name":"upload"},"link":null}],"outputs":[{"localized_name":"IMAGE","name":"IMAGE","type":"IMAGE","links":[39]},{"localized_name":"MASK","name":"MASK","type":"MASK","links":null}],"properties":{"cnr_id":"comfy-core","ver":"0.22.0","Node name for S&R":"LoadImage"},"widgets_values":["Istanbul_asv2020-02_img53_Maiden's_Tower.jpg","image"]},{"id":27,"type":"FluxConfig_BFL","pos":[570.00021913449,891.0725086783283],"size":[270,106],"flags":{},"order":1,"mode":0,"inputs":[{"localized_name":"x_key","name":"x_key","type":"STRING","widget":{"name":"x_key"},"link":null},{"localized_name":"base_url","name":"base_url","type":"STRING","widget":{"name":"base_url"},"link":null},{"localized_name":"region","name":"region","shape":7,"type":"COMBO","widget":{"name":"region"},"link":null}],"outputs":[{"localized_name":"config","name":"config","type":"BFL_CONFIG","links":[29,37]}],"properties":{"aux_id":"gelasdev/ComfyUI-FLUX-BFL-API","ver":"ba3a4964dbfd53b34c5fe441dc21cfc6475400e3","Node name for S&R":"FluxConfig_BFL","cnr_id":"comfyui-flux-bfl-api"},"widgets_values":["bfl_xxxx","https://api.bfl.ai/v1/","none"]},{"id":34,"type":"ImageToBase64_BFL","pos":[1329.5552748363166,949.3725725334741],"size":[270,58],"flags":{},"order":5,"mode":0,"inputs":[{"localized_name":"image","name":"image","type":"IMAGE","link":39},{"localized_name":"image_format","name":"image_format","shape":7,"type":"COMBO","widget":{"name":"image_format"},"link":null}],"outputs":[{"localized_name":"STRING","name":"STRING","type":"STRING","links":[38]}],"properties":{"aux_id":"gelasdev/ComfyUI-FLUX-BFL-API","ver":"ba3a4964dbfd53b34c5fe441dc21cfc6475400e3","Node name for S&R":"ImageToBase64_BFL"},"widgets_values":["jpeg"]},{"id":32,"type":"FluxOutpaint_BFL","pos":[1329.463803106348,1055.7655738102728],"size":[400,280],"flags":{},"order":7,"mode":0,"inputs":[{"localized_name":"config","name":"config","shape":7,"type":"BFL_CONFIG","link":37},{"localized_name":"input_image","name":"input_image","type":"STRING","widget":{"name":"input_image"},"link":38},{"localized_name":"width","name":"width","type":"INT","widget":{"name":"width"},"link":null},{"localized_name":"height","name":"height","type":"INT","widget":{"name":"height"},"link":null},{"localized_name":"output_format","name":"output_format","type":"COMBO","widget":{"name":"output_format"},"link":null},{"localized_name":"prompt","name":"prompt","shape":7,"type":"STRING","widget":{"name":"prompt"},"link":null},{"localized_name":"center_reference","name":"center_reference","shape":7,"type":"BOOLEAN","widget":{"name":"center_reference"},"link":null},{"localized_name":"reference_offset_x","name":"reference_offset_x","shape":7,"type":"INT","widget":{"name":"reference_offset_x"},"link":null},{"localized_name":"reference_offset_y","name":"reference_offset_y","shape":7,"type":"INT","widget":{"name":"reference_offset_y"},"link":null},{"localized_name":"auto_crop","name":"auto_crop","shape":7,"type":"BOOLEAN","widget":{"name":"auto_crop"},"link":null}],"outputs":[{"localized_name":"IMAGE","name":"IMAGE","type":"IMAGE","links":[40]}],"properties":{"aux_id":"gelasdev/ComfyUI-FLUX-BFL-API","ver":"ba3a4964dbfd53b34c5fe441dc21cfc6475400e3","Node name for S&R":"FluxOutpaint_BFL"},"widgets_values":["",2048,1088,"png","",true,0,0,true]},{"id":35,"type":"PreviewImage","pos":[1749.2458984892883,1057.1330599818646],"size":[140,246.0000000000001],"flags":{},"order":9,"mode":0,"inputs":[{"localized_name":"images","name":"images","type":"IMAGE","link":40}],"outputs":[],"properties":{"cnr_id":"comfy-core","ver":"0.22.0","Node name for S&R":"PreviewImage"},"widgets_values":[]}],"links":[[23,23,0,22,0,"IMAGE"],[29,27,0,26,0,"BFL_CONFIG"],[30,21,0,26,1,"STRING"],[31,22,0,26,2,"STRING"],[34,26,0,30,0,"IMAGE"],[35,31,0,21,0,"IMAGE"],[36,31,1,23,0,"MASK"],[37,27,0,32,0,"BFL_CONFIG"],[38,34,0,32,1,"STRING"],[39,33,0,34,0,"IMAGE"],[40,32,0,35,0,"IMAGE"]],"groups":[{"id":1,"title":"Flux Erase","bounding":[1022.6198623827217,449.91286866142275,1035.2033408614727,394.0000000000001],"color":"#3f789e","flags":{}},{"id":2,"title":"FLUX Outpainting","bounding":[1023.1079416303592,872.0385377778939,876.1379568589291,473.7270360323789],"color":"#3f789e","flags":{}}],"config":{},"extra":{"ds":{"scale":0.5730855330116803,"offset":[277.3399210790535,-195.10862556840937]}},"version":0.4} \ No newline at end of file