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
30 changed files with 144 additions and 573 deletions
+5 -19
View File
@@ -8,7 +8,10 @@ This repository provides custom nodes for ComfyUI, enabling direct access to **B
BRIA's APIs and models are built for commercial use and trained on 100% licensed data and does not contain copyrighted materials, such as fictional characters, logos, trademarks, public figures, harmful content, or privacy-infringing content.
An API token is required to use the nodes in your workflows. Get yours at the [BRIA Platform](https://platform.bria.ai/organization-management/api-keys).
An API token is required to use the nodes in your workflows. Get started quickly here
<a href="https://bria.ai/api/" style="text-decoration:none; vertical-align:middle;">
<img src="https://img.shields.io/badge/GET%20YOUR%20TOKEN-1000%20Free%20Calls-blue?style=flat-square" alt="Get Your Token" height="20">
</a>.
for direct API endpoint use, you can find our APIs through partners like [**fal.ai**](https://fal.ai/models?keywords=bria).
For source code and weigths access, go to our [**Hugging Face**](https://huggingface.co/briaai) space.
@@ -104,30 +107,13 @@ These nodes create high-quality product images for eCommerce workflows.
| **ShotByText** | Modifies an image's background by providing a text prompt. Powered by BRIA's ControlNet Background-Generation. |
| **ShotByImage** | Modifies an image's background by providing a reference image. Uses BRIA's ControlNet Background-Generation and Image-Prompt. |
## Video Editing Nodes
These nodes perform high-quality edits for a given video.
| Node | Description |
|------|-------------|
| **Bria Video Remove Background** | Remove the background from a video. |
| **Bria Video Green Screen** | Replace the background of a video with a Chroma-green color. |
| **Bria Video Replace Background** | Replaces the background of a video with a user-provided image or video |
| **Bria SolidColor Background Video** | Replace the background of a video with a solid color. |
| **Bria Video Increase Resolution** | Upscales video resolution |
| **Bria Video Erase Elements** | Erases selected elements from the video using a mask |
| **Bria Video Mask By Prompt** | Generates a mask video using a text prompt describing what to mask. |
| **Bria Video Mask By Key Points** | Generates a mask video using key-points guidance |
Check out the example workflow in the workflows/ folder to see how the nodes should be wired together for loading and previewing a video end-to-end.
## Attribution Node
| Node | Description |
|-------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| **Attribution By Image Node** | This node shares generated images via API for Bria to pay attribution to the data owners who contributed to the generation. Once the images are shared with Bria, Bria calculates the attribution, completes the payment on behalf of the user, and erases the images immediately. This node should be included in any workflow using nodes of Bria’s Models (not necessary for Bria’s API nodes). You can also refer to the [**API documentation**]( https://docs.bria.ai/bria-attribution-service/other/postattributionbyimage) |
An example workflow in the [workflows](workflows) folder is **`Video_Editig_Workflow.json`**, which wires several of these nodes together. Video API details are covered in the [**BRIA API documentation**](https://docs.bria.ai/).
# Installation
+1 -7
View File
@@ -33,8 +33,6 @@ from .nodes import (
ShotByTextCustomCoordinatesNode,
AttributionByImageNode,
RemoveVideoBackgroundNode,
GreenScreenVideoNode,
ReplaceVideoBackgroundNode,
VideoSolidColorBackgroundNode,
VideoMaskByPromptNode,
VideoMaskByKeyPointsNode,
@@ -84,8 +82,6 @@ NODE_CLASS_MAPPINGS = {
"GenerateStructuredPromptNodeV2": GenerateStructuredPromptNodeV2,
"GenerateStructuredPromptLiteNodeV2": GenerateStructuredPromptLiteNodeV2,
"RemoveVideoBackgroundNode":RemoveVideoBackgroundNode,
"GreenScreenVideoNode": GreenScreenVideoNode,
"ReplaceVideoBackgroundNode": ReplaceVideoBackgroundNode,
"VideoSolidColorBackgroundNode":VideoSolidColorBackgroundNode,
"VideoMaskByPromptNode":VideoMaskByPromptNode,
"VideoMaskByKeyPointsNode":VideoMaskByKeyPointsNode,
@@ -134,9 +130,7 @@ NODE_DISPLAY_NAME_MAPPINGS = {
"RefineImageLiteNodeV2": "FIBO - Refine Image - Lite",
"GenerateStructuredPromptNodeV2": "FIBO - Generate Structured Prompt",
"GenerateStructuredPromptLiteNodeV2": "FIBO - Generate Structured Prompt - Lite",
"RemoveVideoBackgroundNode": "Bria Video Remove Background",
"GreenScreenVideoNode": "Bria Video Green Screen",
"ReplaceVideoBackgroundNode": "Bria Video Replace Background",
"RemoveVideoBackgroundNode": "Bria Remove Video Background",
"VideoSolidColorBackgroundNode":"Bria SolidColor Background Video",
"VideoMaskByPromptNode":"Bria Video Mask By Prompt",
"VideoMaskByKeyPointsNode":"Bria Video Mask By Key Points",
-2
View File
@@ -34,8 +34,6 @@ from .shot_by_image_manual_placement_node import ShotByImageManualPlacementNode
from .shot_by_image_manual_padding_node import ShotByImageManualPaddingNode
from .attribution_by_image_node import AttributionByImageNode
from .video_nodes.remove_video_background_node import RemoveVideoBackgroundNode
from .video_nodes.green_screen_video_node import GreenScreenVideoNode
from .video_nodes.replace_video_background_node import ReplaceVideoBackgroundNode
from .video_nodes.video_increase_resolution_node import VideoIncreaseResolutionNode
from .video_nodes.video_solid_color_background_node import VideoSolidColorBackgroundNode
from .video_nodes.video_erase_elements_node import VideoEraseElementsNode
+15 -83
View File
@@ -6,10 +6,21 @@ import base64
from torchvision.transforms import ToPILImage
import requests
import time
import os
import uuid
BRIA_COMFYUI_USER_AGENT = "bria/ComfyUI"
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}"
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."""
@@ -126,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
@@ -209,82 +220,3 @@ def normalize_images_input(images):
raise ValueError(f"Unsupported input type: {type(images)}")
_EXT_TO_PIL_AND_MIME = {
".png": ("PNG", "image/png"),
".jpg": ("JPEG", "image/jpeg"),
".jpeg": ("JPEG", "image/jpeg"),
".webp": ("WEBP", "image/webp"),
".gif": ("GIF", "image/gif"),
".bmp": ("BMP", "image/bmp"),
".tif": ("TIFF", "image/tiff"),
".tiff": ("TIFF", "image/tiff"),
}
def _pil_format_and_mime_for_filename(file_name):
"""Return (pil_format, content_type, file_name_with_ext). Uses .png only when there is no extension."""
base = file_name.strip() if file_name else ""
if not base:
base = f"{uuid.uuid4()}_background"
root, ext = os.path.splitext(base)
ext = ext.lower()
if not ext:
ext = ".png"
base = f"{root}{ext}"
elif ext not in _EXT_TO_PIL_AND_MIME:
ext = ".png"
base = f"{root}{ext}"
pil_format, mime = _EXT_TO_PIL_AND_MIME[ext]
return pil_format, mime, base
def upload_pil_image_to_temp(pil_image, api_token, file_name=None):
"""
Request an anonymous presigned PUT URL, upload the image bytes, return the public temp URL.
``file_name`` keeps its extension for format and Content-Type; if it has no extension, ``.png``
is appended. Matches platform POST /upload-image/anonymous/presigned-url (same pattern as video).
"""
api_url = "https://platform.prod.bria-api.com/upload-image/anonymous/presigned-url"
headers = {"Content-Type": "application/json"}
if api_token:
headers["api_token"] = api_token
pil_format, content_type, file_name = _pil_format_and_mime_for_filename(file_name or "")
payload = {
"file_name": file_name,
"content_type": content_type,
}
buf = io.BytesIO()
to_save = pil_image
if pil_format == "JPEG" and to_save.mode in ("RGBA", "P"):
to_save = to_save.convert("RGB")
save_kwargs = {}
if pil_format == "JPEG":
save_kwargs["quality"] = 95
to_save.save(buf, format=pil_format, **save_kwargs)
buf.seek(0)
image_bytes = buf.read()
response = requests.post(api_url, json=payload, headers=headers)
if response.status_code != 200:
raise Exception(f"Failed to get image presigned URL: {response.status_code} {response.text}")
response_data = response.json()
image_url = response_data.get("image_url")
upload_url = response_data.get("upload_url")
if not image_url or not upload_url:
raise Exception(f"Invalid response from image presigned URL API: {response_data}")
upload_response = requests.put(
upload_url,
data=image_bytes,
headers={"Content-Type": content_type},
)
if upload_response.status_code not in (200, 204):
raise Exception(f"Failed to upload image to S3: {upload_response.status_code}")
return image_url
+5 -1
View File
@@ -2,6 +2,7 @@ import requests
import torch
from .common import (
bria_asset_headers,
bria_json_headers,
image_to_base64,
poll_status_until_completed,
@@ -149,7 +150,10 @@ class FIBOEditNode:
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)
+5 -1
View File
@@ -1,6 +1,7 @@
import requests
import torch
from .common import (
bria_asset_headers,
bria_json_headers,
image_to_base64,
normalize_images_input,
@@ -137,7 +138,10 @@ class GenerateImageLiteNodeV2:
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)
batch_results.append(result_image)
+5 -1
View File
@@ -2,6 +2,7 @@ import requests
import torch
from .common import (
bria_asset_headers,
bria_json_headers,
image_to_base64,
normalize_images_input,
@@ -145,7 +146,10 @@ class GenerateImageNodeV2:
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)
batch_results.append(result_image)
+5 -1
View File
@@ -5,6 +5,7 @@ import io
import torch
from .common import (
bria_asset_headers,
bria_json_headers,
image_to_base64,
poll_status_until_completed,
@@ -87,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
+5 -1
View File
@@ -5,6 +5,7 @@ from PIL import Image
import torch
from .common import (
bria_asset_headers,
bria_json_headers,
image_to_base64,
normalize_images_input,
@@ -90,7 +91,10 @@ class ImageEnhanceNode():
used_seed = final_response["result"].get("seed", seed)
# Download and process 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)).convert("RGB")
result_array = np.array(result_image).astype(np.float32) / 255.0
result_tensor = torch.from_numpy(result_array) # shape: (H,W,C)
+5 -1
View File
@@ -5,6 +5,7 @@ from PIL import Image
import torch
from .common import (
bria_asset_headers,
bria_json_headers,
image_to_base64,
normalize_images_input,
@@ -119,7 +120,10 @@ class ImageExpansionNode():
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)).convert("RGB")
result_tensor = torch.from_numpy(np.array(result_image).astype(np.float32) / 255.0)
+5 -1
View File
@@ -2,6 +2,7 @@ import requests
import torch
from .common import (
bria_asset_headers,
bria_json_headers,
image_to_base64,
poll_status_until_completed,
@@ -121,7 +122,10 @@ class ProductIntegrateNode:
result_image_url = result.get("image_url")
used_seed = result.get("seed", 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, used_seed)
+10 -2
View File
@@ -1,6 +1,11 @@
import requests
from .common import bria_json_headers, poll_status_until_completed, postprocess_image
from .common import (
bria_asset_headers,
bria_json_headers,
poll_status_until_completed,
postprocess_image,
)
@@ -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)
+10 -2
View File
@@ -1,6 +1,11 @@
import requests
from .common import bria_json_headers, poll_status_until_completed, postprocess_image
from .common import (
bria_asset_headers,
bria_json_headers,
poll_status_until_completed,
postprocess_image,
)
class RefineImageNodeV2:
@@ -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)
+5 -1
View File
@@ -1,6 +1,7 @@
import requests
from .common import (
bria_asset_headers,
bria_json_headers,
image_to_base64,
postprocess_image,
@@ -67,7 +68,10 @@ class ReimagineNode():
)
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:
+5 -1
View File
@@ -5,6 +5,7 @@ from PIL import Image
import torch
from .common import (
bria_asset_headers,
bria_json_headers,
image_to_base64,
normalize_images_input,
@@ -74,7 +75,10 @@ class RemoveForegroundNode():
result_image_url = final_response["result"]["image_url"]
# Download result
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)).convert("RGB")
# Convert to float32 tensor (H, W, C)
+5 -1
View File
@@ -5,6 +5,7 @@ from PIL import Image
import torch
from .common import (
bria_asset_headers,
bria_json_headers,
image_to_base64,
normalize_images_input,
@@ -108,7 +109,10 @@ class ReplaceBgNode():
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)).convert("RGB")
result_tensor = torch.from_numpy(np.array(result_image).astype(np.float32) / 255.0)
+5 -1
View File
@@ -5,6 +5,7 @@ from PIL import Image
import torch
from .common import (
bria_asset_headers,
bria_json_headers,
image_to_base64,
normalize_images_input,
@@ -71,7 +72,10 @@ class RmbgNode():
result_image_url = final_response['result']['image_url']
# Download result
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))
# Convert to float32 tensor (H, W, C), 0-1
+5 -1
View File
@@ -1,6 +1,7 @@
import requests
from .common import (
bria_asset_headers,
bria_json_headers,
image_to_base64,
postprocess_image,
@@ -82,7 +83,10 @@ class TailoredGenNode():
)
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:
+5 -1
View File
@@ -5,6 +5,7 @@ from PIL import Image
import torch
from .common import (
bria_asset_headers,
bria_json_headers,
image_to_base64,
normalize_images_input,
@@ -70,7 +71,10 @@ class TailoredPortraitNode():
raise Exception(f"API request failed with status {response.status_code}: {response.text}")
response_dict = response.json()
image_response = requests.get(response_dict["image_res"])
image_response = requests.get(
response_dict["image_res"],
headers=bria_asset_headers(),
)
result_image = Image.open(io.BytesIO(image_response.content)).convert("RGB")
# Convert to float32 tensor (H,W,C), 0-1
+5 -1
View File
@@ -1,6 +1,7 @@
import requests
from .common import (
bria_asset_headers,
bria_json_headers,
image_to_base64,
postprocess_image,
@@ -92,7 +93,10 @@ class Text2ImageBaseNode():
)
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:
+5 -1
View File
@@ -1,6 +1,7 @@
import requests
from .common import (
bria_asset_headers,
bria_json_headers,
image_to_base64,
postprocess_image,
@@ -85,7 +86,10 @@ class Text2ImageFastNode():
)
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:
+5 -2
View File
@@ -1,6 +1,6 @@
import requests
from .common import bria_json_headers, postprocess_image
from .common import bria_asset_headers, bria_json_headers, postprocess_image
class Text2ImageHDNode():
@@ -56,7 +56,10 @@ class Text2ImageHDNode():
)
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:
+9 -2
View File
@@ -1,6 +1,7 @@
import requests
import torch
from ..common import (
bria_asset_headers,
bria_json_headers,
image_to_base64,
postprocess_image,
@@ -145,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)
@@ -156,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:
@@ -1,116 +0,0 @@
import os
import uuid
import requests
from ..common import (
bria_json_headers,
poll_status_until_completed,
)
from .video_utils import upload_video_to_s3
class GreenScreenVideoNode():
"""
Applies green-screen (chroma key) background removal using the Bria API
(POST /v2/video/edit/green_screen). Output is a processed video with a solid-color background.
"""
@classmethod
def INPUT_TYPES(self):
return {
"required": {
"api_key": ("STRING", {"default": "BRIA_API_TOKEN"}),
"video_url": ("STRING", {
"default": "",
"tooltip": "Local path or publicly accessible URL of the video to process.",
}),
},
"optional": {
"green_shade": ([
"broadcast_green",
"chroma_green",
"blue_screen",
], {"default": "broadcast_green"}),
"output_container_and_codec": ([
"mp4_h264",
"mp4_h265",
"webm_vp9",
"mov_h265",
"mov_proresks",
"mkv_h264",
"mkv_h265",
"mkv_vp9",
"gif"
], {"default": "mp4_h264"}),
"preserve_audio": ("BOOLEAN", {"default": True}),
}
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("result_video_url",)
CATEGORY = "API Nodes"
FUNCTION = "execute"
def __init__(self):
self.api_url = "https://engine.prod.bria-api.com/v2/video/edit/green_screen"
def execute(
self,
api_key,
video_url,
green_shade="broadcast_green",
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.")
if not video_url or not str(video_url).strip():
raise Exception("video_url is required: provide a local path or a publicly accessible video URL.")
if os.path.exists(video_url):
filename = f"{str(uuid.uuid4())}_{os.path.basename(video_url)}"
input_video_url = upload_video_to_s3(video_url, filename, api_key)
if not input_video_url or not (
input_video_url.startswith("http://") or input_video_url.startswith("https://")
):
raise Exception(f"Failed to upload video to S3. Got: {input_video_url}")
else:
input_video_url = video_url.strip()
try:
print("Calling Bria API for video green screen...")
payload = {
"video": input_video_url,
"green_shade": green_shade,
"output_container_and_codec": output_container_and_codec,
"preserve_audio": preserve_audio,
}
headers = bria_json_headers(api_key)
response = requests.post(self.api_url, json=payload, headers=headers)
if response.status_code == 200 or response.status_code == 202:
print("Initial video green-screen request accepted, 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_key, timeout=3600, check_interval=5
)
result_video_url = final_response["result"]["video_url"]
print(f"Video processing completed. Result URL: {result_video_url}")
return (result_video_url,)
raise Exception(f"Error: API request failed with status code {response.status_code} {response.text}")
except Exception as e:
raise Exception(f"{e}")
@@ -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
@@ -14,7 +14,6 @@ class RemoveVideoBackgroundNode():
video_url (str): Local path or URL of the video to process.
preserve_audio (bool, optional): Whether to keep the audio track. Default is True.
output_container_and_codec (str, optional): Desired output format and codec. Default is "webm_vp9".
background_color Predefined string only - one of the predefined enum values
Returns:
result_video_url (STRING): URL of the video with background removed.
@@ -42,19 +41,6 @@ class RemoveVideoBackgroundNode():
"mkv_vp9",
"gif"
], {"default": "webm_vp9"}),
"background_color": ([
"Transparent",
"Black",
"White",
"Gray",
"Red",
"Green",
"Blue",
"Yellow",
"Cyan",
"Magenta",
"Orange"
], {"default": "Black"})
}
}
@@ -66,7 +52,7 @@ class RemoveVideoBackgroundNode():
def __init__(self):
self.api_url = "https://engine.prod.bria-api.com/v2/video/edit/remove_background"
def execute(self, api_key, video_url, preserve_audio=True, output_container_and_codec="webm_vp9",background_color="Black"):
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.")
video_path = None
@@ -87,8 +73,7 @@ class RemoveVideoBackgroundNode():
payload = {
"video": input_video_url,
"preserve_audio": preserve_audio,
"output_container_and_codec": output_container_and_codec,
"background_color":background_color
"output_container_and_codec": output_container_and_codec
}
headers = bria_json_headers(api_key)
@@ -1,152 +0,0 @@
import os
import uuid
import requests
from ..common import (
bria_json_headers,
normalize_images_input,
poll_status_until_completed,
upload_pil_image_to_temp
)
from .video_utils import upload_video_to_s3
class ReplaceVideoBackgroundNode():
"""
Composites a new background (image or video URL, or an IMAGE from another node) behind the
foreground video using the Bria API (POST /v2/video/edit/replace_background).
When ``background_image`` is connected, only the first image is used (no batch); it is uploaded
via the platform anonymous image presigned URL (same pattern as video) and the resulting
``https://temp.bria.ai/...`` URL is sent in ``background_url``.
The background asset must match the foreground aspect ratio; otherwise the API may return
BACKGROUND_ASPECT_RATIO_MISMATCH (surfaced with foreground and background aspect ratio values).
"""
@classmethod
def INPUT_TYPES(self):
return {
"required": {
"api_key": ("STRING", {"default": "BRIA_API_TOKEN"}),
"video_url": ("STRING", {
"default": "",
"tooltip": "Local path or publicly accessible URL of the foreground video.",
}),
},
"optional": {
"background_url": ("STRING", {
"default": "",
"tooltip": "Public HTTPS image or video URL, if not using background_image.",
}),
"background_image": ("IMAGE",),
"output_container_and_codec": ([
"mp4_h264",
"mp4_h265",
"webm_vp9",
"mov_h265",
"mov_proresks",
"mkv_h264",
"mkv_h265",
"mkv_vp9",
"gif"
], {"default": "mp4_h264"}),
"preserve_audio": ("BOOLEAN", {"default": True}),
}
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("result_video_url",)
CATEGORY = "API Nodes"
FUNCTION = "execute"
def __init__(self):
self.api_url = "https://engine.prod.bria-api.com/v2/video/edit/replace_background"
@staticmethod
def _background_image_to_temp_url(background_image, api_key):
"""First image only; upload to temp bucket (format from file_name extension; .png if none)."""
if background_image is None:
return None
try:
pil_images = normalize_images_input(background_image)
except (ValueError, TypeError) as e:
raise Exception(f"Invalid background_image: {e}") from e
if not pil_images:
raise Exception("background_image produced no images.")
file_name = f"{uuid.uuid4()}_background"
return upload_pil_image_to_temp(pil_images[0], api_key, file_name=file_name)
def execute(
self,
api_key,
video_url,
background_url="",
background_image=None,
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.")
if not video_url or not str(video_url).strip():
raise Exception("video_url is required: provide a local path or a publicly accessible video URL.")
bg_from_image = self._background_image_to_temp_url(background_image, api_key)
bg_from_url = str(background_url).strip() if background_url else ""
if bg_from_image:
bg = bg_from_image
elif bg_from_url:
bg = bg_from_url
else:
raise Exception(
"Provide either background_image (IMAGE from Load Image, Generate Image, etc.) "
"or a non-empty background_url (HTTPS image or video URL)."
)
if os.path.exists(video_url):
filename = f"{str(uuid.uuid4())}_{os.path.basename(video_url)}"
input_video_url = upload_video_to_s3(video_url, filename, api_key)
if not input_video_url or not (
input_video_url.startswith("http://") or input_video_url.startswith("https://")
):
raise Exception(f"Failed to upload video to S3. Got: {input_video_url}")
else:
input_video_url = video_url.strip()
try:
print("Calling Bria API for video replace background...")
payload = {
"video": input_video_url,
"background_url": bg,
"output_container_and_codec": output_container_and_codec,
"preserve_audio": preserve_audio,
}
headers = bria_json_headers(api_key)
response = requests.post(self.api_url, json=payload, headers=headers)
if response.status_code == 200 or response.status_code == 202:
print("Initial video replace-background request accepted, 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_key, timeout=3600, check_interval=5
)
result_video_url = final_response["result"]["video_url"]
print(f"Video processing completed. Result URL: {result_video_url}")
return (result_video_url,)
raise Exception(f"Error: API request failed with status code {response.status_code} {response.text}")
except Exception as e:
raise Exception(f"{e}")
+3 -2
View File
@@ -1,7 +1,7 @@
import os
import requests
from ..common import BRIA_COMFYUI_USER_AGENT
from ..common import BRIA_COMFYUI_USER_AGENT, bria_asset_headers
def upload_video_to_s3(video_path, filename, api_token):
@@ -55,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.19"
version = "2.1.17"
license = {file = "LICENSE"}
[project.urls]
-150
View File
@@ -1,150 +0,0 @@
{
"id": "faacd69e-30b7-4ae8-a9e0-11e3523139f9",
"revision": 0,
"last_node_id": 14,
"last_link_id": 9,
"nodes": [
{
"id": 5,
"type": "LoadVideoFramesNode",
"pos": [
-377.4424627503264,
265.04224992480954
],
"size": [
270,
276.890625
],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "video_path",
"type": "STRING",
"links": [
3
]
}
],
"properties": {
"Node name for S&R": "LoadVideoFramesNode"
},
"widgets_values": [
"6952253-uhd_3840_2160_25fps.mp4",
"image"
]
},
{
"id": 7,
"type": "RemoveVideoBackgroundNode",
"pos": [
176.90761152601942,
244.1153564319173
],
"size": [
550.953125,
348.203125
],
"flags": {},
"order": 1,
"mode": 0,
"inputs": [
{
"name": "video_url",
"type": "STRING",
"widget": {
"name": "video_url"
},
"link": 3
}
],
"outputs": [
{
"name": "result_video_url",
"type": "STRING",
"links": [
4
]
}
],
"properties": {
"Node name for S&R": "RemoveVideoBackgroundNode"
},
"widgets_values": [
"",
"",
true,
"webm_vp9"
]
},
{
"id": 8,
"type": "PreviewVideoURLNode",
"pos": [
1020.6454228509147,
264.9389174606007
],
"size": [
270,
252.890625
],
"flags": {},
"order": 2,
"mode": 0,
"inputs": [
{
"name": "video_url",
"type": "STRING",
"widget": {
"name": "video_url"
},
"link": 4
}
],
"outputs": [],
"properties": {
"Node name for S&R": "PreviewVideoURLNode"
},
"widgets_values": [
""
]
}
],
"links": [
[
3,
5,
0,
7,
0,
"STRING"
],
[
4,
7,
0,
8,
0,
"STRING"
]
],
"groups": [],
"config": {},
"extra": {
"ds": {
"scale": 0.43094644375454877,
"offset": [
685.6754838043107,
494.0393339664482
]
},
"frontendVersion": "1.43.18",
"VHS_latentpreview": false,
"VHS_latentpreviewrate": 0,
"VHS_MetadataImage": true,
"VHS_KeepIntermediate": true
},
"version": 0.4
}