Compare commits
16
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
68910c584b | ||
|
|
822e3b2fac | ||
|
|
4974b03494 | ||
|
|
62dc71a50c | ||
|
|
0708ec927c | ||
|
|
9e43b59148 | ||
|
|
ed4e307625 | ||
|
|
4bd6cedc90 | ||
|
|
12357dfa88 | ||
|
|
97a7b41718 | ||
|
|
b914aa4c13 | ||
|
|
fc5dd7b3c4 | ||
|
|
a96edfc94e | ||
|
|
1cdf246a6e | ||
|
|
e42c2727af | ||
|
|
c5468b2525 |
@@ -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`).
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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 .
|
||||
@@ -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 }}
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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]
|
||||
@@ -0,0 +1,3 @@
|
||||
{
|
||||
".": "2.1.19"
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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
@@ -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}")
|
||||
@@ -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
@@ -1,7 +1,7 @@
|
||||
[project]
|
||||
name = "comfyui-bria-api"
|
||||
description = "Custom nodes for ComfyUI using BRIA's API."
|
||||
version = "2.1.17"
|
||||
version = "2.1.19"
|
||||
license = {file = "LICENSE"}
|
||||
|
||||
[project.urls]
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user