Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
11c60b5558 |
@@ -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
@@ -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",
|
||||
|
||||
@@ -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
@@ -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
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,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,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,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)
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,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,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
@@ -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
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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}")
|
||||
@@ -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
@@ -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]
|
||||
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user