Compare commits

..
Author SHA1 Message Date
omriandClaude Opus 4.8 68910c584b ci: release-please flow + trunk-based dev, hygiene
Onboard ComfyUI-BRIA-API onto the Bria client-artifact conventions (matching
figma-plugin / photoshop-plugin / obs-backgroundremoval), adapted for a ComfyUI
custom node published to the Comfy Registry:

- release-please (release-please.yml, config, manifest baseline 2.1.19,
  release-type python): watches dev, opens a version-bump PR from Conventional
  Commits, cuts a vX.Y.Z tag + GitHub release on merge. Plain GITHUB_TOKEN is
  enough — publishing keys off the pyproject.toml version, not the tag.
- publish.yml: retargeted from main -> dev. Merging the release PR lands the
  pyproject.toml bump on dev, triggering the Comfy Registry publish.
- ci.yml: build check (compileall + pyproject.toml validity) + advisory ruff
  format, on PRs and pushes to dev.
- pr-title.yml: conventional-commit PR title gate.
- Hygiene: .agents convention (AGENTS.md + CLAUDE.md + canonical code-style rule
  + .claude/.cursor stubs), .pre-commit-config.yaml (ruff + generic hooks).

Version is no longer hand-bumped; dev is the single trunk.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-09 15:25:39 +03:00
Yazan Numoor 822e3b2fac Merge pull request #46 from Bria-AI/WAI-4807
WAI-4807
2026-06-02 11:48:50 +03:00
Yazan Numoor 4974b03494 Update default background color in execute method
Changed default background color from 'Transparent' to 'Black' in execute method.
2026-06-02 11:48:31 +03:00
Yazan Numoor 62dc71a50c Change default color from Transparent to Black 2026-06-02 11:47:47 +03:00
Lizayaro 0708ec927c Revise API token section in Readme.md
Updated API token acquisition instructions and links.
2026-06-01 20:14:38 +03:00
Lizayaro 9e43b59148 Add example workflow note to Readme
Added a note about example workflow for video loading and previewing.
2026-06-01 20:09:38 +03:00
Lizayaro ed4e307625 Add files via upload 2026-06-01 18:13:10 +03:00
Ubuntu 4bd6cedc90 WAI-4807 2026-06-01 15:06:15 +00:00
Yazan Numoor 12357dfa88 Bump version to 2.1.18 2026-05-05 12:10:17 +03:00
Yazan Numoor 97a7b41718 Merge pull request #45 from Bria-AI/WAI-4748
WAI-4748
2026-05-05 12:09:57 +03:00
Ubuntu b914aa4c13 remove special header token and remove background_type param 2026-05-05 08:21:41 +00:00
Gilad-brudner-bria fc5dd7b3c4 Update Readme.md 2026-05-05 07:11:13 +03:00
mabualrob1997 a96edfc94e Update Readme.md 2026-05-04 18:44:20 +03:00
Ubuntu 1cdf246a6e WAI-4748 2026-05-04 14:55:48 +00:00
Ubuntu e42c2727af WAI-4748 2026-05-04 12:12:42 +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 a3451b6b86 update_fibo_edit_node_default_steps_num 2026-02-12 16:43:29 +00:00
22 changed files with 866 additions and 12 deletions
+48
View File
@@ -0,0 +1,48 @@
# Code Style
Conventions for this Bria **ComfyUI custom-node** package: **Python** nodes in `nodes/` calling
BRIA's API, front-end assets in `web/`.
## Formatting
- **Ruff** is the formatter/linter (`ruff format`, `ruff check`). Format with `ruff format <files>`
before committing; do not hand-format.
- Format the files **you touch** — do not reformat the whole tree in a feature PR. The existing tree
predates the formatter and has drift; a repo-wide reformat belongs in its own change. The CI
format check is advisory for this reason (see `AGENTS.md`).
- There is no committed Ruff config yet; defaults apply. Introduce a `[tool.ruff]` config in its own
change if wanted, not in a feature PR.
## ComfyUI nodes
- Each node is a class exposing the ComfyUI contract (`INPUT_TYPES`, `RETURN_TYPES`, `FUNCTION`,
`CATEGORY`). Register it in **both** `NODE_CLASS_MAPPINGS` and `NODE_DISPLAY_NAME_MAPPINGS` in
`__init__.py`.
- **No network or heavy work at import time** — the package is imported on every ComfyUI startup.
Keep BRIA API calls inside the node's execute method.
- `torch`/`comfy`/`folder_paths` etc. are host-provided by the ComfyUI runtime — import them, do not
add them as install dependencies in `pyproject.toml`.
- Never hardcode a BRIA API token; it is a runtime node input.
## Versioning
- The version of record is `pyproject.toml` `[project].version`; it is bumped by release-please and
read by the Comfy Registry. Never bump it by hand — releases are cut from Conventional Commits.
## Commit Messages
Follow [Conventional Commits](https://www.conventionalcommits.org/). Allowed types: `feat`, `fix`,
`docs`, `style`, `refactor`, `perf`, `test`, `build`, `ci`, `chore`, `revert`. PR titles are
enforced by the `pr-title` workflow (`amannn/action-semantic-pull-request`), and these types drive
release-please's version bumps.
```
feat: add FIBO edit node with structured instruction
fix: guard empty selection in Bria Multi Image Select
```
## Before You Commit
Install the hooks once (`pre-commit install`) so `.pre-commit-config.yaml` runs on every commit:
generic file hygiene plus Ruff on the files you touch. The build check
(`python -m compileall` + pyproject validation) runs in CI (`.github/workflows/ci.yml`).
+3
View File
@@ -0,0 +1,3 @@
Canonical rule: [`.agents/rules/code-style.md`](../../.agents/rules/code-style.md).
This is a stub — do not duplicate content here. Read and apply the canonical rule.
+8
View File
@@ -0,0 +1,8 @@
---
description: Code style for the Bria ComfyUI-BRIA-API custom nodes
alwaysApply: true
---
Canonical rule: [`.agents/rules/code-style.md`](../../.agents/rules/code-style.md).
This is a stub — do not duplicate content here. Read and apply the canonical rule.
+71
View File
@@ -0,0 +1,71 @@
# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
# SPDX-FileCopyrightText: 2026 Bria AI <support@bria.ai>
#
# SPDX-License-Identifier: Apache-2.0
name: CI
on:
pull_request:
push:
branches: [dev]
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
build:
name: Build check
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.11"
# ComfyUI + torch are heavy host deps we don't install in CI; compileall
# catches syntax errors across all node modules without importing them.
- name: Compile all sources
run: python -m compileall -q nodes __init__.py
# pyproject.toml is the version source of record (release-please bumps it,
# and the Comfy Registry reads it) — fail fast if it stops parsing.
- name: Validate pyproject.toml
run: |
python - <<'PY'
import tomllib
with open("pyproject.toml", "rb") as f:
data = tomllib.load(f)
assert data["project"]["version"], "pyproject.toml [project].version missing"
assert data["tool"]["comfy"]["PublisherId"], "[tool.comfy].PublisherId missing"
print("pyproject.toml OK — version", data["project"]["version"])
PY
format:
name: Ruff format check (advisory)
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install ruff
run: pip install ruff
# Advisory only: the existing tree predates any formatter and a whole-repo
# run would flag pre-existing files, so this never fails the check yet.
# Format files you touch with `ruff format <files>`; once the tree is
# clean, drop continue-on-error to make it a hard gate.
- name: Check formatting
continue-on-error: true
run: ruff format --check .
+23
View File
@@ -0,0 +1,23 @@
# SPDX-FileCopyrightText: 2026 Bria AI <support@bria.ai>
#
# SPDX-License-Identifier: Apache-2.0
name: PR Title
on:
pull_request:
permissions:
pull-requests: read
contents: read
jobs:
pr-title:
name: Validate PR Title
if: github.event_name == 'pull_request'
runs-on: ubuntu-latest
steps:
- name: Check PR title follows conventional commits
uses: amannn/action-semantic-pull-request@v6
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+5 -1
View File
@@ -2,8 +2,12 @@ name: Publish to Comfy registry
on:
workflow_dispatch:
push:
# dev is the single trunk (§ branch model). Publishing fires when the
# pyproject.toml version bump lands on dev — i.e. when release-please's
# release PR is merged. The Comfy publish action reads the version from
# pyproject.toml, so no tag is involved.
branches:
- main
- dev
paths:
- "pyproject.toml"
+35
View File
@@ -0,0 +1,35 @@
# yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json
# SPDX-FileCopyrightText: 2026 Bria AI <support@bria.ai>
#
# SPDX-License-Identifier: Apache-2.0
name: Release Please
on:
push:
branches: [dev]
permissions: {}
jobs:
release-please:
name: Release Please
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
# Plain GITHUB_TOKEN is sufficient here: publishing to the Comfy Registry
# is triggered by the pyproject.toml version bump landing on dev (see
# publish.yml), NOT by the tag release-please pushes — so we don't need a
# PAT to make a tag trigger a downstream workflow. release-please still
# cuts the tag + GitHub release for provenance.
- uses: googleapis/release-please-action@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
config-file: release-please-config.json
manifest-file: .release-please-manifest.json
+38
View File
@@ -0,0 +1,38 @@
# SPDX-FileCopyrightText: 2026 Bria AI <support@bria.ai>
#
# SPDX-License-Identifier: Apache-2.0
# yaml-language-server: $schema=https://json.schemastore.org/pre-commit-config.json
default_install_hook_types:
- pre-commit
- commit-msg
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-merge-conflict
- id: check-yaml
args: [--allow-multiple-documents]
- id: check-toml
- id: check-added-large-files
- id: mixed-line-ending
args: [--fix=lf]
# Ruff runs only on the files you stage (format + lint).
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.7.4
hooks:
- id: ruff-format
- id: ruff
args: [--fix]
- repo: https://github.com/compilerla/conventional-pre-commit
rev: v3.4.0
hooks:
- id: conventional-pre-commit
stages: [commit-msg]
args: [feat, fix, docs, style, refactor, perf, test, build, ci, chore, revert]
+3
View File
@@ -0,0 +1,3 @@
{
".": "2.1.19"
}
+75
View File
@@ -0,0 +1,75 @@
# AGENTS.md
Bria's **ComfyUI custom-node package** — Python nodes that call **BRIA's API** from ComfyUI
(`comfyui-bria-api`, published to the **Comfy Registry** as publisher `briaai`). Node
implementations live in `nodes/`, are re-exported from `__init__.py` (`NODE_CLASS_MAPPINGS` +
`NODE_DISPLAY_NAME_MAPPINGS`), with front-end assets in `web/` (`WEB_DIRECTORY`) and example graphs
in `workflows/`.
Project rules live in `.agents/rules/`. Read and apply each:
- `.agents/rules/**`
## Environment
- **Python 3.11+**. ComfyUI + torch are the host runtime — provided by the ComfyUI install the node
runs inside, **not** declared as install deps here.
- The **version of record is `pyproject.toml` `[project].version`** — the Comfy Registry reads it,
and release-please bumps it. **Do not bump it by hand**; releases are cut from Conventional
Commits (see below).
- API tokens are supplied by the user at runtime through the node inputs — never hardcode or commit
a BRIA API token.
## Nodes
- Each node is a class in `nodes/` exposing the ComfyUI contract (`INPUT_TYPES`, `RETURN_TYPES`,
`FUNCTION`, `CATEGORY`, an `execute`-style method). Register a new node by adding it to both
`NODE_CLASS_MAPPINGS` and `NODE_DISPLAY_NAME_MAPPINGS` in `__init__.py`.
- Keep BRIA API calls in the node's method (not at import time) — importing the package must not do
network I/O, or ComfyUI startup breaks.
- Front-end widgets go under `web/` and are exposed via `WEB_DIRECTORY = "./web"`.
## Checks — keep them green
CI (`.github/workflows/ci.yml`, on PRs and pushes to `dev`) gates on:
| Check | Command |
| ----- | ------- |
| Build | `python -m compileall nodes __init__.py` + a `pyproject.toml` validity check |
| Format | `ruff format --check .` — **advisory** (see below) |
The Ruff format check is intentionally **advisory** (`continue-on-error: true`) today: the existing
tree predates any formatter and a whole-repo run flags pre-existing files. Format the files you
touch with `ruff format <files>`; once the tree is clean, drop `continue-on-error` to make it a hard
gate. PR titles are validated against Conventional Commits by `pr-title.yml`.
Locally, `pre-commit` adds generic file hygiene plus Ruff on the files you touch — install it once
with `pre-commit install` (config: `.pre-commit-config.yaml`).
## Agent Configuration
All agent rules are canonical in `.agents/`. Thin stubs under `.claude/` and `.cursor/` point back
here — never duplicate content.
```
.agents/
└── rules/ Always-on rules — loaded by all agents on startup
```
| Tool | Rules |
| ----------- | ------------------------------------------------------------------------------ |
| Claude Code | `.claude/rules/` stubs → `.agents/rules/` (and root `CLAUDE.md` → `AGENTS.md`) |
| Cursor | `.cursor/rules/*.mdc` → `.agents/rules/` |
## Release flow (trunk-based on `dev`)
- **`dev`** is the single trunk and default branch. Do not use `main`.
- **release**: `release-please` (`.github/workflows/release-please.yml`) watches `dev`, opens a
release PR from Conventional Commits that bumps `pyproject.toml` + `CHANGELOG.md`; merging it cuts
a `vX.Y.Z` tag + GitHub release.
- **publish**: merging that release PR lands the version bump on `dev`, which triggers
`.github/workflows/publish.yml` (`Comfy-Org/publish-node-action`) to publish the new version to
the **Comfy Registry**. Publishing keys off the `pyproject.toml` version, not the tag — so plain
`GITHUB_TOKEN` is enough for release-please (no PAT needed to trigger a downstream tag build).
See `Readme.md` for the user-facing node documentation.
+1
View File
@@ -0,0 +1 @@
Read and apply all instructions from @AGENTS.md.
+19 -5
View File
@@ -8,10 +8,7 @@ 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 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>.
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).
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.
@@ -107,13 +104,30 @@ 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
+7 -1
View File
@@ -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",
+2
View File
@@ -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
View File
@@ -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
+1 -1
View File
@@ -31,7 +31,7 @@ class FIBOEditNode:
"steps_num": (
"INT",
{
"default": 50,
"default": 30,
"min": 1,
"max": 100,
},
@@ -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}")
@@ -14,6 +14,7 @@ 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.
@@ -41,6 +42,19 @@ class RemoveVideoBackgroundNode():
"mkv_vp9",
"gif"
], {"default": "webm_vp9"}),
"background_color": ([
"Transparent",
"Black",
"White",
"Gray",
"Red",
"Green",
"Blue",
"Yellow",
"Cyan",
"Magenta",
"Orange"
], {"default": "Black"})
}
}
@@ -52,7 +66,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",):
def execute(self, api_key, video_url, preserve_audio=True, output_container_and_codec="webm_vp9",background_color="Black"):
if api_key.strip() == "" or api_key.strip() == "BRIA_API_TOKEN":
raise Exception("Please insert a valid API key.")
video_path = None
@@ -73,7 +87,8 @@ class RemoveVideoBackgroundNode():
payload = {
"video": input_video_url,
"preserve_audio": preserve_audio,
"output_container_and_codec": output_container_and_codec
"output_container_and_codec": output_container_and_codec,
"background_color":background_color
}
headers = bria_json_headers(api_key)
@@ -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}")
+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.16"
version = "2.1.19"
license = {file = "LICENSE"}
[project.urls]
+10
View File
@@ -0,0 +1,10 @@
{
"$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json",
"release-type": "python",
"include-component-in-tag": false,
"packages": {
".": {
"changelog-path": "CHANGELOG.md"
}
}
}
+150
View File
@@ -0,0 +1,150 @@
{
"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
}