Compare commits

...
Author SHA1 Message Date
Cursor AgentandGilad-brudner-bria 11c60b5558 Use versioned User-Agent on all HTTP requests to Bria APIs
- Build User-Agent from installed comfyui-bria-api package version
- Add bria_asset_headers for CDN/S3 and preview downloads without api_token
- Include User-Agent on presigned S3 PUT uploads

Co-authored-by: Gilad-brudner-bria <Gilad-brudner-bria@users.noreply.github.com>
2026-04-16 09:35:39 +00:00
Yazan Numoor c5468b2525 Merge pull request #41 from Bria-AI/update_fibo_edit_node_default_steps_num
update_fibo_edit_node_default_steps_num
2026-04-15 10:01:55 +03:00
Yazan Numoor 22d4cf5ba1 Merge branch 'main' into update_fibo_edit_node_default_steps_num 2026-04-15 10:01:48 +03:00
Yazan Numoor 525938a740 Merge pull request #43 from Bria-AI/WAI-4698
WAI-4698:set User-Agent header
2026-04-15 10:00:55 +03:00
Ubuntu 339f26b64f WAI-4698:set User-Agent header 2026-04-14 11:58:02 +00:00
Yazan Numoor 221f31068c Merge pull request #42 from Bria-AI/WAI-4545
WAI-4545
2026-03-01 16:27:02 +02:00
Ubuntu d394eda558 WAI-4545 2026-02-25 11:19:43 +00:00
Ubuntu 54cadf8578 WAI-4545 2026-02-25 09:34:48 +00:00
Ubuntu a3451b6b86 update_fibo_edit_node_default_steps_num 2026-02-12 16:43:29 +00:00
Yazan Numoor 95e5c74266 Merge pull request #39 from Bria-AI/WAI-4383
WAI-4383
2026-01-25 12:07:01 +02:00
Ubuntu e61b45a79a fix conflict 2026-01-25 10:00:28 +00:00
Yazan Numoor e8ad98c0a6 Merge pull request #40 from Bria-AI/WAI-4150
Wai 4150
2026-01-25 11:10:02 +02:00
Ubuntu 3e9599c0fb WAI-4383: update to support multiple images 2026-01-24 21:06:12 +00:00
Ubuntu a52486bbb1 WAI-4150 2026-01-24 20:58:43 +00:00
Ubuntu aa768b6cdf WAI-4150 2026-01-24 20:36:38 +00:00
Ubuntu fda3d917b2 WAI-4383 2026-01-14 16:37:04 +00:00
Ubuntu a970f19bf2 WAI-4383 2026-01-13 19:38:15 +00:00
Yazan Numoor 2c97deb804 Merge pull request #38 from Bria-AI/WAI-4306
WAI-4306
2025-12-24 11:34:19 +02:00
Ubuntu e9b797ae90 update version 2025-12-24 08:53:18 +00:00
Ubuntu 2c6f9754d3 WAI-4306 2025-12-23 06:44:41 +00:00
Yazan Numoor 7dd5d3a250 Merge pull request #37 from Bria-AI/add_fibo_prefix_to_fibo_nodes
Add fibo prefix to fibo nodes
2025-12-22 13:40:23 +02:00
Ubuntu ddd1252ecb update version 2025-12-22 11:23:11 +00:00
Ubuntu 812f7f88fa add_fibo_prefix_to_fibo_nodes 2025-12-22 11:21:52 +00:00
Yazan Numoor 80332ca13c Update pyproject.toml 2025-12-16 17:12:41 +02:00
Yazan Numoor 2b636d8a57 Merge pull request #35 from Bria-AI/WAI-4253
WAI-4253
2025-12-16 17:12:21 +02:00
40 changed files with 2489 additions and 760 deletions
+27 -10
View File
@@ -2,6 +2,7 @@ from .nodes import (
EraserNode,
GenFillNode,
ImageExpansionNode,
ImageEnhanceNode,
ReplaceBgNode,
RmbgNode,
RemoveForegroundNode,
@@ -38,7 +39,11 @@ from .nodes import (
VideoIncreaseResolutionNode,
VideoEraseElementsNode,
LoadVideoFramesNode,
PreviewVideoURLNode
PreviewVideoURLNode,
FIBOEditNode,
FIBOEditStructuredInstructionNode,
BriaMultiImageSelect,
ProductIntegrateNode
)
# Map the node class to a name used internally by ComfyUI
@@ -46,6 +51,7 @@ NODE_CLASS_MAPPINGS = {
"BriaEraser": EraserNode, # Return the class, not an instance
"BriaGenFill": GenFillNode,
"ImageExpansionNode": ImageExpansionNode,
"ImageEnhanceNode": ImageEnhanceNode,
"ReplaceBgNode": ReplaceBgNode,
"RmbgNode": RmbgNode,
"RemoveForegroundNode": RemoveForegroundNode,
@@ -82,13 +88,19 @@ NODE_CLASS_MAPPINGS = {
"VideoIncreaseResolutionNode":VideoIncreaseResolutionNode,
"VideoEraseElementsNode":VideoEraseElementsNode,
"LoadVideoFramesNode":LoadVideoFramesNode,
"PreviewVideoURLNode":PreviewVideoURLNode
"PreviewVideoURLNode":PreviewVideoURLNode,
"FIBOEditNode": FIBOEditNode,
"FIBOEditStructuredInstructionNode": FIBOEditStructuredInstructionNode,
"BriaMultiImageSelect":BriaMultiImageSelect,
"ProductIntegrateNode": ProductIntegrateNode
}
# Map the node display name to the one shown in the ComfyUI node interface
NODE_DISPLAY_NAME_MAPPINGS = {
"BriaEraser": "Bria Eraser",
"BriaGenFill": "Bria GenFill",
"ImageExpansionNode": "Bria Image Expansion",
"ImageEnhanceNode": "Bria Image Enhance",
"ReplaceBgNode": "Bria Replace Background",
"RmbgNode": "Bria RMBG",
"RemoveForegroundNode": "Bria Remove Foreground",
@@ -112,12 +124,12 @@ NODE_DISPLAY_NAME_MAPPINGS = {
"Text2ImageHDNode": "Bria Text2Image HD",
"ReimagineNode": "Bria Reimagine",
"AttributionByImageNode": "Attribution By Image Node",
"GenerateImageNodeV2": "Generate Image",
"GenerateImageLiteNodeV2": "Generate Image - Lite",
"RefineImageNodeV2": "Refine and Regenerate Image",
"RefineImageLiteNodeV2": "Refine Image - Lite",
"GenerateStructuredPromptNodeV2": "Generate Structured Prompt",
"GenerateStructuredPromptLiteNodeV2": "Generate Structured Prompt - Lite",
"GenerateImageNodeV2": "FIBO - Generate Image",
"GenerateImageLiteNodeV2": "FIBO - Generate Image - Lite",
"RefineImageNodeV2": "FIBO - Refine and Regenerate Image",
"RefineImageLiteNodeV2": "FIBO - Refine Image - Lite",
"GenerateStructuredPromptNodeV2": "FIBO - Generate Structured Prompt",
"GenerateStructuredPromptLiteNodeV2": "FIBO - Generate Structured Prompt - Lite",
"RemoveVideoBackgroundNode": "Bria Remove Video Background",
"VideoSolidColorBackgroundNode":"Bria SolidColor Background Video",
"VideoMaskByPromptNode":"Bria Video Mask By Prompt",
@@ -125,7 +137,12 @@ NODE_DISPLAY_NAME_MAPPINGS = {
"VideoIncreaseResolutionNode":"Bria Video Increase Resolution",
"VideoEraseElementsNode":"Bria Video Erase Elements",
"LoadVideoFramesNode":"Bria Load Video",
"PreviewVideoURLNode":"Bria Preview Video"
"PreviewVideoURLNode":"Bria Preview Video",
"FIBOEditNode": "FIBO - Edit",
"FIBOEditStructuredInstructionNode": "FIBO - Edit - Structured Instruction",
"BriaMultiImageSelect":"Bria Multi Image Select",
"ProductIntegrateNode": "Bria Product Integrate"
}
WEB_DIRECTORY = "./web"
+5 -1
View File
@@ -1,6 +1,7 @@
from .eraser_node import EraserNode
from .generative_fill_node import GenFillNode
from .image_expansion_node import ImageExpansionNode
from .image_enhance_node import ImageEnhanceNode
from .replace_bg_node import ReplaceBgNode
from .rmbg_node import RmbgNode
from .remove_foreground_node import RemoveForegroundNode
@@ -40,4 +41,7 @@ from .video_nodes.video_mask_by_prompt_node import VideoMaskByPromptNode
from .video_nodes.video_mask_by_key_points_node import VideoMaskByKeyPointsNode
from .video_nodes.load_video import LoadVideoFramesNode
from .video_nodes.preview_video_node_from_url import PreviewVideoURLNode
from .fibo_edit_node import FIBOEditNode
from .fibo_edit_structured_instruction_node import FIBOEditStructuredInstructionNode
from .multi_image_select import BriaMultiImageSelect
from .product_integrate_node import ProductIntegrateNode
+39 -37
View File
@@ -1,17 +1,22 @@
import requests
import torch
from .common import deserialize_and_get_comfy_key, preprocess_image, image_to_base64, poll_status_until_completed
from .common import (
bria_json_headers,
image_to_base64,
normalize_images_input,
poll_status_until_completed,
to_pil_safe,
)
class AttributionByImageNode():
@classmethod
def INPUT_TYPES(self):
return {
"required": {
"image": ("IMAGE",),
"model_version": (["2.3", "3.0","3.2"], {"default": "2.3"}),
"images": ("IMAGE",),
"model_version": (["2.3", "3.0", "3.2"], {"default": "2.3"}),
"api_key": ("STRING", {"default": "BRIA_API_TOKEN"}),
},
},
}
RETURN_TYPES = ("STRING",)
@@ -22,46 +27,43 @@ class AttributionByImageNode():
def __init__(self):
self.api_url = "https://engine.prod.bria-api.com/v2/image/attribution/by_image"
# Define the execute method as expected by ComfyUI
def execute(self, image, model_version, api_key):
def execute(self, images, model_version, api_key):
if api_key.strip() == "" or api_key.strip() == "BRIA_API_TOKEN":
raise Exception("Please insert a valid API key.")
api_key = deserialize_and_get_comfy_key(api_key)
images = normalize_images_input(images)
# Check if image is tensor, if so, convert to NumPy array
if isinstance(image, torch.Tensor):
image = preprocess_image(image)
batch_results = []
# Convert image to base64 for the new API format
image_base64 = image_to_base64(image)
payload = {
"image": image_base64,
"model_version": model_version,
}
for idx, pil_image in enumerate(images):
try:
image_base64 = image_to_base64(pil_image)
headers = {
"Content-Type": "application/json",
"api_token": f"{api_key}"
}
payload = {
"image": image_base64,
"model_version": model_version,
}
try:
response = requests.post(self.api_url, json=payload, headers=headers)
if response.status_code == 200 or response.status_code == 202:
print('Initial Attribution via Images API request successful, polling for completion...')
response_dict = response.json()
headers = bria_json_headers(api_key)
status_url = response_dict.get('status_url')
request_id = response_dict.get('request_id')
response = requests.post(self.api_url, json=payload, headers=headers)
if response.status_code not in (200, 202):
raise Exception(f"API request failed with status {response.status_code}: {response.text}")
# Poll until completion
response_dict = response.json()
status_url = response_dict.get('status_url')
if not status_url:
raise Exception("No status_url returned from API")
print(f"Request ID: {request_id}, Status URL: {status_url}")
final_response = poll_status_until_completed(status_url, api_key)
return (str(final_response.get("result",{}).get("content")),)
else:
raise Exception(f"Error: API request failed with status code {response.status_code} {response.text}")
except Exception as e:
raise Exception(f"{e}")
content = str(final_response.get("result", {}).get("content", ""))
batch_results.append(content)
except Exception as e:
print(f"[AttributionByImageNode] Skipping image {idx} due to error: {e}")
batch_results.append("")
# Join all responses with a delimiter
combined_response = "\n---\n".join(batch_results)
return (combined_response,)
+72 -25
View File
@@ -6,16 +6,29 @@ import base64
from torchvision.transforms import ToPILImage
import requests
import time
import json
from importlib.metadata import PackageNotFoundError, version as _package_version
try:
_BRIA_COMFYUI_PACKAGE_VERSION = _package_version("comfyui-bria-api")
except PackageNotFoundError:
_BRIA_COMFYUI_PACKAGE_VERSION = "dev"
BRIA_COMFYUI_USER_AGENT = f"bria/ComfyUI-BRIA-API/{_BRIA_COMFYUI_PACKAGE_VERSION}"
COMFY_KEY_ERROR = (
"Invalid Token Type\n\n"
"The API token you’ve entered is not a ComfyUI token.\n"
"Please use the valid token from your BRIA Account API Keys page:\n"
"https://platform.bria.ai/console/account/api-keys"
)
def bria_asset_headers() -> dict:
"""Headers for asset fetches (CDN/S3 URLs) where api_token is not sent."""
return {"User-Agent": BRIA_COMFYUI_USER_AGENT}
def bria_json_headers(api_token: str) -> dict:
"""Headers for JSON POST requests to Bria API."""
return {
"Content-Type": "application/json",
"api_token": api_token,
"User-Agent": BRIA_COMFYUI_USER_AGENT,
}
def postprocess_image(image):
result_image = Image.open(io.BytesIO(image))
result_image = result_image.convert("RGB")
@@ -41,6 +54,35 @@ def preprocess_image(image):
print("Unexpected image dimensions. Expected 4D tensor.")
return image
def to_pil_safe(image):
"""
Converts a single image tensor or numpy array (H,W,C) to PIL Image.
Handles float32 in 0-1 and uint8.
"""
if isinstance(image, torch.Tensor):
image = image.detach().cpu().numpy()
# If image is empty, replace with 1x1 black
if image.size == 0:
image = np.zeros((1,1,3), dtype=np.uint8)
# Ensure float images are scaled 0-255
if image.dtype in [np.float32, np.float64]:
if image.max() <= 1.0:
image = (image * 255).astype(np.uint8)
else:
image = image.astype(np.uint8)
# Handle grayscale images
if image.ndim == 2:
return Image.fromarray(image, mode="L")
elif image.shape[2] == 3:
return Image.fromarray(image, mode="RGB")
elif image.shape[2] == 4:
return Image.fromarray(image, mode="RGBA")
else:
raise ValueError(f"Cannot convert image with shape {image.shape} to PIL")
def preprocess_mask(mask):
if isinstance(mask, torch.Tensor):
@@ -57,7 +99,6 @@ def preprocess_mask(mask):
def process_request(api_url, image, mask, api_key, visual_input_content_moderation, visual_output_content_moderation):
if api_key.strip() == "" or api_key.strip() == "BRIA_API_TOKEN":
raise Exception("Please insert a valid API key.")
api_key = deserialize_and_get_comfy_key(api_key)
# Check if image and mask are tensors, if so, convert to NumPy arrays
if isinstance(image, torch.Tensor):
@@ -77,10 +118,7 @@ def process_request(api_url, image, mask, api_key, visual_input_content_moderati
"visual_output_content_moderation":visual_output_content_moderation
}
headers = {
"Content-Type": "application/json",
"api_token": f"{api_key}"
}
headers = bria_json_headers(api_key)
try:
response = requests.post(api_url, json=payload, headers=headers)
@@ -99,7 +137,7 @@ def process_request(api_url, image, mask, api_key, visual_input_content_moderati
result_image_url = final_response['result']['image_url']
# Download and process the result image
image_response = requests.get(result_image_url)
image_response = requests.get(result_image_url, headers=bria_asset_headers())
result_image = Image.open(io.BytesIO(image_response.content))
result_image = result_image.convert("RGBA")
result_image = np.array(result_image).astype(np.float32) / 255.0
@@ -132,7 +170,7 @@ def poll_status_until_completed(status_url, api_key, timeout=360, check_interval
Exception: If timeout is reached or API request fails
"""
start_time = time.time()
headers = {"api_token": api_key}
headers = bria_json_headers(api_key)
while time.time() - start_time < timeout:
try:
@@ -156,20 +194,29 @@ def poll_status_until_completed(status_url, api_key, timeout=360, check_interval
raise Exception(f"Timeout reached after {timeout} seconds")
def deserialize_and_get_comfy_key(encoded: str) -> str:
def normalize_images_input(images):
"""
Decodes a base64-encoded JSON token and returns the ComfyUI API key.
Converts various image inputs into a list of PIL images:
- PIL.Image → [PIL.Image]
- list of PIL.Image → unchanged
- torch.Tensor (H,W,C) → [PIL.Image]
- torch.Tensor (B,H,W,C) → list of PIL.Images
"""
try:
decoded = base64.b64decode(encoded).decode("utf-8")
payload = json.loads(decoded)
if payload.get("type") != "comfy":
raise Exception(COMFY_KEY_ERROR)
return payload.get("apiKey")
except Exception as e:
raise Exception(COMFY_KEY_ERROR)
if isinstance(images, Image.Image):
return [images]
elif isinstance(images, list):
return [to_pil_safe(img) if isinstance(img, torch.Tensor) else img for img in images]
elif isinstance(images, torch.Tensor):
if images.ndim == 3: # (H,W,C)
return [to_pil_safe(images)]
elif images.ndim == 4: # (B,H,W,C)
return [to_pil_safe(img) for img in images]
else:
raise ValueError(f"Unsupported tensor shape: {images.shape}")
else:
raise ValueError(f"Unsupported input type: {type(images)}")
+166
View File
@@ -0,0 +1,166 @@
import requests
import torch
from .common import (
bria_asset_headers,
bria_json_headers,
image_to_base64,
poll_status_until_completed,
preprocess_image,
preprocess_mask,
postprocess_image,
)
class FIBOEditNode:
"""FIBO Edit Node - Edit images with instructions"""
api_url = "https://engine.prod.bria-api.com/v2/image/edit"
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"api_token": ("STRING", {"default": "BRIA_API_TOKEN"}),
"images": ("IMAGE",),
},
"optional": {
"instruction": ("STRING",),
"mask": ("MASK",),
"structured_instruction": ("STRING",),
"negative_prompt": ("STRING",),
"steps_num": (
"INT",
{
"default": 30,
"min": 1,
"max": 100,
},
),
"guidance_scale": (
"INT",
{
"default": 5,
"min": 1,
"max": 20,
},
),
"seed": ("INT", {"default": 123456}),
},
}
RETURN_TYPES = ("IMAGE", "STRING", "INT")
RETURN_NAMES = ("IMAGE", "structured_instruction", "seed")
CATEGORY = "API Nodes"
FUNCTION = "execute"
def _validate_token(self, api_token: str):
if api_token.strip() == "" or api_token.strip() == "BRIA_API_TOKEN":
raise Exception("Please insert a valid API token.")
def _build_payload(
self,
instruction,
images,
mask=None,
structured_instruction=None,
negative_prompt=None,
steps_num=50,
guidance_scale=5,
seed=123456,
):
# Process images
if isinstance(images, torch.Tensor):
processed_images = preprocess_image(images)
else:
processed_images = images
payload = {
"images": [image_to_base64(processed_images)],
"steps_num": steps_num,
"guidance_scale": guidance_scale,
"seed": seed,
}
# Add optional mask
if mask is not None:
if isinstance(mask, torch.Tensor):
processed_mask = preprocess_mask(mask)
else:
processed_mask = mask
payload["mask"] = image_to_base64(processed_mask)
# Add optional structured_instruction
if structured_instruction:
payload["structured_instruction"] = structured_instruction
# Add optional structured_instruction
if instruction:
payload["instruction"] = instruction
# Add optional negative_prompt
if negative_prompt:
payload["negative_prompt"] = negative_prompt
return payload
def execute(
self,
api_token,
instruction,
images,
mask=None,
structured_instruction=None,
negative_prompt=None,
steps_num=50,
guidance_scale=5,
seed=123456,
):
self._validate_token(api_token)
payload = self._build_payload(
instruction,
images,
mask,
structured_instruction,
negative_prompt,
steps_num,
guidance_scale,
seed,
)
headers = bria_json_headers(api_token)
try:
response = requests.post(self.api_url, json=payload, headers=headers)
if response.status_code in (200, 202):
print(
f"Initial request successful to {self.api_url}, polling for completion..."
)
response_dict = response.json()
status_url = response_dict.get("status_url")
request_id = response_dict.get("request_id")
if not status_url:
raise Exception("No status_url returned from API")
print(f"Request ID: {request_id}, Status URL: {status_url}")
final_response = poll_status_until_completed(status_url, api_token)
result = final_response.get("result", {})
result_image_url = result.get("image_url")
structured_prompt = result.get("structured_prompt", "")
used_seed = result.get("seed")
image_response = requests.get(
result_image_url,
headers=bria_asset_headers(),
)
result_image = postprocess_image(image_response.content)
return (result_image, structured_prompt, used_seed)
raise Exception(
f"Error: API request failed with status code {response.status_code} {response.text}"
)
except Exception as e:
raise Exception(f"{e}")
@@ -0,0 +1,74 @@
import requests
from .common import (
bria_json_headers,
image_to_base64,
normalize_images_input,
poll_status_until_completed,
)
class FIBOEditStructuredInstructionNode:
"""FIBO Edit Structured Instruction Node - Generate structured instructions for image editing"""
api_url = "https://engine.prod.bria-api.com/v2/structured_instruction/generate"
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"api_token": ("STRING", {"default": "BRIA_API_TOKEN"}),
"images": ("IMAGE",),
"instruction": ("STRING",),
},
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("structured_instruction",)
CATEGORY = "API Nodes"
FUNCTION = "execute"
def _validate_token(self, api_token: str):
if api_token.strip() == "" or api_token.strip() == "BRIA_API_TOKEN":
raise Exception("Please insert a valid API token.")
def _build_payload(self, processed_image, instruction):
payload = {
"instruction": instruction,
"images": [image_to_base64(processed_image)],
}
return payload
def execute(self, api_token, images, instruction):
self._validate_token(api_token)
# Normalize input to list of PIL images
images = normalize_images_input(images)
batch_results = []
for idx, pil_image in enumerate(images):
try:
payload = self._build_payload(pil_image, instruction)
headers = bria_json_headers(api_token)
response = requests.post(self.api_url, json=payload, headers=headers)
if response.status_code not in (200, 202):
raise Exception(f"API request failed with status {response.status_code}: {response.text}")
print(f"Initial request successful for image {idx}, polling for completion...")
response_dict = response.json()
status_url = response_dict.get("status_url")
if not status_url:
raise Exception("No status_url returned from API")
final_response = poll_status_until_completed(status_url, api_token)
result = final_response.get("result", {})
structured_instruction = result.get("structured_instruction", "")
batch_results.append(structured_instruction)
except Exception as e:
print(f"[FIBOEditStructuredInstructionNode] Skipping image {idx} due to error: {e}")
batch_results.append("")
combined_instructions = "\n---\n".join(batch_results)
return (combined_instructions,)
+75 -65
View File
@@ -1,17 +1,15 @@
import requests
import torch
from .common import (
deserialize_and_get_comfy_key,
postprocess_image,
preprocess_image,
bria_asset_headers,
bria_json_headers,
image_to_base64,
normalize_images_input,
poll_status_until_completed,
postprocess_image,
)
class GenerateImageLiteNodeV2:
"""Lite Image Generation Node"""
"""Lite Image Generation Node (multi-image compatible)"""
api_url = "https://engine.prod.bria-api.com/v2/image/generate/lite"
@@ -30,27 +28,13 @@ class GenerateImageLiteNodeV2:
["1:1", "2:3", "3:2", "3:4", "4:3", "4:5", "5:4", "9:16", "16:9"],
{"default": "1:1"},
),
"steps_num": (
"INT",
{
"default": 8,
"min": 8,
"max": 30,
},
),
"guidance_scale": (
"INT",
{
"default": 5,
"min": 3,
"max": 5,
},
),
"seed": ("INT", {"default": 123456}),
"steps_num": ("INT", {"default": 8, "min": 8, "max": 30}),
"guidance_scale": ("INT", {"default": 5, "min": 3, "max": 5}),
"seed": ("STRING", {"default": "123456"}),
},
}
RETURN_TYPES = ("IMAGE", "STRING", "INT")
RETURN_TYPES = ("IMAGE", "STRING", "STRING")
RETURN_NAMES = ("image", "structured_prompt", "seed")
CATEGORY = "API Nodes"
FUNCTION = "execute"
@@ -68,7 +52,7 @@ class GenerateImageLiteNodeV2:
steps_num,
guidance_scale,
seed,
images=None,
processed_image=None,
):
payload = {
"prompt": prompt,
@@ -76,16 +60,12 @@ class GenerateImageLiteNodeV2:
"aspect_ratio": aspect_ratio,
"steps_num": steps_num,
"guidance_scale": guidance_scale,
"seed": seed,
"seed": int(seed),
}
if structured_prompt:
payload["structured_prompt"] = structured_prompt
if images is not None:
if isinstance(images, torch.Tensor):
preprocess_images = preprocess_image(images)
payload["images"] = [image_to_base64(preprocess_images)]
payload["structured_prompt"] = structured_prompt
if processed_image is not None:
payload["images"] = [image_to_base64(processed_image)]
return payload
def execute(
@@ -101,51 +81,81 @@ class GenerateImageLiteNodeV2:
images=None,
):
self._validate_token(api_token)
payload = self._build_payload(
prompt,
model_version,
structured_prompt,
aspect_ratio,
steps_num,
guidance_scale,
seed,
images,
)
api_token = deserialize_and_get_comfy_key(api_token)
images_list = normalize_images_input(images) if images is not None else [None]
headers = {"Content-Type": "application/json", "api_token": api_token}
# Structured prompts per image
if isinstance(structured_prompt, str):
structured_prompts_list = structured_prompt.split("\n---\n")
elif isinstance(structured_prompt, list):
structured_prompts_list = structured_prompt
else:
structured_prompts_list = [""] * len(images_list)
if len(structured_prompts_list) < len(images_list):
structured_prompts_list += [structured_prompts_list[-1]] * (len(images_list) - len(structured_prompts_list))
try:
response = requests.post(self.api_url, json=payload, headers=headers)
# Seeds per image
if isinstance(seed, str):
seed_values = [int(s.strip()) for s in seed.split(",")]
else:
seed_values = [int(seed)]
if len(seed_values) < len(images_list):
seed_values += [seed_values[-1]] * (len(images_list) - len(seed_values))
if response.status_code in (200, 202):
print(
f"Initial request successful to {self.api_url}, polling for completion..."
batch_results = []
batch_structured_prompts = []
batch_seeds = []
for idx, ref_image in enumerate(images_list):
try:
payload = self._build_payload(
prompt,
model_version,
structured_prompts_list[idx],
aspect_ratio,
steps_num,
guidance_scale,
seed_values[idx],
ref_image,
)
headers = bria_json_headers(api_token)
response = requests.post(self.api_url, json=payload, headers=headers)
if response.status_code not in (200, 202):
raise Exception(
f"API request failed with status code {response.status_code}: {response.text}"
)
print(f"GenerateImageLiteNodeV2 - Initial request successful for image {idx}, polling for completion...")
response_dict = response.json()
status_url = response_dict.get("status_url")
request_id = response_dict.get("request_id")
if not status_url:
raise Exception("No status_url returned from API")
print(f"Request ID: {request_id}, Status URL: {status_url}")
final_response = poll_status_until_completed(status_url, api_token)
result = final_response.get("result", {})
result_image_url = result.get("image_url")
structured_prompt = result.get("structured_prompt", "")
used_seed = result.get("seed")
structured_prompt_result = result.get("structured_prompt", "")
used_seed = result.get("seed", seed_values[idx])
image_response = requests.get(result_image_url)
image_response = requests.get(
result_image_url,
headers=bria_asset_headers(),
)
result_image = postprocess_image(image_response.content)
return (result_image, structured_prompt, used_seed)
batch_results.append(result_image)
batch_structured_prompts.append(structured_prompt_result)
batch_seeds.append(str(used_seed))
raise Exception(
f"Error: API request failed with status code {response.status_code} {response.text}"
)
except Exception as e:
print(f"[GenerateImageLiteNodeV2] Skipping iteration {idx} due to error: {e}")
batch_results.append(torch.zeros((1, 512, 512, 3), dtype=torch.float32))
batch_structured_prompts.append("")
batch_seeds.append(str(seed_values[idx]))
except Exception as e:
raise Exception(f"{e}")
output_batch = torch.cat(batch_results, dim=0)
combined_structured_prompts = "\n---\n".join(batch_structured_prompts)
combined_seeds = ",".join(batch_seeds)
return output_batch, combined_structured_prompts, combined_seeds
+78 -65
View File
@@ -2,16 +2,17 @@ import requests
import torch
from .common import (
deserialize_and_get_comfy_key,
postprocess_image,
preprocess_image,
bria_asset_headers,
bria_json_headers,
image_to_base64,
normalize_images_input,
poll_status_until_completed,
postprocess_image,
)
class GenerateImageNodeV2:
"""Standard Image Generation Node"""
"""Standard Image Generation Node (multi-image compatible)"""
api_url = "https://engine.prod.bria-api.com/v2/image/generate"
@@ -31,32 +32,17 @@ class GenerateImageNodeV2:
["1:1", "2:3", "3:2", "3:4", "4:3", "4:5", "5:4", "9:16", "16:9"],
{"default": "1:1"},
),
"steps_num": (
"INT",
{
"default": 50,
"min": 35,
"max": 50,
},
),
"guidance_scale": (
"INT",
{
"default": 5,
"min": 3,
"max": 5,
},
),
"seed": ("INT", {"default": 123456}),
"steps_num": ("INT", {"default": 50, "min": 35, "max": 50}),
"guidance_scale": ("INT", {"default": 5, "min": 3, "max": 5}),
"seed": ("STRING", {"default": "123456"}), # Accept string to match previous node
},
}
RETURN_TYPES = ("IMAGE", "STRING", "INT")
RETURN_TYPES = ("IMAGE", "STRING", "STRING") # images, structured_prompts, seeds
RETURN_NAMES = ("image", "structured_prompt", "seed")
CATEGORY = "API Nodes"
FUNCTION = "execute"
def _validate_token(self, api_token: str):
if api_token.strip() == "" or api_token.strip() == "BRIA_API_TOKEN":
raise Exception("Please insert a valid API token.")
@@ -71,7 +57,7 @@ class GenerateImageNodeV2:
guidance_scale,
seed,
negative_prompt=None,
images=None,
processed_image=None,
):
payload = {
"prompt": prompt,
@@ -79,17 +65,14 @@ class GenerateImageNodeV2:
"aspect_ratio": aspect_ratio,
"steps_num": steps_num,
"guidance_scale": guidance_scale,
"seed": seed,
"negative_prompt":negative_prompt
"seed": int(seed),
}
if structured_prompt:
payload["structured_prompt"] = structured_prompt
if images is not None:
if isinstance(images, torch.Tensor):
preprocess_images = preprocess_image(images)
payload["images"] = [image_to_base64(preprocess_images)]
payload["structured_prompt"] = structured_prompt
if negative_prompt:
payload["negative_prompt"] = negative_prompt
if processed_image is not None:
payload["images"] = [image_to_base64(processed_image)]
return payload
def execute(
@@ -106,52 +89,82 @@ class GenerateImageNodeV2:
images=None,
):
self._validate_token(api_token)
payload = self._build_payload(
prompt,
model_version,
structured_prompt,
aspect_ratio,
steps_num,
guidance_scale,
seed,
negative_prompt,
images,
)
api_token = deserialize_and_get_comfy_key(api_token)
images_list = normalize_images_input(images) if images is not None else [None]
headers = {"Content-Type": "application/json", "api_token": api_token}
# Structured prompts per image
if isinstance(structured_prompt, str):
structured_prompts_list = structured_prompt.split("\n---\n")
elif isinstance(structured_prompt, list):
structured_prompts_list = structured_prompt
else:
structured_prompts_list = [""] * len(images_list)
if len(structured_prompts_list) < len(images_list):
structured_prompts_list += [structured_prompts_list[-1]] * (len(images_list) - len(structured_prompts_list))
try:
response = requests.post(self.api_url, json=payload, headers=headers)
# Seeds per image
if isinstance(seed, str):
seed_values = [int(s.strip()) for s in seed.split(",")]
else:
seed_values = [int(seed)]
if len(seed_values) < len(images_list):
seed_values += [seed_values[-1]] * (len(images_list) - len(seed_values))
if response.status_code in (200, 202):
print(
f"Initial request successful to {self.api_url}, polling for completion..."
batch_results = []
batch_structured_prompts = []
batch_seeds = []
for idx, ref_image in enumerate(images_list):
try:
payload = self._build_payload(
prompt,
model_version,
structured_prompts_list[idx],
aspect_ratio,
steps_num,
guidance_scale,
seed_values[idx],
negative_prompt,
ref_image,
)
headers = bria_json_headers(api_token)
response = requests.post(self.api_url, json=payload, headers=headers)
if response.status_code not in (200, 202):
raise Exception(
f"API request failed with status code {response.status_code}: {response.text}"
)
print(f"GenerateImageNodeV2 - Initial request successful for image {idx}, polling for completion...")
response_dict = response.json()
status_url = response_dict.get("status_url")
request_id = response_dict.get("request_id")
if not status_url:
raise Exception("No status_url returned from API")
print(f"Request ID: {request_id}, Status URL: {status_url}")
final_response = poll_status_until_completed(status_url, api_token)
result = final_response.get("result", {})
result_image_url = result.get("image_url")
structured_prompt = result.get("structured_prompt", "")
used_seed = result.get("seed")
structured_prompt_result = result.get("structured_prompt", "")
used_seed = result.get("seed", seed_values[idx])
image_response = requests.get(result_image_url)
image_response = requests.get(
result_image_url,
headers=bria_asset_headers(),
)
result_image = postprocess_image(image_response.content)
return (result_image, structured_prompt, used_seed)
batch_results.append(result_image)
batch_structured_prompts.append(structured_prompt_result)
batch_seeds.append(str(used_seed))
raise Exception(
f"Error: API request failed with status code {response.status_code} {response.text}"
)
except Exception as e:
print(f"[GenerateImageNodeV2] Skipping iteration {idx} due to error: {e}")
batch_results.append(torch.zeros((1, 512, 512, 3), dtype=torch.float32))
batch_structured_prompts.append("")
batch_seeds.append(str(seed_values[idx]))
except Exception as e:
raise Exception(f"{e}")
# Return all as strings for proper chaining
output_batch = torch.cat(batch_results, dim=0)
combined_structured_prompts = "\n---\n".join(batch_structured_prompts)
combined_seeds = ",".join(batch_seeds)
return output_batch, combined_structured_prompts, combined_seeds
@@ -1,15 +1,14 @@
import requests
from .common import (
deserialize_and_get_comfy_key,
bria_json_headers,
image_to_base64,
normalize_images_input,
poll_status_until_completed,
preprocess_image,
)
import torch
class GenerateStructuredPromptLiteNodeV2:
"""Lite Structured Prompt Generation Node"""
"""Lite Structured Prompt Generation Node (multi-image compatible)"""
api_url = "https://engine.prod.bria-api.com/v2/structured_prompt/generate/lite"
@@ -23,11 +22,11 @@ class GenerateStructuredPromptLiteNodeV2:
"optional": {
"structured_prompt": ("STRING",),
"images": ("IMAGE",),
"seed": ("INT", {"default": 123456}),
"seed": ("STRING", {"default": "123456"}),
},
}
RETURN_TYPES = ("STRING", "INT")
RETURN_TYPES = ("STRING", "STRING") # structured_prompts, seeds as comma-separated string
RETURN_NAMES = ("structured_prompt", "seed")
CATEGORY = "API Nodes"
FUNCTION = "execute"
@@ -36,71 +35,76 @@ class GenerateStructuredPromptLiteNodeV2:
if api_token.strip() == "" or api_token.strip() == "BRIA_API_TOKEN":
raise Exception("Please insert a valid API token.")
def _build_payload(
self,
prompt,
seed,
structured_prompt,
images=None
):
payload = {
"prompt": prompt,
"seed": seed,
}
def _build_payload(self, prompt, seed, structured_prompt, processed_image=None):
payload = {"prompt": prompt, "seed": int(seed)}
if structured_prompt:
payload["structured_prompt"] = structured_prompt
if images is not None:
if isinstance(images, torch.Tensor):
preprocess_images = preprocess_image(images)
payload["images"] = [image_to_base64(preprocess_images)]
if processed_image is not None:
payload["images"] = [image_to_base64(processed_image)]
return payload
def execute(
self,
api_token,
prompt,
seed,
structured_prompt,
images=None,
):
def execute(self, api_token, prompt, seed, structured_prompt, images=None):
self._validate_token(api_token)
payload = self._build_payload(
prompt,
seed,
structured_prompt,
images
)
api_token = deserialize_and_get_comfy_key(api_token)
images_list = normalize_images_input(images) if images is not None else [None]
headers = {"Content-Type": "application/json", "api_token": api_token}
# Seeds per image
if isinstance(seed, str):
seed_values = [int(s.strip()) for s in seed.split(",")]
else:
seed_values = [seed]
if len(seed_values) < len(images_list):
seed_values += [seed_values[-1]] * (len(images_list) - len(seed_values))
try:
response = requests.post(self.api_url, json=payload, headers=headers)
# Structured prompts per image
if isinstance(structured_prompt, str):
structured_prompts_list = structured_prompt.split("\n---\n")
elif isinstance(structured_prompt, list):
structured_prompts_list = structured_prompt
else:
structured_prompts_list = [""] * len(images_list)
if len(structured_prompts_list) < len(images_list):
structured_prompts_list += [structured_prompts_list[-1]] * (len(images_list) - len(structured_prompts_list))
if response.status_code in (200, 202):
print(
f"Initial request successful to {self.api_url}, polling for completion..."
batch_structured_prompts = []
batch_seeds = []
for idx, image in enumerate(images_list):
try:
payload = self._build_payload(
prompt,
seed_values[idx],
structured_prompts_list[idx],
image,
)
response_dict = response.json()
status_url = response_dict.get("status_url")
request_id = response_dict.get("request_id")
headers = bria_json_headers(api_token)
response = requests.post(self.api_url, json=payload, headers=headers)
if response.status_code not in (200, 202):
raise Exception(
f"API request failed with status code {response.status_code}: {response.text}"
)
response_dict = response.json()
print(f"GenerateStructuredPromptLiteNodeV2 - Initial request successful for image {idx}, polling for completion...")
status_url = response_dict.get("status_url")
if not status_url:
raise Exception("No status_url returned from API")
print(f"Request ID: {request_id}, Status URL: {status_url}")
final_response = poll_status_until_completed(status_url, api_token)
result = final_response.get("result", {})
structured_prompt = result.get("structured_prompt", "")
used_seed = result.get("seed", seed)
structured_prompt_result = result.get("structured_prompt", "")
used_seed = result.get("seed", seed_values[idx])
return (structured_prompt, used_seed)
batch_structured_prompts.append(structured_prompt_result)
batch_seeds.append(str(used_seed))
raise Exception(
f"Error: API request failed with status code {response.status_code} {response.text}"
)
except Exception as e:
print(f"[GenerateStructuredPromptLiteNodeV2] Skipping iteration {idx} due to error: {e}")
batch_structured_prompts.append("")
batch_seeds.append(str(seed_values[idx]))
except Exception as e:
raise Exception(f"{e}")
combined_prompts = "\n---\n".join(batch_structured_prompts)
combined_seeds = ",".join(batch_seeds)
return combined_prompts, combined_seeds
+63 -55
View File
@@ -1,14 +1,15 @@
import requests
from .common import (
deserialize_and_get_comfy_key,
bria_json_headers,
image_to_base64,
normalize_images_input,
poll_status_until_completed,
preprocess_image,
)
import torch
class GenerateStructuredPromptNodeV2:
"""Standard Structured Prompt Generation Node"""
"""Structured Prompt Generation Node (multi-image compatible)"""
api_url = "https://engine.prod.bria-api.com/v2/structured_prompt/generate"
@@ -22,11 +23,11 @@ class GenerateStructuredPromptNodeV2:
"optional": {
"structured_prompt": ("STRING",),
"images": ("IMAGE",),
"seed": ("INT", {"default": 123456}),
"seed": ("STRING", {"default": "123456"}),
},
}
RETURN_TYPES = ("STRING", "INT")
RETURN_TYPES = ("STRING", "STRING") # structured_prompts, seeds as comma-separated string
RETURN_NAMES = ("structured_prompt", "seed")
CATEGORY = "API Nodes"
FUNCTION = "execute"
@@ -35,71 +36,78 @@ class GenerateStructuredPromptNodeV2:
if api_token.strip() == "" or api_token.strip() == "BRIA_API_TOKEN":
raise Exception("Please insert a valid API token.")
def _build_payload(
self,
prompt,
seed,
structured_prompt,
images=None
):
payload = {
"prompt": prompt,
"seed": seed,
}
def _build_payload(self, prompt, seed, structured_prompt, processed_image=None):
payload = {"prompt": prompt, "seed": int(seed)}
if structured_prompt:
payload["structured_prompt"] = structured_prompt
if images is not None:
if isinstance(images, torch.Tensor):
preprocess_images = preprocess_image(images)
payload["images"] = [image_to_base64(preprocess_images)]
if processed_image is not None:
payload["images"] = [image_to_base64(processed_image)]
return payload
def execute(
self,
api_token,
prompt,
seed,
structured_prompt,
images=None,
):
def execute(self, api_token, prompt, seed, structured_prompt, images=None):
self._validate_token(api_token)
payload = self._build_payload(
prompt,
seed,
structured_prompt,
images
)
api_token = deserialize_and_get_comfy_key(api_token)
images_list = normalize_images_input(images) if images is not None else [None]
headers = {"Content-Type": "application/json", "api_token": api_token}
# Seeds per image
if isinstance(seed, str):
seed_values = [int(s.strip()) for s in seed.split(",")]
else:
seed_values = [seed]
if len(seed_values) < len(images_list):
seed_values += [seed_values[-1]] * (len(images_list) - len(seed_values))
try:
response = requests.post(self.api_url, json=payload, headers=headers)
# Structured prompts per image
if isinstance(structured_prompt, str):
structured_prompts_list = structured_prompt.split("\n---\n")
elif isinstance(structured_prompt, list):
structured_prompts_list = structured_prompt
else:
structured_prompts_list = [""] * len(images_list)
if len(structured_prompts_list) < len(images_list):
structured_prompts_list += [structured_prompts_list[-1]] * (len(images_list) - len(structured_prompts_list))
if response.status_code in (200, 202):
print(
f"Initial request successful to {self.api_url}, polling for completion..."
batch_structured_prompts = []
batch_seeds = []
for idx, image in enumerate(images_list):
try:
payload = self._build_payload(
prompt,
seed_values[idx],
structured_prompts_list[idx],
image
)
response_dict = response.json()
status_url = response_dict.get("status_url")
request_id = response_dict.get("request_id")
headers = bria_json_headers(api_token)
response = requests.post(self.api_url, json=payload, headers=headers)
if response.status_code not in (200, 202):
raise Exception(
f"API request failed with status code {response.status_code}: {response.text}"
)
response_dict = response.json()
print(f"GenerateStructuredPromptNodeV2 - Initial request successful for image {idx}, polling for completion...")
status_url = response_dict.get("status_url")
if not status_url:
raise Exception("No status_url returned from API")
print(f"Request ID: {request_id}, Status URL: {status_url}")
final_response = poll_status_until_completed(status_url, api_token)
result = final_response.get("result", {})
structured_prompt = result.get("structured_prompt", "")
used_seed = result.get("seed", seed)
structured_prompt_result = result.get("structured_prompt", "")
used_seed = result.get("seed", seed_values[idx])
return (structured_prompt, used_seed)
batch_structured_prompts.append(structured_prompt_result)
batch_seeds.append(str(used_seed)) # Keep as string for passing between nodes
raise Exception(
f"Error: API request failed with status code {response.status_code} {response.text}"
)
except Exception as e:
print(f"[GenerateStructuredPromptNodeV2] Skipping iteration {idx} due to error: {e}")
batch_structured_prompts.append("")
batch_seeds.append(str(seed_values[idx]))
except Exception as e:
raise Exception(f"{e}")
# Return combined structured prompts and seeds as strings
combined_prompts = "\n---\n".join(batch_structured_prompts)
combined_seeds = ",".join(batch_seeds)
return combined_prompts, combined_seeds
+13 -8
View File
@@ -4,7 +4,14 @@ from PIL import Image
import io
import torch
from .common import deserialize_and_get_comfy_key, preprocess_image, preprocess_mask, image_to_base64, poll_status_until_completed
from .common import (
bria_asset_headers,
bria_json_headers,
image_to_base64,
poll_status_until_completed,
preprocess_image,
preprocess_mask,
)
class GenFillNode():
@@ -39,8 +46,6 @@ class GenFillNode():
def execute(self, image, mask, prompt, api_key, seed, prompt_content_moderation, visual_input_content_moderation, visual_output_content_moderation):
if api_key.strip() == "" or api_key.strip() == "BRIA_API_TOKEN":
raise Exception("Please insert a valid API key.")
api_key = deserialize_and_get_comfy_key(api_key)
# Check if image and mask are tensors, if so, convert to NumPy arrays
if isinstance(image, torch.Tensor):
image = preprocess_image(image)
@@ -64,10 +69,7 @@ class GenFillNode():
"version": 2
}
headers = {
"Content-Type": "application/json",
"api_token": f"{api_key}"
}
headers = bria_json_headers(api_key)
try:
# Send initial request to get status URL
@@ -86,7 +88,10 @@ class GenFillNode():
final_response = poll_status_until_completed(status_url, api_key)
result_image_url = final_response['result']['image_url']
image_response = requests.get(result_image_url)
image_response = requests.get(
result_image_url,
headers=bria_asset_headers(),
)
result_image = Image.open(io.BytesIO(image_response.content))
result_image = result_image.convert("RGB")
result_image = np.array(result_image).astype(np.float32) / 255.0
+114
View File
@@ -0,0 +1,114 @@
import io
import requests
import numpy as np
from PIL import Image
import torch
from .common import (
bria_asset_headers,
bria_json_headers,
image_to_base64,
normalize_images_input,
poll_status_until_completed,
)
class ImageEnhanceNode():
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"images": ("IMAGE",),
"api_key": ("STRING", {"default": "BRIA_API_TOKEN"}),
},
"optional": {
"steps_num": ("INT", {"default": 20, "min": 10, "max": 50}),
"resolution": (["1MP", "2MP", "4MP"], {"default": "1MP"}),
"visual_input_content_moderation": ("BOOLEAN", {"default": False}),
"visual_output_content_moderation": ("BOOLEAN", {"default": False}),
"seed": ("INT", {"default": 681794}),
"preserve_alpha": ("BOOLEAN", {"default": True}),
}
}
RETURN_TYPES = ("IMAGE", "STRING")
RETURN_NAMES = ("output_images", "seeds",)
CATEGORY = "API Nodes"
FUNCTION = "execute"
def __init__(self):
self.api_url = "https://engine.prod.bria-api.com/v2/image/edit/enhance"
def execute(
self,
images,
api_key,
visual_input_content_moderation,
visual_output_content_moderation,
seed,
steps_num,
resolution,
preserve_alpha
):
# Validate API key
if api_key.strip() == "" or api_key.strip() == "BRIA_API_TOKEN":
raise Exception("Please insert a valid API key.")
# Normalize input to list of PIL images
images = normalize_images_input(images)
batch_results = []
batch_seeds = []
for idx, pil_image in enumerate(images):
try:
image_base64 = image_to_base64(pil_image)
payload = {
"image": image_base64,
"visual_input_content_moderation": visual_input_content_moderation,
"visual_output_content_moderation": visual_output_content_moderation,
"seed": seed,
"steps_num": steps_num,
"resolution": resolution,
"preserve_alpha": preserve_alpha
}
headers = bria_json_headers(api_key)
# Send request
response = requests.post(self.api_url, json=payload, headers=headers)
if response.status_code not in (200, 202):
raise Exception(f"API request failed with status {response.status_code}: {response.text}")
# Poll until completion
response_dict = response.json()
status_url = response_dict.get("status_url")
if not status_url:
raise Exception("No status_url returned from API")
print(f"ImageEnhanceNode - Initial request successful for image {idx}, polling for completion...")
final_response = poll_status_until_completed(status_url, api_key)
result_image_url = final_response["result"]["image_url"]
used_seed = final_response["result"].get("seed", seed)
# Download and process image
image_response = requests.get(
result_image_url,
headers=bria_asset_headers(),
)
result_image = Image.open(io.BytesIO(image_response.content)).convert("RGB")
result_array = np.array(result_image).astype(np.float32) / 255.0
result_tensor = torch.from_numpy(result_array) # shape: (H,W,C)
batch_results.append(result_tensor)
batch_seeds.append(used_seed)
except Exception as e:
print(f"[ImageEnhanceNode] Skipping image {idx} due to error: {e}")
# Append fallback tensor with same size as input
fallback_array = np.array(pil_image).astype(np.float32) / 255.0
batch_results.append(torch.from_numpy(fallback_array))
batch_seeds.append(seed)
# Return list of tensors (not concatenated) + comma-separated seeds
combined_seeds = ",".join(map(str, batch_seeds))
return (batch_results, combined_seeds)
+101 -100
View File
@@ -1,136 +1,137 @@
import numpy as np
import requests
from PIL import Image
import io
import requests
import numpy as np
from PIL import Image
import torch
from .common import deserialize_and_get_comfy_key, image_to_base64, preprocess_image, poll_status_until_completed
from .common import (
bria_asset_headers,
bria_json_headers,
image_to_base64,
normalize_images_input,
poll_status_until_completed,
)
class ImageExpansionNode():
@classmethod
def INPUT_TYPES(self):
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",), # Input image from another node
"api_key": ("STRING", {"default": "BRIA_API_TOKEN"}), # API Key input with a default value
"images": ("IMAGE",),
"api_key": ("STRING", {"default": "BRIA_API_TOKEN"}),
},
"optional": {
"original_image_size": ("STRING",),
"original_image_location": ("STRING",),
"canvas_size": ("STRING", {"default": "1000, 1000"}),
"aspect_ratio": (["1:1", "2:3", "3:2", "3:4", "4:3", "4:5", "5:4", "9:16", "16:9","None"], {"default": "None"}),
"aspect_ratio": (["1:1", "2:3", "3:2", "3:4", "4:3", "4:5", "5:4", "9:16", "16:9", "None"], {"default": "None"}),
"prompt": ("STRING", {"default": ""}),
"seed": ("INT", {"default": 681794}),
"seed": ("STRING", {"default": "681794"}), # <-- accepts seeds from Enhance
"negative_prompt": ("STRING", {"default": "Ugly, mutated"}),
"prompt_content_moderation": ("BOOLEAN", {"default": False}),
"preserve_alpha": ("BOOLEAN", {"default": True}),
"visual_input_content_moderation": ("BOOLEAN", {"default": False}),
"visual_output_content_moderation": ("BOOLEAN", {"default": False}),
}
}
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("output_image",)
RETURN_NAMES = ("output_images",)
CATEGORY = "API Nodes"
FUNCTION = "execute" # This is the method that will be executed
FUNCTION = "execute"
def __init__(self):
self.api_url = "https://engine.prod.bria-api.com/v2/image/edit/expand" # Image Expansion API URL
self.api_url = "https://engine.prod.bria-api.com/v2/image/edit/expand"
# Define the execute method as expected by ComfyUI
def execute(self, image,
original_image_size,
original_image_location,
canvas_size,
aspect_ratio,
prompt,
seed,
negative_prompt,
prompt_content_moderation,
preserve_alpha,
visual_input_content_moderation,
visual_output_content_moderation,
api_key):
if api_key.strip() == "" or api_key.strip() == "BRIA_API_TOKEN":
def execute(
self,
images,
original_image_size,
original_image_location,
canvas_size,
aspect_ratio,
prompt,
seed,
negative_prompt,
prompt_content_moderation,
preserve_alpha,
visual_input_content_moderation,
visual_output_content_moderation,
api_key
):
if api_key.strip() in ("", "BRIA_API_TOKEN"):
raise Exception("Please insert a valid API key.")
api_key = deserialize_and_get_comfy_key(api_key)
images = normalize_images_input(images)
canvas_size = [int(x.strip()) for x in canvas_size.split(",")] if canvas_size else ()
original_image_size = [int(x.strip()) for x in original_image_size.split(",")] if original_image_size else ()
original_image_location = [int(x.strip()) for x in original_image_location.split(",")] if original_image_location else ()
canvas_size = [int(x.strip()) for x in canvas_size.split(",")] if canvas_size else ()
if negative_prompt == "":
negative_prompt = " " # hack to avoid error in triton which expects non-empty string
# Check if image and mask are tensors, if so, convert to NumPy arrays
if isinstance(image, torch.Tensor):
image = preprocess_image(image)
# Prepare per-image seeds
seed_values = [int(s.strip()) for s in seed.split(",")] if isinstance(seed, str) else [seed]
if len(seed_values) < len(images):
seed_values += [seed_values[-1]] * (len(images) - len(seed_values))
# Convert the image directly to Base64 string
image_base64 = image_to_base64(image)
if aspect_ratio and aspect_ratio != "None":
payload = {
"image": image_base64,
"aspect_ratio": aspect_ratio,
"prompt": prompt,
"negative_prompt": negative_prompt,
"seed": seed,
"prompt_content_moderation": prompt_content_moderation,
"preserve_alpha": preserve_alpha,
"visual_input_content_moderation": visual_input_content_moderation,
"visual_output_content_moderation": visual_output_content_moderation
}
else:
payload = {
"image": image_base64,
"original_image_size": original_image_size,
"original_image_location": original_image_location,
"canvas_size": canvas_size,
"prompt": prompt,
"negative_prompt": negative_prompt,
"seed": seed,
"prompt_content_moderation": prompt_content_moderation,
"preserve_alpha": preserve_alpha,
"visual_input_content_moderation": visual_input_content_moderation,
"visual_output_content_moderation": visual_output_content_moderation
}
if not negative_prompt:
negative_prompt = " "
headers = {
"Content-Type": "application/json",
"api_token": f"{api_key}"
}
batch_results = []
for idx, pil_image in enumerate(images):
try:
image_base64 = image_to_base64(pil_image)
if aspect_ratio and aspect_ratio != "None":
payload = {
"image": image_base64,
"aspect_ratio": aspect_ratio,
"prompt": prompt,
"negative_prompt": negative_prompt,
"seed": seed_values[idx],
"prompt_content_moderation": prompt_content_moderation,
"preserve_alpha": preserve_alpha,
"visual_input_content_moderation": visual_input_content_moderation,
"visual_output_content_moderation": visual_output_content_moderation
}
else:
payload = {
"image": image_base64,
"original_image_size": original_image_size,
"original_image_location": original_image_location,
"canvas_size": canvas_size,
"prompt": prompt,
"negative_prompt": negative_prompt,
"seed": seed_values[idx],
"prompt_content_moderation": prompt_content_moderation,
"preserve_alpha": preserve_alpha,
"visual_input_content_moderation": visual_input_content_moderation,
"visual_output_content_moderation": visual_output_content_moderation
}
headers = bria_json_headers(api_key)
response = requests.post(self.api_url, json=payload, headers=headers)
if response.status_code not in (200, 202):
raise Exception(f"API request failed with status {response.status_code}: {response.text}")
try:
response = requests.post(self.api_url, json=payload, headers=headers)
if response.status_code == 200 or response.status_code == 202:
print('Initial image expansion request successful, polling for completion...')
response_dict = response.json()
status_url = response_dict.get('status_url')
request_id = response_dict.get('request_id')
status_url = response_dict.get("status_url")
if not status_url:
raise Exception("No status_url returned from API")
print(f"Request ID: {request_id}, Status URL: {status_url}")
# Poll status URL until completion
final_response = poll_status_until_completed(status_url, api_key)
# Get the result image URL
result_image_url = final_response['result']['image_url']
# Download and process the result image
image_response = requests.get(result_image_url)
result_image = Image.open(io.BytesIO(image_response.content))
result_image = result_image.convert("RGB")
result_image = np.array(result_image).astype(np.float32) / 255.0
result_image = torch.from_numpy(result_image)[None,]
return (result_image,)
else:
raise Exception(f"Error: API request failed with status code {response.status_code}: {response.text}")
except Exception as e:
raise Exception(f"{e}")
print(f"ImageExpansionNode - Initial request successful for image {idx}, polling for completion...")
final_response = poll_status_until_completed(status_url, api_key)
result_image_url = final_response["result"]["image_url"]
image_response = requests.get(
result_image_url,
headers=bria_asset_headers(),
)
result_image = Image.open(io.BytesIO(image_response.content)).convert("RGB")
result_tensor = torch.from_numpy(np.array(result_image).astype(np.float32) / 255.0)
batch_results.append(result_tensor)
except Exception as e:
print(f"[ImageExpansionNode] Skipping image {idx} due to error: {e}")
fallback_array = np.array(pil_image).astype(np.float32) / 255.0
batch_results.append(torch.from_numpy(fallback_array))
return (batch_results,)
+85
View File
@@ -0,0 +1,85 @@
import os
import json
from typing import List
import torch
import torch.nn.functional as F
import numpy as np
from PIL import Image, ImageOps
try:
from folder_paths import get_input_directory
except Exception:
get_input_directory = None
IMG_EXTS = (".png", ".jpg", ".jpeg", ".webp", ".bmp", ".tif", ".tiff")
def input_root() -> str:
return os.path.abspath(get_input_directory() if get_input_directory else "input")
def parse_paths(value: str) -> List[str]:
if not value:
return []
try:
data = json.loads(value)
if isinstance(data, list):
return [str(x) for x in data]
except Exception:
pass
return []
class BriaMultiImageSelect:
"""
Select multiple images and return them as a list of PIL Images.
Images keep their original size.
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"selected_paths": (
"STRING",
{
"multiline": True,
"default": "",
"placeholder": "Filled automatically by Select Images button",
},
),
}
}
RETURN_TYPES = ("IMAGE", "STRING")
RETURN_NAMES = ("images", "filenames")
FUNCTION = "load"
CATEGORY = "API Nodes"
def load(self, selected_paths: str):
paths = parse_paths(selected_paths)
if not paths:
raise RuntimeError("BriaMultiImageSelect: No images selected")
root = input_root()
pil_images: List[Image.Image] = []
names: List[str] = []
for rel in paths:
abs_path = os.path.join(root, rel)
if not abs_path.lower().endswith(IMG_EXTS):
continue
if not os.path.isfile(abs_path):
continue
pil_images.append(Image.open(abs_path))
names.append(os.path.splitext(os.path.basename(rel))[0])
if not pil_images:
raise RuntimeError("BriaMultiImageSelect: No valid images found")
filenames = ", ".join(names)
return (pil_images, filenames)
+138
View File
@@ -0,0 +1,138 @@
import requests
import torch
from .common import (
bria_asset_headers,
bria_json_headers,
image_to_base64,
poll_status_until_completed,
postprocess_image,
preprocess_image,
)
class ProductIntegrateNode:
"""Product Integrate Node - Integrate a single product into a background scene"""
api_url = "https://engine.prod.bria-api.com/v2/image/edit/product/integrate"
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"api_token": ("STRING", {"default": "BRIA_API_TOKEN"}),
"scene": ("IMAGE",),
"product_image": ("IMAGE",),
"x_coordinate": ("INT", {"default": 0, "min": 0, "max": 10000}),
"y_coordinate": ("INT", {"default": 0, "min": 0, "max": 10000}),
"width": ("INT", {"default": 512, "min": 1, "max": 10000}),
"height": ("INT", {"default": 512, "min": 1, "max": 10000}),
},
"optional": {
"seed": ("STRING", {"default": "123456"}),
}
}
RETURN_TYPES = ("IMAGE", "INT")
RETURN_NAMES = ("IMAGE", "seed")
CATEGORY = "API Nodes"
FUNCTION = "execute"
def _validate_token(self, api_token: str):
if api_token.strip() == "" or api_token.strip() == "BRIA_API_TOKEN":
raise Exception("Please insert a valid API token.")
def _build_payload(
self,
scene_image,
product_image,
x_coordinate,
y_coordinate,
width,
height,
seed,
):
payload = {
"scene": image_to_base64(scene_image),
"products": [
{
"image": image_to_base64(product_image),
"coordinates": {
"x": x_coordinate,
"y": y_coordinate,
"width": width,
"height": height,
}
}
],
"seed": int(seed),
}
return payload
def execute(
self,
api_token,
scene,
product_image,
x_coordinate,
y_coordinate,
width,
height,
seed,
):
self._validate_token(api_token)
# Process single scene image
if isinstance(scene, torch.Tensor):
processed_scene = preprocess_image(scene)
if isinstance(product_image, torch.Tensor):
processed_product = preprocess_image(product_image)
payload = self._build_payload(
processed_scene,
processed_product,
x_coordinate,
y_coordinate,
width,
height,
seed,
)
headers = bria_json_headers(api_token)
try:
response = requests.post(self.api_url, json=payload, headers=headers)
if response.status_code in (200, 202):
print(
f"Initial product integrate request successful, polling for completion..."
)
response_dict = response.json()
status_url = response_dict.get("status_url")
request_id = response_dict.get("request_id")
if not status_url:
raise Exception("No status_url returned from API")
print(f"Request ID: {request_id}, Status URL: {status_url}")
final_response = poll_status_until_completed(status_url, api_token)
result = final_response.get("result", {})
result_image_url = result.get("image_url")
used_seed = result.get("seed", seed)
image_response = requests.get(
result_image_url,
headers=bria_asset_headers(),
)
result_image = postprocess_image(image_response.content)
return (result_image, used_seed)
else:
raise Exception(
f"API request failed with status code {response.status_code} {response.text}"
)
except Exception as e:
raise Exception(f"[ProductIntegrateNode] Error: {e}")
+13 -5
View File
@@ -1,5 +1,11 @@
import requests
from .common import deserialize_and_get_comfy_key, poll_status_until_completed, postprocess_image
from .common import (
bria_asset_headers,
bria_json_headers,
poll_status_until_completed,
postprocess_image,
)
@@ -96,8 +102,7 @@ class RefineImageLiteNodeV2:
guidance_scale,
seed,
)
api_token = deserialize_and_get_comfy_key(api_token)
headers = {"Content-Type": "application/json", "api_token": api_token}
headers = bria_json_headers(api_token)
try:
response = requests.post(self.api_url, json=payload, headers=headers)
@@ -130,7 +135,7 @@ class RefineImageLiteNodeV2:
"seed": used_seed,
}
headers = {"Content-Type": "application/json", "api_token": api_token}
headers = bria_json_headers(api_token)
response = requests.post(self.generate_api_url, json=payloadForImageGenetrate, headers=headers)
@@ -154,7 +159,10 @@ class RefineImageLiteNodeV2:
structured_prompt = result.get("structured_prompt", "")
used_seed = result.get("seed")
image_response = requests.get(result_image_url)
image_response = requests.get(
result_image_url,
headers=bria_asset_headers(),
)
result_image = postprocess_image(image_response.content)
return (result_image, structured_prompt, used_seed)
+13 -5
View File
@@ -1,5 +1,11 @@
import requests
from .common import deserialize_and_get_comfy_key, poll_status_until_completed, postprocess_image
from .common import (
bria_asset_headers,
bria_json_headers,
poll_status_until_completed,
postprocess_image,
)
class RefineImageNodeV2:
@@ -97,8 +103,7 @@ class RefineImageNodeV2:
guidance_scale,
seed,
)
api_token = deserialize_and_get_comfy_key(api_token)
headers = {"Content-Type": "application/json", "api_token": api_token}
headers = bria_json_headers(api_token)
try:
response = requests.post(self.api_url, json=payload, headers=headers)
@@ -132,7 +137,7 @@ class RefineImageNodeV2:
"negative_prompt":negative_prompt
}
headers = {"Content-Type": "application/json", "api_token": api_token}
headers = bria_json_headers(api_token)
response = requests.post(self.generate_api_url, json=payloadForImageGenetrate, headers=headers)
@@ -156,7 +161,10 @@ class RefineImageNodeV2:
structured_prompt = result.get("structured_prompt", "")
used_seed = result.get("seed")
image_response = requests.get(result_image_url)
image_response = requests.get(
result_image_url,
headers=bria_asset_headers(),
)
result_image = postprocess_image(image_response.content)
return (result_image, structured_prompt, used_seed)
+12 -4
View File
@@ -1,6 +1,12 @@
import requests
from .common import deserialize_and_get_comfy_key, postprocess_image, preprocess_image, image_to_base64
from .common import (
bria_asset_headers,
bria_json_headers,
image_to_base64,
postprocess_image,
preprocess_image,
)
class ReimagineNode():
@@ -38,7 +44,6 @@ class ReimagineNode():
tailored_model_id=None, tailored_model_influence=None, tailored_generation_prefix=None,
content_moderation=0,
):
api_key = deserialize_and_get_comfy_key(api_key)
payload = {
"prompt": tailored_generation_prefix + prompt,
"num_results": 1,
@@ -59,11 +64,14 @@ class ReimagineNode():
response = requests.post(
self.api_url,
json=payload,
headers={"api_token": api_key}
headers=bria_json_headers(api_key),
)
if response.status_code == 200:
response_dict = response.json()
image_response = requests.get(response_dict['result'][0]["urls"][0])
image_response = requests.get(
response_dict['result'][0]["urls"][0],
headers=bria_asset_headers(),
)
result_image = postprocess_image(image_response.content)
return (result_image,)
else:
+68 -62
View File
@@ -1,91 +1,97 @@
import numpy as np
import requests
from PIL import Image
import io
import requests
import numpy as np
from PIL import Image
import torch
from .common import deserialize_and_get_comfy_key, preprocess_image, image_to_base64, poll_status_until_completed
from .common import (
bria_asset_headers,
bria_json_headers,
image_to_base64,
normalize_images_input,
poll_status_until_completed,
)
class RemoveForegroundNode():
@classmethod
def INPUT_TYPES(self):
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",), # Input image from another node
"api_key": ("STRING", {"default": "BRIA_API_TOKEN"}), # API Key input with a default value
"images": ("IMAGE",),
"api_key": ("STRING", {"default": "BRIA_API_TOKEN"}),
},
"optional": {
"visual_input_content_moderation": ("BOOLEAN", {"default": False}),
"visual_output_content_moderation": ("BOOLEAN", {"default": False}),
"preserve_alpha": ("BOOLEAN", {"default": True}),
"visual_input_content_moderation": ("BOOLEAN", {"default": False}),
"visual_output_content_moderation": ("BOOLEAN", {"default": False}),
"preserve_alpha": ("BOOLEAN", {"default": True}),
}
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("output_image",)
RETURN_NAMES = ("output_images",)
CATEGORY = "API Nodes"
FUNCTION = "execute" # This is the method that will be executed
FUNCTION = "execute"
def __init__(self):
self.api_url = "https://engine.prod.bria-api.com/v2/image/edit/erase_foreground" # remove foreground API URL
self.api_url = "https://engine.prod.bria-api.com/v2/image/edit/erase_foreground"
# Define the execute method as expected by ComfyUI
def execute(self, image, visual_input_content_moderation, visual_output_content_moderation, preserve_alpha, api_key):
def execute(
self,
images,
visual_input_content_moderation,
visual_output_content_moderation,
preserve_alpha,
api_key
):
if api_key.strip() == "" or api_key.strip() == "BRIA_API_TOKEN":
raise Exception("Please insert a valid API key.")
api_key = deserialize_and_get_comfy_key(api_key)
images = normalize_images_input(images)
batch_results = []
# Check if image is tensor, if so, convert to NumPy array
if isinstance(image, torch.Tensor):
image = preprocess_image(image)
for idx, pil_image in enumerate(images):
try:
image_base64 = image_to_base64(pil_image)
# Prepare the API request payload
# temporary save the image to /tmp
# temp_img_path = "/tmp/temp_img.jpeg"
# image.save(temp_img_path, format="JPEG")
# files=[('file',('temp_img.jpeg', open(temp_img_path, 'rb'),'image/jpeg'))
# ]
payload = {
"image": image_to_base64(image),
"visual_input_content_moderation": visual_input_content_moderation,
"visual_output_content_moderation":visual_output_content_moderation,
"preserve_alpha": preserve_alpha
}
payload = {
"image": image_base64,
"visual_input_content_moderation": visual_input_content_moderation,
"visual_output_content_moderation": visual_output_content_moderation,
"preserve_alpha": preserve_alpha
}
headers = {
"Content-Type": "application/json",
"api_token": f"{api_key}"
}
headers = bria_json_headers(api_key)
try:
response = requests.post(self.api_url, json=payload, headers=headers)
response = requests.post(self.api_url, json=payload, headers=headers)
if response.status_code not in (200, 202):
raise Exception(f"API request failed with status {response.status_code}: {response.text}")
if response.status_code == 200 or response.status_code == 202:
print('Initial request successful, polling for completion...')
response_dict = response.json()
status_url = response_dict.get('status_url')
request_id = response_dict.get('request_id')
status_url = response_dict.get("status_url")
if not status_url:
raise Exception("No status_url returned from API")
print(f"Request ID: {request_id}, Status URL: {status_url}")
# Poll status URL until completion
final_response = poll_status_until_completed(status_url, api_key)
# Get the result image URL
result_image_url = final_response['result']['image_url']
image_response = requests.get(result_image_url)
result_image = Image.open(io.BytesIO(image_response.content))
result_image = np.array(result_image).astype(np.float32) / 255.0
result_image = torch.from_numpy(result_image)[None,]
return (result_image,)
else:
raise Exception(f"Error: API request failed with status code {response.status_code} {response.text}")
except Exception as e:
raise Exception(f"{e}")
print(f"RemoveForegroundNode - Initial request successful for image {idx}, polling for completion...")
final_response = poll_status_until_completed(status_url, api_key)
result_image_url = final_response["result"]["image_url"]
# Download result
image_response = requests.get(
result_image_url,
headers=bria_asset_headers(),
)
result_image = Image.open(io.BytesIO(image_response.content)).convert("RGB")
# Convert to float32 tensor (H, W, C)
result_array = np.array(result_image).astype(np.float32) / 255.0
result_tensor = torch.from_numpy(result_array)
batch_results.append(result_tensor)
except Exception as e:
print(f"[RemoveForegroundNode] Skipping image {idx} due to error: {e}")
# fallback: use original image as tensor
fallback_array = np.array(pil_image).astype(np.float32) / 255.0
batch_results.append(torch.from_numpy(fallback_array))
# Return list of tensors
return (batch_results,)
+94 -92
View File
@@ -1,124 +1,126 @@
import numpy as np
import requests
from PIL import Image
import io
import requests
import numpy as np
from PIL import Image
import torch
from .common import deserialize_and_get_comfy_key, image_to_base64, preprocess_image, preprocess_mask, poll_status_until_completed
from .common import (
bria_asset_headers,
bria_json_headers,
image_to_base64,
normalize_images_input,
poll_status_until_completed,
)
class ReplaceBgNode():
@classmethod
def INPUT_TYPES(self):
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",), # Input image from another node
"api_key": ("STRING", {"default": "BRIA_API_TOKEN"}), # API Key input with a default value
"images": ("IMAGE",),
"api_key": ("STRING", {"default": "BRIA_API_TOKEN"}),
},
"optional": {
"mode": (["base", "fast", "high_control"], {"default": "base"}),
"prompt": ("STRING",),
"prompt": ("STRING", {"default": ""}),
"ref_images": ("IMAGE",),
"refine_prompt": ("BOOLEAN", {"default": True}),
"enhance_ref_images": ("BOOLEAN", {"default": True}),
"original_quality": ("BOOLEAN", {"default": False}),
"negative_prompt": ("STRING", {"default": None}),
"seed": ("INT", {"default": 681794}),
"visual_output_content_moderation": ("BOOLEAN", {"default": False}),
"refine_prompt": ("BOOLEAN", {"default": True}),
"enhance_ref_images": ("BOOLEAN", {"default": True}),
"original_quality": ("BOOLEAN", {"default": False}),
"negative_prompt": ("STRING", {"default": None}),
"seed": ("STRING", {"default": "681794"}), # Accept comma-separated seeds
"visual_output_content_moderation": ("BOOLEAN", {"default": False}),
"prompt_content_moderation": ("BOOLEAN", {"default": False}),
"force_background_detection": ("BOOLEAN", {"default": False}),
}
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("output_image",)
RETURN_NAMES = ("output_images",)
CATEGORY = "API Nodes"
FUNCTION = "execute" # This is the method that will be executed
FUNCTION = "execute"
def __init__(self):
self.api_url = "https://engine.prod.bria-api.com/v2/image/edit/replace_background" # Replace BG API URL
self.api_url = "https://engine.prod.bria-api.com/v2/image/edit/replace_background"
# Define the execute method as expected by ComfyUI
def execute(self, image, mode,
refine_prompt,
original_quality,
negative_prompt,
seed,
api_key,
visual_output_content_moderation,
prompt_content_moderation,
enhance_ref_images,
force_background_detection,
prompt=None,
ref_images=None,):
if api_key.strip() == "" or api_key.strip() == "BRIA_API_TOKEN":
def execute(
self,
images,
mode,
refine_prompt,
original_quality,
negative_prompt,
seed,
api_key,
visual_output_content_moderation,
prompt_content_moderation,
enhance_ref_images,
force_background_detection,
prompt=None,
ref_images=None
):
if api_key.strip() in ("", "BRIA_API_TOKEN"):
raise Exception("Please insert a valid API key.")
api_key = deserialize_and_get_comfy_key(api_key)
images = normalize_images_input(images)
# Check if image and mask are tensors, if so, convert to NumPy arrays
if isinstance(image, torch.Tensor):
image = preprocess_image(image)
# Convert the image to Base64 string
image_base64 = image_to_base64(image)
# Normalize reference images
ref_images_base64 = []
if ref_images is not None:
ref_images = preprocess_image(ref_images)
ref_images = [image_to_base64(ref_images)]
else:
ref_images=[]
ref_images_list = normalize_images_input(ref_images)
ref_images_base64 = [image_to_base64(img) for img in ref_images_list]
# Prepare the API request payload for v2 API
payload = {
"image": image_base64,
"mode": mode,
"prompt": prompt,
"ref_images":ref_images,
"refine_prompt": refine_prompt,
"original_quality": original_quality,
"negative_prompt": negative_prompt,
"seed": seed,
"prompt_content_moderation": prompt_content_moderation,
"visual_output_content_moderation":visual_output_content_moderation,
"enhance_ref_images":enhance_ref_images,
"force_background_detection": force_background_detection
}
# Prepare per-image seeds
seed_values = [int(s.strip()) for s in seed.split(",")] if isinstance(seed, str) else [seed]
if len(seed_values) < len(images):
seed_values += [seed_values[-1]] * (len(images) - len(seed_values))
headers = {
"Content-Type": "application/json",
"api_token": f"{api_key}"
}
batch_results = []
for idx, pil_image in enumerate(images):
try:
image_base64 = image_to_base64(pil_image)
payload = {
"image": image_base64,
"mode": mode,
"prompt": prompt,
"ref_images": ref_images_base64,
"refine_prompt": refine_prompt,
"original_quality": original_quality,
"negative_prompt": negative_prompt,
"seed": seed_values[idx],
"prompt_content_moderation": prompt_content_moderation,
"visual_output_content_moderation": visual_output_content_moderation,
"enhance_ref_images": enhance_ref_images,
"force_background_detection": force_background_detection
}
headers = bria_json_headers(api_key)
response = requests.post(self.api_url, json=payload, headers=headers)
if response.status_code not in (200, 202):
raise Exception(f"API request failed with status {response.status_code}: {response.text}")
try:
response = requests.post(self.api_url, json=payload, headers=headers)
if response.status_code == 200 or response.status_code == 202:
print('Initial replace background request successful, polling for completion...')
response_dict = response.json()
status_url = response_dict.get('status_url')
request_id = response_dict.get('request_id')
status_url = response_dict.get("status_url")
if not status_url:
raise Exception("No status_url returned from API")
print(f"Request ID: {request_id}, Status URL: {status_url}")
# Poll status URL until completion
final_response = poll_status_until_completed(status_url, api_key)
# Get the result image URL
result_image_url = final_response['result']['image_url']
# Download and process the result image
image_response = requests.get(result_image_url)
result_image = Image.open(io.BytesIO(image_response.content))
result_image = result_image.convert("RGB")
result_image = np.array(result_image).astype(np.float32) / 255.0
result_image = torch.from_numpy(result_image)[None,]
return (result_image,)
else:
raise Exception(f"Error: API request failed with status code {response.status_code}{response.text}")
except Exception as e:
raise Exception(f"{e}")
print(f"ReplaceBgNode - Initial request successful for image {idx}, polling for completion...")
final_response = poll_status_until_completed(status_url, api_key)
result_image_url = final_response["result"]["image_url"]
image_response = requests.get(
result_image_url,
headers=bria_asset_headers(),
)
result_image = Image.open(io.BytesIO(image_response.content)).convert("RGB")
result_tensor = torch.from_numpy(np.array(result_image).astype(np.float32) / 255.0)
batch_results.append(result_tensor)
except Exception as e:
print(f"[ReplaceBgNode] Skipping image {idx} due to error: {e}")
fallback_array = np.array(pil_image).astype(np.float32) / 255.0
batch_results.append(torch.from_numpy(fallback_array))
return (batch_results,)
+62 -56
View File
@@ -1,87 +1,93 @@
import numpy as np
import requests
from PIL import Image
import io
import requests
import numpy as np
from PIL import Image
import torch
from .common import deserialize_and_get_comfy_key, preprocess_image, image_to_base64, poll_status_until_completed
from .common import (
bria_asset_headers,
bria_json_headers,
image_to_base64,
normalize_images_input,
poll_status_until_completed,
to_pil_safe,
)
class RmbgNode():
@classmethod
def INPUT_TYPES(self):
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",), # Input image from another node
"api_key": ("STRING", {"default": "BRIA_API_TOKEN"}), # API Key input with a default value
"images": ("IMAGE",), # Accepts list of PIL Images or single tensor
"api_key": ("STRING", {"default": "BRIA_API_TOKEN"}),
},
"optional": {
"visual_input_content_moderation": ("BOOLEAN", {"default": False}),
"visual_output_content_moderation": ("BOOLEAN", {"default": False}),
"preserve_alpha": ("BOOLEAN", {"default": True}),
}
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("output_image",)
RETURN_NAMES = ("output_images",)
CATEGORY = "API Nodes"
FUNCTION = "execute" # This is the method that will be executed
FUNCTION = "execute"
def __init__(self):
self.api_url = "https://engine.prod.bria-api.com/v2/image/edit/remove_background" # RMBG API URL
self.api_url = "https://engine.prod.bria-api.com/v2/image/edit/remove_background"
# Define the execute method as expected by ComfyUI
def execute(self, image, visual_input_content_moderation, visual_output_content_moderation, preserve_alpha, api_key):
def execute(self, images, visual_input_content_moderation, visual_output_content_moderation, preserve_alpha, api_key):
if api_key.strip() == "" or api_key.strip() == "BRIA_API_TOKEN":
raise Exception("Please insert a valid API key.")
api_key = deserialize_and_get_comfy_key(api_key)
# Check if image is tensor, if so, convert to NumPy array
if isinstance(image, torch.Tensor):
image = preprocess_image(image)
# Normalize input to list of PIL images
images = normalize_images_input(images)
# Convert image to base64 for the new API format
image_base64 = image_to_base64(image)
payload = {
"image": image_base64,
"visual_input_content_moderation": visual_input_content_moderation,
"visual_output_content_moderation":visual_output_content_moderation,
"preserve_alpha":preserve_alpha
}
batch_results = []
headers = {
"Content-Type": "application/json",
"api_token": f"{api_key}"
}
for idx, pil_image in enumerate(images):
try:
image_base64 = image_to_base64(pil_image)
try:
response = requests.post(self.api_url, json=payload, headers=headers)
if response.status_code == 200 or response.status_code == 202:
print('Initial RMBG request successful, polling for completion...')
payload = {
"image": image_base64,
"visual_input_content_moderation": visual_input_content_moderation,
"visual_output_content_moderation": visual_output_content_moderation,
"preserve_alpha": preserve_alpha
}
headers = bria_json_headers(api_key)
response = requests.post(self.api_url, json=payload, headers=headers)
if response.status_code not in (200, 202):
raise Exception(f"API request failed with status {response.status_code}: {response.text}")
# Poll until completion
response_dict = response.json()
status_url = response_dict.get('status_url')
request_id = response_dict.get('request_id')
if not status_url:
raise Exception("No status_url returned from API")
print(f"Request ID: {request_id}, Status URL: {status_url}")
final_response = poll_status_until_completed(status_url, api_key)
# Get the result image URL
result_image_url = final_response['result']['image_url']
# Download and process the result image
image_response = requests.get(result_image_url)
result_image = Image.open(io.BytesIO(image_response.content))
result_image = np.array(result_image).astype(np.float32) / 255.0
result_image = torch.from_numpy(result_image)[None,]
return (result_image,)
else:
raise Exception(f"Error: API request failed with status code {response.status_code} {response.text}")
except Exception as e:
raise Exception(f"{e}")
print(f"RmbgNode - Initial request successful for image {idx}, polling for completion...")
final_response = poll_status_until_completed(status_url, api_key)
result_image_url = final_response['result']['image_url']
# Download result
image_response = requests.get(
result_image_url,
headers=bria_asset_headers(),
)
result_image = Image.open(io.BytesIO(image_response.content))
# Convert to float32 tensor (H, W, C), 0-1
result_array = np.array(result_image).astype(np.float32) / 255.0
result_tensor = torch.from_numpy(result_array) # shape: (H,W,4)
batch_results.append(result_tensor)
except Exception as e:
print(f"[RmbgNode] Skipping image {idx} due to error: {e}")
# Append empty tensor of the same size as original
empty_array = np.zeros((pil_image.height, pil_image.width, 4), dtype=np.float32)
batch_results.append(torch.from_numpy(empty_array))
# Return list of tensors (Comfy preview handles this correctly)
return (batch_results,)
+12 -4
View File
@@ -1,6 +1,12 @@
import requests
from .common import deserialize_and_get_comfy_key, postprocess_image, preprocess_image, image_to_base64
from .common import (
bria_asset_headers,
bria_json_headers,
image_to_base64,
postprocess_image,
preprocess_image,
)
class TailoredGenNode():
@@ -45,7 +51,6 @@ class TailoredGenNode():
guidance_method_2=None, guidance_method_2_scale=None, guidance_method_2_image=None,
content_moderation=0,
):
api_key = deserialize_and_get_comfy_key(api_key)
payload = {
"prompt": generation_prefix + prompt,
"num_results": 1,
@@ -74,11 +79,14 @@ class TailoredGenNode():
response = requests.post(
self.api_url + model_id,
json=payload,
headers={"api_token": api_key}
headers=bria_json_headers(api_key),
)
if response.status_code == 200:
response_dict = response.json()
image_response = requests.get(response_dict['result'][0]["urls"][0])
image_response = requests.get(
response_dict['result'][0]["urls"][0],
headers=bria_asset_headers(),
)
result_image = postprocess_image(image_response.content)
return (result_image,)
else:
+2 -3
View File
@@ -1,5 +1,5 @@
import requests
from .common import deserialize_and_get_comfy_key
from .common import bria_json_headers
class TailoredModelInfoNode():
@classmethod
@@ -21,10 +21,9 @@ class TailoredModelInfoNode():
# Define the execute method as expected by ComfyUI
def execute(self, model_id, api_key):
api_key = deserialize_and_get_comfy_key(api_key)
response = requests.get(
self.api_url + model_id,
headers={"api_token": api_key}
headers=bria_json_headers(api_key),
)
if response.status_code == 200:
generation_prefix = response.json()["generation_prefix"]
+63 -47
View File
@@ -1,19 +1,25 @@
import numpy as np
import requests
from PIL import Image
import io
import requests
import numpy as np
from PIL import Image
import torch
from .common import deserialize_and_get_comfy_key, image_to_base64, preprocess_image
from .common import (
bria_asset_headers,
bria_json_headers,
image_to_base64,
normalize_images_input,
to_pil_safe,
)
class TailoredPortraitNode():
@classmethod
def INPUT_TYPES(self):
def INPUT_TYPES(cls):
return {
"required": {
"image": ("IMAGE",), # Input image from another node
"tailored_model_id": ("STRING",), # API Key input with a default value
"api_key": ("STRING", {"default": "BRIA_API_TOKEN"}), # API Key input with a default value
"images": ("IMAGE",),
"tailored_model_id": ("STRING",),
"api_key": ("STRING", {"default": "BRIA_API_TOKEN"}),
},
"optional": {
"seed": ("INT", {"default": 123456}),
@@ -23,54 +29,64 @@ class TailoredPortraitNode():
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("output_image",)
RETURN_NAMES = ("output_images",)
CATEGORY = "API Nodes"
FUNCTION = "execute" # This is the method that will be executed
FUNCTION = "execute"
def __init__(self):
self.api_url = "https://engine.prod.bria-api.com/v1/tailored-gen/restyle_portrait" # Eraser API URL
self.api_url = "https://engine.prod.bria-api.com/v1/tailored-gen/restyle_portrait"
# Define the execute method as expected by ComfyUI
def execute(self, image, tailored_model_id, api_key, seed, tailored_model_influence, id_strength):
def execute(
self,
images,
tailored_model_id,
api_key,
seed,
tailored_model_influence,
id_strength
):
if api_key.strip() == "" or api_key.strip() == "BRIA_API_TOKEN":
raise Exception("Please insert a valid API key.")
api_key = deserialize_and_get_comfy_key(api_key)
# Normalize images to list of PIL images
images = normalize_images_input(images)
# Convert the image and mask directly to if isinstance(image, torch.Tensor):
if isinstance(image, torch.Tensor):
image = preprocess_image(image)
image_base64 = image_to_base64(image)
batch_results = []
# Prepare the API request payload
payload = {
"id_image_file": f"{image_base64}",
"tailored_model_id": int(tailored_model_id),
"tailored_model_influence": tailored_model_influence,
"id_strength": id_strength,
"seed": seed
}
for idx, pil_image in enumerate(images):
try:
image_base64 = image_to_base64(pil_image)
headers = {
"Content-Type": "application/json",
"api_token": f"{api_key}"
}
payload = {
"id_image_file": image_base64,
"tailored_model_id": int(tailored_model_id),
"tailored_model_influence": tailored_model_influence,
"id_strength": id_strength,
"seed": seed
}
headers = bria_json_headers(api_key)
response = requests.post(self.api_url, json=payload, headers=headers)
if response.status_code != 200:
raise Exception(f"API request failed with status {response.status_code}: {response.text}")
try:
response = requests.post(self.api_url, json=payload, headers=headers)
# Check for successful response
if response.status_code == 200:
print('response is 200')
# Process the output image from API response
response_dict = response.json()
image_response = requests.get(response_dict['image_res'])
result_image = Image.open(io.BytesIO(image_response.content))
result_image = result_image.convert("RGB")
result_image = np.array(result_image).astype(np.float32) / 255.0
result_image = torch.from_numpy(result_image)[None,]
return (result_image,)
else:
raise Exception(f"Error: API request failed with status code {response.status_code} {response.text}")
image_response = requests.get(
response_dict["image_res"],
headers=bria_asset_headers(),
)
result_image = Image.open(io.BytesIO(image_response.content)).convert("RGB")
except Exception as e:
raise Exception(f"{e}")
# Convert to float32 tensor (H,W,C), 0-1
result_array = np.array(result_image).astype(np.float32) / 255.0
result_tensor = torch.from_numpy(result_array)
batch_results.append(result_tensor)
except Exception as e:
print(f"[TailoredPortraitNode] Skipping image {idx} due to error: {e}")
fallback_array = np.array(pil_image).astype(np.float32) / 255.0
batch_results.append(torch.from_numpy(fallback_array))
# Return list of tensors (avoids size/dtype mismatch)
return (batch_results,)
+12 -4
View File
@@ -1,6 +1,12 @@
import requests
from .common import deserialize_and_get_comfy_key, postprocess_image, preprocess_image, image_to_base64
from .common import (
bria_asset_headers,
bria_json_headers,
image_to_base64,
postprocess_image,
preprocess_image,
)
class Text2ImageBaseNode():
@@ -48,7 +54,6 @@ class Text2ImageBaseNode():
image_prompt_mode=None, image_prompt_image=None, image_prompt_scale=None,
content_moderation=0,
):
api_key = deserialize_and_get_comfy_key(api_key)
payload = {
"prompt": prompt,
"num_results": 1,
@@ -84,11 +89,14 @@ class Text2ImageBaseNode():
response = requests.post(
self.api_url,
json=payload,
headers={"api_token": api_key}
headers=bria_json_headers(api_key),
)
if response.status_code == 200:
response_dict = response.json()
image_response = requests.get(response_dict['result'][0]["urls"][0])
image_response = requests.get(
response_dict['result'][0]["urls"][0],
headers=bria_asset_headers(),
)
result_image = postprocess_image(image_response.content)
return (result_image,)
else:
+12 -4
View File
@@ -1,6 +1,12 @@
import requests
from .common import deserialize_and_get_comfy_key, postprocess_image, preprocess_image, image_to_base64
from .common import (
bria_asset_headers,
bria_json_headers,
image_to_base64,
postprocess_image,
preprocess_image,
)
class Text2ImageFastNode():
@@ -45,7 +51,6 @@ class Text2ImageFastNode():
image_prompt_mode=None, image_prompt_image=None, image_prompt_scale=None,
content_moderation=0,
):
api_key = deserialize_and_get_comfy_key(api_key)
payload = {
"prompt": prompt,
"num_results": 1,
@@ -77,11 +82,14 @@ class Text2ImageFastNode():
response = requests.post(
self.api_url,
json=payload,
headers={"api_token": api_key}
headers=bria_json_headers(api_key),
)
if response.status_code == 200:
response_dict = response.json()
image_response = requests.get(response_dict['result'][0]["urls"][0])
image_response = requests.get(
response_dict['result'][0]["urls"][0],
headers=bria_asset_headers(),
)
result_image = postprocess_image(image_response.content)
return (result_image,)
else:
+6 -4
View File
@@ -1,6 +1,6 @@
import requests
from .common import deserialize_and_get_comfy_key, postprocess_image
from .common import bria_asset_headers, bria_json_headers, postprocess_image
class Text2ImageHDNode():
@@ -35,7 +35,6 @@ class Text2ImageHDNode():
self, api_key, prompt, aspect_ratio, seed, negative_prompt,
steps_num, prompt_enhancement, text_guidance_scale, medium, content_moderation=0,
):
api_key = deserialize_and_get_comfy_key(api_key)
payload = {
"prompt": prompt,
"num_results": 1,
@@ -53,11 +52,14 @@ class Text2ImageHDNode():
response = requests.post(
self.api_url,
json=payload,
headers={"api_token": api_key}
headers=bria_json_headers(api_key),
)
if response.status_code == 200:
response_dict = response.json()
image_response = requests.get(response_dict['result'][0]["urls"][0])
image_response = requests.get(
response_dict['result'][0]["urls"][0],
headers=bria_asset_headers(),
)
result_image = postprocess_image(image_response.content)
return (result_image,)
else:
+16 -5
View File
@@ -1,6 +1,12 @@
import requests
import torch
from ..common import deserialize_and_get_comfy_key, postprocess_image, preprocess_image, image_to_base64
from ..common import (
bria_asset_headers,
bria_json_headers,
image_to_base64,
postprocess_image,
preprocess_image,
)
shot_by_text_api_url = (
"https://engine.prod.bria-api.com/v1/product/lifestyle_shot_by_text"
@@ -130,8 +136,7 @@ def make_api_request(api_url, payload, api_key, Placement_type = None):
try:
api_key = deserialize_and_get_comfy_key(api_key)
headers = {"Content-Type": "application/json", "api_token": f"{api_key}"}
headers = bria_json_headers(api_key)
response = requests.post(api_url, json=payload, headers=headers)
if response.status_code == 200:
@@ -141,7 +146,10 @@ def make_api_request(api_url, payload, api_key, Placement_type = None):
result_images = []
for i, result in enumerate(response_dict.get("result", [])[:7]):
image_url = result[0]
image_response = requests.get(image_url)
image_response = requests.get(
image_url,
headers=bria_asset_headers(),
)
processed = postprocess_image(image_response.content)
result_images.append(processed)
@@ -152,7 +160,10 @@ def make_api_request(api_url, payload, api_key, Placement_type = None):
return tuple(result_images)
image_response = requests.get(response_dict["result"][0][0])
image_response = requests.get(
response_dict["result"][0][0],
headers=bria_asset_headers(),
)
result_image = postprocess_image(image_response.content)
return (result_image,)
else:
@@ -3,6 +3,8 @@ import uuid
import folder_paths
import requests
from ..common import bria_asset_headers
class PreviewVideoURLNode:
"""
Bria Preview Video URL Node
@@ -62,7 +64,12 @@ class PreviewVideoURLNode:
# Download video from URL
try:
response = requests.get(video_url, stream=True, timeout=60)
response = requests.get(
video_url,
stream=True,
timeout=60,
headers=bria_asset_headers(),
)
response.raise_for_status()
# Determine file extension from URL or Content-Type
@@ -2,7 +2,7 @@ import os
import uuid
import requests
import folder_paths
from ..common import deserialize_and_get_comfy_key, poll_status_until_completed
from ..common import bria_json_headers, poll_status_until_completed
from .video_utils import upload_video_to_s3
class RemoveVideoBackgroundNode():
@@ -55,7 +55,6 @@ class RemoveVideoBackgroundNode():
def execute(self, api_key, video_url, preserve_audio=True, output_container_and_codec="webm_vp9",):
if api_key.strip() == "" or api_key.strip() == "BRIA_API_TOKEN":
raise Exception("Please insert a valid API key.")
api_key = deserialize_and_get_comfy_key(api_key)
video_path = None
input_video_url = ""
@@ -77,10 +76,7 @@ class RemoveVideoBackgroundNode():
"output_container_and_codec": output_container_and_codec
}
headers = {
"Content-Type": "application/json",
"api_token": f"{api_key}"
}
headers = bria_json_headers(api_key)
response = requests.post(self.api_url, json=payload, headers=headers)
@@ -2,7 +2,7 @@ import os
import uuid
import requests
import folder_paths
from ..common import deserialize_and_get_comfy_key, poll_status_until_completed
from ..common import bria_json_headers, poll_status_until_completed
from .video_utils import upload_video_to_s3
class VideoEraseElementsNode():
@@ -60,7 +60,6 @@ class VideoEraseElementsNode():
def execute(self, api_key, video_url, mask_url="", output_container_and_codec="mp4_h264", preserve_audio=True):
if api_key.strip() == "" or api_key.strip() == "BRIA_API_TOKEN":
raise Exception("Please insert a valid API key.")
api_key = deserialize_and_get_comfy_key(api_key)
video_path = None
if video_url and video_url.strip() != "":
@@ -86,10 +85,7 @@ class VideoEraseElementsNode():
"preserve_audio": preserve_audio
}
headers = {
"Content-Type": "application/json",
"api_token": f"{api_key}"
}
headers = bria_json_headers(api_key)
response = requests.post(self.api_url, json=payload, headers=headers)
@@ -2,7 +2,7 @@ import os
import uuid
import requests
import folder_paths
from ..common import deserialize_and_get_comfy_key, poll_status_until_completed
from ..common import bria_json_headers, poll_status_until_completed
from .video_utils import upload_video_to_s3
class VideoIncreaseResolutionNode():
@@ -57,7 +57,6 @@ class VideoIncreaseResolutionNode():
def execute(self, api_key, video_url, desired_increase='2', output_container_and_codec="mp4_h264", preserve_audio=True):
if api_key.strip() == "" or api_key.strip() == "BRIA_API_TOKEN":
raise Exception("Please insert a valid API key.")
api_key = deserialize_and_get_comfy_key(api_key)
video_path = None
if video_url and video_url.strip() != "":
@@ -83,10 +82,7 @@ class VideoIncreaseResolutionNode():
"preserve_audio": preserve_audio
}
headers = {
"Content-Type": "application/json",
"api_token": f"{api_key}"
}
headers = bria_json_headers(api_key)
response = requests.post(self.api_url, json=payload, headers=headers)
@@ -2,7 +2,7 @@ import os
import uuid
import requests
import folder_paths
from ..common import deserialize_and_get_comfy_key, poll_status_until_completed
from ..common import bria_json_headers, poll_status_until_completed
from .video_utils import upload_video_to_s3
import json
@@ -58,8 +58,6 @@ class VideoMaskByKeyPointsNode():
def execute(self, key_points, api_key, video_url, output_container_and_codec="mp4_h264", preserve_audio=True):
if api_key.strip() == "" or api_key.strip() == "BRIA_API_TOKEN":
raise Exception("Please insert a valid API key.")
api_key = deserialize_and_get_comfy_key(api_key)
try:
key_points_array = json.loads(key_points)
except json.JSONDecodeError as e:
@@ -91,10 +89,7 @@ class VideoMaskByKeyPointsNode():
"preserve_audio": preserve_audio
}
headers = {
"Content-Type": "application/json",
"api_token": f"{api_key}"
}
headers = bria_json_headers(api_key)
response = requests.post(self.api_url, json=payload, headers=headers)
@@ -2,7 +2,7 @@ import os
import uuid
import requests
import folder_paths
from ..common import deserialize_and_get_comfy_key, poll_status_until_completed
from ..common import bria_json_headers, poll_status_until_completed
from .video_utils import upload_video_to_s3
class VideoMaskByPromptNode():
@@ -57,8 +57,6 @@ class VideoMaskByPromptNode():
def execute(self, prompt, api_key, video_url, output_container_and_codec="mp4_h264", preserve_audio=True):
if api_key.strip() == "" or api_key.strip() == "BRIA_API_TOKEN":
raise Exception("Please insert a valid API key.")
api_key = deserialize_and_get_comfy_key(api_key)
video_path = None
if video_url and video_url.strip() != "":
@@ -85,10 +83,7 @@ class VideoMaskByPromptNode():
"preserve_audio": preserve_audio
}
headers = {
"Content-Type": "application/json",
"api_token": f"{api_key}"
}
headers = bria_json_headers(api_key)
response = requests.post(self.api_url, json=payload, headers=headers)
@@ -2,7 +2,7 @@ import os
import uuid
import requests
import folder_paths
from ..common import deserialize_and_get_comfy_key, poll_status_until_completed
from ..common import bria_json_headers, poll_status_until_completed
from .video_utils import upload_video_to_s3
class VideoSolidColorBackgroundNode():
@@ -69,7 +69,6 @@ class VideoSolidColorBackgroundNode():
def execute(self, api_key, video_url, background_color="Transparent", output_container_and_codec="webm_vp9", preserve_audio=True):
if api_key.strip() == "" or api_key.strip() == "BRIA_API_TOKEN":
raise Exception("Please insert a valid API key.")
api_key = deserialize_and_get_comfy_key(api_key)
video_path = None
if video_url and video_url.strip() != "":
@@ -96,10 +95,7 @@ class VideoSolidColorBackgroundNode():
"preserve_audio": preserve_audio
}
headers = {
"Content-Type": "application/json",
"api_token": f"{api_key}"
}
headers = bria_json_headers(api_key)
response = requests.post(self.api_url, json=payload, headers=headers)
+6 -2
View File
@@ -1,11 +1,14 @@
import os
import requests
from ..common import BRIA_COMFYUI_USER_AGENT, bria_asset_headers
def upload_video_to_s3(video_path, filename, api_token):
api_url = "https://platform.prod.bria-api.com/upload-video/anonymous/presigned-url"
headers = {
"Content-Type": "application/json"
"Content-Type": "application/json",
"User-Agent": BRIA_COMFYUI_USER_AGENT,
}
extension = os.path.splitext(filename)[1].lower()
content_type_map = {
@@ -52,7 +55,8 @@ def upload_video_to_s3(video_path, filename, api_token):
# Determine content type based on file extension
upload_headers = {
"Content-Type": content_type
"Content-Type": content_type,
**bria_asset_headers(),
}
upload_response = requests.put(upload_url, data=video_data, headers=upload_headers)
+1 -1
View File
@@ -1,7 +1,7 @@
[project]
name = "comfyui-bria-api"
description = "Custom nodes for ComfyUI using BRIA's API."
version = "2.1.10"
version = "2.1.17"
license = {file = "LICENSE"}
[project.urls]
+145
View File
@@ -0,0 +1,145 @@
import { app } from "/scripts/app.js";
import { api } from "/scripts/api.js";
app.registerExtension({
name: "BriaMultiImageSelect",
async nodeCreated(node) {
if (node.comfyClass !== "BriaMultiImageSelect") return;
const getWidget = (name) =>
node.widgets?.find(w => w.name === name);
const pathsWidget = getWidget("selected_paths");
if (!pathsWidget) return;
pathsWidget.hidden = true;
pathsWidget.draw = () => {};
pathsWidget.computeSize = () => [0, 0];
const viewURLFromRel = (rel) => {
const parts = (rel || "").split("/");
const filename = parts.pop();
const subfolder = parts.join("/");
const params = new URLSearchParams({ filename, type: "input", subfolder });
return api.apiURL(`/view?${params.toString()}`);
};
const loadImg = (url) =>
new Promise((resolve, reject) => {
const img = new Image();
img.crossOrigin = "anonymous";
img.onload = () => resolve(img);
img.onerror = () => reject();
img.src = url;
});
const refreshPreview = async () => {
let raw = node.properties.selected_paths;
if (!raw) raw = pathsWidget.value;
let rels = [];
try {
rels = raw ? JSON.parse(raw) : [];
} catch (e) {
console.error("Failed to parse selected_paths:", e);
node.imgs = null;
node.imgError = "Invalid image data";
node.setDirtyCanvas(true, true);
return;
}
if (!rels.length) {
node.imgs = null;
node.imgError = "No images selected";
node.setDirtyCanvas(true, true);
return;
}
const imgs = [];
await Promise.allSettled(
rels.map(async (rel) => {
try {
const img = await loadImg(viewURLFromRel(rel));
imgs.push(img);
} catch (e) {
console.warn(`Failed to load image: ${rel}`, e);
}
})
);
if (imgs.length) {
node.imgs = imgs;
node.imgError = null;
} else {
node.imgs = null;
node.imgError = "Failed to load images";
}
node.setDirtyCanvas(true, true);
};
// Update node size to accommodate preview
const originalComputeSize = node.computeSize;
node.computeSize = function () {
const size = originalComputeSize ? originalComputeSize.apply(this, arguments) : [200, 100];
size[1] = Math.max(size[1], 200); // Ensure minimum height for preview
return size;
};
const btn = node.addWidget("button", "Select Images", null, async () => {
const input = document.createElement("input");
input.type = "file";
input.multiple = true;
input.accept = "image/*";
input.style.display = "none";
document.body.appendChild(input);
input.onchange = async () => {
const files = Array.from(input.files || []);
document.body.removeChild(input);
if (!files.length) return;
const rels = [];
for (const f of files) {
const form = new FormData();
form.append("image", f, f.name);
const resp = await api.fetchApi("/upload/image", {
method: "POST",
body: form,
});
if (!resp.ok) continue;
const data = await resp.json();
const rel = data.subfolder
? `${data.subfolder}/${data.name}`
: data.name;
rels.push(rel);
}
const json = JSON.stringify(rels);
pathsWidget.value = json;
node.properties.selected_paths = json;
await refreshPreview();
};
input.click();
});
node.widgets.unshift(
node.widgets.splice(node.widgets.indexOf(btn), 1)[0]
);
// Initialize properties if not present
if (!node.properties) {
node.properties = {};
}
await refreshPreview();
setTimeout(async () => {
await refreshPreview();
}, 100);
},
});
+331
View File
@@ -0,0 +1,331 @@
{
"id": "3eb93704-25f0-4511-b147-cf0403c5d060",
"revision": 0,
"last_node_id": 10,
"last_link_id": 11,
"nodes": [
{
"id": 7,
"type": "PreviewImage",
"pos": [
756.945556640625,
406.1631774902344
],
"size": [
140,
246
],
"flags": {},
"order": 5,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 9
}
],
"outputs": [],
"properties": {
"Node name for S&R": "PreviewImage"
},
"widgets_values": []
},
{
"id": 6,
"type": "PreviewImage",
"pos": [
702.7532348632812,
830.3753662109375
],
"size": [
140,
246
],
"flags": {},
"order": 3,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 5
}
],
"outputs": [],
"properties": {
"Node name for S&R": "PreviewImage"
},
"widgets_values": []
},
{
"id": 8,
"type": "FIBOEditNode",
"pos": [
390.8927307128906,
339.14031982421875
],
"size": [
278.720703125,
266
],
"flags": {},
"order": 4,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 8
},
{
"name": "mask",
"shape": 7,
"type": "MASK",
"link": null
},
{
"name": "structured_instruction",
"shape": 7,
"type": "STRING",
"widget": {
"name": "structured_instruction"
},
"link": 11
}
],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
9
]
},
{
"name": "structured_instruction",
"type": "STRING",
"links": null
},
{
"name": "seed",
"type": "INT",
"links": null
}
],
"properties": {
"Node name for S&R": "FIBOEditNode"
},
"widgets_values": [
"",
"",
"",
"",
50,
5,
1199,
"randomize"
]
},
{
"id": 4,
"type": "FIBOEditNode",
"pos": [
301.3495788574219,
884.5100708007812
],
"size": [
278.720703125,
266
],
"flags": {},
"order": 1,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 4
},
{
"name": "mask",
"shape": 7,
"type": "MASK",
"link": null
}
],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
5
]
},
{
"name": "structured_instruction",
"type": "STRING",
"links": null
},
{
"name": "seed",
"type": "INT",
"links": null
}
],
"properties": {
"Node name for S&R": "FIBOEditNode"
},
"widgets_values": [
"",
"change the lamp to a radio",
"",
"",
50,
5,
55,
"randomize"
]
},
{
"id": 2,
"type": "LoadImage",
"pos": [
-245.09060668945312,
882.640869140625
],
"size": [
274.080078125,
314
],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
4,
8,
10
]
},
{
"name": "MASK",
"type": "MASK",
"links": null
}
],
"properties": {
"Node name for S&R": "LoadImage"
},
"widgets_values": [
"0a9d91e579872d653daf3243df4598f0 (2).png",
"image"
]
},
{
"id": 10,
"type": "FIBOEditStructuredInstructionNode",
"pos": [
-139.52833557128906,
410.5189208984375
],
"size": [
314.6372985839844,
82
],
"flags": {},
"order": 2,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 10
}
],
"outputs": [
{
"name": "structured_instruction",
"type": "STRING",
"links": [
11
]
}
],
"properties": {
"Node name for S&R": "FIBOEditStructuredInstructionNode"
},
"widgets_values": [
"BRIA_API_TOKEN",
""
]
}
],
"links": [
[
4,
2,
0,
4,
0,
"IMAGE"
],
[
5,
4,
0,
6,
0,
"IMAGE"
],
[
8,
2,
0,
8,
0,
"IMAGE"
],
[
9,
8,
0,
7,
0,
"IMAGE"
],
[
10,
2,
0,
10,
0,
"IMAGE"
],
[
11,
10,
0,
8,
2,
"STRING"
]
],
"groups": [],
"config": {},
"extra": {
"ds": {
"scale": 0.7522123482651067,
"offset": [
684.0553164416729,
-258.9658284524182
]
},
"frontendVersion": "1.25.11"
},
"version": 0.4
}
+491 -1
View File
@@ -1 +1,491 @@
{"last_node_id":39,"last_link_id":65,"nodes":[{"id":34,"type":"LoadImage","pos":[19.94045066833496,1075.806640625],"size":[315,314],"flags":{},"order":0,"mode":0,"inputs":[],"outputs":[{"name":"IMAGE","type":"IMAGE","links":[61],"slot_index":0,"localized_name":"IMAGE"},{"name":"MASK","type":"MASK","links":null,"localized_name":"MASK"}],"properties":{"Node name for S&R":"LoadImage"},"widgets_values":["quirky-red-brick-brick-wallpaper.jpg","image"]},{"id":36,"type":"PreviewImage","pos":[468.7108154296875,1210.288818359375],"size":[210,246],"flags":{},"order":5,"mode":0,"inputs":[{"name":"images","type":"IMAGE","link":63,"localized_name":"images"}],"outputs":[],"properties":{"Node name for S&R":"PreviewImage"},"widgets_values":[]},{"id":33,"type":"PreviewImage","pos":[1317.4083251953125,1118.4864501953125],"size":[210,246],"flags":{},"order":8,"mode":0,"inputs":[{"name":"images","type":"IMAGE","link":59,"localized_name":"images"}],"outputs":[],"properties":{"Node name for S&R":"PreviewImage"},"widgets_values":[]},{"id":31,"type":"PreviewImage","pos":[1668.734619140625,796.7755737304688],"size":[210,246],"flags":{},"order":10,"mode":0,"inputs":[{"name":"images","type":"IMAGE","link":57,"localized_name":"images"}],"outputs":[],"properties":{"Node name for S&R":"PreviewImage"},"widgets_values":[]},{"id":29,"type":"PreviewImage","pos":[872.8858032226562,679.9429321289062],"size":[210,246],"flags":{},"order":6,"mode":0,"inputs":[{"name":"images","type":"IMAGE","link":55,"localized_name":"images"}],"outputs":[],"properties":{"Node name for S&R":"PreviewImage"},"widgets_values":[]},{"id":28,"type":"LoadImage","pos":[29.629886627197266,676.8370361328125],"size":[315,314],"flags":{},"order":1,"mode":0,"inputs":[],"outputs":[{"name":"IMAGE","type":"IMAGE","links":[54],"slot_index":0,"localized_name":"IMAGE"},{"name":"MASK","type":"MASK","links":null,"localized_name":"MASK"}],"properties":{"Node name for S&R":"LoadImage"},"widgets_values":["pexels-photo-1808399.jpeg","image"]},{"id":35,"type":"RemoveForegroundNode","pos":[414.2638854980469,1079.1705322265625],"size":[315,58],"flags":{},"order":3,"mode":0,"inputs":[{"name":"image","type":"IMAGE","link":61,"localized_name":"image"}],"outputs":[{"name":"output_image","type":"IMAGE","links":[62,63],"slot_index":0,"localized_name":"output_image"}],"properties":{"Node name for S&R":"RemoveForegroundNode"},"widgets_values":["BRIA_API_TOKEN"]},{"id":32,"type":"ReplaceBgNode","pos":[834.6115112304688,1063.0406494140625],"size":[315,294],"flags":{},"order":7,"mode":0,"inputs":[{"name":"image","type":"IMAGE","link":65,"localized_name":"image"},{"name":"ref_image","type":"IMAGE","link":62,"shape":7,"localized_name":"ref_image"}],"outputs":[{"name":"output_image","type":"IMAGE","links":[59,64],"slot_index":0,"localized_name":"output_image"}],"properties":{"Node name for S&R":"ReplaceBgNode"},"widgets_values":["BRIA_API_TOKEN",false,"",true,true,true,false,"",1978,"randomize"]},{"id":30,"type":"ImageExpansionNode","pos":[1265.001220703125,798.8323974609375],"size":[315,226],"flags":{},"order":9,"mode":0,"inputs":[{"name":"image","type":"IMAGE","link":64,"localized_name":"image"}],"outputs":[{"name":"output_image","type":"IMAGE","links":[57],"slot_index":0,"localized_name":"output_image"}],"properties":{"Node name for S&R":"ImageExpansionNode"},"widgets_values":["600,760","200, 0","BRIA_API_TOKEN","1200, 800","",1729,"randomize","Ugly, mutated"]},{"id":38,"type":"Note","pos":[436.7110900878906,566.2047119140625],"size":[306.28387451171875,58],"flags":{},"order":2,"mode":0,"inputs":[],"outputs":[],"properties":{},"widgets_values":["You can get your BRIA API token at:\nhttps://bria.ai/api/"],"color":"#432","bgcolor":"#653"},{"id":27,"type":"RmbgNode","pos":[430.8815002441406,678.7727661132812],"size":[315,58],"flags":{},"order":4,"mode":0,"inputs":[{"name":"image","type":"IMAGE","link":54,"localized_name":"image"}],"outputs":[{"name":"output_image","type":"IMAGE","links":[55,65],"slot_index":0,"localized_name":"output_image"}],"properties":{"Node name for S&R":"RmbgNode"},"widgets_values":["BRIA_API_TOKEN"]}],"links":[[51,5,0,15,2,"STRING"],[54,28,0,27,0,"IMAGE"],[55,27,0,29,0,"IMAGE"],[57,30,0,31,0,"IMAGE"],[59,32,0,33,0,"IMAGE"],[61,34,0,35,0,"IMAGE"],[62,35,0,32,1,"IMAGE"],[63,35,0,36,0,"IMAGE"],[64,32,0,30,0,"IMAGE"],[65,27,0,32,0,"IMAGE"]],"groups":[],"config":{},"extra":{"ds":{"scale":0.8140274938684037,"offset":[101.53311990208498,-477.0342684311694]},"node_versions":{"comfyui-bria-api":"c72754d15b53a13ee0c0419d70401232c56b7fdb","comfy-core":"v0.3.8-1-gc441048","ComfyUI-Jjk-Nodes":"b3c99bb78a99551776b5eab1a820e1cd58f84f31"}},"version":0.4}
{
"id": "f47c8ffe-20e8-473c-a56b-c6241da2c469",
"revision": 0,
"last_node_id": 43,
"last_link_id": 78,
"nodes": [
{
"id": 31,
"type": "PreviewImage",
"pos": [
1668.734619140625,
796.7755737304688
],
"size": [
210,
246
],
"flags": {},
"order": 10,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 78
}
],
"outputs": [],
"properties": {
"Node name for S&R": "PreviewImage"
},
"widgets_values": []
},
{
"id": 29,
"type": "PreviewImage",
"pos": [
872.8858032226562,
679.9429321289062
],
"size": [
210,
246
],
"flags": {},
"order": 5,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 66
}
],
"outputs": [],
"properties": {
"Node name for S&R": "PreviewImage"
},
"widgets_values": []
},
{
"id": 38,
"type": "Note",
"pos": [
436.7110900878906,
566.2047119140625
],
"size": [
306.28387451171875,
88
],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [],
"outputs": [],
"properties": {},
"widgets_values": [
"You can get your BRIA API token at:\nhttps://bria.ai/api/"
],
"color": "#432",
"bgcolor": "#653"
},
{
"id": 28,
"type": "LoadImage",
"pos": [
29.629886627197266,
676.8370361328125
],
"size": [
315,
314
],
"flags": {},
"order": 1,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"slot_index": 0,
"links": [
68
]
},
{
"name": "MASK",
"type": "MASK",
"links": null
}
],
"properties": {
"Node name for S&R": "LoadImage"
},
"widgets_values": [
"pexels-photo-1808399.jpeg",
"image"
]
},
{
"id": 34,
"type": "LoadImage",
"pos": [
19.94045066833496,
1075.806640625
],
"size": [
315,
314
],
"flags": {},
"order": 2,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"slot_index": 0,
"links": [
69
]
},
{
"name": "MASK",
"type": "MASK",
"links": null
}
],
"properties": {
"Node name for S&R": "LoadImage"
},
"widgets_values": [
"quirky-red-brick-brick-wallpaper.jpg",
"image"
]
},
{
"id": 36,
"type": "PreviewImage",
"pos": [
455.6301574707031,
1240.017578125
],
"size": [
210,
246
],
"flags": {},
"order": 6,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 71
}
],
"outputs": [],
"properties": {
"Node name for S&R": "PreviewImage"
},
"widgets_values": []
},
{
"id": 40,
"type": "RmbgNode",
"pos": [
414.095703125,
749.6687622070312
],
"size": [
347.462890625,
130
],
"flags": {},
"order": 3,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 68
}
],
"outputs": [
{
"name": "output_images",
"type": "IMAGE",
"links": [
66,
74
]
}
],
"properties": {
"Node name for S&R": "RmbgNode"
},
"widgets_values": [
"BRIA_API_TOKEN",
false,
false,
true
]
},
{
"id": 41,
"type": "RemoveForegroundNode",
"pos": [
397.44732666015625,
1127.818359375
],
"size": [
347.462890625,
130
],
"flags": {},
"order": 4,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 69
}
],
"outputs": [
{
"name": "output_images",
"type": "IMAGE",
"links": [
71,
75
]
}
],
"properties": {
"Node name for S&R": "RemoveForegroundNode"
},
"widgets_values": [
"BRIA_API_TOKEN",
false,
false,
true
]
},
{
"id": 42,
"type": "ReplaceBgNode",
"pos": [
800.56884765625,
1050.5228271484375
],
"size": [
347.462890625,
318
],
"flags": {},
"order": 7,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 74
},
{
"name": "ref_images",
"shape": 7,
"type": "IMAGE",
"link": 75
}
],
"outputs": [
{
"name": "output_images",
"type": "IMAGE",
"links": [
72,
77
]
}
],
"properties": {
"Node name for S&R": "ReplaceBgNode"
},
"widgets_values": [
"BRIA_API_TOKEN",
"base",
"",
true,
true,
false,
"",
"681794",
false,
false,
false
]
},
{
"id": 43,
"type": "ImageExpansionNode",
"pos": [
1241.743896484375,
731.8318481445312
],
"size": [
347.462890625,
322
],
"flags": {},
"order": 9,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 77
}
],
"outputs": [
{
"name": "output_images",
"type": "IMAGE",
"links": [
78
]
}
],
"properties": {
"Node name for S&R": "ImageExpansionNode"
},
"widgets_values": [
"BRIA_API_TOKEN",
"",
"",
"1000, 1000",
"None",
"",
"681794",
"Ugly, mutated",
false,
true,
false,
false
]
},
{
"id": 33,
"type": "PreviewImage",
"pos": [
1316.2191162109375,
1169.61962890625
],
"size": [
210,
246
],
"flags": {},
"order": 8,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 72
}
],
"outputs": [],
"properties": {
"Node name for S&R": "PreviewImage"
},
"widgets_values": []
}
],
"links": [
[
66,
40,
0,
29,
0,
"IMAGE"
],
[
68,
28,
0,
40,
0,
"IMAGE"
],
[
69,
34,
0,
41,
0,
"IMAGE"
],
[
71,
41,
0,
36,
0,
"IMAGE"
],
[
72,
42,
0,
33,
0,
"IMAGE"
],
[
74,
40,
0,
42,
0,
"IMAGE"
],
[
75,
41,
0,
42,
1,
"IMAGE"
],
[
77,
42,
0,
43,
0,
"IMAGE"
],
[
78,
43,
0,
31,
0,
"IMAGE"
]
],
"groups": [],
"config": {},
"extra": {
"ds": {
"scale": 0.61159090448415,
"offset": [
138.07082928418134,
-488.9478800208311
]
},
"node_versions": {
"comfyui-bria-api": "c72754d15b53a13ee0c0419d70401232c56b7fdb",
"comfy-core": "v0.3.8-1-gc441048",
"ComfyUI-Jjk-Nodes": "b3c99bb78a99551776b5eab1a820e1cd58f84f31"
},
"frontendVersion": "1.25.11"
},
"version": 0.4
}