@@ -107,13 +107,29 @@ 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 |
|
||||
|
||||
|
||||
## 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
|
||||
|
||||
+7
-1
@@ -33,6 +33,8 @@ from .nodes import (
|
||||
ShotByTextCustomCoordinatesNode,
|
||||
AttributionByImageNode,
|
||||
RemoveVideoBackgroundNode,
|
||||
GreenScreenVideoNode,
|
||||
ReplaceVideoBackgroundNode,
|
||||
VideoSolidColorBackgroundNode,
|
||||
VideoMaskByPromptNode,
|
||||
VideoMaskByKeyPointsNode,
|
||||
@@ -82,6 +84,8 @@ NODE_CLASS_MAPPINGS = {
|
||||
"GenerateStructuredPromptNodeV2": GenerateStructuredPromptNodeV2,
|
||||
"GenerateStructuredPromptLiteNodeV2": GenerateStructuredPromptLiteNodeV2,
|
||||
"RemoveVideoBackgroundNode":RemoveVideoBackgroundNode,
|
||||
"GreenScreenVideoNode": GreenScreenVideoNode,
|
||||
"ReplaceVideoBackgroundNode": ReplaceVideoBackgroundNode,
|
||||
"VideoSolidColorBackgroundNode":VideoSolidColorBackgroundNode,
|
||||
"VideoMaskByPromptNode":VideoMaskByPromptNode,
|
||||
"VideoMaskByKeyPointsNode":VideoMaskByKeyPointsNode,
|
||||
@@ -130,7 +134,9 @@ NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"RefineImageLiteNodeV2": "FIBO - Refine Image - Lite",
|
||||
"GenerateStructuredPromptNodeV2": "FIBO - Generate Structured Prompt",
|
||||
"GenerateStructuredPromptLiteNodeV2": "FIBO - Generate Structured Prompt - Lite",
|
||||
"RemoveVideoBackgroundNode": "Bria Remove Video Background",
|
||||
"RemoveVideoBackgroundNode": "Bria Video Remove Background",
|
||||
"GreenScreenVideoNode": "Bria Video Green Screen",
|
||||
"ReplaceVideoBackgroundNode": "Bria Video Replace Background",
|
||||
"VideoSolidColorBackgroundNode":"Bria SolidColor Background Video",
|
||||
"VideoMaskByPromptNode":"Bria Video Mask By Prompt",
|
||||
"VideoMaskByKeyPointsNode":"Bria Video Mask By Key Points",
|
||||
|
||||
@@ -34,6 +34,8 @@ 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
|
||||
|
||||
+81
-1
@@ -6,7 +6,8 @@ import base64
|
||||
from torchvision.transforms import ToPILImage
|
||||
import requests
|
||||
import time
|
||||
|
||||
import os
|
||||
import uuid
|
||||
|
||||
BRIA_COMFYUI_USER_AGENT = "bria/ComfyUI"
|
||||
|
||||
@@ -208,3 +209,82 @@ 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
|
||||
@@ -0,0 +1,116 @@
|
||||
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}")
|
||||
@@ -0,0 +1,152 @@
|
||||
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}")
|
||||
Reference in New Issue
Block a user