Compare commits

...
Author SHA1 Message Date
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 339f26b64f WAI-4698:set User-Agent header 2026-04-14 11:58:02 +00:00
Yazan Numoor 221f31068c Merge pull request #42 from Bria-AI/WAI-4545
WAI-4545
2026-03-01 16:27:02 +02:00
Ubuntu d394eda558 WAI-4545 2026-02-25 11:19:43 +00:00
Ubuntu 54cadf8578 WAI-4545 2026-02-25 09:34:48 +00:00
Ubuntu a3451b6b86 update_fibo_edit_node_default_steps_num 2026-02-12 16:43:29 +00:00
Yazan Numoor 95e5c74266 Merge pull request #39 from Bria-AI/WAI-4383
WAI-4383
2026-01-25 12:07:01 +02:00
Ubuntu e61b45a79a fix conflict 2026-01-25 10:00:28 +00:00
Yazan Numoor e8ad98c0a6 Merge pull request #40 from Bria-AI/WAI-4150
Wai 4150
2026-01-25 11:10:02 +02:00
Ubuntu 3e9599c0fb WAI-4383: update to support multiple images 2026-01-24 21:06:12 +00:00
Ubuntu a52486bbb1 WAI-4150 2026-01-24 20:58:43 +00:00
Ubuntu aa768b6cdf WAI-4150 2026-01-24 20:36:38 +00:00
Ubuntu fda3d917b2 WAI-4383 2026-01-14 16:37:04 +00:00
Ubuntu a970f19bf2 WAI-4383 2026-01-13 19:38:15 +00:00
Yazan Numoor 2c97deb804 Merge pull request #38 from Bria-AI/WAI-4306
WAI-4306
2025-12-24 11:34:19 +02:00
Ubuntu e9b797ae90 update version 2025-12-24 08:53:18 +00:00
Ubuntu 2c6f9754d3 WAI-4306 2025-12-23 06:44:41 +00:00
Yazan Numoor 7dd5d3a250 Merge pull request #37 from Bria-AI/add_fibo_prefix_to_fibo_nodes
Add fibo prefix to fibo nodes
2025-12-22 13:40:23 +02:00
Ubuntu ddd1252ecb update version 2025-12-22 11:23:11 +00:00
Ubuntu 812f7f88fa add_fibo_prefix_to_fibo_nodes 2025-12-22 11:21:52 +00:00
Yazan Numoor 80332ca13c Update pyproject.toml 2025-12-16 17:12:41 +02:00
Yazan Numoor 2b636d8a57 Merge pull request #35 from Bria-AI/WAI-4253
WAI-4253
2025-12-16 17:12:21 +02:00
Yazan Numoor 855ed814ed Merge branch 'main' into WAI-4253 2025-12-16 17:11:53 +02:00
Yazan Numoor 8f7edd926d Update pyproject.toml 2025-12-16 12:17:30 +02:00
Yazan Numoor c2cf2f044f Merge pull request #36 from Bria-AI/split_fibo_nodes
split_fibo_nodes
2025-12-16 12:16:37 +02:00
Ubuntu 5ccf16a4d3 split_fibo_nodes 2025-12-16 09:42:20 +00:00
Yazan Numoor 2cab356750 Merge pull request #34 from Bria-AI/update_Readme_file_for_Fibo
Update Readme.md
2025-12-16 08:28:20 +02:00
Yazan Numoor c9f880bdd8 Merge pull request #33 from Bria-AI/WAI-4234
WAI-4234
2025-12-16 08:27:11 +02:00
Ubuntu 82f31ddf72 refactor video flow 2025-12-15 11:23:41 +00:00
Ubuntu 00de95a160 update min steps for fibo 2025-12-11 20:35:51 +00:00
Ubuntu 15b69d2a03 fix_fibo_lite_steps 2025-12-11 14:01:19 +00:00
Ubuntu 1cfe5758f1 output the resukt url 2025-12-10 13:34:36 +00:00
Ubuntu 51e75b8f9e update fibo lite flow 2025-12-10 07:27:23 +00:00
Ubuntu 98322c3c96 update default steps for fibo lite 2025-12-10 07:20:19 +00:00
Ubuntu 862403893f fix default value 2025-12-09 08:31:08 +00:00
Ubuntu 0e0e59bbd4 fix default value 2025-12-08 11:25:00 +00:00
Ubuntu a611bddb9f support load, preview videos nodes 2025-12-08 11:08:19 +00:00
Ubuntu 6a94e8b91b WAI-4253 2025-12-03 10:23:07 +00:00
mabualrob1997 725868a7eb Update Readme.md 2025-12-01 16:46:53 +02:00
Ubuntu 0a81405f6a fix Gabi Feedback 2025-11-30 20:29:27 +00:00
Ubuntu ff8489b8d3 update version 2025-11-26 20:23:32 +00:00
Ubuntu 05406c1cef fix Structured prompt generate parameters 2025-11-26 20:17:22 +00:00
Ubuntu dffed52b27 WAI-4234 2025-11-23 20:22:26 +00:00
Ubuntu 3d7c41a15a WAI-4234 2025-11-23 20:11:25 +00:00
Yazan Numoor a5a88e2dbd Merge pull request #32 from Bria-AI/WAI-4174
WAI-4174
2025-11-20 11:43:06 +02:00
Ubuntu 64ab007f27 WAI-4174 2025-11-20 07:43:01 +00:00
Yazan Numoor 3ac0209193 Merge pull request #31 from Bria-AI/fix-invalid-token-error-message
fix invalid token error message
2025-11-12 14:31:14 +02:00
Ubuntu b5d4d41bb8 update version 2025-11-12 12:03:13 +00:00
Ubuntu 0f2b1115c5 fix invalid token error message 2025-11-12 09:44:45 +00:00
Yazan Numoor b408c8781a Update pyproject.toml 2025-11-11 15:50:02 +02:00
Yazan Numoor f6e134cfd3 Merge pull request #30 from Bria-AI/WAI-4030
WAI-4030
2025-11-11 15:39:44 +02:00
Ubuntu 7855c4dba2 WAI-4030 2025-11-09 06:51:29 +00:00
galbria c1f67dd4fb remove FIBO Workflow.json file 2025-10-30 15:41:37 +02:00
ליזה ירושבסקי c1a77106b4 adding fibo workflow 2025-10-30 15:39:49 +02:00
Yazan Numoor 0f170bfc6b Merge pull request #29 from Bria-AI/fix_comfy_refine_to_support_generate
fix_comfy_refine_to_support_generate
2025-10-29 18:27:06 +02:00
Ubuntu 6b4b9e2dee update version 2025-10-29 16:26:29 +00:00
Ubuntu 4385a8c429 fix_comfy_refine_to_support_generate 2025-10-29 16:18:44 +00:00
Yazan Numoor dd8d70000f Merge pull request #27 from Bria-AI/WAI-4116
WAI-4116
2025-10-29 17:08:48 +02:00
Ubuntu 582e16600e Merge branch 'WAI-4116' of https://github.com/Bria-AI/ComfyUI-BRIA-API into WAI-4116 2025-10-29 12:12:53 +00:00
Ubuntu 33f657ccbd update version 2025-10-29 12:12:29 +00:00
mabualrob1997 f8a6650b4b Update Readme.md 2025-10-29 14:10:43 +02:00
Ubuntu 493d23bdcb remove pro nodes 2025-10-29 11:24:22 +00:00
Ubuntu ecf52dd764 fix images paramter 2025-10-28 15:08:16 +00:00
Ubuntu a9f894102d remmove duplicate import 2025-10-28 07:06:24 +00:00
Ubuntu e50dd5dd63 remmove duplicate import 2025-10-28 07:04:34 +00:00
Ubuntu 00ce39dae5 Merge branch 'main' of https://github.com/Bria-AI/ComfyUI-BRIA-API into WAI-4116 2025-10-28 07:01:47 +00:00
Ubuntu 270128d32a add fibo pro nodes 2025-10-27 20:19:53 +00:00
Yazan Numoor 513fec79b5 Merge pull request #26 from Bria-AI/WAI-4049
WAI-4049
2025-10-23 09:48:24 +03:00
Yazan Numoor ddbf7d0695 Update pyproject.toml 2025-10-22 20:38:36 +03:00
Ubuntu 43893286cc update Gaia model name to Fibo 2025-10-16 09:27:30 +00:00
Ubuntu 3befc0a2ac update automatic nodes to return 7 results 2025-10-16 08:15:20 +00:00
Ubuntu af6ef2a829 fix Gabi Feedback 2025-10-15 10:39:44 +00:00
Ubuntu 96d2924dfc WAI-4116 2025-10-14 12:30:13 +00:00
Ubuntu 327ace887b WAI-4116 2025-10-14 08:57:45 +00:00
Ubuntu bc5dacb9ef Merge branch 'main' of https://github.com/Bria-AI/ComfyUI-BRIA-API into WAI-4049 2025-09-29 13:44:10 +00:00
Ubuntu fc8aa8b6a7 WAI-4049 2025-09-29 13:39:47 +00:00
Yazan Numoor 9960a93044 Merge pull request #25 from Bria-AI/WAI-4011
WAI-4011
2025-09-29 15:47:10 +03:00
Ubuntu 8d3eef85ca update version 2025-09-29 12:44:46 +00:00
Ubuntu bb4108b6c4 WAI-4049 2025-09-29 11:48:24 +00:00
mabualrob1997 18a5ffbcca Update Readme.md 2025-09-29 12:45:46 +03:00
Ubuntu 4f3b7fb77a WAI-4011 2025-09-23 06:49:12 +00:00
Yazan Numoor c3b5fea335 Update pyproject.toml 2025-09-18 19:31:00 +03:00
Yazan Numoor b8e40e90bc Merge pull request #24 from Bria-AI/WAI-3976-feedback-fixes
WAI-3976-feedback-fixes
2025-09-18 19:30:33 +03:00
Ubuntu 9b3f15b7bd WAI-3976-feedback-fixes 2025-09-18 12:03:07 +00:00
Yazan Numoor f7c9ed12b0 Merge pull request #23 from Bria-AI/WAI-3976
WAI-3976
2025-09-17 17:55:23 +03:00
Yazan Numoor bdd053a81b update version 2025-09-17 17:55:05 +03:00
Ubuntu 2913a9eb6b update polling status url 2025-09-17 12:54:10 +00:00
Ubuntu 080c17f4a4 WAI-3976 2025-09-16 17:11:06 +00:00
Yazan Numoor 711080ffb0 Update pyproject.toml 2025-09-10 15:06:35 +03:00
Yazan Numoor 69f111bfe9 Merge pull request #22 from Bria-AI/WAI-3922
WAI-3922
2025-09-10 15:04:09 +03:00
Ubuntu 011f728b0d WAI-3922 2025-09-10 09:23:52 +00:00
gabiburtman ac9678cd23 Update pyproject.toml
fixed issue in shot_by_text_node
2025-09-09 20:10:44 +03:00
gabiburtman daede982ac Delete nodes/shot_by_text_auto_placement_node.py 2025-09-09 20:10:11 +03:00
gabiburtman e716e585df Create shot_by_text_auto_placement_node.py 2025-09-09 20:07:26 +03:00
gabiburtman 1296b6a27c Update shot_by_text_node.py 2025-09-09 20:04:48 +03:00
gabiburtman 19f15f4ba4 Update shot_by_text_node.py 2025-09-09 19:54:21 +03:00
gabiburtman 30d0154caf Update pyproject.toml 2025-09-09 13:00:51 +03:00
gabiburtman 283665f654 Update shot_by_text_node.py 2025-09-09 13:00:30 +03:00
gabiburtman 03e3bec952 Update pyproject.toml 2025-09-09 12:39:08 +03:00
gabiburtman f1793ba9ad Merge pull request #21 from ComfyNodePRs/update-publish-yaml
Update Github Action for Publishing to Comfy Registry
2025-09-09 12:22:17 +03:00
gabiburtman eda093ea66 Update shot_by_text_node.py 2025-09-09 11:13:41 +03:00
gabiburtman 165a103f0c Update replace_bg_node.py 2025-09-09 11:11:35 +03:00
gabiburtman ad2f4a5853 Update text_2_image_base_node.py 2025-06-16 16:24:17 +03:00
gabiburtman 43f858dca4 Update text_2_image_base_node.py 2025-06-12 16:06:17 +03:00
gabiburtman 538cd53ac8 Update text_2_image_base_node.py 2025-06-12 15:31:08 +03:00
BriaOr bae0ed3842 Added restyle portrait to readme 2025-03-17 14:43:25 +02:00
BriaOr a164f8ec45 Update Readme.md 2025-03-13 14:03:38 +02:00
BriaOr ebe9e2e6b1 Update Readme.md 2025-03-13 14:03:18 +02:00
BriaOr 429c51ac6d Update pyproject.toml 2025-03-11 14:50:01 +02:00
BriaOr aed4832984 Update __init__.py 2025-03-11 14:29:10 +02:00
BriaOr a78aff0fb2 Update pyproject.toml 2025-03-11 14:02:49 +02:00
xenia-kra 6edfc55109 Comfy tailored portrait (#20) 2025-03-10 17:32:37 +04:00
snomiao a4855c1a1a chore(publish): update workflow for node publishing
- Added permissions for issue writing in the workflow.
- Modified condition to check repository owner instead of fork status.
- Updated action version from `main` to `v1` for `publish-node-action`.
2025-01-25 07:47:26 +00:00
64 changed files with 7683 additions and 595 deletions
+5 -3
View File
@@ -7,17 +7,19 @@ on:
paths: paths:
- "pyproject.toml" - "pyproject.toml"
permissions:
issues: write
jobs: jobs:
publish-node: publish-node:
name: Publish Custom Node to registry name: Publish Custom Node to registry
runs-on: ubuntu-latest runs-on: ubuntu-latest
# if this is a forked repository. Skipping the workflow. if: ${{ github.repository_owner == 'Bria-AI' }}
if: github.event.repository.fork == false
steps: steps:
- name: Check out code - name: Check out code
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Publish Custom Node - name: Publish Custom Node
uses: Comfy-Org/publish-node-action@main uses: Comfy-Org/publish-node-action@v1
with: with:
## Add your own personal access token to your Github Repository secrets and reference it here. ## Add your own personal access token to your Github Repository secrets and reference it here.
personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }} personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }}
+1
View File
@@ -1 +1,2 @@
*.pyc *.pyc
.idea
+76 -13
View File
@@ -8,12 +8,9 @@ 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. 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 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).
<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, look for the endpoint in our of our API partners like: [**fal.ai**](https://fal.ai/models?keywords=bria). 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. For source code and weigths access, go to our [**Hugging Face**](https://huggingface.co/briaai) space.
To load a workflow, import the compatible workflow.json files from this [folder](workflows). To load a workflow, import the compatible workflow.json files from this [folder](workflows).
@@ -30,14 +27,53 @@ To load a workflow, import the compatible workflow.json files from this [folder]
# Available Nodes # Available Nodes
## Image Generation Nodes ## Image Generation Nodes
These nodes create high-quality images from text or image prompts, generating photorealistic or artistic results with support for various aspect ratios.
| Node | Description | These nodes allow you to leverage Bria's image generation capabilities within ComfyUI. We offer our latest **V2 nodes** (powered by the **FIBO** model) for precise control via structured prompts, alongside our legacy **V1 nodes**.
|------------------------|--------------------------------------------------------------------|
| **Text2Image Base** | Generates images from text prompts, serving as the foundation for text-based image creation. | ### V2 Generation Nodes (FIBO)
| **Text2Image Fast** | Optimized for speed, this node generates images from text prompts with faster results while maintaining quality. |
| **Text2Image HD** | Optimized for high-resolution outputs, this node generates detailed and sharp visuals from text prompts. | Our V2 nodes utilize a state-of-the-art **two-step process** for enhanced control and consistency:
| **Reimagine** | Guides image generation using both prompts and an input image. Preserve the original structure and depth while introducing new materials, colors, and textures. |
- **Translation**: A VLM Bridge translates your input (prompt/images) into a machine-readable `structured_prompt` (JSON).
- **Generation**: The FIBO model generates the final image based on that specific JSON.
**Available Versions:**
- **Regular**: Uses **Gemini 2.5 Flash** as the bridge for state-of-the-art, detailed prompt creation.
- **Lite**: Uses **FIBO-VLM** (Bria's open-source bridge) for faster, flexible, or on-prem deployment.
**Available V2 Nodes & Input Rules**
We offer three distinct nodes to give you full control over this pipeline:
1. **Structured Prompt Bridge**
- Outputs a JSON string only (no image).
- This node decouples the "intent translation" step from generation. It is ideal for "human-in-the-loop" workflows where you want to inspect, audit, or version-control the JSON instructions before generating.
- **Supported Input Combinations:**
- `prompt`: Generates a structured prompt from text.
- `images`: Generates a structured prompt based on an input image.
- `images + prompt`: Generates a structured prompt based on an image, guided by text.
- `structured_prompt + prompt`: Updates an existing structured prompt using new text instructions (outputs updated JSON).
2. **Generate Image**
- Outputs an Image.
- The primary node for generation. It automatically handles translation and generation in one go, or accepts a pre-made structured prompt for reproducible results.
- **Supported Input Combinations:**
- `prompt`: Generates a new image from text.
- `images`: Generates a new image inspired by a reference image.
- `images + prompt`: Generates a new image inspired by an image and guided by text.
- `structured_prompt`: Recreates a previous image exactly (when combined with a seed).
3. **Refine and Regenerate**
- Outputs a Refined Image.
- This node allows you to take a result you like and tweak it without losing the original composition.
- **Supported Input Combination:**
- `structured_prompt + prompt`: Refines a previous image using new text instructions (combined with a seed) to adjust details while maintaining consistency.
### V1 Generation Nodes (Legacy)
These nodes utilize Bria's previous generation pipeline. While V2 is recommended for the highest control and quality, V1 remains available for backward compatibility with established workflows.
These nodes create high-quality images using Bria's V1 pipelines, supporting various aspect ratios and styles.
## Tailored Generation Nodes ## Tailored Generation Nodes
These nodes use pre-trained tailored models to generate images that faithfully reproduce specific visual IP elements or guidelines. These nodes use pre-trained tailored models to generate images that faithfully reproduce specific visual IP elements or guidelines.
@@ -45,7 +81,8 @@ These nodes use pre-trained tailored models to generate images that faithfully r
| Node | Description | | Node | Description |
|------------------------|--------------------------------------------------------------------| |------------------------|--------------------------------------------------------------------|
| **Tailored Gen** | Generates images using a trained tailored model, reproducing specific visual IP elements or guidelines. Use the Tailored Model Info node to load the model's default settings. | | **Tailored Gen** | Generates images using a trained tailored model, reproducing specific visual IP elements or guidelines. Use the Tailored Model Info node to load the model's default settings. |
| **Tailored Model Info** | Retrieves the default settings and prompt prefix of a trained tailored model, which can be used to configure the Tailored Gen node. | | **Tailored Model Info**| Retrieves the default settings and prompt prefix of a trained tailored model, which can be used to configure the Tailored Gen node. |
| **Restyle Portrait** | Transforms the style of a portrait while preserving the person's facial features. |
## Image Editing Nodes ## Image Editing Nodes
These nodes modify specific parts of images, enabling adjustments while maintaining the integrity of the rest of the image. These nodes modify specific parts of images, enabling adjustments while maintaining the integrity of the rest of the image.
@@ -67,6 +104,32 @@ 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. | | **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. | | **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 # Installation
There are two methods to install the BRIA ComfyUI API nodes: There are two methods to install the BRIA ComfyUI API nodes:
+124 -7
View File
@@ -1,37 +1,154 @@
from .nodes import (EraserNode, GenFillNode, ImageExpansionNode, ReplaceBgNode, RmbgNode, RemoveForegroundNode, ShotByTextNode, ShotByImageNode, TailoredGenNode, from .nodes import (
TailoredModelInfoNode, Text2ImageBaseNode, Text2ImageFastNode, Text2ImageHDNode, EraserNode,
ReimagineNode) GenFillNode,
ImageExpansionNode,
ImageEnhanceNode,
ReplaceBgNode,
RmbgNode,
RemoveForegroundNode,
ShotByTextOriginalNode,
ShotByImageOriginalNode,
TailoredGenNode,
TailoredModelInfoNode,
Text2ImageBaseNode,
Text2ImageFastNode,
Text2ImageHDNode,
TailoredPortraitNode,
ReimagineNode,
GenerateImageNodeV2,
GenerateImageLiteNodeV2,
RefineImageNodeV2,
RefineImageLiteNodeV2,
GenerateStructuredPromptNodeV2,
GenerateStructuredPromptLiteNodeV2,
ShotByTextAutomaticNode,
ShotByImageManualPaddingNode,
ShotByImageAutomaticAspectRatioNode,
ShotByImageCustomCoordinatesNode,
ShotByImageManualPlacementNode,
ShotByImageAutomaticNode,
ShotByTextAutomaticAspectRatioNode,
ShotByTextManualPlacementNode,
ShotByTextManualPaddingNode,
ShotByTextCustomCoordinatesNode,
AttributionByImageNode,
RemoveVideoBackgroundNode,
GreenScreenVideoNode,
ReplaceVideoBackgroundNode,
VideoSolidColorBackgroundNode,
VideoMaskByPromptNode,
VideoMaskByKeyPointsNode,
VideoIncreaseResolutionNode,
VideoEraseElementsNode,
LoadVideoFramesNode,
PreviewVideoURLNode,
FIBOEditNode,
FIBOEditStructuredInstructionNode,
BriaMultiImageSelect,
ProductIntegrateNode
)
# Map the node class to a name used internally by ComfyUI # Map the node class to a name used internally by ComfyUI
NODE_CLASS_MAPPINGS = { NODE_CLASS_MAPPINGS = {
"BriaEraser": EraserNode, # Return the class, not an instance "BriaEraser": EraserNode, # Return the class, not an instance
"BriaGenFill": GenFillNode, "BriaGenFill": GenFillNode,
"ImageExpansionNode": ImageExpansionNode, "ImageExpansionNode": ImageExpansionNode,
"ImageEnhanceNode": ImageEnhanceNode,
"ReplaceBgNode": ReplaceBgNode, "ReplaceBgNode": ReplaceBgNode,
"RmbgNode": RmbgNode, "RmbgNode": RmbgNode,
"RemoveForegroundNode": RemoveForegroundNode, "RemoveForegroundNode": RemoveForegroundNode,
"ShotByTextNode": ShotByTextNode, "ShotByTextOriginal": ShotByTextOriginalNode,
"ShotByImageNode": ShotByImageNode, "ShotByImageOriginal": ShotByImageOriginalNode,
"ShotByTextAutomatic": ShotByTextAutomaticNode,
"ShotByTextManualPlacement": ShotByTextManualPlacementNode,
"ShotByTextCustomCoordinates": ShotByTextCustomCoordinatesNode,
"ShotByTextManualPadding": ShotByTextManualPaddingNode,
"ShotByTextAutomaticAspectRatio": ShotByTextAutomaticAspectRatioNode,
"ShotByImageAutomatic": ShotByImageAutomaticNode,
"ShotByImageManualPlacement": ShotByImageManualPlacementNode,
"ShotByImageCustomCoordinates": ShotByImageCustomCoordinatesNode,
"ShotByImageManualPadding": ShotByImageManualPaddingNode,
"ShotByImageAutomaticAspectRatio": ShotByImageAutomaticAspectRatioNode,
"BriaTailoredGen": TailoredGenNode, "BriaTailoredGen": TailoredGenNode,
"TailoredModelInfoNode": TailoredModelInfoNode, "TailoredModelInfoNode": TailoredModelInfoNode,
"TailoredPortraitNode": TailoredPortraitNode,
"Text2ImageBaseNode": Text2ImageBaseNode, "Text2ImageBaseNode": Text2ImageBaseNode,
"Text2ImageFastNode": Text2ImageFastNode, "Text2ImageFastNode": Text2ImageFastNode,
"Text2ImageHDNode": Text2ImageHDNode, "Text2ImageHDNode": Text2ImageHDNode,
"ReimagineNode": ReimagineNode, "ReimagineNode": ReimagineNode,
"AttributionByImageNode": AttributionByImageNode,
"GenerateImageNodeV2": GenerateImageNodeV2,
"GenerateImageLiteNodeV2": GenerateImageLiteNodeV2,
"RefineImageNodeV2": RefineImageNodeV2,
"RefineImageLiteNodeV2": RefineImageLiteNodeV2,
"GenerateStructuredPromptNodeV2": GenerateStructuredPromptNodeV2,
"GenerateStructuredPromptLiteNodeV2": GenerateStructuredPromptLiteNodeV2,
"RemoveVideoBackgroundNode":RemoveVideoBackgroundNode,
"GreenScreenVideoNode": GreenScreenVideoNode,
"ReplaceVideoBackgroundNode": ReplaceVideoBackgroundNode,
"VideoSolidColorBackgroundNode":VideoSolidColorBackgroundNode,
"VideoMaskByPromptNode":VideoMaskByPromptNode,
"VideoMaskByKeyPointsNode":VideoMaskByKeyPointsNode,
"VideoIncreaseResolutionNode":VideoIncreaseResolutionNode,
"VideoEraseElementsNode":VideoEraseElementsNode,
"LoadVideoFramesNode":LoadVideoFramesNode,
"PreviewVideoURLNode":PreviewVideoURLNode,
"FIBOEditNode": FIBOEditNode,
"FIBOEditStructuredInstructionNode": FIBOEditStructuredInstructionNode,
"BriaMultiImageSelect":BriaMultiImageSelect,
"ProductIntegrateNode": ProductIntegrateNode
} }
# Map the node display name to the one shown in the ComfyUI node interface # Map the node display name to the one shown in the ComfyUI node interface
NODE_DISPLAY_NAME_MAPPINGS = { NODE_DISPLAY_NAME_MAPPINGS = {
"BriaEraser": "Bria Eraser", "BriaEraser": "Bria Eraser",
"BriaGenFill": "Bria GenFill", "BriaGenFill": "Bria GenFill",
"ImageExpansionNode": "Bria Image Expansion", "ImageExpansionNode": "Bria Image Expansion",
"ImageEnhanceNode": "Bria Image Enhance",
"ReplaceBgNode": "Bria Replace Background", "ReplaceBgNode": "Bria Replace Background",
"RmbgNode": "Bria RMBG", "RmbgNode": "Bria RMBG",
"RemoveForegroundNode": "Bria Remove Foreground", "RemoveForegroundNode": "Bria Remove Foreground",
"ShotByTextNode": "Bria Shot By Text", "ShotByTextOriginal": "Shot by Text - Original",
"ShotByImageNode": "Bria Shot By Image", "ShotByImageOriginal": "Shot by Image - Original",
"ShotByTextAutomatic": "Shot by Text - Automatic",
"ShotByTextManualPlacement": "Shot by Text - Manual Placement",
"ShotByTextCustomCoordinates": "Shot by Text - Custom Coordinates",
"ShotByTextManualPadding": "Shot by Text - Manual Padding",
"ShotByTextAutomaticAspectRatio": "Shot by Text - Automatic Aspect Ratio",
"ShotByImageAutomatic": "Shot by Image - Automatic",
"ShotByImageManualPlacement": "Shot by Image - Manual Placement",
"ShotByImageCustomCoordinates": "Shot by Image - Custom Coordinates",
"ShotByImageManualPadding": "Shot by Image - Manual Padding",
"ShotByImageAutomaticAspectRatio": "Shot by Image - Automatic Aspect Ratio",
"BriaTailoredGen": "Bria Tailored Gen", "BriaTailoredGen": "Bria Tailored Gen",
"TailoredModelInfoNode": "Bria Tailored Model Info", "TailoredModelInfoNode": "Bria Tailored Model Info",
"TailoredPortraitNode": "Bria Restyle Portrait",
"Text2ImageBaseNode": "Bria Text2Image Base", "Text2ImageBaseNode": "Bria Text2Image Base",
"Text2ImageFastNode": "Bria Text2Image Fast", "Text2ImageFastNode": "Bria Text2Image Fast",
"Text2ImageHDNode": "Bria Text2Image HD", "Text2ImageHDNode": "Bria Text2Image HD",
"ReimagineNode": "Bria Reimagine", "ReimagineNode": "Bria Reimagine",
"AttributionByImageNode": "Attribution By Image Node",
"GenerateImageNodeV2": "FIBO - Generate Image",
"GenerateImageLiteNodeV2": "FIBO - Generate Image - Lite",
"RefineImageNodeV2": "FIBO - Refine and Regenerate Image",
"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",
"VideoSolidColorBackgroundNode":"Bria SolidColor Background Video",
"VideoMaskByPromptNode":"Bria Video Mask By Prompt",
"VideoMaskByKeyPointsNode":"Bria Video Mask By Key Points",
"VideoIncreaseResolutionNode":"Bria Video Increase Resolution",
"VideoEraseElementsNode":"Bria Video Erase Elements",
"LoadVideoFramesNode":"Bria Load Video",
"PreviewVideoURLNode":"Bria Preview Video",
"FIBOEditNode": "FIBO - Edit",
"FIBOEditStructuredInstructionNode": "FIBO - Edit - Structured Instruction",
"BriaMultiImageSelect":"Bria Multi Image Select",
"ProductIntegrateNode": "Bria Product Integrate"
} }
WEB_DIRECTORY = "./web"
+37 -2
View File
@@ -1,14 +1,49 @@
from .eraser_node import EraserNode from .eraser_node import EraserNode
from .generative_fill_node import GenFillNode from .generative_fill_node import GenFillNode
from .image_expansion_node import ImageExpansionNode from .image_expansion_node import ImageExpansionNode
from .image_enhance_node import ImageEnhanceNode
from .replace_bg_node import ReplaceBgNode from .replace_bg_node import ReplaceBgNode
from .rmbg_node import RmbgNode from .rmbg_node import RmbgNode
from .remove_foreground_node import RemoveForegroundNode from .remove_foreground_node import RemoveForegroundNode
from .shot_by_text_node import ShotByTextNode
from .shot_by_image_node import ShotByImageNode
from .tailored_gen_node import TailoredGenNode from .tailored_gen_node import TailoredGenNode
from .tailored_model_info_node import TailoredModelInfoNode from .tailored_model_info_node import TailoredModelInfoNode
from .tailored_portrait_node import TailoredPortraitNode
from .text_2_image_base_node import Text2ImageBaseNode from .text_2_image_base_node import Text2ImageBaseNode
from .text_2_image_fast_node import Text2ImageFastNode from .text_2_image_fast_node import Text2ImageFastNode
from .text_2_image_hd_node import Text2ImageHDNode from .text_2_image_hd_node import Text2ImageHDNode
from .reimagine_node import ReimagineNode from .reimagine_node import ReimagineNode
from .generate_image_node_v2 import GenerateImageNodeV2
from .generate_image_lite_node_v2 import GenerateImageLiteNodeV2
from .refine_image_node_v2 import RefineImageNodeV2
from .refine_image_lite_node_v2 import RefineImageLiteNodeV2
from .generate_structured_prompt_node_v2 import GenerateStructuredPromptNodeV2
from .generate_structured_prompt_lite_node_v2 import GenerateStructuredPromptLiteNodeV2
from .shot_by_text_node import ShotByTextOriginalNode
from .shot_by_text_automatic_aspect_ratio_node import ShotByTextAutomaticAspectRatioNode
from .shot_by_text_automatic_node import ShotByTextAutomaticNode
from .shot_by_text_custom_coordinates_node import ShotByTextCustomCoordinatesNode
from .shot_by_text_manual_placement_node import ShotByTextManualPlacementNode
from .shot_by_text_manual_padding_node import ShotByTextManualPaddingNode
from .shot_by_image_automatic_aspect_ratio_node import (
ShotByImageAutomaticAspectRatioNode,
)
from .shot_by_image_automatic_node import ShotByImageAutomaticNode
from .shot_by_image_custom_coordinates_node import ShotByImageCustomCoordinatesNode
from .shot_by_image_node import ShotByImageOriginalNode
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
from .video_nodes.video_mask_by_prompt_node import VideoMaskByPromptNode
from .video_nodes.video_mask_by_key_points_node import VideoMaskByKeyPointsNode
from .video_nodes.load_video import LoadVideoFramesNode
from .video_nodes.preview_video_node_from_url import PreviewVideoURLNode
from .fibo_edit_node import FIBOEditNode
from .fibo_edit_structured_instruction_node import FIBOEditStructuredInstructionNode
from .multi_image_select import BriaMultiImageSelect
from .product_integrate_node import ProductIntegrateNode
+69
View File
@@ -0,0 +1,69 @@
import requests
from .common import (
bria_json_headers,
image_to_base64,
normalize_images_input,
poll_status_until_completed,
to_pil_safe,
)
class AttributionByImageNode():
@classmethod
def INPUT_TYPES(self):
return {
"required": {
"images": ("IMAGE",),
"model_version": (["2.3", "3.0", "3.2"], {"default": "2.3"}),
"api_key": ("STRING", {"default": "BRIA_API_TOKEN"}),
},
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("api_response",)
CATEGORY = "API Nodes"
FUNCTION = "execute" # This is the method that will be executed
def __init__(self):
self.api_url = "https://engine.prod.bria-api.com/v2/image/attribution/by_image"
def execute(self, images, model_version, api_key):
if api_key.strip() == "" or api_key.strip() == "BRIA_API_TOKEN":
raise Exception("Please insert a valid API key.")
images = normalize_images_input(images)
batch_results = []
for idx, pil_image in enumerate(images):
try:
image_base64 = image_to_base64(pil_image)
payload = {
"image": image_base64,
"model_version": model_version,
}
headers = bria_json_headers(api_key)
response = requests.post(self.api_url, json=payload, headers=headers)
if response.status_code not in (200, 202):
raise Exception(f"API request failed with status {response.status_code}: {response.text}")
# Poll until completion
response_dict = response.json()
status_url = response_dict.get('status_url')
if not status_url:
raise Exception("No status_url returned from API")
final_response = poll_status_until_completed(status_url, api_key)
content = str(final_response.get("result", {}).get("content", ""))
batch_results.append(content)
except Exception as e:
print(f"[AttributionByImageNode] Skipping image {idx} due to error: {e}")
batch_results.append("")
# Join all responses with a delimiter
combined_response = "\n---\n".join(batch_results)
return (combined_response,)
+212 -14
View File
@@ -5,7 +5,19 @@ import torch
import base64 import base64
from torchvision.transforms import ToPILImage from torchvision.transforms import ToPILImage
import requests import requests
import time
import os
import uuid
BRIA_COMFYUI_USER_AGENT = "bria/ComfyUI"
def bria_json_headers(api_token: str) -> dict:
"""Headers for JSON POST requests to Bria API."""
return {
"Content-Type": "application/json",
"api_token": api_token,
"User-Agent": BRIA_COMFYUI_USER_AGENT,
}
def postprocess_image(image): def postprocess_image(image):
result_image = Image.open(io.BytesIO(image)) result_image = Image.open(io.BytesIO(image))
result_image = result_image.convert("RGB") result_image = result_image.convert("RGB")
@@ -31,6 +43,35 @@ def preprocess_image(image):
print("Unexpected image dimensions. Expected 4D tensor.") print("Unexpected image dimensions. Expected 4D tensor.")
return image return image
def to_pil_safe(image):
"""
Converts a single image tensor or numpy array (H,W,C) to PIL Image.
Handles float32 in 0-1 and uint8.
"""
if isinstance(image, torch.Tensor):
image = image.detach().cpu().numpy()
# If image is empty, replace with 1x1 black
if image.size == 0:
image = np.zeros((1,1,3), dtype=np.uint8)
# Ensure float images are scaled 0-255
if image.dtype in [np.float32, np.float64]:
if image.max() <= 1.0:
image = (image * 255).astype(np.uint8)
else:
image = image.astype(np.uint8)
# Handle grayscale images
if image.ndim == 2:
return Image.fromarray(image, mode="L")
elif image.shape[2] == 3:
return Image.fromarray(image, mode="RGB")
elif image.shape[2] == 4:
return Image.fromarray(image, mode="RGBA")
else:
raise ValueError(f"Cannot convert image with shape {image.shape} to PIL")
def preprocess_mask(mask): def preprocess_mask(mask):
if isinstance(mask, torch.Tensor): if isinstance(mask, torch.Tensor):
@@ -44,7 +85,7 @@ def preprocess_mask(mask):
return mask return mask
def process_request(api_url, image, mask, api_key): def process_request(api_url, image, mask, api_key, visual_input_content_moderation, visual_output_content_moderation):
if api_key.strip() == "" or api_key.strip() == "BRIA_API_TOKEN": if api_key.strip() == "" or api_key.strip() == "BRIA_API_TOKEN":
raise Exception("Please insert a valid API key.") raise Exception("Please insert a valid API key.")
@@ -58,25 +99,34 @@ def process_request(api_url, image, mask, api_key):
image_base64 = image_to_base64(image) image_base64 = image_to_base64(image)
mask_base64 = image_to_base64(mask) mask_base64 = image_to_base64(mask)
# Prepare the API request payload # Prepare the API request payload for v2 API
payload = { payload = {
"file": f"{image_base64}", "image": image_base64,
"mask_file": f"{mask_base64}" "mask": mask_base64,
"visual_input_content_moderation":visual_input_content_moderation,
"visual_output_content_moderation":visual_output_content_moderation
} }
headers = { headers = bria_json_headers(api_key)
"Content-Type": "application/json",
"api_token": f"{api_key}"
}
try: try:
response = requests.post(api_url, json=payload, headers=headers) response = requests.post(api_url, json=payload, headers=headers)
# Check for successful response if response.status_code == 200 or response.status_code == 202:
if response.status_code == 200: print('Initial request successful, polling for completion...')
print('response is 200')
# Process the output image from API response
response_dict = response.json() response_dict = response.json()
image_response = requests.get(response_dict['result_url']) 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)
result_image_url = final_response['result']['image_url']
# Download and process the result image
image_response = requests.get(result_image_url)
result_image = Image.open(io.BytesIO(image_response.content)) result_image = Image.open(io.BytesIO(image_response.content))
result_image = result_image.convert("RGBA") result_image = result_image.convert("RGBA")
result_image = np.array(result_image).astype(np.float32) / 255.0 result_image = np.array(result_image).astype(np.float32) / 255.0
@@ -86,7 +136,155 @@ def process_request(api_url, image, mask, api_key):
# print(f"output tensor shape is: {image_tensor.shape}") # print(f"output tensor shape is: {image_tensor.shape}")
return (result_image,) return (result_image,)
else: else:
raise Exception(f"Error: API request failed with status code {response.status_code}") raise Exception(f"Error: API request failed with status code {response.status_code} {response.text}")
except Exception as e: except Exception as e:
raise Exception(f"{e}") raise Exception(f"{e}")
def poll_status_until_completed(status_url, api_key, timeout=360, check_interval=2):
"""
Poll a status URL until the status is COMPLETED or timeout is reached.
Args:
status_url (str): The status URL to poll
api_key (str): API token for authentication
timeout (int): Maximum time to wait in seconds (default: 360)
check_interval (int): Time between checks in seconds (default: 2)
Returns:
dict: The final response containing the result
Raises:
Exception: If timeout is reached or API request fails
"""
start_time = time.time()
headers = bria_json_headers(api_key)
while time.time() - start_time < timeout:
try:
response = requests.get(status_url, headers=headers)
if response.status_code == 200 or response.status_code == 202:
response_dict = response.json()
status = response_dict.get("status", "").upper()
if status == "COMPLETED":
return response_dict
elif status == "ERROR":
raise Exception(f"Request failed: {response_dict}")
else:
print(f"Status: {status}, waiting...")
time.sleep(check_interval)
else:
raise Exception(f"Status check failed with status code {response.status_code}")
except requests.exceptions.RequestException as e:
raise Exception(f"Error checking status: {e}")
raise Exception(f"Timeout reached after {timeout} seconds")
def normalize_images_input(images):
"""
Converts various image inputs into a list of PIL images:
- PIL.Image → [PIL.Image]
- list of PIL.Image → unchanged
- torch.Tensor (H,W,C) → [PIL.Image]
- torch.Tensor (B,H,W,C) → list of PIL.Images
"""
if isinstance(images, Image.Image):
return [images]
elif isinstance(images, list):
return [to_pil_safe(img) if isinstance(img, torch.Tensor) else img for img in images]
elif isinstance(images, torch.Tensor):
if images.ndim == 3: # (H,W,C)
return [to_pil_safe(images)]
elif images.ndim == 4: # (B,H,W,C)
return [to_pil_safe(img) for img in images]
else:
raise ValueError(f"Unsupported tensor shape: {images.shape}")
else:
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
+7 -3
View File
@@ -8,6 +8,10 @@ class EraserNode():
"image": ("IMAGE",), # Input image from another node "image": ("IMAGE",), # Input image from another node
"mask": ("MASK",), # Binary mask input "mask": ("MASK",), # Binary mask input
"api_key": ("STRING", {"default": "BRIA_API_TOKEN"}) # API Key input with a default value "api_key": ("STRING", {"default": "BRIA_API_TOKEN"}) # API Key input with a default value
},
"optional": {
"visual_input_content_moderation": ("BOOLEAN", {"default": False}),
"visual_output_content_moderation": ("BOOLEAN", {"default": False}),
} }
} }
@@ -17,9 +21,9 @@ class EraserNode():
FUNCTION = "execute" # This is the method that will be executed FUNCTION = "execute" # This is the method that will be executed
def __init__(self): def __init__(self):
self.api_url = "https://engine.prod.bria-api.com/v1/eraser" # Eraser API URL self.api_url = "https://engine.prod.bria-api.com/v2/image/edit/erase" # Eraser API URL
# Define the execute method as expected by ComfyUI # Define the execute method as expected by ComfyUI
def execute(self, image, mask, api_key): def execute(self, image, mask, api_key, visual_input_content_moderation, visual_output_content_moderation):
return process_request(self.api_url, image, mask, api_key) return process_request(self.api_url, image, mask, api_key, visual_input_content_moderation, visual_output_content_moderation)
+162
View File
@@ -0,0 +1,162 @@
import requests
import torch
from .common import (
bria_json_headers,
image_to_base64,
poll_status_until_completed,
preprocess_image,
preprocess_mask,
postprocess_image,
)
class FIBOEditNode:
"""FIBO Edit Node - Edit images with instructions"""
api_url = "https://engine.prod.bria-api.com/v2/image/edit"
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"api_token": ("STRING", {"default": "BRIA_API_TOKEN"}),
"images": ("IMAGE",),
},
"optional": {
"instruction": ("STRING",),
"mask": ("MASK",),
"structured_instruction": ("STRING",),
"negative_prompt": ("STRING",),
"steps_num": (
"INT",
{
"default": 30,
"min": 1,
"max": 100,
},
),
"guidance_scale": (
"INT",
{
"default": 5,
"min": 1,
"max": 20,
},
),
"seed": ("INT", {"default": 123456}),
},
}
RETURN_TYPES = ("IMAGE", "STRING", "INT")
RETURN_NAMES = ("IMAGE", "structured_instruction", "seed")
CATEGORY = "API Nodes"
FUNCTION = "execute"
def _validate_token(self, api_token: str):
if api_token.strip() == "" or api_token.strip() == "BRIA_API_TOKEN":
raise Exception("Please insert a valid API token.")
def _build_payload(
self,
instruction,
images,
mask=None,
structured_instruction=None,
negative_prompt=None,
steps_num=50,
guidance_scale=5,
seed=123456,
):
# Process images
if isinstance(images, torch.Tensor):
processed_images = preprocess_image(images)
else:
processed_images = images
payload = {
"images": [image_to_base64(processed_images)],
"steps_num": steps_num,
"guidance_scale": guidance_scale,
"seed": seed,
}
# Add optional mask
if mask is not None:
if isinstance(mask, torch.Tensor):
processed_mask = preprocess_mask(mask)
else:
processed_mask = mask
payload["mask"] = image_to_base64(processed_mask)
# Add optional structured_instruction
if structured_instruction:
payload["structured_instruction"] = structured_instruction
# Add optional structured_instruction
if instruction:
payload["instruction"] = instruction
# Add optional negative_prompt
if negative_prompt:
payload["negative_prompt"] = negative_prompt
return payload
def execute(
self,
api_token,
instruction,
images,
mask=None,
structured_instruction=None,
negative_prompt=None,
steps_num=50,
guidance_scale=5,
seed=123456,
):
self._validate_token(api_token)
payload = self._build_payload(
instruction,
images,
mask,
structured_instruction,
negative_prompt,
steps_num,
guidance_scale,
seed,
)
headers = bria_json_headers(api_token)
try:
response = requests.post(self.api_url, json=payload, headers=headers)
if response.status_code in (200, 202):
print(
f"Initial request successful to {self.api_url}, 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_token)
result = final_response.get("result", {})
result_image_url = result.get("image_url")
structured_prompt = result.get("structured_prompt", "")
used_seed = result.get("seed")
image_response = requests.get(result_image_url)
result_image = postprocess_image(image_response.content)
return (result_image, structured_prompt, used_seed)
raise Exception(
f"Error: API request failed with status code {response.status_code} {response.text}"
)
except Exception as e:
raise Exception(f"{e}")
@@ -0,0 +1,74 @@
import requests
from .common import (
bria_json_headers,
image_to_base64,
normalize_images_input,
poll_status_until_completed,
)
class FIBOEditStructuredInstructionNode:
"""FIBO Edit Structured Instruction Node - Generate structured instructions for image editing"""
api_url = "https://engine.prod.bria-api.com/v2/structured_instruction/generate"
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"api_token": ("STRING", {"default": "BRIA_API_TOKEN"}),
"images": ("IMAGE",),
"instruction": ("STRING",),
},
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("structured_instruction",)
CATEGORY = "API Nodes"
FUNCTION = "execute"
def _validate_token(self, api_token: str):
if api_token.strip() == "" or api_token.strip() == "BRIA_API_TOKEN":
raise Exception("Please insert a valid API token.")
def _build_payload(self, processed_image, instruction):
payload = {
"instruction": instruction,
"images": [image_to_base64(processed_image)],
}
return payload
def execute(self, api_token, images, instruction):
self._validate_token(api_token)
# Normalize input to list of PIL images
images = normalize_images_input(images)
batch_results = []
for idx, pil_image in enumerate(images):
try:
payload = self._build_payload(pil_image, instruction)
headers = bria_json_headers(api_token)
response = requests.post(self.api_url, json=payload, headers=headers)
if response.status_code not in (200, 202):
raise Exception(f"API request failed with status {response.status_code}: {response.text}")
print(f"Initial request successful for image {idx}, polling for completion...")
response_dict = response.json()
status_url = response_dict.get("status_url")
if not status_url:
raise Exception("No status_url returned from API")
final_response = poll_status_until_completed(status_url, api_token)
result = final_response.get("result", {})
structured_instruction = result.get("structured_instruction", "")
batch_results.append(structured_instruction)
except Exception as e:
print(f"[FIBOEditStructuredInstructionNode] Skipping image {idx} due to error: {e}")
batch_results.append("")
combined_instructions = "\n---\n".join(batch_results)
return (combined_instructions,)
+157
View File
@@ -0,0 +1,157 @@
import requests
import torch
from .common import (
bria_json_headers,
image_to_base64,
normalize_images_input,
poll_status_until_completed,
postprocess_image,
)
class GenerateImageLiteNodeV2:
"""Lite Image Generation Node (multi-image compatible)"""
api_url = "https://engine.prod.bria-api.com/v2/image/generate/lite"
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"api_token": ("STRING", {"default": "BRIA_API_TOKEN"}),
"prompt": ("STRING",),
},
"optional": {
"model_version": (["FIBO"], {"default": "FIBO"}),
"structured_prompt": ("STRING", {"default": ""}),
"images": ("IMAGE",),
"aspect_ratio": (
["1:1", "2:3", "3:2", "3:4", "4:3", "4:5", "5:4", "9:16", "16:9"],
{"default": "1:1"},
),
"steps_num": ("INT", {"default": 8, "min": 8, "max": 30}),
"guidance_scale": ("INT", {"default": 5, "min": 3, "max": 5}),
"seed": ("STRING", {"default": "123456"}),
},
}
RETURN_TYPES = ("IMAGE", "STRING", "STRING")
RETURN_NAMES = ("image", "structured_prompt", "seed")
CATEGORY = "API Nodes"
FUNCTION = "execute"
def _validate_token(self, api_token: str):
if api_token.strip() == "" or api_token.strip() == "BRIA_API_TOKEN":
raise Exception("Please insert a valid API token.")
def _build_payload(
self,
prompt,
model_version,
structured_prompt,
aspect_ratio,
steps_num,
guidance_scale,
seed,
processed_image=None,
):
payload = {
"prompt": prompt,
"model_version": model_version,
"aspect_ratio": aspect_ratio,
"steps_num": steps_num,
"guidance_scale": guidance_scale,
"seed": int(seed),
}
if structured_prompt:
payload["structured_prompt"] = structured_prompt
if processed_image is not None:
payload["images"] = [image_to_base64(processed_image)]
return payload
def execute(
self,
api_token,
prompt,
model_version,
structured_prompt,
aspect_ratio,
steps_num,
guidance_scale,
seed,
images=None,
):
self._validate_token(api_token)
images_list = normalize_images_input(images) if images is not None else [None]
# Structured prompts per image
if isinstance(structured_prompt, str):
structured_prompts_list = structured_prompt.split("\n---\n")
elif isinstance(structured_prompt, list):
structured_prompts_list = structured_prompt
else:
structured_prompts_list = [""] * len(images_list)
if len(structured_prompts_list) < len(images_list):
structured_prompts_list += [structured_prompts_list[-1]] * (len(images_list) - len(structured_prompts_list))
# Seeds per image
if isinstance(seed, str):
seed_values = [int(s.strip()) for s in seed.split(",")]
else:
seed_values = [int(seed)]
if len(seed_values) < len(images_list):
seed_values += [seed_values[-1]] * (len(images_list) - len(seed_values))
batch_results = []
batch_structured_prompts = []
batch_seeds = []
for idx, ref_image in enumerate(images_list):
try:
payload = self._build_payload(
prompt,
model_version,
structured_prompts_list[idx],
aspect_ratio,
steps_num,
guidance_scale,
seed_values[idx],
ref_image,
)
headers = bria_json_headers(api_token)
response = requests.post(self.api_url, json=payload, headers=headers)
if response.status_code not in (200, 202):
raise Exception(
f"API request failed with status code {response.status_code}: {response.text}"
)
print(f"GenerateImageLiteNodeV2 - Initial request successful for image {idx}, polling for completion...")
response_dict = response.json()
status_url = response_dict.get("status_url")
if not status_url:
raise Exception("No status_url returned from API")
final_response = poll_status_until_completed(status_url, api_token)
result = final_response.get("result", {})
result_image_url = result.get("image_url")
structured_prompt_result = result.get("structured_prompt", "")
used_seed = result.get("seed", seed_values[idx])
image_response = requests.get(result_image_url)
result_image = postprocess_image(image_response.content)
batch_results.append(result_image)
batch_structured_prompts.append(structured_prompt_result)
batch_seeds.append(str(used_seed))
except Exception as e:
print(f"[GenerateImageLiteNodeV2] Skipping iteration {idx} due to error: {e}")
batch_results.append(torch.zeros((1, 512, 512, 3), dtype=torch.float32))
batch_structured_prompts.append("")
batch_seeds.append(str(seed_values[idx]))
output_batch = torch.cat(batch_results, dim=0)
combined_structured_prompts = "\n---\n".join(batch_structured_prompts)
combined_seeds = ",".join(batch_seeds)
return output_batch, combined_structured_prompts, combined_seeds
+166
View File
@@ -0,0 +1,166 @@
import requests
import torch
from .common import (
bria_json_headers,
image_to_base64,
normalize_images_input,
poll_status_until_completed,
postprocess_image,
)
class GenerateImageNodeV2:
"""Standard Image Generation Node (multi-image compatible)"""
api_url = "https://engine.prod.bria-api.com/v2/image/generate"
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"api_token": ("STRING", {"default": "BRIA_API_TOKEN"}),
"prompt": ("STRING",),
},
"optional": {
"model_version": (["FIBO"], {"default": "FIBO"}),
"structured_prompt": ("STRING", {"default": ""}),
"negative_prompt": ("STRING",),
"images": ("IMAGE",),
"aspect_ratio": (
["1:1", "2:3", "3:2", "3:4", "4:3", "4:5", "5:4", "9:16", "16:9"],
{"default": "1:1"},
),
"steps_num": ("INT", {"default": 50, "min": 35, "max": 50}),
"guidance_scale": ("INT", {"default": 5, "min": 3, "max": 5}),
"seed": ("STRING", {"default": "123456"}), # Accept string to match previous node
},
}
RETURN_TYPES = ("IMAGE", "STRING", "STRING") # images, structured_prompts, seeds
RETURN_NAMES = ("image", "structured_prompt", "seed")
CATEGORY = "API Nodes"
FUNCTION = "execute"
def _validate_token(self, api_token: str):
if api_token.strip() == "" or api_token.strip() == "BRIA_API_TOKEN":
raise Exception("Please insert a valid API token.")
def _build_payload(
self,
prompt,
model_version,
structured_prompt,
aspect_ratio,
steps_num,
guidance_scale,
seed,
negative_prompt=None,
processed_image=None,
):
payload = {
"prompt": prompt,
"model_version": model_version,
"aspect_ratio": aspect_ratio,
"steps_num": steps_num,
"guidance_scale": guidance_scale,
"seed": int(seed),
}
if structured_prompt:
payload["structured_prompt"] = structured_prompt
if negative_prompt:
payload["negative_prompt"] = negative_prompt
if processed_image is not None:
payload["images"] = [image_to_base64(processed_image)]
return payload
def execute(
self,
api_token,
prompt,
model_version,
structured_prompt,
aspect_ratio,
steps_num,
guidance_scale,
seed,
negative_prompt=None,
images=None,
):
self._validate_token(api_token)
images_list = normalize_images_input(images) if images is not None else [None]
# Structured prompts per image
if isinstance(structured_prompt, str):
structured_prompts_list = structured_prompt.split("\n---\n")
elif isinstance(structured_prompt, list):
structured_prompts_list = structured_prompt
else:
structured_prompts_list = [""] * len(images_list)
if len(structured_prompts_list) < len(images_list):
structured_prompts_list += [structured_prompts_list[-1]] * (len(images_list) - len(structured_prompts_list))
# Seeds per image
if isinstance(seed, str):
seed_values = [int(s.strip()) for s in seed.split(",")]
else:
seed_values = [int(seed)]
if len(seed_values) < len(images_list):
seed_values += [seed_values[-1]] * (len(images_list) - len(seed_values))
batch_results = []
batch_structured_prompts = []
batch_seeds = []
for idx, ref_image in enumerate(images_list):
try:
payload = self._build_payload(
prompt,
model_version,
structured_prompts_list[idx],
aspect_ratio,
steps_num,
guidance_scale,
seed_values[idx],
negative_prompt,
ref_image,
)
headers = bria_json_headers(api_token)
response = requests.post(self.api_url, json=payload, headers=headers)
if response.status_code not in (200, 202):
raise Exception(
f"API request failed with status code {response.status_code}: {response.text}"
)
print(f"GenerateImageNodeV2 - Initial request successful for image {idx}, polling for completion...")
response_dict = response.json()
status_url = response_dict.get("status_url")
if not status_url:
raise Exception("No status_url returned from API")
final_response = poll_status_until_completed(status_url, api_token)
result = final_response.get("result", {})
result_image_url = result.get("image_url")
structured_prompt_result = result.get("structured_prompt", "")
used_seed = result.get("seed", seed_values[idx])
image_response = requests.get(result_image_url)
result_image = postprocess_image(image_response.content)
batch_results.append(result_image)
batch_structured_prompts.append(structured_prompt_result)
batch_seeds.append(str(used_seed))
except Exception as e:
print(f"[GenerateImageNodeV2] Skipping iteration {idx} due to error: {e}")
batch_results.append(torch.zeros((1, 512, 512, 3), dtype=torch.float32))
batch_structured_prompts.append("")
batch_seeds.append(str(seed_values[idx]))
# Return all as strings for proper chaining
output_batch = torch.cat(batch_results, dim=0)
combined_structured_prompts = "\n---\n".join(batch_structured_prompts)
combined_seeds = ",".join(batch_seeds)
return output_batch, combined_structured_prompts, combined_seeds
@@ -0,0 +1,110 @@
import requests
from .common import (
bria_json_headers,
image_to_base64,
normalize_images_input,
poll_status_until_completed,
)
class GenerateStructuredPromptLiteNodeV2:
"""Lite Structured Prompt Generation Node (multi-image compatible)"""
api_url = "https://engine.prod.bria-api.com/v2/structured_prompt/generate/lite"
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"api_token": ("STRING", {"default": "BRIA_API_TOKEN"}),
"prompt": ("STRING",),
},
"optional": {
"structured_prompt": ("STRING",),
"images": ("IMAGE",),
"seed": ("STRING", {"default": "123456"}),
},
}
RETURN_TYPES = ("STRING", "STRING") # structured_prompts, seeds as comma-separated string
RETURN_NAMES = ("structured_prompt", "seed")
CATEGORY = "API Nodes"
FUNCTION = "execute"
def _validate_token(self, api_token: str):
if api_token.strip() == "" or api_token.strip() == "BRIA_API_TOKEN":
raise Exception("Please insert a valid API token.")
def _build_payload(self, prompt, seed, structured_prompt, processed_image=None):
payload = {"prompt": prompt, "seed": int(seed)}
if structured_prompt:
payload["structured_prompt"] = structured_prompt
if processed_image is not None:
payload["images"] = [image_to_base64(processed_image)]
return payload
def execute(self, api_token, prompt, seed, structured_prompt, images=None):
self._validate_token(api_token)
images_list = normalize_images_input(images) if images is not None else [None]
# Seeds per image
if isinstance(seed, str):
seed_values = [int(s.strip()) for s in seed.split(",")]
else:
seed_values = [seed]
if len(seed_values) < len(images_list):
seed_values += [seed_values[-1]] * (len(images_list) - len(seed_values))
# Structured prompts per image
if isinstance(structured_prompt, str):
structured_prompts_list = structured_prompt.split("\n---\n")
elif isinstance(structured_prompt, list):
structured_prompts_list = structured_prompt
else:
structured_prompts_list = [""] * len(images_list)
if len(structured_prompts_list) < len(images_list):
structured_prompts_list += [structured_prompts_list[-1]] * (len(images_list) - len(structured_prompts_list))
batch_structured_prompts = []
batch_seeds = []
for idx, image in enumerate(images_list):
try:
payload = self._build_payload(
prompt,
seed_values[idx],
structured_prompts_list[idx],
image,
)
headers = bria_json_headers(api_token)
response = requests.post(self.api_url, json=payload, headers=headers)
if response.status_code not in (200, 202):
raise Exception(
f"API request failed with status code {response.status_code}: {response.text}"
)
response_dict = response.json()
print(f"GenerateStructuredPromptLiteNodeV2 - Initial request successful for image {idx}, polling for completion...")
status_url = response_dict.get("status_url")
if not status_url:
raise Exception("No status_url returned from API")
final_response = poll_status_until_completed(status_url, api_token)
result = final_response.get("result", {})
structured_prompt_result = result.get("structured_prompt", "")
used_seed = result.get("seed", seed_values[idx])
batch_structured_prompts.append(structured_prompt_result)
batch_seeds.append(str(used_seed))
except Exception as e:
print(f"[GenerateStructuredPromptLiteNodeV2] Skipping iteration {idx} due to error: {e}")
batch_structured_prompts.append("")
batch_seeds.append(str(seed_values[idx]))
combined_prompts = "\n---\n".join(batch_structured_prompts)
combined_seeds = ",".join(batch_seeds)
return combined_prompts, combined_seeds
+113
View File
@@ -0,0 +1,113 @@
import requests
from .common import (
bria_json_headers,
image_to_base64,
normalize_images_input,
poll_status_until_completed,
)
class GenerateStructuredPromptNodeV2:
"""Structured Prompt Generation Node (multi-image compatible)"""
api_url = "https://engine.prod.bria-api.com/v2/structured_prompt/generate"
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"api_token": ("STRING", {"default": "BRIA_API_TOKEN"}),
"prompt": ("STRING",),
},
"optional": {
"structured_prompt": ("STRING",),
"images": ("IMAGE",),
"seed": ("STRING", {"default": "123456"}),
},
}
RETURN_TYPES = ("STRING", "STRING") # structured_prompts, seeds as comma-separated string
RETURN_NAMES = ("structured_prompt", "seed")
CATEGORY = "API Nodes"
FUNCTION = "execute"
def _validate_token(self, api_token: str):
if api_token.strip() == "" or api_token.strip() == "BRIA_API_TOKEN":
raise Exception("Please insert a valid API token.")
def _build_payload(self, prompt, seed, structured_prompt, processed_image=None):
payload = {"prompt": prompt, "seed": int(seed)}
if structured_prompt:
payload["structured_prompt"] = structured_prompt
if processed_image is not None:
payload["images"] = [image_to_base64(processed_image)]
return payload
def execute(self, api_token, prompt, seed, structured_prompt, images=None):
self._validate_token(api_token)
images_list = normalize_images_input(images) if images is not None else [None]
# Seeds per image
if isinstance(seed, str):
seed_values = [int(s.strip()) for s in seed.split(",")]
else:
seed_values = [seed]
if len(seed_values) < len(images_list):
seed_values += [seed_values[-1]] * (len(images_list) - len(seed_values))
# Structured prompts per image
if isinstance(structured_prompt, str):
structured_prompts_list = structured_prompt.split("\n---\n")
elif isinstance(structured_prompt, list):
structured_prompts_list = structured_prompt
else:
structured_prompts_list = [""] * len(images_list)
if len(structured_prompts_list) < len(images_list):
structured_prompts_list += [structured_prompts_list[-1]] * (len(images_list) - len(structured_prompts_list))
batch_structured_prompts = []
batch_seeds = []
for idx, image in enumerate(images_list):
try:
payload = self._build_payload(
prompt,
seed_values[idx],
structured_prompts_list[idx],
image
)
headers = bria_json_headers(api_token)
response = requests.post(self.api_url, json=payload, headers=headers)
if response.status_code not in (200, 202):
raise Exception(
f"API request failed with status code {response.status_code}: {response.text}"
)
response_dict = response.json()
print(f"GenerateStructuredPromptNodeV2 - Initial request successful for image {idx}, polling for completion...")
status_url = response_dict.get("status_url")
if not status_url:
raise Exception("No status_url returned from API")
final_response = poll_status_until_completed(status_url, api_token)
result = final_response.get("result", {})
structured_prompt_result = result.get("structured_prompt", "")
used_seed = result.get("seed", seed_values[idx])
batch_structured_prompts.append(structured_prompt_result)
batch_seeds.append(str(used_seed)) # Keep as string for passing between nodes
except Exception as e:
print(f"[GenerateStructuredPromptNodeV2] Skipping iteration {idx} due to error: {e}")
batch_structured_prompts.append("")
batch_seeds.append(str(seed_values[idx]))
# Return combined structured prompts and seeds as strings
combined_prompts = "\n---\n".join(batch_structured_prompts)
combined_seeds = ",".join(batch_seeds)
return combined_prompts, combined_seeds
+38 -18
View File
@@ -4,7 +4,13 @@ from PIL import Image
import io import io
import torch import torch
from .common import image_to_base64, preprocess_image, preprocess_mask from .common import (
bria_json_headers,
image_to_base64,
poll_status_until_completed,
preprocess_image,
preprocess_mask,
)
class GenFillNode(): class GenFillNode():
@@ -18,7 +24,12 @@ class GenFillNode():
"api_key": ("STRING", {"default": "BRIA_API_TOKEN"}), # API Key input with a default value "api_key": ("STRING", {"default": "BRIA_API_TOKEN"}), # API Key input with a default value
}, },
"optional": { "optional": {
"seed": ("INT", {"default": 123456}) "seed": ("INT", {"default": 123456}),
"prompt_content_moderation": ("BOOLEAN", {"default": True}),
"visual_input_content_moderation": ("BOOLEAN", {"default": False}),
"visual_output_content_moderation": ("BOOLEAN", {"default": False}),
} }
} }
@@ -28,13 +39,12 @@ class GenFillNode():
FUNCTION = "execute" # This is the method that will be executed FUNCTION = "execute" # This is the method that will be executed
def __init__(self): def __init__(self):
self.api_url = "https://engine.prod.bria-api.com/v1/gen_fill" # Eraser API URL self.api_url = "https://engine.prod.bria-api.com/v2/image/edit/gen_fill"
# Define the execute method as expected by ComfyUI # Define the execute method as expected by ComfyUI
def execute(self, image, mask, prompt, api_key, seed): def execute(self, image, mask, prompt, api_key, seed, prompt_content_moderation, visual_input_content_moderation, visual_output_content_moderation):
if api_key.strip() == "" or api_key.strip() == "BRIA_API_TOKEN": if api_key.strip() == "" or api_key.strip() == "BRIA_API_TOKEN":
raise Exception("Please insert a valid API key.") raise Exception("Please insert a valid API key.")
# Check if image and mask are tensors, if so, convert to NumPy arrays # Check if image and mask are tensors, if so, convert to NumPy arrays
if isinstance(image, torch.Tensor): if isinstance(image, torch.Tensor):
image = preprocess_image(image) image = preprocess_image(image)
@@ -47,34 +57,44 @@ class GenFillNode():
# Prepare the API request payload # Prepare the API request payload
payload = { payload = {
"file": f"{image_base64}", "image": image_base64,
"mask_file": f"{mask_base64}", "mask": mask_base64,
"prompt": prompt, "prompt": prompt,
"negative_prompt": "blurry", "negative_prompt": "blurry",
"sync": True,
"seed": seed, "seed": seed,
"prompt_content_moderation":prompt_content_moderation,
"visual_input_content_moderation":visual_input_content_moderation,
"visual_output_content_moderation":visual_output_content_moderation,
"version": 2
} }
headers = { headers = bria_json_headers(api_key)
"Content-Type": "application/json",
"api_token": f"{api_key}"
}
try: try:
# Send initial request to get status URL
response = requests.post(self.api_url, json=payload, headers=headers) response = requests.post(self.api_url, json=payload, headers=headers)
# Check for successful response
if response.status_code == 200: if response.status_code == 200 or response.status_code == 202:
print('response is 200') print('Initial genfill request successful, polling for completion...')
# Process the output image from API response
response_dict = response.json() response_dict = response.json()
image_response = requests.get(response_dict['urls'][0]) 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)
result_image_url = final_response['result']['image_url']
image_response = requests.get(result_image_url)
result_image = Image.open(io.BytesIO(image_response.content)) result_image = Image.open(io.BytesIO(image_response.content))
result_image = result_image.convert("RGB") result_image = result_image.convert("RGB")
result_image = np.array(result_image).astype(np.float32) / 255.0 result_image = np.array(result_image).astype(np.float32) / 255.0
result_image = torch.from_numpy(result_image)[None,] result_image = torch.from_numpy(result_image)[None,]
return (result_image,) return (result_image,)
else: else:
raise Exception(f"Error: API request failed with status code {response.status_code}") raise Exception(f"Error: API request failed with status code {response.status_code} {response.text}")
except Exception as e: except Exception as e:
raise Exception(f"{e}") raise Exception(f"{e}")
+110
View File
@@ -0,0 +1,110 @@
import io
import requests
import numpy as np
from PIL import Image
import torch
from .common import (
bria_json_headers,
image_to_base64,
normalize_images_input,
poll_status_until_completed,
)
class ImageEnhanceNode():
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"images": ("IMAGE",),
"api_key": ("STRING", {"default": "BRIA_API_TOKEN"}),
},
"optional": {
"steps_num": ("INT", {"default": 20, "min": 10, "max": 50}),
"resolution": (["1MP", "2MP", "4MP"], {"default": "1MP"}),
"visual_input_content_moderation": ("BOOLEAN", {"default": False}),
"visual_output_content_moderation": ("BOOLEAN", {"default": False}),
"seed": ("INT", {"default": 681794}),
"preserve_alpha": ("BOOLEAN", {"default": True}),
}
}
RETURN_TYPES = ("IMAGE", "STRING")
RETURN_NAMES = ("output_images", "seeds",)
CATEGORY = "API Nodes"
FUNCTION = "execute"
def __init__(self):
self.api_url = "https://engine.prod.bria-api.com/v2/image/edit/enhance"
def execute(
self,
images,
api_key,
visual_input_content_moderation,
visual_output_content_moderation,
seed,
steps_num,
resolution,
preserve_alpha
):
# Validate API key
if api_key.strip() == "" or api_key.strip() == "BRIA_API_TOKEN":
raise Exception("Please insert a valid API key.")
# Normalize input to list of PIL images
images = normalize_images_input(images)
batch_results = []
batch_seeds = []
for idx, pil_image in enumerate(images):
try:
image_base64 = image_to_base64(pil_image)
payload = {
"image": image_base64,
"visual_input_content_moderation": visual_input_content_moderation,
"visual_output_content_moderation": visual_output_content_moderation,
"seed": seed,
"steps_num": steps_num,
"resolution": resolution,
"preserve_alpha": preserve_alpha
}
headers = bria_json_headers(api_key)
# Send request
response = requests.post(self.api_url, json=payload, headers=headers)
if response.status_code not in (200, 202):
raise Exception(f"API request failed with status {response.status_code}: {response.text}")
# Poll until completion
response_dict = response.json()
status_url = response_dict.get("status_url")
if not status_url:
raise Exception("No status_url returned from API")
print(f"ImageEnhanceNode - Initial request successful for image {idx}, polling for completion...")
final_response = poll_status_until_completed(status_url, api_key)
result_image_url = final_response["result"]["image_url"]
used_seed = final_response["result"].get("seed", seed)
# Download and process image
image_response = requests.get(result_image_url)
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)
batch_results.append(result_tensor)
batch_seeds.append(used_seed)
except Exception as e:
print(f"[ImageEnhanceNode] Skipping image {idx} due to error: {e}")
# Append fallback tensor with same size as input
fallback_array = np.array(pil_image).astype(np.float32) / 255.0
batch_results.append(torch.from_numpy(fallback_array))
batch_seeds.append(seed)
# Return list of tensors (not concatenated) + comma-separated seeds
combined_seeds = ",".join(map(str, batch_seeds))
return (batch_results, combined_seeds)
+103 -71
View File
@@ -1,101 +1,133 @@
import numpy as np
import requests
from PIL import Image
import io import io
import requests
import numpy as np
from PIL import Image
import torch import torch
from .common import image_to_base64, preprocess_image from .common import (
bria_json_headers,
image_to_base64,
normalize_images_input,
poll_status_until_completed,
)
class ImageExpansionNode(): class ImageExpansionNode():
@classmethod @classmethod
def INPUT_TYPES(self): def INPUT_TYPES(cls):
return { return {
"required": { "required": {
"image": ("IMAGE",), # Input image from another node "images": ("IMAGE",),
"api_key": ("STRING", {"default": "BRIA_API_TOKEN"}),
},
"optional": {
"original_image_size": ("STRING",), "original_image_size": ("STRING",),
"original_image_location": ("STRING",), "original_image_location": ("STRING",),
"api_key": ("STRING", {"default": "BRIA_API_TOKEN"}), # API Key input with a default value
},
"optional": {
"canvas_size": ("STRING", {"default": "1000, 1000"}), "canvas_size": ("STRING", {"default": "1000, 1000"}),
"aspect_ratio": (["1:1", "2:3", "3:2", "3:4", "4:3", "4:5", "5:4", "9:16", "16:9", "None"], {"default": "None"}),
"prompt": ("STRING", {"default": ""}), "prompt": ("STRING", {"default": ""}),
"seed": ("INT", {"default": 681794}), "seed": ("STRING", {"default": "681794"}), # <-- accepts seeds from Enhance
"negative_prompt": ("STRING", {"default": "Ugly, mutated"}), "negative_prompt": ("STRING", {"default": "Ugly, mutated"}),
"content_moderation": ("BOOLEAN", {"default": False}), "prompt_content_moderation": ("BOOLEAN", {"default": False}),
"preserve_alpha": ("BOOLEAN", {"default": True}),
"visual_input_content_moderation": ("BOOLEAN", {"default": False}),
"visual_output_content_moderation": ("BOOLEAN", {"default": False}),
} }
} }
RETURN_TYPES = ("IMAGE",) RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("output_image",) RETURN_NAMES = ("output_images",)
CATEGORY = "API Nodes" CATEGORY = "API Nodes"
FUNCTION = "execute" # This is the method that will be executed FUNCTION = "execute"
def __init__(self): def __init__(self):
self.api_url = "https://engine.prod.bria-api.com/v1/image_expansion" # Image Expansion API URL self.api_url = "https://engine.prod.bria-api.com/v2/image/edit/expand"
# Define the execute method as expected by ComfyUI def execute(
def execute(self, image, self,
original_image_size, images,
original_image_location, original_image_size,
canvas_size, original_image_location,
prompt, canvas_size,
seed, aspect_ratio,
negative_prompt, prompt,
content_moderation, seed,
api_key): negative_prompt,
if api_key.strip() == "" or api_key.strip() == "BRIA_API_TOKEN": prompt_content_moderation,
preserve_alpha,
visual_input_content_moderation,
visual_output_content_moderation,
api_key
):
if api_key.strip() in ("", "BRIA_API_TOKEN"):
raise Exception("Please insert a valid API key.") raise Exception("Please insert a valid API key.")
images = normalize_images_input(images)
canvas_size = [int(x.strip()) for x in canvas_size.split(",")] if canvas_size else ()
original_image_size = [int(x.strip()) for x in original_image_size.split(",")] if original_image_size else ()
original_image_location = [int(x.strip()) for x in original_image_location.split(",")] if original_image_location else ()
original_image_size = [int(x.strip()) for x in original_image_size.split(",")] # Prepare per-image seeds
original_image_location = [int(x.strip()) for x in original_image_location.split(",")] seed_values = [int(s.strip()) for s in seed.split(",")] if isinstance(seed, str) else [seed]
canvas_size = [int(x.strip()) for x in canvas_size.split(",")] if len(seed_values) < len(images):
seed_values += [seed_values[-1]] * (len(images) - len(seed_values))
if prompt == "": if not negative_prompt:
prompt = None negative_prompt = " "
if negative_prompt == "":
negative_prompt = " " # hack to avoid error in triton which expects non-empty string
# Check if image and mask are tensors, if so, convert to NumPy arrays batch_results = []
if isinstance(image, torch.Tensor):
image = preprocess_image(image)
# Convert the image directly to Base64 string for idx, pil_image in enumerate(images):
image_base64 = image_to_base64(image) try:
image_base64 = image_to_base64(pil_image)
# Prepare the API request payload if aspect_ratio and aspect_ratio != "None":
payload = { payload = {
"file": f"{image_base64}", "image": image_base64,
"original_image_size": original_image_size, "aspect_ratio": aspect_ratio,
"original_image_location": original_image_location, "prompt": prompt,
"canvas_size": canvas_size, "negative_prompt": negative_prompt,
"prompt": prompt, "seed": seed_values[idx],
"negative_prompt": negative_prompt, "prompt_content_moderation": prompt_content_moderation,
"seed": seed, "preserve_alpha": preserve_alpha,
"content_moderation": content_moderation "visual_input_content_moderation": visual_input_content_moderation,
} "visual_output_content_moderation": visual_output_content_moderation
}
else:
payload = {
"image": image_base64,
"original_image_size": original_image_size,
"original_image_location": original_image_location,
"canvas_size": canvas_size,
"prompt": prompt,
"negative_prompt": negative_prompt,
"seed": seed_values[idx],
"prompt_content_moderation": prompt_content_moderation,
"preserve_alpha": preserve_alpha,
"visual_input_content_moderation": visual_input_content_moderation,
"visual_output_content_moderation": visual_output_content_moderation
}
headers = { headers = bria_json_headers(api_key)
"Content-Type": "application/json", response = requests.post(self.api_url, json=payload, headers=headers)
"api_token": f"{api_key}" if response.status_code not in (200, 202):
} raise Exception(f"API request failed with status {response.status_code}: {response.text}")
try:
response = requests.post(self.api_url, json=payload, headers=headers)
# Check for successful response
if response.status_code == 200:
print('response is 200')
# Process the output image from API response
response_dict = response.json() response_dict = response.json()
image_response = requests.get(response_dict['result_url']) status_url = response_dict.get("status_url")
result_image = Image.open(io.BytesIO(image_response.content)) if not status_url:
result_image = result_image.convert("RGB") raise Exception("No status_url returned from API")
result_image = np.array(result_image).astype(np.float32) / 255.0
result_image = torch.from_numpy(result_image)[None,]
return (result_image,)
else:
raise Exception(f"Error: API request failed with status code {response.status_code}")
except Exception as e: print(f"ImageExpansionNode - Initial request successful for image {idx}, polling for completion...")
raise Exception(f"{e}") 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)
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)
batch_results.append(result_tensor)
except Exception as e:
print(f"[ImageExpansionNode] Skipping image {idx} due to error: {e}")
fallback_array = np.array(pil_image).astype(np.float32) / 255.0
batch_results.append(torch.from_numpy(fallback_array))
return (batch_results,)
+85
View File
@@ -0,0 +1,85 @@
import os
import json
from typing import List
import torch
import torch.nn.functional as F
import numpy as np
from PIL import Image, ImageOps
try:
from folder_paths import get_input_directory
except Exception:
get_input_directory = None
IMG_EXTS = (".png", ".jpg", ".jpeg", ".webp", ".bmp", ".tif", ".tiff")
def input_root() -> str:
return os.path.abspath(get_input_directory() if get_input_directory else "input")
def parse_paths(value: str) -> List[str]:
if not value:
return []
try:
data = json.loads(value)
if isinstance(data, list):
return [str(x) for x in data]
except Exception:
pass
return []
class BriaMultiImageSelect:
"""
Select multiple images and return them as a list of PIL Images.
Images keep their original size.
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"selected_paths": (
"STRING",
{
"multiline": True,
"default": "",
"placeholder": "Filled automatically by Select Images button",
},
),
}
}
RETURN_TYPES = ("IMAGE", "STRING")
RETURN_NAMES = ("images", "filenames")
FUNCTION = "load"
CATEGORY = "API Nodes"
def load(self, selected_paths: str):
paths = parse_paths(selected_paths)
if not paths:
raise RuntimeError("BriaMultiImageSelect: No images selected")
root = input_root()
pil_images: List[Image.Image] = []
names: List[str] = []
for rel in paths:
abs_path = os.path.join(root, rel)
if not abs_path.lower().endswith(IMG_EXTS):
continue
if not os.path.isfile(abs_path):
continue
pil_images.append(Image.open(abs_path))
names.append(os.path.splitext(os.path.basename(rel))[0])
if not pil_images:
raise RuntimeError("BriaMultiImageSelect: No valid images found")
filenames = ", ".join(names)
return (pil_images, filenames)
+134
View File
@@ -0,0 +1,134 @@
import requests
import torch
from .common import (
bria_json_headers,
image_to_base64,
poll_status_until_completed,
postprocess_image,
preprocess_image,
)
class ProductIntegrateNode:
"""Product Integrate Node - Integrate a single product into a background scene"""
api_url = "https://engine.prod.bria-api.com/v2/image/edit/product/integrate"
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"api_token": ("STRING", {"default": "BRIA_API_TOKEN"}),
"scene": ("IMAGE",),
"product_image": ("IMAGE",),
"x_coordinate": ("INT", {"default": 0, "min": 0, "max": 10000}),
"y_coordinate": ("INT", {"default": 0, "min": 0, "max": 10000}),
"width": ("INT", {"default": 512, "min": 1, "max": 10000}),
"height": ("INT", {"default": 512, "min": 1, "max": 10000}),
},
"optional": {
"seed": ("STRING", {"default": "123456"}),
}
}
RETURN_TYPES = ("IMAGE", "INT")
RETURN_NAMES = ("IMAGE", "seed")
CATEGORY = "API Nodes"
FUNCTION = "execute"
def _validate_token(self, api_token: str):
if api_token.strip() == "" or api_token.strip() == "BRIA_API_TOKEN":
raise Exception("Please insert a valid API token.")
def _build_payload(
self,
scene_image,
product_image,
x_coordinate,
y_coordinate,
width,
height,
seed,
):
payload = {
"scene": image_to_base64(scene_image),
"products": [
{
"image": image_to_base64(product_image),
"coordinates": {
"x": x_coordinate,
"y": y_coordinate,
"width": width,
"height": height,
}
}
],
"seed": int(seed),
}
return payload
def execute(
self,
api_token,
scene,
product_image,
x_coordinate,
y_coordinate,
width,
height,
seed,
):
self._validate_token(api_token)
# Process single scene image
if isinstance(scene, torch.Tensor):
processed_scene = preprocess_image(scene)
if isinstance(product_image, torch.Tensor):
processed_product = preprocess_image(product_image)
payload = self._build_payload(
processed_scene,
processed_product,
x_coordinate,
y_coordinate,
width,
height,
seed,
)
headers = bria_json_headers(api_token)
try:
response = requests.post(self.api_url, json=payload, headers=headers)
if response.status_code in (200, 202):
print(
f"Initial product integrate request successful, 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_token)
result = final_response.get("result", {})
result_image_url = result.get("image_url")
used_seed = result.get("seed", seed)
image_response = requests.get(result_image_url)
result_image = postprocess_image(image_response.content)
return (result_image, used_seed)
else:
raise Exception(
f"API request failed with status code {response.status_code} {response.text}"
)
except Exception as e:
raise Exception(f"[ProductIntegrateNode] Error: {e}")
+167
View File
@@ -0,0 +1,167 @@
import requests
from .common import bria_json_headers, poll_status_until_completed, postprocess_image
class RefineImageLiteNodeV2:
"""Lite Refine Image Node"""
api_url = "https://engine.prod.bria-api.com/v2/structured_prompt/generate/lite"
generate_api_url = "https://engine.prod.bria-api.com/v2/image/generate/lite"
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"api_token": ("STRING", {"default": "BRIA_API_TOKEN"}),
"prompt": ("STRING",),
"structured_prompt": ("STRING",),
},
"optional": {
"model_version": (["FIBO"], {"default": "FIBO"}),
"aspect_ratio": (
["1:1", "2:3", "3:2", "3:4", "4:3", "4:5", "5:4", "9:16", "16:9"],
{"default": "1:1"},
),
"steps_num": (
"INT",
{
"default": 8,
"min": 8,
"max": 30,
},
),
"guidance_scale": (
"INT",
{
"default": 5,
"min": 3,
"max": 5,
},
),
"seed": ("INT", {"default": 123456}),
},
}
RETURN_TYPES = ("IMAGE", "STRING", "INT")
RETURN_NAMES = ("image", "structured_prompt", "seed")
CATEGORY = "API Nodes"
FUNCTION = "execute"
def _validate_token(self, api_token: str):
if api_token.strip() == "" or api_token.strip() == "BRIA_API_TOKEN":
raise Exception("Please insert a valid API token.")
def _build_payload(
self,
prompt,
structured_prompt,
model_version,
aspect_ratio,
steps_num,
guidance_scale,
seed,
):
payload = {
"prompt": prompt,
"model_version": model_version,
"aspect_ratio": aspect_ratio,
"steps_num": steps_num,
"guidance_scale": guidance_scale,
"seed": seed,
}
if structured_prompt:
payload["structured_prompt"] = structured_prompt
return payload
def execute(
self,
api_token,
prompt,
structured_prompt,
model_version,
aspect_ratio,
steps_num,
guidance_scale,
seed
):
self._validate_token(api_token)
payload = self._build_payload(
prompt,
structured_prompt,
model_version,
aspect_ratio,
steps_num,
guidance_scale,
seed,
)
headers = bria_json_headers(api_token)
try:
response = requests.post(self.api_url, json=payload, headers=headers)
if response.status_code in (200, 202):
print(f"Initial refine request successful to {self.api_url}, 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_token)
result = final_response.get("result", {})
structured_prompt = result.get("structured_prompt", "")
used_seed = result.get("seed", seed)
# Step 2 to call genearte image
payloadForImageGenetrate = {
"prompt": prompt,
"structured_prompt":structured_prompt,
"model_version": model_version,
"aspect_ratio": aspect_ratio,
"steps_num": steps_num,
"guidance_scale": guidance_scale,
"seed": used_seed,
}
headers = bria_json_headers(api_token)
response = requests.post(self.generate_api_url, json=payloadForImageGenetrate, headers=headers)
if response.status_code in (200, 202):
print(
f"Initial request successful to {self.generate_api_url}, 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_token)
result = final_response.get("result", {})
result_image_url = result.get("image_url")
structured_prompt = result.get("structured_prompt", "")
used_seed = result.get("seed")
image_response = requests.get(result_image_url)
result_image = postprocess_image(image_response.content)
return (result_image, structured_prompt, used_seed)
raise Exception(
f"Error: API request failed with status code {response.status_code} {response.text}"
)
except Exception as e:
raise Exception(f"{e}")
+169
View File
@@ -0,0 +1,169 @@
import requests
from .common import bria_json_headers, poll_status_until_completed, postprocess_image
class RefineImageNodeV2:
"""Standard Refine Image Node"""
api_url = "https://engine.prod.bria-api.com/v2/structured_prompt/generate" # Must be overridden by subclasses
generate_api_url = "https://engine.prod.bria-api.com/v2/image/generate"
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"api_token": ("STRING", {"default": "BRIA_API_TOKEN"}),
"prompt": ("STRING",),
"structured_prompt": ("STRING",),
},
"optional": {
"model_version": (["FIBO"], {"default": "FIBO"}),
"aspect_ratio": (
["1:1", "2:3", "3:2", "3:4", "4:3", "4:5", "5:4", "9:16", "16:9"],
{"default": "1:1"},
),
"steps_num": (
"INT",
{
"default": 50,
"min": 35,
"max": 50,
},
),
"guidance_scale": (
"INT",
{
"default": 5,
"min": 3,
"max": 5,
},
),
"seed": ("INT", {"default": 123456}),
"negative_prompt": ("STRING", {"default": ""}),
},
}
RETURN_TYPES = ("IMAGE", "STRING", "INT")
RETURN_NAMES = ("image", "structured_prompt", "seed")
CATEGORY = "API Nodes"
FUNCTION = "execute"
def _validate_token(self, api_token: str):
if api_token.strip() == "" or api_token.strip() == "BRIA_API_TOKEN":
raise Exception("Please insert a valid API token.")
def _build_payload(
self,
prompt,
structured_prompt,
model_version,
aspect_ratio,
steps_num,
guidance_scale,
seed,
):
payload = {
"prompt": prompt,
"model_version": model_version,
"aspect_ratio": aspect_ratio,
"steps_num": steps_num,
"guidance_scale": guidance_scale,
"seed": seed,
}
if structured_prompt:
payload["structured_prompt"] = structured_prompt
return payload
def execute(
self,
api_token,
prompt,
structured_prompt,
model_version,
aspect_ratio,
steps_num,
guidance_scale,
seed,
negative_prompt=None,
):
self._validate_token(api_token)
payload = self._build_payload(
prompt,
structured_prompt,
model_version,
aspect_ratio,
steps_num,
guidance_scale,
seed,
)
headers = bria_json_headers(api_token)
try:
response = requests.post(self.api_url, json=payload, headers=headers)
if response.status_code in (200, 202):
print(f"Initial refine request successful to {self.api_url}, 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_token)
result = final_response.get("result", {})
structured_prompt = result.get("structured_prompt", "")
used_seed = result.get("seed", seed)
# Step 2 to call genearte image
payloadForImageGenetrate = {
"prompt": prompt,
"structured_prompt":structured_prompt,
"model_version": model_version,
"aspect_ratio": aspect_ratio,
"steps_num": steps_num,
"guidance_scale": guidance_scale,
"seed": used_seed,
"negative_prompt":negative_prompt
}
headers = bria_json_headers(api_token)
response = requests.post(self.generate_api_url, json=payloadForImageGenetrate, headers=headers)
if response.status_code in (200, 202):
print(
f"Initial request successful to {self.generate_api_url}, 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_token)
result = final_response.get("result", {})
result_image_url = result.get("image_url")
structured_prompt = result.get("structured_prompt", "")
used_seed = result.get("seed")
image_response = requests.get(result_image_url)
result_image = postprocess_image(image_response.content)
return (result_image, structured_prompt, used_seed)
raise Exception(
f"Error: API request failed with status code {response.status_code} {response.text}"
)
except Exception as e:
raise Exception(f"{e}")
+7 -2
View File
@@ -1,6 +1,11 @@
import requests import requests
from .common import postprocess_image, preprocess_image, image_to_base64 from .common import (
bria_json_headers,
image_to_base64,
postprocess_image,
preprocess_image,
)
class ReimagineNode(): class ReimagineNode():
@@ -58,7 +63,7 @@ class ReimagineNode():
response = requests.post( response = requests.post(
self.api_url, self.api_url,
json=payload, json=payload,
headers={"api_token": api_key} headers=bria_json_headers(api_key),
) )
if response.status_code == 200: if response.status_code == 200:
response_dict = response.json() response_dict = response.json()
+66 -43
View File
@@ -1,70 +1,93 @@
import numpy as np
import requests
from PIL import Image
import io import io
import requests
import numpy as np
from PIL import Image
import torch import torch
from .common import preprocess_image, image_to_base64 from .common import (
bria_json_headers,
image_to_base64,
normalize_images_input,
poll_status_until_completed,
)
class RemoveForegroundNode(): class RemoveForegroundNode():
@classmethod @classmethod
def INPUT_TYPES(self): def INPUT_TYPES(cls):
return { return {
"required": { "required": {
"image": ("IMAGE",), # Input image from another node "images": ("IMAGE",),
"api_key": ("STRING", {"default": "BRIA_API_TOKEN"}), # API Key input with a default value "api_key": ("STRING", {"default": "BRIA_API_TOKEN"}),
}, },
"optional": { "optional": {
"content_moderation": ("BOOLEAN", {"default": False}), "visual_input_content_moderation": ("BOOLEAN", {"default": False}),
"visual_output_content_moderation": ("BOOLEAN", {"default": False}),
"preserve_alpha": ("BOOLEAN", {"default": True}),
} }
} }
RETURN_TYPES = ("IMAGE",) RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("output_image",) RETURN_NAMES = ("output_images",)
CATEGORY = "API Nodes" CATEGORY = "API Nodes"
FUNCTION = "execute" # This is the method that will be executed FUNCTION = "execute"
def __init__(self): def __init__(self):
self.api_url = "https://engine.internal.prod.bria-api.com/v1/erase_foreground" # remove foreground API URL self.api_url = "https://engine.prod.bria-api.com/v2/image/edit/erase_foreground"
# Define the execute method as expected by ComfyUI def execute(
def execute(self, image, content_moderation, api_key): self,
images,
visual_input_content_moderation,
visual_output_content_moderation,
preserve_alpha,
api_key
):
if api_key.strip() == "" or api_key.strip() == "BRIA_API_TOKEN": if api_key.strip() == "" or api_key.strip() == "BRIA_API_TOKEN":
raise Exception("Please insert a valid API key.") raise Exception("Please insert a valid API key.")
images = normalize_images_input(images)
batch_results = []
# Check if image is tensor, if so, convert to NumPy array for idx, pil_image in enumerate(images):
if isinstance(image, torch.Tensor): try:
image = preprocess_image(image) image_base64 = image_to_base64(pil_image)
# Prepare the API request payload payload = {
# temporary save the image to /tmp "image": image_base64,
# temp_img_path = "/tmp/temp_img.jpeg" "visual_input_content_moderation": visual_input_content_moderation,
# image.save(temp_img_path, format="JPEG") "visual_output_content_moderation": visual_output_content_moderation,
"preserve_alpha": preserve_alpha
}
# files=[('file',('temp_img.jpeg', open(temp_img_path, 'rb'),'image/jpeg')) headers = bria_json_headers(api_key)
# ]
payload = {"file": image_to_base64(image), "content_moderation": content_moderation}
headers = { response = requests.post(self.api_url, json=payload, headers=headers)
"Content-Type": "application/json", if response.status_code not in (200, 202):
"api_token": f"{api_key}" raise Exception(f"API request failed with status {response.status_code}: {response.text}")
}
try:
response = requests.post(self.api_url, json=payload, headers=headers)
# Check for successful response
if response.status_code == 200:
print('response is 200')
# Process the output image from API response
response_dict = response.json() response_dict = response.json()
image_response = requests.get(response_dict['result_url']) status_url = response_dict.get("status_url")
result_image = Image.open(io.BytesIO(image_response.content)) if not status_url:
result_image = np.array(result_image).astype(np.float32) / 255.0 raise Exception("No status_url returned from API")
result_image = torch.from_numpy(result_image)[None,]
return (result_image,)
else:
raise Exception(f"Error: API request failed with status code {response.status_code}")
except Exception as e: print(f"RemoveForegroundNode - Initial request successful for image {idx}, polling for completion...")
raise Exception(f"{e}") final_response = poll_status_until_completed(status_url, api_key)
result_image_url = final_response["result"]["image_url"]
# Download result
image_response = requests.get(result_image_url)
result_image = Image.open(io.BytesIO(image_response.content)).convert("RGB")
# Convert to float32 tensor (H, W, C)
result_array = np.array(result_image).astype(np.float32) / 255.0
result_tensor = torch.from_numpy(result_array)
batch_results.append(result_tensor)
except Exception as e:
print(f"[RemoveForegroundNode] Skipping image {idx} due to error: {e}")
# fallback: use original image as tensor
fallback_array = np.array(pil_image).astype(np.float32) / 255.0
batch_results.append(torch.from_numpy(fallback_array))
# Return list of tensors
return (batch_results,)
+93 -76
View File
@@ -1,105 +1,122 @@
import numpy as np
import requests
from PIL import Image
import io import io
import requests
import numpy as np
from PIL import Image
import torch import torch
from .common import image_to_base64, preprocess_image, preprocess_mask from .common import (
bria_json_headers,
image_to_base64,
normalize_images_input,
poll_status_until_completed,
)
class ReplaceBgNode(): class ReplaceBgNode():
@classmethod @classmethod
def INPUT_TYPES(self): def INPUT_TYPES(cls):
return { return {
"required": { "required": {
"image": ("IMAGE",), # Input image from another node "images": ("IMAGE",),
"api_key": ("STRING", {"default": "BRIA_API_TOKEN"}), # API Key input with a default value "api_key": ("STRING", {"default": "BRIA_API_TOKEN"}),
}, },
"optional": { "optional": {
"fast": ("BOOLEAN", {"default": True}), "mode": (["base", "fast", "high_control"], {"default": "base"}),
"bg_prompt": ("STRING",), "prompt": ("STRING", {"default": ""}),
"ref_image": ("IMAGE",), # Input ref image from another node "ref_images": ("IMAGE",),
"refine_prompt": ("BOOLEAN", {"default": True}), "refine_prompt": ("BOOLEAN", {"default": True}),
"enhance_ref_image": ("BOOLEAN", {"default": True}), "enhance_ref_images": ("BOOLEAN", {"default": True}),
"original_quality": ("BOOLEAN", {"default": False}), "original_quality": ("BOOLEAN", {"default": False}),
"force_rmbg": ("BOOLEAN", {"default": False}),
"negative_prompt": ("STRING", {"default": None}), "negative_prompt": ("STRING", {"default": None}),
"seed": ("INT", {"default": 681794}), "seed": ("STRING", {"default": "681794"}), # Accept comma-separated seeds
"content_moderation": ("BOOLEAN", {"default": False}), "visual_output_content_moderation": ("BOOLEAN", {"default": False}),
"prompt_content_moderation": ("BOOLEAN", {"default": False}),
"force_background_detection": ("BOOLEAN", {"default": False}),
} }
} }
RETURN_TYPES = ("IMAGE",) RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("output_image",) RETURN_NAMES = ("output_images",)
CATEGORY = "API Nodes" CATEGORY = "API Nodes"
FUNCTION = "execute" # This is the method that will be executed FUNCTION = "execute"
def __init__(self): def __init__(self):
self.api_url = "https://engine.prod.bria-api.com/v1/background/replace" # Replace BG API URL self.api_url = "https://engine.prod.bria-api.com/v2/image/edit/replace_background"
# Define the execute method as expected by ComfyUI def execute(
def execute(self, image, fast, self,
refine_prompt, images,
enhance_ref_image, mode,
original_quality, refine_prompt,
force_rmbg, original_quality,
negative_prompt, negative_prompt,
seed, seed,
api_key, api_key,
content_moderation, visual_output_content_moderation,
bg_prompt=None, prompt_content_moderation,
ref_image=None,): enhance_ref_images,
if api_key.strip() == "" or api_key.strip() == "BRIA_API_TOKEN": force_background_detection,
prompt=None,
ref_images=None
):
if api_key.strip() in ("", "BRIA_API_TOKEN"):
raise Exception("Please insert a valid API key.") raise Exception("Please insert a valid API key.")
images = normalize_images_input(images)
# Check if image and mask are tensors, if so, convert to NumPy arrays # Normalize reference images
if isinstance(image, torch.Tensor): ref_images_base64 = []
image = preprocess_image(image) if ref_images is not None:
ref_images_list = normalize_images_input(ref_images)
ref_images_base64 = [image_to_base64(img) for img in ref_images_list]
# Convert the image and mask directly to Base64 strings # Prepare per-image seeds
image_base64 = image_to_base64(image) seed_values = [int(s.strip()) for s in seed.split(",")] if isinstance(seed, str) else [seed]
ref_image_file = None # initialization, will be updated if it is supplied if len(seed_values) < len(images):
if ref_image is not None: seed_values += [seed_values[-1]] * (len(images) - len(seed_values))
ref_image = preprocess_image(ref_image)
ref_image_file = image_to_base64(ref_image)
# Prepare the API request payload batch_results = []
payload = {
"file": f"{image_base64}",
"fast": fast,
"bg_prompt": bg_prompt,
"ref_image_file": ref_image_file,
"refine_prompt": refine_prompt,
"enhance_ref_image": enhance_ref_image,
"original_quality": original_quality,
"force_rmbg": force_rmbg,
"negative_prompt": negative_prompt,
"seed": seed,
"sync": True,
"num_results": 1,
"content_moderation": content_moderation
}
headers = { for idx, pil_image in enumerate(images):
"Content-Type": "application/json", try:
"api_token": f"{api_key}" image_base64 = image_to_base64(pil_image)
}
payload = {
"image": image_base64,
"mode": mode,
"prompt": prompt,
"ref_images": ref_images_base64,
"refine_prompt": refine_prompt,
"original_quality": original_quality,
"negative_prompt": negative_prompt,
"seed": seed_values[idx],
"prompt_content_moderation": prompt_content_moderation,
"visual_output_content_moderation": visual_output_content_moderation,
"enhance_ref_images": enhance_ref_images,
"force_background_detection": force_background_detection
}
headers = bria_json_headers(api_key)
response = requests.post(self.api_url, json=payload, headers=headers)
if response.status_code not in (200, 202):
raise Exception(f"API request failed with status {response.status_code}: {response.text}")
try:
response = requests.post(self.api_url, json=payload, headers=headers)
# Check for successful response
if response.status_code == 200:
print('response is 200')
# Process the output image from API response
response_dict = response.json() response_dict = response.json()
image_response = requests.get(response_dict['result'][0][0]) # first indexing for batched, second for url status_url = response_dict.get("status_url")
result_image = Image.open(io.BytesIO(image_response.content)) if not status_url:
result_image = result_image.convert("RGB") raise Exception("No status_url returned from API")
result_image = np.array(result_image).astype(np.float32) / 255.0
result_image = torch.from_numpy(result_image)[None,]
return (result_image,)
else:
raise Exception(f"Error: API request failed with status code {response.status_code}")
except Exception as e: print(f"ReplaceBgNode - Initial request successful for image {idx}, polling for completion...")
raise Exception(f"{e}") 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)
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)
batch_results.append(result_tensor)
except Exception as e:
print(f"[ReplaceBgNode] Skipping image {idx} due to error: {e}")
fallback_array = np.array(pil_image).astype(np.float32) / 255.0
batch_results.append(torch.from_numpy(fallback_array))
return (batch_results,)
+62 -40
View File
@@ -1,67 +1,89 @@
import numpy as np
import requests
from PIL import Image
import io import io
import requests
import numpy as np
from PIL import Image
import torch import torch
from .common import preprocess_image from .common import (
from io import BytesIO bria_json_headers,
image_to_base64,
normalize_images_input,
poll_status_until_completed,
to_pil_safe,
)
class RmbgNode(): class RmbgNode():
@classmethod @classmethod
def INPUT_TYPES(self): def INPUT_TYPES(cls):
return { return {
"required": { "required": {
"image": ("IMAGE",), # Input image from another node "images": ("IMAGE",), # Accepts list of PIL Images or single tensor
"api_key": ("STRING", {"default": "BRIA_API_TOKEN"}), # API Key input with a default value "api_key": ("STRING", {"default": "BRIA_API_TOKEN"}),
}, },
"optional": { "optional": {
"content_moderation": ("BOOLEAN", {"default": False}), "visual_input_content_moderation": ("BOOLEAN", {"default": False}),
"visual_output_content_moderation": ("BOOLEAN", {"default": False}),
"preserve_alpha": ("BOOLEAN", {"default": True}),
} }
} }
RETURN_TYPES = ("IMAGE",) RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("output_image",) RETURN_NAMES = ("output_images",)
CATEGORY = "API Nodes" CATEGORY = "API Nodes"
FUNCTION = "execute" # This is the method that will be executed FUNCTION = "execute"
def __init__(self): def __init__(self):
self.api_url = "https://engine.prod.bria-api.com/v1/background/remove" # RMBG API URL self.api_url = "https://engine.prod.bria-api.com/v2/image/edit/remove_background"
# Define the execute method as expected by ComfyUI def execute(self, images, visual_input_content_moderation, visual_output_content_moderation, preserve_alpha, api_key):
def execute(self, image, content_moderation, api_key):
if api_key.strip() == "" or api_key.strip() == "BRIA_API_TOKEN": if api_key.strip() == "" or api_key.strip() == "BRIA_API_TOKEN":
raise Exception("Please insert a valid API key.") raise Exception("Please insert a valid API key.")
# Normalize input to list of PIL images
images = normalize_images_input(images)
# Check if image is tensor, if so, convert to NumPy array batch_results = []
if isinstance(image, torch.Tensor):
image = preprocess_image(image)
# Prepare the API request payload for idx, pil_image in enumerate(images):
image_buffer = BytesIO() try:
image.save(image_buffer, format="JPEG") image_base64 = image_to_base64(pil_image)
# Get binary data from buffer payload = {
image_buffer.seek(0) # Move cursor to the start of the buffer "image": image_base64,
binary_data = image_buffer.read() "visual_input_content_moderation": visual_input_content_moderation,
"visual_output_content_moderation": visual_output_content_moderation,
"preserve_alpha": preserve_alpha
}
files=[('file',('temp_img.jpeg', BytesIO(binary_data),'image/jpeg'))] headers = bria_json_headers(api_key)
payload = {"content_moderation": content_moderation}
try: response = requests.post(self.api_url, json=payload, headers=headers)
response = requests.post(self.api_url, data=payload, headers={"api_token": api_key}, files=files) if response.status_code not in (200, 202):
# Check for successful response raise Exception(f"API request failed with status {response.status_code}: {response.text}")
if response.status_code == 200:
print('response is 200') # Poll until completion
# Process the output image from API response
response_dict = response.json() response_dict = response.json()
image_response = requests.get(response_dict['result_url']) status_url = response_dict.get('status_url')
result_image = Image.open(io.BytesIO(image_response.content)) if not status_url:
result_image = np.array(result_image).astype(np.float32) / 255.0 raise Exception("No status_url returned from API")
result_image = torch.from_numpy(result_image)[None,]
return (result_image,)
else:
raise Exception(f"Error: API request failed with status code {response.status_code}")
except Exception as e: print(f"RmbgNode - Initial request successful for image {idx}, polling for completion...")
raise Exception(f"{e}") final_response = poll_status_until_completed(status_url, api_key)
result_image_url = final_response['result']['image_url']
# Download result
image_response = requests.get(result_image_url)
result_image = Image.open(io.BytesIO(image_response.content))
# Convert to float32 tensor (H, W, C), 0-1
result_array = np.array(result_image).astype(np.float32) / 255.0
result_tensor = torch.from_numpy(result_array) # shape: (H,W,4)
batch_results.append(result_tensor)
except Exception as e:
print(f"[RmbgNode] Skipping image {idx} due to error: {e}")
# Append empty tensor of the same size as original
empty_array = np.zeros((pil_image.height, pil_image.width, 4), dtype=np.float32)
batch_results.append(torch.from_numpy(empty_array))
# Return list of tensors (Comfy preview handles this correctly)
return (batch_results,)
@@ -0,0 +1,46 @@
from .utils.shot_utils import get_image_input_types, create_image_payload, make_api_request, shot_by_image_api_url, PlacementType
class ShotByImageAutomaticAspectRatioNode:
@classmethod
def INPUT_TYPES(self):
input_types = get_image_input_types()
input_types["required"]["aspect_ratio"] = (
["1:1", "2:3", "3:2", "3:4", "4:3", "4:5", "5:4", "9:16", "16:9"],
{"default": "1:1"},
)
return input_types
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("output_image",)
CATEGORY = "API Nodes"
FUNCTION = "execute"
def __init__(self):
self.api_url = shot_by_image_api_url
def execute(
self,
image,
ref_image,
aspect_ratio,
api_key,
sync=False,
enhance_ref_image=True,
ref_image_influence=1.0,
force_rmbg=False,
content_moderation=False,
):
payload = create_image_payload(
image,
ref_image,
api_key,
PlacementType.AUTOMATIC_ASPECT_RATIO.value,
aspect_ratio=aspect_ratio,
sync=sync,
enhance_ref_image=enhance_ref_image,
ref_image_influence=ref_image_influence,
force_rmbg=force_rmbg,
content_moderation=content_moderation,
)
return make_api_request(self.api_url, payload, api_key)
+51
View File
@@ -0,0 +1,51 @@
from .utils.shot_utils import get_image_input_types, create_image_payload, make_api_request, shot_by_image_api_url, PlacementType
class ShotByImageAutomaticNode:
@classmethod
def INPUT_TYPES(self):
input_types = get_image_input_types()
input_types["required"]["shot_size"] = ("STRING", {"default": "1000, 1000"})
return input_types
RETURN_TYPES = ("IMAGE", "IMAGE", "IMAGE", "IMAGE", "IMAGE", "IMAGE", "IMAGE")
RETURN_NAMES = (
"output_image_1",
"output_image_2",
"output_image_3",
"output_image_4",
"output_image_5",
"output_image_6",
"output_image_7",
)
CATEGORY = "API Nodes"
FUNCTION = "execute"
def __init__(self):
self.api_url = shot_by_image_api_url
def execute(
self,
image,
ref_image,
shot_size,
api_key,
sync=False,
enhance_ref_image=True,
ref_image_influence=1.0,
force_rmbg=False,
content_moderation=False,
):
payload = create_image_payload(
image,
ref_image,
api_key,
PlacementType.AUTOMATIC.value,
shot_size=shot_size,
sync=sync,
enhance_ref_image=enhance_ref_image,
ref_image_influence=ref_image_influence,
force_rmbg=force_rmbg,
content_moderation=content_moderation,
)
return make_api_request(self.api_url, payload, api_key, Placement_type = PlacementType.AUTOMATIC.value)
@@ -0,0 +1,55 @@
from .utils.shot_utils import get_image_input_types, create_image_payload, make_api_request, shot_by_image_api_url, PlacementType
class ShotByImageCustomCoordinatesNode:
@classmethod
def INPUT_TYPES(self):
input_types = get_image_input_types()
input_types["required"]["shot_size"] = ("STRING", {"default": "1000, 1000"})
input_types["required"]["foreground_image_size"] = (
"STRING",
{"default": "500,500"},
)
input_types["required"]["foreground_image_location"] = (
"STRING",
{"default": "0, 0"},
)
return input_types
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("output_image",)
CATEGORY = "API Nodes"
FUNCTION = "execute"
def __init__(self):
self.api_url = shot_by_image_api_url
def execute(
self,
image,
ref_image,
shot_size,
foreground_image_size,
foreground_image_location,
api_key,
sync=False,
enhance_ref_image=True,
ref_image_influence=1.0,
force_rmbg=False,
content_moderation=False,
):
payload = create_image_payload(
image,
ref_image,
api_key,
PlacementType.CUSTOM_COORDINATES.value,
shot_size=shot_size,
foreground_image_size=foreground_image_size,
foreground_image_location=foreground_image_location,
sync=sync,
enhance_ref_image=enhance_ref_image,
ref_image_influence=ref_image_influence,
force_rmbg=force_rmbg,
content_moderation=content_moderation,
)
return make_api_request(self.api_url, payload, api_key)
@@ -0,0 +1,44 @@
from .utils.shot_utils import get_image_input_types, create_image_payload, make_api_request, shot_by_image_api_url, PlacementType
class ShotByImageManualPaddingNode:
@classmethod
def INPUT_TYPES(self):
input_types = get_image_input_types()
input_types["required"]["padding_values"] = ("STRING", {"default": "0,0,0,0"})
return input_types
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("output_image",)
CATEGORY = "API Nodes"
FUNCTION = "execute"
def __init__(self):
self.api_url = shot_by_image_api_url
def execute(
self,
image,
ref_image,
padding_values,
api_key,
sync=False,
enhance_ref_image=True,
ref_image_influence=1.0,
force_rmbg=False,
content_moderation=False,
):
payload = create_image_payload(
image,
ref_image,
api_key,
PlacementType.MANUAL_PADDING.value,
padding_values=padding_values,
sync=sync,
enhance_ref_image=enhance_ref_image,
ref_image_influence=ref_image_influence,
force_rmbg=force_rmbg,
content_moderation=content_moderation,
)
return make_api_request(self.api_url, payload, api_key)
@@ -0,0 +1,59 @@
from .utils.shot_utils import get_image_input_types, create_image_payload, make_api_request, shot_by_image_api_url, PlacementType
class ShotByImageManualPlacementNode:
@classmethod
def INPUT_TYPES(self):
input_types = get_image_input_types()
input_types["required"]["shot_size"] = ("STRING", {"default": "1000, 1000"})
input_types["required"]["manual_placement_selection"] = (
[
"upper_left",
"upper_right",
"bottom_left",
"bottom_right",
"right_center",
"left_center",
"upper_center",
"bottom_center",
"center_vertical",
"center_horizontal",
],
{"default": "upper_left"},
)
return input_types
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("output_image",)
CATEGORY = "API Nodes"
FUNCTION = "execute"
def __init__(self):
self.api_url = shot_by_image_api_url
def execute(
self,
image,
ref_image,
shot_size,
manual_placement_selection,
api_key,
sync=False,
enhance_ref_image=True,
ref_image_influence=1.0,
force_rmbg=False,
content_moderation=False,
):
payload = create_image_payload(
image,
ref_image,
api_key,
PlacementType.MANUAL_PLACEMENT.value,
shot_size=shot_size,
manual_placement_selection=manual_placement_selection,
sync=sync,
enhance_ref_image=enhance_ref_image,
ref_image_influence=ref_image_influence,
force_rmbg=force_rmbg,
content_moderation=content_moderation,
)
return make_api_request(self.api_url, payload, api_key)
+30 -61
View File
@@ -1,72 +1,41 @@
import requests from .utils.shot_utils import get_image_input_types, create_image_payload, make_api_request, shot_by_image_api_url, PlacementType
import torch
from .common import postprocess_image, preprocess_image, image_to_base64
class ShotByImageNode(): class ShotByImageOriginalNode:
@classmethod @classmethod
def INPUT_TYPES(self): def INPUT_TYPES(self):
return { input_types = get_image_input_types()
"required": { return input_types
"image": ("IMAGE",), # Input image from another node
"ref_image": ("IMAGE",), # ref image from another node
"enhance_ref_image": ("INT", {"default": 1}),
"api_key": ("STRING", {"default": "BRIA_API_TOKEN"}) # API Key input with a default value
},
"optional": {
"content_moderation": ("BOOLEAN", {"default": False}),
}
}
RETURN_TYPES = ("IMAGE",) RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("output_image",) RETURN_NAMES = ("output_image",)
CATEGORY = "API Nodes" CATEGORY = "API Nodes"
FUNCTION = "execute" # This is the method that will be executed FUNCTION = "execute"
def __init__(self): def __init__(self):
self.api_url = "https://engine.prod.bria-api.com/v1/product/lifestyle_shot_by_image" # Eraser API URL self.api_url = shot_by_image_api_url
# Define the execute method as expected by ComfyUI def execute(
def execute(self, image, ref_image, api_key, enhance_ref_image, content_moderation): self,
if api_key.strip() == "" or api_key.strip() == "BRIA_API_TOKEN": image,
raise Exception("Please insert a valid API key.") ref_image,
api_key,
# Check if image and mask are tensors, if so, convert to NumPy arrays sync=True,
if isinstance(image, torch.Tensor): enhance_ref_image=True,
image = preprocess_image(image) ref_image_influence=1.0,
if isinstance(ref_image, torch.Tensor): force_rmbg=False,
ref_image = preprocess_image(ref_image) content_moderation=False,
):
# Convert the image and mask directly to Base64 strings payload = create_image_payload(
image_base64 = image_to_base64(image) image,
ref_image_base64 = image_to_base64(ref_image) ref_image,
enhance_ref_image = bool(enhance_ref_image) api_key,
PlacementType.ORIGINAL.value,
payload = { original_quality=True,
"file": image_base64, sync=sync,
"ref_image_file": ref_image_base64, enhance_ref_image=enhance_ref_image,
"enhance_ref_image": enhance_ref_image, ref_image_influence=ref_image_influence,
"placement_type": "original", force_rmbg=force_rmbg,
"original_quality": True, content_moderation=content_moderation,
"sync": True, )
"content_moderation": content_moderation return make_api_request(self.api_url, payload, api_key)
}
headers = {
"Content-Type": "application/json",
"api_token": f"{api_key}"
}
try:
response = requests.post(self.api_url, json=payload, headers=headers)
# Check for successful response
if response.status_code == 200:
print('response is 200')
# Process the output image from API response
response_dict = response.json()
image_response = requests.get(response_dict['result'][0][0])
result_image = postprocess_image(image_response.content)
return (result_image,)
else:
raise Exception(f"Error: API request failed with status code {response.status_code}")
except Exception as e:
raise Exception(f"{e}")
@@ -0,0 +1,48 @@
from .utils.shot_utils import get_text_input_types, create_text_payload, make_api_request, shot_by_text_api_url, PlacementType
class ShotByTextAutomaticAspectRatioNode:
@classmethod
def INPUT_TYPES(self):
input_types = get_text_input_types()
input_types["required"]["aspect_ratio"] = (
["1:1", "2:3", "3:2", "3:4", "4:3", "4:5", "5:4", "9:16", "16:9"],
{"default": "1:1"},
)
return input_types
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("output_image",)
CATEGORY = "API Nodes"
FUNCTION = "execute"
def __init__(self):
self.api_url = shot_by_text_api_url
def execute(
self,
image,
scene_description,
mode,
aspect_ratio,
api_key,
sync=False,
optimize_description=True,
exclude_elements="",
force_rmbg=False,
content_moderation=False,
):
payload = create_text_payload(
image,
api_key,
scene_description,
mode,
PlacementType.AUTOMATIC_ASPECT_RATIO.value,
aspect_ratio=aspect_ratio,
sync=sync,
optimize_description=optimize_description,
exclude_elements=exclude_elements,
force_rmbg=force_rmbg,
content_moderation=content_moderation,
)
return make_api_request(self.api_url, payload, api_key)
+53
View File
@@ -0,0 +1,53 @@
from .utils.shot_utils import get_text_input_types, create_text_payload, make_api_request, shot_by_text_api_url, PlacementType
class ShotByTextAutomaticNode:
@classmethod
def INPUT_TYPES(self):
input_types = get_text_input_types()
input_types["required"]["shot_size"] = ("STRING", {"default": "1000, 1000"})
return input_types
RETURN_TYPES = ("IMAGE", "IMAGE", "IMAGE", "IMAGE", "IMAGE", "IMAGE", "IMAGE")
RETURN_NAMES = (
"output_image_1",
"output_image_2",
"output_image_3",
"output_image_4",
"output_image_5",
"output_image_6",
"output_image_7",
)
CATEGORY = "API Nodes"
FUNCTION = "execute"
def __init__(self):
self.api_url = shot_by_text_api_url
def execute(
self,
image,
scene_description,
mode,
shot_size,
api_key,
sync=False,
optimize_description=True,
exclude_elements="",
force_rmbg=False,
content_moderation=False,
):
payload = create_text_payload(
image,
api_key,
scene_description,
mode,
PlacementType.AUTOMATIC.value,
shot_size=shot_size,
sync=sync,
optimize_description=optimize_description,
exclude_elements=exclude_elements,
force_rmbg=force_rmbg,
content_moderation=content_moderation,
)
return make_api_request(self.api_url, payload, api_key, Placement_type= PlacementType.AUTOMATIC.value)
@@ -0,0 +1,57 @@
from .utils.shot_utils import get_text_input_types, create_text_payload, make_api_request, shot_by_text_api_url, PlacementType
class ShotByTextCustomCoordinatesNode:
@classmethod
def INPUT_TYPES(self):
input_types = get_text_input_types()
input_types["required"]["shot_size"] = ("STRING", {"default": "1000, 1000"})
input_types["required"]["foreground_image_size"] = (
"STRING",
{"default": "500,500"},
)
input_types["required"]["foreground_image_location"] = (
"STRING",
{"default": "0, 0"},
)
return input_types
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("output_image",)
CATEGORY = "API Nodes"
FUNCTION = "execute"
def __init__(self):
self.api_url = shot_by_text_api_url
def execute(
self,
image,
scene_description,
mode,
shot_size,
foreground_image_size,
foreground_image_location,
api_key,
sync=False,
optimize_description=True,
exclude_elements="",
force_rmbg=False,
content_moderation=False,
):
payload = create_text_payload(
image,
api_key,
scene_description,
mode,
PlacementType.CUSTOM_COORDINATES.value,
shot_size=shot_size,
foreground_image_size=foreground_image_size,
foreground_image_location=foreground_image_location,
sync=sync,
optimize_description=optimize_description,
exclude_elements=exclude_elements,
force_rmbg=force_rmbg,
content_moderation=content_moderation,
)
return make_api_request(self.api_url, payload, api_key)
+45
View File
@@ -0,0 +1,45 @@
from .utils.shot_utils import get_text_input_types, create_text_payload, make_api_request, shot_by_text_api_url, PlacementType
class ShotByTextManualPaddingNode:
@classmethod
def INPUT_TYPES(self):
input_types = get_text_input_types()
input_types["required"]["padding_values"] = ("STRING", {"default": "0,0,0,0"})
return input_types
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("output_image",)
CATEGORY = "API Nodes"
FUNCTION = "execute"
def __init__(self):
self.api_url = shot_by_text_api_url
def execute(
self,
image,
scene_description,
mode,
padding_values,
api_key,
sync=False,
optimize_description=True,
exclude_elements="",
force_rmbg=False,
content_moderation=False,
):
payload = create_text_payload(
image,
api_key,
scene_description,
mode,
PlacementType.MANUAL_PADDING.value,
padding_values=padding_values,
sync=sync,
optimize_description=optimize_description,
exclude_elements=exclude_elements,
force_rmbg=force_rmbg,
content_moderation=content_moderation,
)
return make_api_request(self.api_url, payload, api_key)
@@ -0,0 +1,62 @@
from .utils.shot_utils import get_text_input_types, create_text_payload, make_api_request, shot_by_text_api_url, PlacementType
class ShotByTextManualPlacementNode:
@classmethod
def INPUT_TYPES(self):
input_types = get_text_input_types()
input_types["required"]["shot_size"] = ("STRING", {"default": "1000, 1000"})
input_types["required"]["manual_placement_selection"] = (
[
"upper_left",
"upper_right",
"bottom_left",
"bottom_right",
"right_center",
"left_center",
"upper_center",
"bottom_center",
"center_vertical",
"center_horizontal",
],
{"default": "upper_left"},
)
return input_types
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("output_image",)
CATEGORY = "API Nodes"
FUNCTION = "execute"
def __init__(self):
self.api_url = shot_by_text_api_url
def execute(
self,
image,
scene_description,
mode,
shot_size,
manual_placement_selection,
api_key,
sync=False,
optimize_description=True,
exclude_elements="",
force_rmbg=False,
content_moderation=False,
):
payload = create_text_payload(
image,
api_key,
scene_description,
mode,
PlacementType.MANUAL_PLACEMENT.value,
shot_size=shot_size,
manual_placement_selection=manual_placement_selection,
sync=sync,
optimize_description=optimize_description,
exclude_elements=exclude_elements,
force_rmbg=force_rmbg,
content_moderation=content_moderation,
)
return make_api_request(self.api_url, payload, api_key)
+32 -59
View File
@@ -1,69 +1,42 @@
import requests from .utils.shot_utils import get_text_input_types, create_text_payload, make_api_request, shot_by_text_api_url, PlacementType
import torch
from .common import postprocess_image, preprocess_image, image_to_base64
class ShotByTextNode(): class ShotByTextOriginalNode:
@classmethod @classmethod
def INPUT_TYPES(self): def INPUT_TYPES(self):
return { input_types = get_text_input_types()
"required": { return input_types
"image": ("IMAGE",), # Input image from another node
"scene_description": ("STRING",),
"optimize_description": ("INT", {"default": 1}),
"api_key": ("STRING", {"default": "BRIA_API_TOKEN"}) # API Key input with a default value
},
"optional": {
"content_moderation": ("BOOLEAN", {"default": False}),
}
}
RETURN_TYPES = ("IMAGE",) RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("output_image",) RETURN_NAMES = ("output_image",)
CATEGORY = "API Nodes" CATEGORY = "API Nodes"
FUNCTION = "execute" # This is the method that will be executed FUNCTION = "execute"
def __init__(self): def __init__(self):
self.api_url = "https://engine.prod.bria-api.com/v1/product/lifestyle_shot_by_text" # Eraser API URL self.api_url = shot_by_text_api_url
def execute(
# Define the execute method as expected by ComfyUI self,
def execute(self, image, api_key, scene_description, optimize_description, content_moderation): image,
if api_key.strip() == "" or api_key.strip() == "BRIA_API_TOKEN": scene_description,
raise Exception("Please insert a valid API key.") mode,
api_key,
# Check if image and mask are tensors, if so, convert to NumPy arrays sync=True,
if isinstance(image, torch.Tensor): optimize_description=True,
image = preprocess_image(image) exclude_elements="",
force_rmbg=False,
optimize_description = bool(optimize_description) content_moderation=False,
image_base64 = image_to_base64(image) ):
payload = { payload = create_text_payload(
"file": image_base64, image,
"scene_description": scene_description, api_key,
"optimize_description": optimize_description, scene_description,
"placement_type": "original", mode,
"original_quality": True, PlacementType.ORIGINAL.value,
"sync": True, original_quality=True,
"content_moderation": content_moderation sync=sync,
optimize_description=optimize_description,
} exclude_elements=exclude_elements,
headers = { force_rmbg=force_rmbg,
"Content-Type": "application/json", content_moderation=content_moderation,
"api_token": f"{api_key}" )
} return make_api_request(self.api_url, payload, api_key)
try:
response = requests.post(self.api_url, json=payload, headers=headers)
# Check for successful response
if response.status_code == 200:
print('response is 200')
# Process the output image from API response
response_dict = response.json()
image_response = requests.get(response_dict['result'][0][0])
result_image = postprocess_image(image_response.content)
return (result_image,)
else:
raise Exception(f"Error: API request failed with status code {response.status_code}")
except Exception as e:
raise Exception(f"{e}")
+7 -2
View File
@@ -1,6 +1,11 @@
import requests import requests
from .common import postprocess_image, preprocess_image, image_to_base64 from .common import (
bria_json_headers,
image_to_base64,
postprocess_image,
preprocess_image,
)
class TailoredGenNode(): class TailoredGenNode():
@@ -73,7 +78,7 @@ class TailoredGenNode():
response = requests.post( response = requests.post(
self.api_url + model_id, self.api_url + model_id,
json=payload, json=payload,
headers={"api_token": api_key} headers=bria_json_headers(api_key),
) )
if response.status_code == 200: if response.status_code == 200:
response_dict = response.json() response_dict = response.json()
+2 -2
View File
@@ -1,5 +1,5 @@
import requests import requests
from .common import bria_json_headers
class TailoredModelInfoNode(): class TailoredModelInfoNode():
@classmethod @classmethod
@@ -23,7 +23,7 @@ class TailoredModelInfoNode():
def execute(self, model_id, api_key): def execute(self, model_id, api_key):
response = requests.get( response = requests.get(
self.api_url + model_id, self.api_url + model_id,
headers={"api_token": api_key} headers=bria_json_headers(api_key),
) )
if response.status_code == 200: if response.status_code == 200:
generation_prefix = response.json()["generation_prefix"] generation_prefix = response.json()["generation_prefix"]
+88
View File
@@ -0,0 +1,88 @@
import io
import requests
import numpy as np
from PIL import Image
import torch
from .common import (
bria_json_headers,
image_to_base64,
normalize_images_input,
to_pil_safe,
)
class TailoredPortraitNode():
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"images": ("IMAGE",),
"tailored_model_id": ("STRING",),
"api_key": ("STRING", {"default": "BRIA_API_TOKEN"}),
},
"optional": {
"seed": ("INT", {"default": 123456}),
"tailored_model_influence": ("FLOAT", {"default": 0.9}),
"id_strength": ("FLOAT", {"default": 0.7}),
}
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("output_images",)
CATEGORY = "API Nodes"
FUNCTION = "execute"
def __init__(self):
self.api_url = "https://engine.prod.bria-api.com/v1/tailored-gen/restyle_portrait"
def execute(
self,
images,
tailored_model_id,
api_key,
seed,
tailored_model_influence,
id_strength
):
if api_key.strip() == "" or api_key.strip() == "BRIA_API_TOKEN":
raise Exception("Please insert a valid API key.")
# Normalize images to list of PIL images
images = normalize_images_input(images)
batch_results = []
for idx, pil_image in enumerate(images):
try:
image_base64 = image_to_base64(pil_image)
payload = {
"id_image_file": image_base64,
"tailored_model_id": int(tailored_model_id),
"tailored_model_influence": tailored_model_influence,
"id_strength": id_strength,
"seed": seed
}
headers = bria_json_headers(api_key)
response = requests.post(self.api_url, json=payload, headers=headers)
if response.status_code != 200:
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"])
result_image = Image.open(io.BytesIO(image_response.content)).convert("RGB")
# Convert to float32 tensor (H,W,C), 0-1
result_array = np.array(result_image).astype(np.float32) / 255.0
result_tensor = torch.from_numpy(result_array)
batch_results.append(result_tensor)
except Exception as e:
print(f"[TailoredPortraitNode] Skipping image {idx} due to error: {e}")
fallback_array = np.array(pil_image).astype(np.float32) / 255.0
batch_results.append(torch.from_numpy(fallback_array))
# Return list of tensors (avoids size/dtype mismatch)
return (batch_results,)
+8 -3
View File
@@ -1,6 +1,11 @@
import requests import requests
from .common import postprocess_image, preprocess_image, image_to_base64 from .common import (
bria_json_headers,
image_to_base64,
postprocess_image,
preprocess_image,
)
class Text2ImageBaseNode(): class Text2ImageBaseNode():
@@ -38,7 +43,7 @@ class Text2ImageBaseNode():
FUNCTION = "execute" # This is the method that will be executed FUNCTION = "execute" # This is the method that will be executed
def __init__(self): def __init__(self):
self.api_url = "https://engine.prod.bria-api.com/v1/text-to-image/base/2.3" #"http://0.0.0.0:5000/v1/text-to-image/base/2.3" self.api_url = "https://engine.prod.bria-api.com/v1/text-to-image/base/3.2"
def execute( def execute(
self, api_key, prompt, aspect_ratio, seed, negative_prompt, self, api_key, prompt, aspect_ratio, seed, negative_prompt,
@@ -83,7 +88,7 @@ class Text2ImageBaseNode():
response = requests.post( response = requests.post(
self.api_url, self.api_url,
json=payload, json=payload,
headers={"api_token": api_key} headers=bria_json_headers(api_key),
) )
if response.status_code == 200: if response.status_code == 200:
response_dict = response.json() response_dict = response.json()
+7 -2
View File
@@ -1,6 +1,11 @@
import requests import requests
from .common import postprocess_image, preprocess_image, image_to_base64 from .common import (
bria_json_headers,
image_to_base64,
postprocess_image,
preprocess_image,
)
class Text2ImageFastNode(): class Text2ImageFastNode():
@@ -76,7 +81,7 @@ class Text2ImageFastNode():
response = requests.post( response = requests.post(
self.api_url, self.api_url,
json=payload, json=payload,
headers={"api_token": api_key} headers=bria_json_headers(api_key),
) )
if response.status_code == 200: if response.status_code == 200:
response_dict = response.json() response_dict = response.json()
+3 -3
View File
@@ -1,6 +1,6 @@
import requests import requests
from .common import postprocess_image from .common import bria_json_headers, postprocess_image
class Text2ImageHDNode(): class Text2ImageHDNode():
@@ -29,7 +29,7 @@ class Text2ImageHDNode():
FUNCTION = "execute" FUNCTION = "execute"
def __init__(self): def __init__(self):
self.api_url = "https://engine.prod.bria-api.com/v1/text-to-image/hd/2.3" #"http://0.0.0.0:5000/v1/text-to-image/hd/2.3" self.api_url = "https://engine.prod.bria-api.com/v1/text-to-image/hd/2.2" #"http://0.0.0.0:5000/v1/text-to-image/hd/2.3"
def execute( def execute(
self, api_key, prompt, aspect_ratio, seed, negative_prompt, self, api_key, prompt, aspect_ratio, seed, negative_prompt,
@@ -52,7 +52,7 @@ class Text2ImageHDNode():
response = requests.post( response = requests.post(
self.api_url, self.api_url,
json=payload, json=payload,
headers={"api_token": api_key} headers=bria_json_headers(api_key),
) )
if response.status_code == 200: if response.status_code == 200:
response_dict = response.json() response_dict = response.json()
+211
View File
@@ -0,0 +1,211 @@
import requests
import torch
from ..common import (
bria_json_headers,
image_to_base64,
postprocess_image,
preprocess_image,
)
shot_by_text_api_url = (
"https://engine.prod.bria-api.com/v1/product/lifestyle_shot_by_text"
)
shot_by_image_api_url = (
"https://engine.prod.bria-api.com/v1/product/lifestyle_shot_by_image"
)
from enum import Enum
class PlacementType(str, Enum):
ORIGINAL = "original"
AUTOMATIC = "automatic"
MANUAL_PLACEMENT = "manual_placement"
MANUAL_PADDING = "manual_padding"
CUSTOM_COORDINATES = "custom_coordinates"
AUTOMATIC_ASPECT_RATIO = "automatic_aspect_ratio"
def validate_api_key(api_key):
"""Validate API key input"""
if api_key.strip() == "" or api_key.strip() == "BRIA_API_TOKEN":
raise Exception("Please insert a valid API key.")
def update_payload_for_placement(placement_type, payload, **kwargs):
if placement_type == PlacementType.AUTOMATIC.value:
payload["shot_size"] = [
int(x.strip()) for x in kwargs.get("shot_size").split(",")
]
elif placement_type == PlacementType.MANUAL_PLACEMENT.value:
payload["shot_size"] = [
int(x.strip()) for x in kwargs.get("shot_size").split(",")
]
payload["manual_placement_selection"] = [
kwargs.get("manual_placement_selection", "upper_left")
]
elif placement_type == PlacementType.CUSTOM_COORDINATES.value:
payload["shot_size"] = [
int(x.strip()) for x in kwargs.get("shot_size").split(",")
]
payload["foreground_image_size"] = [
int(x.strip()) for x in kwargs.get("foreground_image_size").split(",")
]
payload["foreground_image_location"] = [
int(x.strip()) for x in kwargs.get("foreground_image_location").split(",")
]
elif placement_type == PlacementType.MANUAL_PADDING.value:
payload["padding_values"] = [
int(x.strip()) for x in kwargs.get("padding_values").split(",")
]
elif placement_type == PlacementType.AUTOMATIC_ASPECT_RATIO.value:
payload["aspect_ratio"] = kwargs.get("aspect_ratio", "1:1")
elif placement_type == PlacementType.ORIGINAL.value:
payload["original_quality"] = kwargs.get("original_quality", True)
return payload
def create_text_payload(
image, api_key, scene_description, mode, placement_type, **kwargs
):
validate_api_key(api_key)
# Process image
if isinstance(image, torch.Tensor):
image = preprocess_image(image)
image_base64 = image_to_base64(image)
payload = {
"file": image_base64,
"placement_type": placement_type,
"sync": True,
"num_results": 1,
"force_rmbg": kwargs.get("force_rmbg", False),
"content_moderation": kwargs.get("content_moderation", False),
"scene_description": scene_description,
"mode": mode,
"optimize_description": kwargs.get("optimize_description", True),
}
if kwargs.get("exclude_elements", "").strip():
payload["exclude_elements"] = kwargs["exclude_elements"]
payload = update_payload_for_placement(placement_type, payload, **kwargs)
return payload
def create_image_payload(image, ref_image, api_key, placement_type, **kwargs):
"""Create payload for image-based shot nodes"""
validate_api_key(api_key)
if isinstance(image, torch.Tensor):
image = preprocess_image(image)
if isinstance(ref_image, torch.Tensor):
ref_image = preprocess_image(ref_image)
image_base64 = image_to_base64(image)
ref_image_base64 = image_to_base64(ref_image)
# Base payload
payload = {
"file": image_base64,
"ref_image_file": ref_image_base64,
"enhance_ref_image": kwargs.get("enhance_ref_image", True),
"ref_image_influence": kwargs.get("ref_image_influence", 1.0),
"placement_type": placement_type,
"sync": True,
"num_results": 1,
"force_rmbg": kwargs.get("force_rmbg", False),
"content_moderation": kwargs.get("content_moderation", False),
}
payload = update_payload_for_placement(placement_type, payload, **kwargs)
return payload
def make_api_request(api_url, payload, api_key, Placement_type = None):
"""Make API request and return processed image"""
try:
headers = bria_json_headers(api_key)
response = requests.post(api_url, json=payload, headers=headers)
if response.status_code == 200:
print("response is 200")
response_dict = response.json()
if Placement_type == PlacementType.AUTOMATIC.value:
result_images = []
for i, result in enumerate(response_dict.get("result", [])[:7]):
image_url = result[0]
image_response = requests.get(image_url)
processed = postprocess_image(image_response.content)
result_images.append(processed)
# If less than 7 images, pad with None to match ComfyUI return structure
while len(result_images) < 7:
result_images.append(None)
print(result_images)
return tuple(result_images)
image_response = requests.get(response_dict["result"][0][0])
result_image = postprocess_image(image_response.content)
return (result_image,)
else:
raise Exception(
f"Error: API request failed with status code {response.status_code}{response.text}"
)
except Exception as e:
raise Exception(f"{e}")
def get_common_input_types():
"""Get common input types for all nodes"""
return {
"required": {"api_key": ("STRING", {"default": "BRIA_API_TOKEN"})},
"optional": {
"force_rmbg": ("BOOLEAN", {"default": False}),
"content_moderation": ("BOOLEAN", {"default": False}),
},
}
def get_text_input_types():
"""Get text-specific input types"""
common = get_common_input_types()
common["required"].update(
{
"image": ("IMAGE",),
"scene_description": ("STRING",),
"mode": (["base", "fast", "high_control"], {"default": "fast"}),
}
)
common["optional"].update(
{
"optimize_description": ("BOOLEAN", {"default": True}),
"exclude_elements": ("STRING", {"default": ""}),
}
)
return common
def get_image_input_types():
"""Get image-specific input types"""
common = get_common_input_types()
common["required"].update({"image": ("IMAGE",), "ref_image": ("IMAGE",)})
common["optional"].update(
{
"enhance_ref_image": ("BOOLEAN", {"default": True}),
"ref_image_influence": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 1.0}),
}
)
return common
@@ -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}")
+52
View File
@@ -0,0 +1,52 @@
import os
import folder_paths
class LoadVideoFramesNode:
"""
Load a video file from the input folder or upload.
Parameters:
video (str): Selected or uploaded video filename.
Returns:
video_path (STRING): Absolute path to the video file.
"""
@classmethod
def INPUT_TYPES(cls):
input_dir = folder_paths.get_input_directory()
files = [f for f in os.listdir(input_dir) if os.path.isfile(os.path.join(input_dir, f))]
files = folder_paths.filter_files_content_types(files, ["video"])
return {
"required": {
"video": (sorted(files), {"video_upload": True}),
}
}
RETURN_TYPES = ("STRING",)
RETURN_NAMES = ("video_path",)
FUNCTION = "load_video"
CATEGORY = "API Nodes"
def load_video(self, video):
video_path = folder_paths.get_annotated_filepath(video)
if not os.path.exists(video_path):
raise FileNotFoundError(f"Video file not found: {video_path}")
return (video_path,)
@classmethod
def IS_CHANGED(cls, video, **kwargs):
"""Force re-execution when video file changes"""
video_path = folder_paths.get_annotated_filepath(video)
if os.path.exists(video_path):
return os.path.getmtime(video_path)
return float("nan")
@classmethod
def VALIDATE_INPUTS(cls, video, **kwargs):
"""Validate that the video file exists"""
if not folder_paths.exists_annotated_filepath(video):
return f"Invalid video file: {video}"
return True
@@ -0,0 +1,135 @@
import os
import uuid
import folder_paths
import requests
class PreviewVideoURLNode:
"""
Bria Preview Video URL Node
This node takes a video URL as a string and downloads it to preview
directly in the ComfyUI interface.
Parameters:
- video_url: URL of the video to preview (http/https)
"""
def __init__(self):
self.output_dir = folder_paths.get_temp_directory()
self.type = "temp"
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"video_url": ("STRING", {
"default": "",
"multiline": False,
"tooltip": "URL of the video to preview (http/https)"
}),
},
"hidden": {
"prompt": "PROMPT",
"extra_pnginfo": "EXTRA_PNGINFO"
},
}
RETURN_TYPES = ()
FUNCTION = "preview_video_url"
OUTPUT_NODE = True
CATEGORY = "API Nodes"
DESCRIPTION = "Previews a video from URL directly in the ComfyUI interface."
def preview_video_url(self, video_url, prompt=None, extra_pnginfo=None):
"""
Preview video from URL
Args:
video_url: URL of the video (http/https)
prompt: Hidden parameter for ComfyUI workflow
extra_pnginfo: Hidden parameter for ComfyUI metadata
Returns:
dict: UI output with video file for preview
"""
if not video_url or video_url.strip() == "":
raise ValueError("video_url cannot be empty")
if not video_url.startswith("http://") and not video_url.startswith("https://"):
raise ValueError("video_url must be a valid HTTP or HTTPS URL")
print(f"Downloading video from URL: {video_url}")
# Download video from URL
try:
response = requests.get(video_url, stream=True, timeout=60)
response.raise_for_status()
# Determine file extension from URL or Content-Type
content_type = response.headers.get('Content-Type', '')
extension = self._get_extension_from_content_type(content_type, video_url)
filename_prefix = str(uuid.uuid4()) + "_video_url_preview"
# Get save path
full_output_folder = self.output_dir
filename = f"{filename_prefix}.{extension}"
filepath = os.path.join(full_output_folder, filename)
# Save video to temp directory
print(f"Saving video to: {filepath}")
with open(filepath, 'wb') as f:
for chunk in response.iter_content(chunk_size=8192):
if chunk:
f.write(chunk)
file_size = os.path.getsize(filepath)
print(f"Video downloaded successfully: {filename} ({file_size / (1024*1024):.2f} MB)")
return {
"ui": {
"images": [{
"filename": filename,
"subfolder": "",
"type": self.type,
"format": extension
}],
"animated": (True,),
"has_audio": (True,)
}
}
except requests.exceptions.RequestException as e:
raise Exception(f"Failed to download video from URL: {str(e)}")
except Exception as e:
raise Exception(f"Error previewing video: {str(e)}")
def _get_extension_from_content_type(self, content_type, url):
"""
Determine file extension from Content-Type header or URL
"""
# Map common video MIME types to extensions
content_type_map = {
'video/mp4': 'mp4',
'video/webm': 'webm',
'video/quicktime': 'mov',
'video/x-matroska': 'mkv',
'video/x-msvideo': 'avi',
'image/gif': 'gif',
}
# Try to get extension from Content-Type
for mime_type, ext in content_type_map.items():
if mime_type in content_type.lower():
return ext
# Try to get extension from URL
url_path = url.split('?')[0] # Remove query parameters
if '.' in url_path:
url_ext = url_path.rsplit('.', 1)[-1].lower()
if url_ext in ['mp4', 'webm', 'mov', 'mkv', 'avi', 'gif', 'webp']:
return url_ext
# Default to mp4
return 'mp4'
@@ -0,0 +1,130 @@
import os
import uuid
import requests
import folder_paths
from ..common import bria_json_headers, poll_status_until_completed
from .video_utils import upload_video_to_s3
class RemoveVideoBackgroundNode():
"""
Removes the background from a video using the Bria API.
Parameters:
api_key (str): Your Bria API key.
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.
"""
@classmethod
def INPUT_TYPES(self):
return {
"required": {
"api_key": ("STRING", {"default": "BRIA_API_TOKEN"}),
"video_url": ("STRING", {
"default": "",
"tooltip": "URL of video to process (provide either frames or video_url)"
}),
},
"optional": {
"preserve_audio": ("BOOLEAN", {"default": True}),
"output_container_and_codec": ([
"mp4_h264",
"mp4_h265",
"webm_vp9",
"mov_h265",
"mov_proresks",
"mkv_h264",
"mkv_h265",
"mkv_vp9",
"gif"
], {"default": "webm_vp9"}),
"background_color": ([
"Transparent",
"Black",
"White",
"Gray",
"Red",
"Green",
"Blue",
"Yellow",
"Cyan",
"Magenta",
"Orange"
], {"default": "Black"})
}
}
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/remove_background"
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
input_video_url = ""
if video_url and video_url.strip() != "":
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 video_url.startswith(folder_paths.get_temp_directory()):
video_path = None
else:
input_video_url = video_url
try:
print("Step 3: Calling Bria API for background removal...")
payload = {
"video": input_video_url,
"preserve_audio": preserve_audio,
"output_container_and_codec": output_container_and_codec,
"background_color":background_color
}
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 RMBG request successful, 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}")
print(f"Background removal complete! Use Preview Video URL node to view the result.")
return (result_video_url,)
else:
raise Exception(f"Error: API request failed with status code {response.status_code} {response.text}")
except Exception as e:
raise Exception(f"{e}")
finally:
if video_path:
try:
if os.path.exists(video_path):
os.unlink(video_path)
except:
pass
@@ -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}")
@@ -0,0 +1,123 @@
import os
import uuid
import requests
import folder_paths
from ..common import bria_json_headers, poll_status_until_completed
from .video_utils import upload_video_to_s3
class VideoEraseElementsNode():
"""
Erase elements from a video using the Bria API.
Parameters:
api_key (str): Your Bria API key.
video_url (str): Local path or URL of the video to process.
mask_url (str, optional): URL of a mask video for selective erasing.
output_container_and_codec (str, optional): Desired output format and codec. Default is "mp4_h264".
preserve_audio (bool, optional): Whether to keep the audio track. Default is True.
Returns:
result_video_url (STRING): URL of the processed video with elements erased.
"""
@classmethod
def INPUT_TYPES(self):
return {
"required": {
"api_key": ("STRING", {"default": "BRIA_API_TOKEN"}),
"video_url": ("STRING", {
"default": "",
"tooltip": "URL of video to process (provide either frames or video_url)"
}),
},
"optional": {
"mask_url": ("STRING", {
"default": "",
"tooltip": "URL of mask video (optional)"
}),
"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/erase"
def execute(self, api_key, video_url, mask_url="", 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.")
video_path = None
if video_url and video_url.strip() != "":
# Check if video_url is a local file path or a 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}")
if video_url.startswith(folder_paths.get_temp_directory()):
video_path = None
else:
input_video_url = video_url
try:
print("Step 3: Calling Bria API for element erasure...")
payload = {
"video": input_video_url,
"mask": mask_url,
"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 Erase Elements request successful, 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}")
print(f"Element erasure complete! Use Preview Video URL node to view the result.")
return (result_video_url,)
else:
raise Exception(f"Error: API request failed with status code {response.status_code} {response.text}")
except Exception as e:
raise Exception(f"{e}")
finally:
if video_path:
try:
if os.path.exists(video_path):
os.unlink(video_path)
except:
pass
@@ -0,0 +1,120 @@
import os
import uuid
import requests
import folder_paths
from ..common import bria_json_headers, poll_status_until_completed
from .video_utils import upload_video_to_s3
class VideoIncreaseResolutionNode():
"""
Increase the resolution of a video using the Bria API.
Parameters:
api_key (str): Your Bria API key.
video_url (str): Local path or URL of the video to process.
desired_increase (str, optional): Resolution increase factor, '2' or '4'. Default is '2'.
output_container_and_codec (str, optional): Desired output format and codec. Default is "mp4_h264".
preserve_audio (bool, optional): Whether to keep the audio track. Default is True.
Returns:
result_video_url (STRING): URL of the processed video with increased resolution.
"""
@classmethod
def INPUT_TYPES(self):
return {
"required": {
"api_key": ("STRING", {"default": "BRIA_API_TOKEN"}),
"video_url": ("STRING", {
"default": "",
"tooltip": "URL of video to process (provide either frames or video_url)"
}),
},
"optional": {
"desired_increase": (['2', '4'], {"default": '2'}),
"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/increase_resolution"
def execute(self, api_key, video_url, desired_increase='2', 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.")
video_path = None
if video_url and video_url.strip() != "":
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}")
if video_url.startswith(folder_paths.get_temp_directory()):
video_path = None
else:
input_video_url = video_url
try:
print("Step 3: Calling Bria API for resolution increase...")
payload = {
"video": input_video_url,
"desired_increase": desired_increase,
"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 Increase Resolution request successful, 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}")
print(f"Resolution increase complete! Use Preview Video URL node to view the result.")
return (result_video_url,)
else:
raise Exception(f"Error: API request failed with status code {response.status_code} {response.text}")
except Exception as e:
raise Exception(f"{e}")
finally:
if video_path:
try:
if os.path.exists(video_path):
os.unlink(video_path)
except:
pass
@@ -0,0 +1,127 @@
import os
import uuid
import requests
import folder_paths
from ..common import bria_json_headers, poll_status_until_completed
from .video_utils import upload_video_to_s3
import json
class VideoMaskByKeyPointsNode():
"""
Generate a video mask using key points with the Bria API.
Parameters:
key_points (str): JSON string of key points for masking.
api_key (str): Your Bria API key.
video_url (str): Local path or URL of the video to process.
output_container_and_codec (str, optional): Desired output format and codec. Default is "mp4_h264".
preserve_audio (bool, optional): Whether to keep the audio track. Default is True.
Returns:
mask_url (STRING): URL of the generated video mask.
"""
@classmethod
def INPUT_TYPES(self):
return {
"required": {
"key_points": ("STRING", {"default": "[]", "multiline": True}),
"api_key": ("STRING", {"default": "BRIA_API_TOKEN"}),
"video_url": ("STRING", {
"default": "",
"tooltip": "URL of video to process (provide either frames or video_url)"
}),
},
"optional": {
"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 = ("mask_url",)
CATEGORY = "API Nodes"
FUNCTION = "execute"
def __init__(self):
self.api_url = "https://engine.prod.bria-api.com/v2/video/segment/mask_by_key_points"
def execute(self, key_points, api_key, video_url, 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.")
try:
key_points_array = json.loads(key_points)
except json.JSONDecodeError as e:
raise Exception(f"Invalid JSON format for key_points: {e}")
video_path = None
if video_url and video_url.strip() != "":
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}")
if video_url.startswith(folder_paths.get_temp_directory()):
video_path = None
else:
input_video_url = video_url
try:
print("Step 3: Calling Bria API for video mask generation by key points...")
payload = {
"video": input_video_url,
"key_points": key_points_array,
"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 Mask by Key Points request successful, 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_mask_url = final_response['result']['mask_url']
print(f"Video mask processing completed. Result URL: {result_mask_url}")
print(f"Video mask generation complete! Use Preview Video URL node to view the result.")
return (result_mask_url,)
else:
raise Exception(f"Error: API request failed with status code {response.status_code} {response.text}")
except Exception as e:
raise Exception(f"{e}")
finally:
if video_path:
try:
if os.path.exists(video_path):
os.unlink(video_path)
except:
pass
@@ -0,0 +1,121 @@
import os
import uuid
import requests
import folder_paths
from ..common import bria_json_headers, poll_status_until_completed
from .video_utils import upload_video_to_s3
class VideoMaskByPromptNode():
"""
Generate a video mask using a text prompt with the Bria API.
Parameters:
prompt (str): Text prompt describing what to mask in the video.
api_key (str): Your Bria API key.
video_url (str): Local path or URL of the video to process.
output_container_and_codec (str, optional): Desired output format and codec. Default is "mp4_h264".
preserve_audio (bool, optional): Whether to keep the audio track. Default is True.
Returns:
mask_url (STRING): URL of the generated video mask.
"""
@classmethod
def INPUT_TYPES(self):
return {
"required": {
"prompt": ("STRING", {"default": ""}),
"api_key": ("STRING", {"default": "BRIA_API_TOKEN"}),
"video_url": ("STRING", {
"default": "",
"tooltip": "URL of video to process (provide either frames or video_url)"
}),
},
"optional": {
"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 = ("mask_url",)
CATEGORY = "API Nodes"
FUNCTION = "execute"
def __init__(self):
self.api_url = "https://engine.prod.bria-api.com/v2/video/segment/mask_by_prompt"
def execute(self, prompt, api_key, video_url, 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.")
video_path = None
if video_url and video_url.strip() != "":
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}")
if video_url.startswith(folder_paths.get_temp_directory()):
video_path = None
else:
input_video_url = video_url
try:
print("Step 3: Calling Bria API for video mask generation...")
payload = {
"video": input_video_url,
"prompt": prompt,
"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 Mask by Prompt request successful, 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_mask_url = final_response['result']['mask_url']
print(f"Video mask processing completed. Result URL: {result_mask_url}")
print(f"Video mask generation complete! Use Preview Video URL node to view the result.")
return (result_mask_url,)
else:
raise Exception(f"Error: API request failed with status code {response.status_code} {response.text}")
except Exception as e:
raise Exception(f"{e}")
finally:
if video_path:
try:
if os.path.exists(video_path):
os.unlink(video_path)
except:
pass
@@ -0,0 +1,133 @@
import os
import uuid
import requests
import folder_paths
from ..common import bria_json_headers, poll_status_until_completed
from .video_utils import upload_video_to_s3
class VideoSolidColorBackgroundNode():
"""
Apply a solid color background to a video using the Bria API.
Parameters:
api_key (str): Your Bria API key.
video_url (str): Local path or URL of the video to process.
background_color (str, optional): Color to apply as background. Default is "Transparent".
output_container_and_codec (str, optional): Desired output format and codec. Default is "mp4_h264".
preserve_audio (bool, optional): Whether to keep the audio track. Default is True.
Returns:
result_video_url (STRING): URL of the video with the solid color background applied.
"""
@classmethod
def INPUT_TYPES(self):
return {
"required": {
"api_key": ("STRING", {"default": "BRIA_API_TOKEN"}),
"video_url": ("STRING", {
"default": "",
"tooltip": "URL of video to process (provide either frames or video_url)"
}),
},
"optional": {
"background_color": ([
"Transparent",
"Black",
"White",
"Gray",
"Red",
"Green",
"Blue",
"Yellow",
"Cyan",
"Magenta",
"Orange"
], {"default": "Transparent"}),
"output_container_and_codec": ([
"mp4_h264",
"mp4_h265",
"webm_vp9",
"mov_h265",
"mov_proresks",
"mkv_h264",
"mkv_h265",
"mkv_vp9",
"gif"
], {"default": "webm_vp9"}),
"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/remove_background"
def execute(self, api_key, video_url, background_color="Transparent", output_container_and_codec="webm_vp9", preserve_audio=True):
if api_key.strip() == "" or api_key.strip() == "BRIA_API_TOKEN":
raise Exception("Please insert a valid API key.")
video_path = None
if video_url and video_url.strip() != "":
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}")
if video_url.startswith(folder_paths.get_temp_directory()):
video_path = None
else:
input_video_url = video_url
try:
print("Step 3: Calling Bria API for solid color background...")
payload = {
"video": input_video_url,
"background_color": background_color,
"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 Solid Color Background request successful, 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}")
print(f"Solid color background processing complete! Use Preview Video URL node to view the result.")
return (result_video_url,)
else:
raise Exception(f"Error: API request failed with status code {response.status_code} {response.text}")
except Exception as e:
raise Exception(f"{e}")
finally:
if video_path:
try:
if os.path.exists(video_path):
os.unlink(video_path)
except:
pass
+72
View File
@@ -0,0 +1,72 @@
import os
import requests
from ..common import BRIA_COMFYUI_USER_AGENT
def upload_video_to_s3(video_path, filename, api_token):
api_url = "https://platform.prod.bria-api.com/upload-video/anonymous/presigned-url"
headers = {
"Content-Type": "application/json",
"User-Agent": BRIA_COMFYUI_USER_AGENT,
}
extension = os.path.splitext(filename)[1].lower()
content_type_map = {
'.mp4': 'video/mp4',
'.webm': 'video/webm',
'.mov': 'video/quicktime',
'.mkv': 'video/x-matroska',
'.avi': 'video/x-msvideo',
'.gif': 'image/gif',
'.webp': 'image/webp'
}
content_type = content_type_map.get(extension, 'video/mp4')
if api_token:
headers["api_token"] = api_token
payload = {
"file_name": filename,
"content_type":content_type
}
print(f"Requesting presigned URL for: {filename}")
try:
response = requests.post(api_url, json=payload, headers=headers)
if response.status_code != 200:
raise Exception(f"Failed to get presigned URL: {response.status_code} {response.text}")
response_data = response.json()
video_url = response_data.get("video_url")
upload_url = response_data.get("upload_url")
if not video_url or not upload_url:
raise Exception(f"Invalid response from presigned URL API: {response_data}")
print(f"Received presigned URL")
print(f"Video URL: {video_url}")
# Step 2: Upload video to presigned URL
print(f"Uploading video to S3...")
with open(video_path, 'rb') as f:
video_data = f.read()
# Determine content type based on file extension
upload_headers = {
"Content-Type": content_type
}
upload_response = requests.put(upload_url, data=video_data, headers=upload_headers)
if upload_response.status_code not in [200, 204]:
raise Exception(f"Failed to upload video to S3: {upload_response.status_code}")
print(f"Video uploaded successfully to S3")
return video_url
except Exception as e:
raise Exception(f"Error uploading video to S3: {str(e)}")
+1 -1
View File
@@ -1,7 +1,7 @@
[project] [project]
name = "comfyui-bria-api" name = "comfyui-bria-api"
description = "Custom nodes for ComfyUI using BRIA's API." description = "Custom nodes for ComfyUI using BRIA's API."
version = "2.0.2" version = "2.1.19"
license = {file = "LICENSE"} license = {file = "LICENSE"}
[project.urls] [project.urls]
+145
View File
@@ -0,0 +1,145 @@
import { app } from "/scripts/app.js";
import { api } from "/scripts/api.js";
app.registerExtension({
name: "BriaMultiImageSelect",
async nodeCreated(node) {
if (node.comfyClass !== "BriaMultiImageSelect") return;
const getWidget = (name) =>
node.widgets?.find(w => w.name === name);
const pathsWidget = getWidget("selected_paths");
if (!pathsWidget) return;
pathsWidget.hidden = true;
pathsWidget.draw = () => {};
pathsWidget.computeSize = () => [0, 0];
const viewURLFromRel = (rel) => {
const parts = (rel || "").split("/");
const filename = parts.pop();
const subfolder = parts.join("/");
const params = new URLSearchParams({ filename, type: "input", subfolder });
return api.apiURL(`/view?${params.toString()}`);
};
const loadImg = (url) =>
new Promise((resolve, reject) => {
const img = new Image();
img.crossOrigin = "anonymous";
img.onload = () => resolve(img);
img.onerror = () => reject();
img.src = url;
});
const refreshPreview = async () => {
let raw = node.properties.selected_paths;
if (!raw) raw = pathsWidget.value;
let rels = [];
try {
rels = raw ? JSON.parse(raw) : [];
} catch (e) {
console.error("Failed to parse selected_paths:", e);
node.imgs = null;
node.imgError = "Invalid image data";
node.setDirtyCanvas(true, true);
return;
}
if (!rels.length) {
node.imgs = null;
node.imgError = "No images selected";
node.setDirtyCanvas(true, true);
return;
}
const imgs = [];
await Promise.allSettled(
rels.map(async (rel) => {
try {
const img = await loadImg(viewURLFromRel(rel));
imgs.push(img);
} catch (e) {
console.warn(`Failed to load image: ${rel}`, e);
}
})
);
if (imgs.length) {
node.imgs = imgs;
node.imgError = null;
} else {
node.imgs = null;
node.imgError = "Failed to load images";
}
node.setDirtyCanvas(true, true);
};
// Update node size to accommodate preview
const originalComputeSize = node.computeSize;
node.computeSize = function () {
const size = originalComputeSize ? originalComputeSize.apply(this, arguments) : [200, 100];
size[1] = Math.max(size[1], 200); // Ensure minimum height for preview
return size;
};
const btn = node.addWidget("button", "Select Images", null, async () => {
const input = document.createElement("input");
input.type = "file";
input.multiple = true;
input.accept = "image/*";
input.style.display = "none";
document.body.appendChild(input);
input.onchange = async () => {
const files = Array.from(input.files || []);
document.body.removeChild(input);
if (!files.length) return;
const rels = [];
for (const f of files) {
const form = new FormData();
form.append("image", f, f.name);
const resp = await api.fetchApi("/upload/image", {
method: "POST",
body: form,
});
if (!resp.ok) continue;
const data = await resp.json();
const rel = data.subfolder
? `${data.subfolder}/${data.name}`
: data.name;
rels.push(rel);
}
const json = JSON.stringify(rels);
pathsWidget.value = json;
node.properties.selected_paths = json;
await refreshPreview();
};
input.click();
});
node.widgets.unshift(
node.widgets.splice(node.widgets.indexOf(btn), 1)[0]
);
// Initialize properties if not present
if (!node.properties) {
node.properties = {};
}
await refreshPreview();
setTimeout(async () => {
await refreshPreview();
}, 100);
},
});
+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
}
+331
View File
@@ -0,0 +1,331 @@
{
"id": "3eb93704-25f0-4511-b147-cf0403c5d060",
"revision": 0,
"last_node_id": 10,
"last_link_id": 11,
"nodes": [
{
"id": 7,
"type": "PreviewImage",
"pos": [
756.945556640625,
406.1631774902344
],
"size": [
140,
246
],
"flags": {},
"order": 5,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 9
}
],
"outputs": [],
"properties": {
"Node name for S&R": "PreviewImage"
},
"widgets_values": []
},
{
"id": 6,
"type": "PreviewImage",
"pos": [
702.7532348632812,
830.3753662109375
],
"size": [
140,
246
],
"flags": {},
"order": 3,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 5
}
],
"outputs": [],
"properties": {
"Node name for S&R": "PreviewImage"
},
"widgets_values": []
},
{
"id": 8,
"type": "FIBOEditNode",
"pos": [
390.8927307128906,
339.14031982421875
],
"size": [
278.720703125,
266
],
"flags": {},
"order": 4,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 8
},
{
"name": "mask",
"shape": 7,
"type": "MASK",
"link": null
},
{
"name": "structured_instruction",
"shape": 7,
"type": "STRING",
"widget": {
"name": "structured_instruction"
},
"link": 11
}
],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
9
]
},
{
"name": "structured_instruction",
"type": "STRING",
"links": null
},
{
"name": "seed",
"type": "INT",
"links": null
}
],
"properties": {
"Node name for S&R": "FIBOEditNode"
},
"widgets_values": [
"",
"",
"",
"",
50,
5,
1199,
"randomize"
]
},
{
"id": 4,
"type": "FIBOEditNode",
"pos": [
301.3495788574219,
884.5100708007812
],
"size": [
278.720703125,
266
],
"flags": {},
"order": 1,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 4
},
{
"name": "mask",
"shape": 7,
"type": "MASK",
"link": null
}
],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
5
]
},
{
"name": "structured_instruction",
"type": "STRING",
"links": null
},
{
"name": "seed",
"type": "INT",
"links": null
}
],
"properties": {
"Node name for S&R": "FIBOEditNode"
},
"widgets_values": [
"",
"change the lamp to a radio",
"",
"",
50,
5,
55,
"randomize"
]
},
{
"id": 2,
"type": "LoadImage",
"pos": [
-245.09060668945312,
882.640869140625
],
"size": [
274.080078125,
314
],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"links": [
4,
8,
10
]
},
{
"name": "MASK",
"type": "MASK",
"links": null
}
],
"properties": {
"Node name for S&R": "LoadImage"
},
"widgets_values": [
"0a9d91e579872d653daf3243df4598f0 (2).png",
"image"
]
},
{
"id": 10,
"type": "FIBOEditStructuredInstructionNode",
"pos": [
-139.52833557128906,
410.5189208984375
],
"size": [
314.6372985839844,
82
],
"flags": {},
"order": 2,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 10
}
],
"outputs": [
{
"name": "structured_instruction",
"type": "STRING",
"links": [
11
]
}
],
"properties": {
"Node name for S&R": "FIBOEditStructuredInstructionNode"
},
"widgets_values": [
"BRIA_API_TOKEN",
""
]
}
],
"links": [
[
4,
2,
0,
4,
0,
"IMAGE"
],
[
5,
4,
0,
6,
0,
"IMAGE"
],
[
8,
2,
0,
8,
0,
"IMAGE"
],
[
9,
8,
0,
7,
0,
"IMAGE"
],
[
10,
2,
0,
10,
0,
"IMAGE"
],
[
11,
10,
0,
8,
2,
"STRING"
]
],
"groups": [],
"config": {},
"extra": {
"ds": {
"scale": 0.7522123482651067,
"offset": [
684.0553164416729,
-258.9658284524182
]
},
"frontendVersion": "1.25.11"
},
"version": 0.4
}
+495
View File
@@ -0,0 +1,495 @@
{
"id": "17df2a89-3a7b-4b17-9ed1-fe236151bd3c",
"revision": 0,
"last_node_id": 30,
"last_link_id": 34,
"nodes": [
{
"id": 23,
"type": "PreviewVideoURLNode",
"pos": [
1146.3548583984375,
426.4760437011719
],
"size": [
270,
177.875
],
"flags": {},
"order": 9,
"mode": 0,
"inputs": [
{
"name": "video_url",
"type": "STRING",
"widget": {
"name": "video_url"
},
"link": 28
}
],
"outputs": [],
"properties": {
"Node name for S&R": "PreviewVideoURLNode"
},
"widgets_values": [
""
]
},
{
"id": 24,
"type": "PreviewVideoURLNode",
"pos": [
285.0629577636719,
889.9742431640625
],
"size": [
270,
177.875
],
"flags": {},
"order": 5,
"mode": 0,
"inputs": [
{
"name": "video_url",
"type": "STRING",
"widget": {
"name": "video_url"
},
"link": 29
}
],
"outputs": [],
"properties": {
"Node name for S&R": "PreviewVideoURLNode"
},
"widgets_values": [
""
]
},
{
"id": 28,
"type": "PreviewVideoURLNode",
"pos": [
560.3937377929688,
-124.61785125732422
],
"size": [
270,
177.875
],
"flags": {},
"order": 7,
"mode": 0,
"inputs": [
{
"name": "video_url",
"type": "STRING",
"widget": {
"name": "video_url"
},
"link": 33
}
],
"outputs": [],
"properties": {
"Node name for S&R": "PreviewVideoURLNode"
},
"widgets_values": [
""
]
},
{
"id": 29,
"type": "PreviewVideoURLNode",
"pos": [
484.5365295410156,
-391.69635009765625
],
"size": [
270,
177.875
],
"flags": {},
"order": 8,
"mode": 0,
"inputs": [
{
"name": "video_url",
"type": "STRING",
"widget": {
"name": "video_url"
},
"link": 34
}
],
"outputs": [],
"properties": {
"Node name for S&R": "PreviewVideoURLNode"
},
"widgets_values": [
""
]
},
{
"id": 19,
"type": "LoadVideoFramesNode",
"pos": [
-898.2652587890625,
20.335519790649414
],
"size": [
270,
554
],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "video_path",
"type": "STRING",
"links": [
24,
25,
26,
31,
32
]
}
],
"properties": {
"Node name for S&R": "LoadVideoFramesNode"
},
"widgets_values": [
"plane.mp4",
"image"
]
},
{
"id": 27,
"type": "VideoIncreaseResolutionNode",
"pos": [
-83.17711639404297,
-394.345458984375
],
"size": [
318.79296875,
154
],
"flags": {},
"order": 4,
"mode": 0,
"inputs": [
{
"name": "video_url",
"type": "STRING",
"widget": {
"name": "video_url"
},
"link": 32
}
],
"outputs": [
{
"name": "result_video_url",
"type": "STRING",
"links": [
34
]
}
],
"properties": {
"Node name for S&R": "VideoIncreaseResolutionNode"
},
"widgets_values": [
"BRIA_API_TOKEN",
"",
"2",
"mp4_h264",
true
]
},
{
"id": 26,
"type": "RemoveVideoBackgroundNode",
"pos": [
22.839176177978516,
-98.48242950439453
],
"size": [
318.79296875,
130
],
"flags": {},
"order": 3,
"mode": 0,
"inputs": [
{
"name": "video_url",
"type": "STRING",
"widget": {
"name": "video_url"
},
"link": 31
}
],
"outputs": [
{
"name": "result_video_url",
"type": "STRING",
"links": [
33
]
}
],
"properties": {
"Node name for S&R": "RemoveVideoBackgroundNode"
},
"widgets_values": [
"BRIA_API_TOKEN",
"",
true,
"webm_vp9"
]
},
{
"id": 21,
"type": "VideoMaskByPromptNode",
"pos": [
53.91518783569336,
377.6769714355469
],
"size": [
318.79296875,
154
],
"flags": {},
"order": 2,
"mode": 0,
"inputs": [
{
"name": "video_url",
"type": "STRING",
"widget": {
"name": "video_url"
},
"link": 25
}
],
"outputs": [
{
"name": "mask_url",
"type": "STRING",
"links": [
27
]
}
],
"properties": {
"Node name for S&R": "VideoMaskByPromptNode"
},
"widgets_values": [
"airplane",
"BRIA_API_TOKEN",
"",
"mp4_h264",
true
]
},
{
"id": 22,
"type": "VideoEraseElementsNode",
"pos": [
658.7935180664062,
377.9380187988281
],
"size": [
318.79296875,
154
],
"flags": {},
"order": 6,
"mode": 0,
"inputs": [
{
"name": "video_url",
"type": "STRING",
"widget": {
"name": "video_url"
},
"link": 26
},
{
"name": "mask_url",
"shape": 7,
"type": "STRING",
"widget": {
"name": "mask_url"
},
"link": 27
}
],
"outputs": [
{
"name": "result_video_url",
"type": "STRING",
"links": [
28
]
}
],
"properties": {
"Node name for S&R": "VideoEraseElementsNode"
},
"widgets_values": [
"BRIA_API_TOKEN",
"",
"",
"mp4_h264",
true
]
},
{
"id": 20,
"type": "VideoSolidColorBackgroundNode",
"pos": [
-133.50149536132812,
856.9811401367188
],
"size": [
318.79296875,
154
],
"flags": {},
"order": 1,
"mode": 0,
"inputs": [
{
"name": "video_url",
"type": "STRING",
"widget": {
"name": "video_url"
},
"link": 24
}
],
"outputs": [
{
"name": "result_video_url",
"type": "STRING",
"links": [
29
]
}
],
"properties": {
"Node name for S&R": "VideoSolidColorBackgroundNode"
},
"widgets_values": [
"BRIA_API_TOKEN",
"",
"Transparent",
"webm_vp9",
true
]
}
],
"links": [
[
24,
19,
0,
20,
0,
"STRING"
],
[
25,
19,
0,
21,
0,
"STRING"
],
[
26,
19,
0,
22,
0,
"STRING"
],
[
27,
21,
0,
22,
1,
"STRING"
],
[
28,
22,
0,
23,
0,
"STRING"
],
[
29,
20,
0,
24,
0,
"STRING"
],
[
31,
19,
0,
26,
0,
"STRING"
],
[
32,
19,
0,
27,
0,
"STRING"
],
[
33,
26,
0,
28,
0,
"STRING"
],
[
34,
27,
0,
29,
0,
"STRING"
]
],
"groups": [],
"config": {},
"extra": {
"ds": {
"scale": 0.5644739300537782,
"offset": [
1311.5448975965508,
244.532748842859
]
},
"frontendVersion": "1.25.11"
},
"version": 0.4
}
+491 -1
View File
@@ -1 +1,491 @@
{"last_node_id":39,"last_link_id":65,"nodes":[{"id":34,"type":"LoadImage","pos":[19.94045066833496,1075.806640625],"size":[315,314],"flags":{},"order":0,"mode":0,"inputs":[],"outputs":[{"name":"IMAGE","type":"IMAGE","links":[61],"slot_index":0,"localized_name":"IMAGE"},{"name":"MASK","type":"MASK","links":null,"localized_name":"MASK"}],"properties":{"Node name for S&R":"LoadImage"},"widgets_values":["quirky-red-brick-brick-wallpaper.jpg","image"]},{"id":36,"type":"PreviewImage","pos":[468.7108154296875,1210.288818359375],"size":[210,246],"flags":{},"order":5,"mode":0,"inputs":[{"name":"images","type":"IMAGE","link":63,"localized_name":"images"}],"outputs":[],"properties":{"Node name for S&R":"PreviewImage"},"widgets_values":[]},{"id":33,"type":"PreviewImage","pos":[1317.4083251953125,1118.4864501953125],"size":[210,246],"flags":{},"order":8,"mode":0,"inputs":[{"name":"images","type":"IMAGE","link":59,"localized_name":"images"}],"outputs":[],"properties":{"Node name for S&R":"PreviewImage"},"widgets_values":[]},{"id":31,"type":"PreviewImage","pos":[1668.734619140625,796.7755737304688],"size":[210,246],"flags":{},"order":10,"mode":0,"inputs":[{"name":"images","type":"IMAGE","link":57,"localized_name":"images"}],"outputs":[],"properties":{"Node name for S&R":"PreviewImage"},"widgets_values":[]},{"id":29,"type":"PreviewImage","pos":[872.8858032226562,679.9429321289062],"size":[210,246],"flags":{},"order":6,"mode":0,"inputs":[{"name":"images","type":"IMAGE","link":55,"localized_name":"images"}],"outputs":[],"properties":{"Node name for S&R":"PreviewImage"},"widgets_values":[]},{"id":28,"type":"LoadImage","pos":[29.629886627197266,676.8370361328125],"size":[315,314],"flags":{},"order":1,"mode":0,"inputs":[],"outputs":[{"name":"IMAGE","type":"IMAGE","links":[54],"slot_index":0,"localized_name":"IMAGE"},{"name":"MASK","type":"MASK","links":null,"localized_name":"MASK"}],"properties":{"Node name for S&R":"LoadImage"},"widgets_values":["pexels-photo-1808399.jpeg","image"]},{"id":35,"type":"RemoveForegroundNode","pos":[414.2638854980469,1079.1705322265625],"size":[315,58],"flags":{},"order":3,"mode":0,"inputs":[{"name":"image","type":"IMAGE","link":61,"localized_name":"image"}],"outputs":[{"name":"output_image","type":"IMAGE","links":[62,63],"slot_index":0,"localized_name":"output_image"}],"properties":{"Node name for S&R":"RemoveForegroundNode"},"widgets_values":["BRIA_API_TOKEN"]},{"id":32,"type":"ReplaceBgNode","pos":[834.6115112304688,1063.0406494140625],"size":[315,294],"flags":{},"order":7,"mode":0,"inputs":[{"name":"image","type":"IMAGE","link":65,"localized_name":"image"},{"name":"ref_image","type":"IMAGE","link":62,"shape":7,"localized_name":"ref_image"}],"outputs":[{"name":"output_image","type":"IMAGE","links":[59,64],"slot_index":0,"localized_name":"output_image"}],"properties":{"Node name for S&R":"ReplaceBgNode"},"widgets_values":["BRIA_API_TOKEN",false,"",true,true,true,false,"",1978,"randomize"]},{"id":30,"type":"ImageExpansionNode","pos":[1265.001220703125,798.8323974609375],"size":[315,226],"flags":{},"order":9,"mode":0,"inputs":[{"name":"image","type":"IMAGE","link":64,"localized_name":"image"}],"outputs":[{"name":"output_image","type":"IMAGE","links":[57],"slot_index":0,"localized_name":"output_image"}],"properties":{"Node name for S&R":"ImageExpansionNode"},"widgets_values":["600,760","200, 0","BRIA_API_TOKEN","1200, 800","",1729,"randomize","Ugly, mutated"]},{"id":38,"type":"Note","pos":[436.7110900878906,566.2047119140625],"size":[306.28387451171875,58],"flags":{},"order":2,"mode":0,"inputs":[],"outputs":[],"properties":{},"widgets_values":["You can get your BRIA API token at:\nhttps://bria.ai/api/"],"color":"#432","bgcolor":"#653"},{"id":27,"type":"RmbgNode","pos":[430.8815002441406,678.7727661132812],"size":[315,58],"flags":{},"order":4,"mode":0,"inputs":[{"name":"image","type":"IMAGE","link":54,"localized_name":"image"}],"outputs":[{"name":"output_image","type":"IMAGE","links":[55,65],"slot_index":0,"localized_name":"output_image"}],"properties":{"Node name for S&R":"RmbgNode"},"widgets_values":["BRIA_API_TOKEN"]}],"links":[[51,5,0,15,2,"STRING"],[54,28,0,27,0,"IMAGE"],[55,27,0,29,0,"IMAGE"],[57,30,0,31,0,"IMAGE"],[59,32,0,33,0,"IMAGE"],[61,34,0,35,0,"IMAGE"],[62,35,0,32,1,"IMAGE"],[63,35,0,36,0,"IMAGE"],[64,32,0,30,0,"IMAGE"],[65,27,0,32,0,"IMAGE"]],"groups":[],"config":{},"extra":{"ds":{"scale":0.8140274938684037,"offset":[101.53311990208498,-477.0342684311694]},"node_versions":{"comfyui-bria-api":"c72754d15b53a13ee0c0419d70401232c56b7fdb","comfy-core":"v0.3.8-1-gc441048","ComfyUI-Jjk-Nodes":"b3c99bb78a99551776b5eab1a820e1cd58f84f31"}},"version":0.4} {
"id": "f47c8ffe-20e8-473c-a56b-c6241da2c469",
"revision": 0,
"last_node_id": 43,
"last_link_id": 78,
"nodes": [
{
"id": 31,
"type": "PreviewImage",
"pos": [
1668.734619140625,
796.7755737304688
],
"size": [
210,
246
],
"flags": {},
"order": 10,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 78
}
],
"outputs": [],
"properties": {
"Node name for S&R": "PreviewImage"
},
"widgets_values": []
},
{
"id": 29,
"type": "PreviewImage",
"pos": [
872.8858032226562,
679.9429321289062
],
"size": [
210,
246
],
"flags": {},
"order": 5,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 66
}
],
"outputs": [],
"properties": {
"Node name for S&R": "PreviewImage"
},
"widgets_values": []
},
{
"id": 38,
"type": "Note",
"pos": [
436.7110900878906,
566.2047119140625
],
"size": [
306.28387451171875,
88
],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [],
"outputs": [],
"properties": {},
"widgets_values": [
"You can get your BRIA API token at:\nhttps://bria.ai/api/"
],
"color": "#432",
"bgcolor": "#653"
},
{
"id": 28,
"type": "LoadImage",
"pos": [
29.629886627197266,
676.8370361328125
],
"size": [
315,
314
],
"flags": {},
"order": 1,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"slot_index": 0,
"links": [
68
]
},
{
"name": "MASK",
"type": "MASK",
"links": null
}
],
"properties": {
"Node name for S&R": "LoadImage"
},
"widgets_values": [
"pexels-photo-1808399.jpeg",
"image"
]
},
{
"id": 34,
"type": "LoadImage",
"pos": [
19.94045066833496,
1075.806640625
],
"size": [
315,
314
],
"flags": {},
"order": 2,
"mode": 0,
"inputs": [],
"outputs": [
{
"name": "IMAGE",
"type": "IMAGE",
"slot_index": 0,
"links": [
69
]
},
{
"name": "MASK",
"type": "MASK",
"links": null
}
],
"properties": {
"Node name for S&R": "LoadImage"
},
"widgets_values": [
"quirky-red-brick-brick-wallpaper.jpg",
"image"
]
},
{
"id": 36,
"type": "PreviewImage",
"pos": [
455.6301574707031,
1240.017578125
],
"size": [
210,
246
],
"flags": {},
"order": 6,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 71
}
],
"outputs": [],
"properties": {
"Node name for S&R": "PreviewImage"
},
"widgets_values": []
},
{
"id": 40,
"type": "RmbgNode",
"pos": [
414.095703125,
749.6687622070312
],
"size": [
347.462890625,
130
],
"flags": {},
"order": 3,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 68
}
],
"outputs": [
{
"name": "output_images",
"type": "IMAGE",
"links": [
66,
74
]
}
],
"properties": {
"Node name for S&R": "RmbgNode"
},
"widgets_values": [
"BRIA_API_TOKEN",
false,
false,
true
]
},
{
"id": 41,
"type": "RemoveForegroundNode",
"pos": [
397.44732666015625,
1127.818359375
],
"size": [
347.462890625,
130
],
"flags": {},
"order": 4,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 69
}
],
"outputs": [
{
"name": "output_images",
"type": "IMAGE",
"links": [
71,
75
]
}
],
"properties": {
"Node name for S&R": "RemoveForegroundNode"
},
"widgets_values": [
"BRIA_API_TOKEN",
false,
false,
true
]
},
{
"id": 42,
"type": "ReplaceBgNode",
"pos": [
800.56884765625,
1050.5228271484375
],
"size": [
347.462890625,
318
],
"flags": {},
"order": 7,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 74
},
{
"name": "ref_images",
"shape": 7,
"type": "IMAGE",
"link": 75
}
],
"outputs": [
{
"name": "output_images",
"type": "IMAGE",
"links": [
72,
77
]
}
],
"properties": {
"Node name for S&R": "ReplaceBgNode"
},
"widgets_values": [
"BRIA_API_TOKEN",
"base",
"",
true,
true,
false,
"",
"681794",
false,
false,
false
]
},
{
"id": 43,
"type": "ImageExpansionNode",
"pos": [
1241.743896484375,
731.8318481445312
],
"size": [
347.462890625,
322
],
"flags": {},
"order": 9,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 77
}
],
"outputs": [
{
"name": "output_images",
"type": "IMAGE",
"links": [
78
]
}
],
"properties": {
"Node name for S&R": "ImageExpansionNode"
},
"widgets_values": [
"BRIA_API_TOKEN",
"",
"",
"1000, 1000",
"None",
"",
"681794",
"Ugly, mutated",
false,
true,
false,
false
]
},
{
"id": 33,
"type": "PreviewImage",
"pos": [
1316.2191162109375,
1169.61962890625
],
"size": [
210,
246
],
"flags": {},
"order": 8,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 72
}
],
"outputs": [],
"properties": {
"Node name for S&R": "PreviewImage"
},
"widgets_values": []
}
],
"links": [
[
66,
40,
0,
29,
0,
"IMAGE"
],
[
68,
28,
0,
40,
0,
"IMAGE"
],
[
69,
34,
0,
41,
0,
"IMAGE"
],
[
71,
41,
0,
36,
0,
"IMAGE"
],
[
72,
42,
0,
33,
0,
"IMAGE"
],
[
74,
40,
0,
42,
0,
"IMAGE"
],
[
75,
41,
0,
42,
1,
"IMAGE"
],
[
77,
42,
0,
43,
0,
"IMAGE"
],
[
78,
43,
0,
31,
0,
"IMAGE"
]
],
"groups": [],
"config": {},
"extra": {
"ds": {
"scale": 0.61159090448415,
"offset": [
138.07082928418134,
-488.9478800208311
]
},
"node_versions": {
"comfyui-bria-api": "c72754d15b53a13ee0c0419d70401232c56b7fdb",
"comfy-core": "v0.3.8-1-gc441048",
"ComfyUI-Jjk-Nodes": "b3c99bb78a99551776b5eab1a820e1cd58f84f31"
},
"frontendVersion": "1.25.11"
},
"version": 0.4
}
@@ -0,0 +1,661 @@
{
"id": "1c31a92d-1e48-46b0-b4ad-60d537260922",
"revision": 0,
"last_node_id": 27,
"last_link_id": 46,
"nodes": [
{
"id": 5,
"type": "PreviewImage",
"pos": [
1235.1895751953125,
10.958272933959961
],
"size": [
140,
26
],
"flags": {},
"order": 10,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 43
}
],
"outputs": [],
"properties": {
"Node name for S&R": "PreviewImage"
},
"widgets_values": []
},
{
"id": 6,
"type": "PreviewImage",
"pos": [
774.4347534179688,
-58.6872444152832
],
"size": [
140,
26
],
"flags": {},
"order": 6,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 29
}
],
"outputs": [],
"properties": {
"Node name for S&R": "PreviewImage"
},
"widgets_values": []
},
{
"id": 1,
"type": "Note",
"pos": [
-175.84976196289062,
102.76270294189453
],
"size": [
210,
88
],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [],
"outputs": [],
"properties": {},
"widgets_values": [
"If you would like to start with prompt"
],
"color": "#432",
"bgcolor": "#653"
},
{
"id": 2,
"type": "Note",
"pos": [
-225.1866455078125,
624.7235107421875
],
"size": [
210,
88
],
"flags": {},
"order": 1,
"mode": 0,
"inputs": [],
"outputs": [],
"properties": {},
"widgets_values": [
"If you would like to start with reference image + prompt"
],
"color": "#432",
"bgcolor": "#653"
},
{
"id": 15,
"type": "PreviewImage",
"pos": [
719.5359497070312,
443.11285400390625
],
"size": [
140,
26
],
"flags": {},
"order": 8,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 36
}
],
"outputs": [],
"properties": {
"Node name for S&R": "PreviewImage"
},
"widgets_values": []
},
{
"id": 17,
"type": "PreviewImage",
"pos": [
1132.783935546875,
517.3931884765625
],
"size": [
140,
26
],
"flags": {},
"order": 11,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 44
}
],
"outputs": [],
"properties": {
"Node name for S&R": "PreviewImage"
},
"widgets_values": []
},
{
"id": 20,
"type": "GenerateStructuredPromptLiteNodeV2",
"pos": [
68.9760971069336,
82.11421966552734
],
"size": [
310.7749938964844,
174
],
"flags": {},
"order": 2,
"mode": 0,
"inputs": [
{
"name": "images",
"shape": 7,
"type": "IMAGE",
"link": null
}
],
"outputs": [
{
"name": "structured_prompt",
"type": "STRING",
"links": [
27
]
},
{
"name": "seed",
"type": "INT",
"links": [
28
]
}
],
"properties": {
"Node name for S&R": "GenerateStructuredPromptLiteNodeV2"
},
"widgets_values": [
"BRIA_API_TOKEN",
"",
"",
123456,
"randomize"
]
},
{
"id": 24,
"type": "GenerateImageLiteNodeV2",
"pos": [
460.6510314941406,
44.938873291015625
],
"size": [
270,
290
],
"flags": {},
"order": 4,
"mode": 0,
"inputs": [
{
"name": "images",
"shape": 7,
"type": "IMAGE",
"link": null
},
{
"name": "structured_prompt",
"shape": 7,
"type": "STRING",
"widget": {
"name": "structured_prompt"
},
"link": 27
},
{
"name": "seed",
"shape": 7,
"type": "INT",
"widget": {
"name": "seed"
},
"link": 28
}
],
"outputs": [
{
"name": "image",
"type": "IMAGE",
"links": [
29
]
},
{
"name": "structured_prompt",
"type": "STRING",
"links": [
41
]
},
{
"name": "seed",
"type": "INT",
"links": [
42
]
}
],
"properties": {
"Node name for S&R": "GenerateImageLiteNodeV2"
},
"widgets_values": [
"BRIA_API_TOKEN",
"",
"FIBO",
"",
"1:1",
8,
5,
123456,
"randomize"
]
},
{
"id": 26,
"type": "RefineImageLiteNodeV2",
"pos": [
834.9641723632812,
46.47432327270508
],
"size": [
270,
290
],
"flags": {},
"order": 7,
"mode": 0,
"inputs": [
{
"name": "structured_prompt",
"type": "STRING",
"widget": {
"name": "structured_prompt"
},
"link": 41
},
{
"name": "seed",
"shape": 7,
"type": "INT",
"widget": {
"name": "seed"
},
"link": 42
}
],
"outputs": [
{
"name": "image",
"type": "IMAGE",
"links": [
43
]
},
{
"name": "structured_prompt",
"type": "STRING",
"links": null
},
{
"name": "seed",
"type": "INT",
"links": null
}
],
"properties": {
"Node name for S&R": "RefineImageLiteNodeV2"
},
"widgets_values": [
"BRIA_API_TOKEN",
"",
"",
"FIBO",
"1:1",
8,
5,
123456,
"randomize"
]
},
{
"id": 25,
"type": "GenerateImageLiteNodeV2",
"pos": [
404.58843994140625,
532.5309448242188
],
"size": [
270,
290
],
"flags": {},
"order": 5,
"mode": 0,
"inputs": [
{
"name": "images",
"shape": 7,
"type": "IMAGE",
"link": null
},
{
"name": "structured_prompt",
"shape": 7,
"type": "STRING",
"widget": {
"name": "structured_prompt"
},
"link": 34
},
{
"name": "seed",
"shape": 7,
"type": "INT",
"widget": {
"name": "seed"
},
"link": 35
}
],
"outputs": [
{
"name": "image",
"type": "IMAGE",
"links": [
36
]
},
{
"name": "structured_prompt",
"type": "STRING",
"links": [
45
]
},
{
"name": "seed",
"type": "INT",
"links": [
46
]
}
],
"properties": {
"Node name for S&R": "GenerateImageLiteNodeV2"
},
"widgets_values": [
"BRIA_API_TOKEN",
"",
"FIBO",
"",
"1:1",
8,
5,
123456,
"randomize"
]
},
{
"id": 27,
"type": "RefineImageLiteNodeV2",
"pos": [
820.26611328125,
558.9785766601562
],
"size": [
270,
290
],
"flags": {},
"order": 9,
"mode": 0,
"inputs": [
{
"name": "structured_prompt",
"type": "STRING",
"widget": {
"name": "structured_prompt"
},
"link": 45
},
{
"name": "seed",
"shape": 7,
"type": "INT",
"widget": {
"name": "seed"
},
"link": 46
}
],
"outputs": [
{
"name": "image",
"type": "IMAGE",
"links": [
44
]
},
{
"name": "structured_prompt",
"type": "STRING",
"links": null
},
{
"name": "seed",
"type": "INT",
"links": null
}
],
"properties": {
"Node name for S&R": "RefineImageLiteNodeV2"
},
"widgets_values": [
"BRIA_API_TOKEN",
"",
"",
"FIBO",
"1:1",
8,
5,
123456,
"randomize"
]
},
{
"id": 21,
"type": "GenerateStructuredPromptLiteNodeV2",
"pos": [
3.162916421890259,
587.301025390625
],
"size": [
310.7749938964844,
174
],
"flags": {},
"order": 3,
"mode": 0,
"inputs": [
{
"name": "images",
"shape": 7,
"type": "IMAGE",
"link": null
}
],
"outputs": [
{
"name": "structured_prompt",
"type": "STRING",
"links": [
34
]
},
{
"name": "seed",
"type": "INT",
"links": [
35
]
}
],
"properties": {
"Node name for S&R": "GenerateStructuredPromptLiteNodeV2"
},
"widgets_values": [
"BRIA_API_TOKEN",
"",
"",
123456,
"randomize"
]
}
],
"links": [
[
27,
20,
0,
24,
1,
"STRING"
],
[
28,
20,
1,
24,
2,
"INT"
],
[
29,
24,
0,
6,
0,
"IMAGE"
],
[
34,
21,
0,
25,
1,
"STRING"
],
[
35,
21,
1,
25,
2,
"INT"
],
[
36,
25,
0,
15,
0,
"IMAGE"
],
[
41,
24,
1,
26,
0,
"STRING"
],
[
42,
24,
2,
26,
1,
"INT"
],
[
43,
26,
0,
5,
0,
"IMAGE"
],
[
44,
27,
0,
17,
0,
"IMAGE"
],
[
45,
25,
1,
27,
0,
"STRING"
],
[
46,
25,
2,
27,
1,
"INT"
]
],
"groups": [],
"config": {},
"extra": {
"ds": {
"scale": 0.6276708501927047,
"offset": [
650.5506889681789,
132.55696596915172
]
},
"frontendVersion": "1.25.11"
},
"version": 0.4
}
+696
View File
@@ -0,0 +1,696 @@
{
"id": "3875cd62-7a0e-4c8c-8951-22ec8a63dd8d",
"revision": 0,
"last_node_id": 18,
"last_link_id": 19,
"nodes": [
{
"id": 4,
"type": "PreviewImage",
"pos": [
1284.572509765625,
229.44073486328125
],
"size": [
140,
26
],
"flags": {},
"order": 10,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 16
}
],
"outputs": [],
"properties": {
"Node name for S&R": "PreviewImage",
"cnr_id": "comfy-core",
"ver": "0.3.62"
},
"widgets_values": []
},
{
"id": 3,
"type": "PreviewImage",
"pos": [
809.5054321289062,
38.50392150878906
],
"size": [
140,
26
],
"flags": {},
"order": 6,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 1
}
],
"outputs": [],
"properties": {
"Node name for S&R": "PreviewImage",
"cnr_id": "comfy-core",
"ver": "0.3.62"
},
"widgets_values": []
},
{
"id": 5,
"type": "Note",
"pos": [
-143.02835083007812,
277.0796203613281
],
"size": [
210,
88
],
"flags": {},
"order": 0,
"mode": 0,
"inputs": [],
"outputs": [],
"properties": {},
"widgets_values": [
"If you would like to start with prompt"
],
"color": "#c09430",
"bgcolor": "rgba(24,24,27,.9)"
},
{
"id": 16,
"type": "GenerateStructuredPromptNodeV2",
"pos": [
110.83424377441406,
260.3714294433594
],
"size": [
275.7613220214844,
174
],
"flags": {},
"order": 1,
"mode": 0,
"inputs": [
{
"name": "images",
"shape": 7,
"type": "IMAGE",
"link": null
}
],
"outputs": [
{
"name": "structured_prompt",
"type": "STRING",
"links": [
9
]
},
{
"name": "seed",
"type": "INT",
"links": [
10
]
}
],
"properties": {
"Node name for S&R": "GenerateStructuredPromptNodeV2"
},
"widgets_values": [
"BRIA_API_TOKEN",
"",
"",
123456,
"randomize"
]
},
{
"id": 10,
"type": "PreviewImage",
"pos": [
1261.5439453125,
996.5408935546875
],
"size": [
140,
26
],
"flags": {},
"order": 11,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 19
}
],
"outputs": [],
"properties": {
"Node name for S&R": "PreviewImage",
"cnr_id": "comfy-core",
"ver": "0.3.62"
},
"widgets_values": []
},
{
"id": 14,
"type": "PreviewImage",
"pos": [
751.5670166015625,
845.8276977539062
],
"size": [
140,
26
],
"flags": {},
"order": 8,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 8
}
],
"outputs": [],
"properties": {
"Node name for S&R": "PreviewImage",
"cnr_id": "comfy-core",
"ver": "0.3.62"
},
"widgets_values": []
},
{
"id": 6,
"type": "Note",
"pos": [
-135.44485473632812,
1056.937744140625
],
"size": [
210,
95.76702880859375
],
"flags": {},
"order": 2,
"mode": 0,
"inputs": [],
"outputs": [],
"properties": {},
"widgets_values": [
"If you would like to start with reference image + prompt"
],
"color": "#c09430",
"bgcolor": "rgba(24,24,27,.9)"
},
{
"id": 15,
"type": "GenerateStructuredPromptNodeV2",
"pos": [
117.20681762695312,
1049.3199462890625
],
"size": [
275.7613220214844,
174
],
"flags": {},
"order": 3,
"mode": 0,
"inputs": [
{
"name": "images",
"shape": 7,
"type": "IMAGE",
"link": null
}
],
"outputs": [
{
"name": "structured_prompt",
"type": "STRING",
"links": [
11
]
},
{
"name": "seed",
"type": "INT",
"links": [
12
]
}
],
"properties": {
"Node name for S&R": "GenerateStructuredPromptNodeV2"
},
"widgets_values": [
"BRIA_API_TOKEN",
"",
"",
123456,
"randomize"
]
},
{
"id": 1,
"type": "GenerateImageNodeV2",
"pos": [
482.9381103515625,
222.8486785888672
],
"size": [
270,
314
],
"flags": {},
"order": 4,
"mode": 0,
"inputs": [
{
"name": "images",
"shape": 7,
"type": "IMAGE",
"link": null
},
{
"name": "structured_prompt",
"shape": 7,
"type": "STRING",
"widget": {
"name": "structured_prompt"
},
"link": 9
},
{
"name": "seed",
"shape": 7,
"type": "INT",
"widget": {
"name": "seed"
},
"link": 10
}
],
"outputs": [
{
"name": "image",
"type": "IMAGE",
"links": [
1
]
},
{
"name": "structured_prompt",
"type": "STRING",
"links": [
13
]
},
{
"name": "seed",
"type": "INT",
"links": [
15
]
}
],
"properties": {
"Node name for S&R": "GenerateImageNodeV2",
"cnr_id": "comfyui-bria-api",
"ver": "2.1.4",
"ue_properties": {
"widget_ue_connectable": {
"api_token": true,
"prompt": true,
"model_version": true,
"negative_prompt": true,
"aspect_ratio": true,
"steps_num": true,
"guidance_scale": true,
"seed": true
},
"version": "7.1",
"input_ue_unconnectable": {}
}
},
"widgets_values": [
"BRIA_API_TOKEN",
"",
"FIBO",
"",
"1:1",
50,
5,
123456,
"randomize",
"randomize"
]
},
{
"id": 17,
"type": "RefineImageNodeV2",
"pos": [
876.5194091796875,
234.5377655029297
],
"size": [
287.4712829589844,
314
],
"flags": {},
"order": 7,
"mode": 0,
"inputs": [
{
"name": "structured_prompt",
"type": "STRING",
"widget": {
"name": "structured_prompt"
},
"link": 13
},
{
"name": "seed",
"shape": 7,
"type": "INT",
"widget": {
"name": "seed"
},
"link": 15
}
],
"outputs": [
{
"name": "image",
"type": "IMAGE",
"links": [
16
]
},
{
"name": "structured_prompt",
"type": "STRING",
"links": null
},
{
"name": "seed",
"type": "INT",
"links": null
}
],
"properties": {
"Node name for S&R": "RefineImageNodeV2"
},
"widgets_values": [
"BRIA_API_TOKEN",
"",
"",
"FIBO",
"1:1",
50,
5,
123456,
"randomize",
""
]
},
{
"id": 7,
"type": "GenerateImageNodeV2",
"pos": [
482.86688232421875,
1034.027099609375
],
"size": [
270,
314
],
"flags": {},
"order": 5,
"mode": 0,
"inputs": [
{
"name": "images",
"shape": 7,
"type": "IMAGE",
"link": null
},
{
"name": "structured_prompt",
"shape": 7,
"type": "STRING",
"widget": {
"name": "structured_prompt"
},
"link": 11
},
{
"name": "seed",
"shape": 7,
"type": "INT",
"widget": {
"name": "seed"
},
"link": 12
}
],
"outputs": [
{
"name": "image",
"type": "IMAGE",
"links": [
8
]
},
{
"name": "structured_prompt",
"type": "STRING",
"links": [
17
]
},
{
"name": "seed",
"type": "INT",
"links": [
18
]
}
],
"properties": {
"Node name for S&R": "GenerateImageNodeV2",
"cnr_id": "comfyui-bria-api",
"ver": "2.1.5"
},
"widgets_values": [
"BRIA_API_TOKEN",
"",
"FIBO",
"",
"1:1",
50,
5,
123456,
"randomize",
"randomize"
]
},
{
"id": 18,
"type": "RefineImageNodeV2",
"pos": [
915.1214599609375,
1000.7938842773438
],
"size": [
287.4712829589844,
314
],
"flags": {},
"order": 9,
"mode": 0,
"inputs": [
{
"name": "structured_prompt",
"type": "STRING",
"widget": {
"name": "structured_prompt"
},
"link": 17
},
{
"name": "seed",
"shape": 7,
"type": "INT",
"widget": {
"name": "seed"
},
"link": 18
}
],
"outputs": [
{
"name": "image",
"type": "IMAGE",
"links": [
19
]
},
{
"name": "structured_prompt",
"type": "STRING",
"links": null
},
{
"name": "seed",
"type": "INT",
"links": null
}
],
"properties": {
"Node name for S&R": "RefineImageNodeV2"
},
"widgets_values": [
"BRIA_API_TOKEN",
"",
"",
"FIBO",
"1:1",
50,
5,
123456,
"randomize",
""
]
}
],
"links": [
[
1,
1,
0,
3,
0,
"IMAGE"
],
[
8,
7,
0,
14,
0,
"IMAGE"
],
[
9,
16,
0,
1,
1,
"STRING"
],
[
10,
16,
1,
1,
2,
"INT"
],
[
11,
15,
0,
7,
1,
"STRING"
],
[
12,
15,
1,
7,
2,
"INT"
],
[
13,
1,
1,
17,
0,
"STRING"
],
[
15,
1,
2,
17,
1,
"INT"
],
[
16,
17,
0,
4,
0,
"IMAGE"
],
[
17,
7,
1,
18,
0,
"STRING"
],
[
18,
7,
2,
18,
1,
"INT"
],
[
19,
18,
0,
10,
0,
"IMAGE"
]
],
"groups": [],
"config": {},
"extra": {
"ds": {
"scale": 0.6904379352119744,
"offset": [
213.80311902834225,
-667.3066536880174
]
},
"frontendVersion": "1.25.11",
"ue_links": [],
"VHS_latentpreview": false,
"VHS_latentpreviewrate": 0,
"VHS_MetadataImage": true,
"VHS_KeepIntermediate": true
},
"version": 0.4
}
@@ -1,18 +1,47 @@
{ {
"last_node_id": 42, "id": "1cdd7d4c-58b5-4047-947b-1977ad36d364",
"last_link_id": 65, "revision": 0,
"last_node_id": 14,
"last_link_id": 11,
"nodes": [ "nodes": [
{ {
"id": 42, "id": 6,
"type": "PreviewImage",
"pos": [
1351.83154296875,
26.696861267089844
],
"size": [
399.811279296875,
246
],
"flags": {},
"order": 5,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 5
}
],
"outputs": [],
"properties": {
"Node name for S&R": "PreviewImage"
},
"widgets_values": []
},
{
"id": 1,
"type": "LoadImage", "type": "LoadImage",
"pos": { "pos": [
"0": 591, 383.92852783203125,
"1": 593 38.40964889526367
}, ],
"size": { "size": [
"0": 315, 397.5969543457031,
"1": 314 314
}, ],
"flags": {}, "flags": {},
"order": 0, "order": 0,
"mode": 0, "mode": 0,
@@ -22,10 +51,9 @@
"name": "IMAGE", "name": "IMAGE",
"type": "IMAGE", "type": "IMAGE",
"links": [ "links": [
64, 1,
65 3
], ]
"slot_index": 0
}, },
{ {
"name": "MASK", "name": "MASK",
@@ -37,21 +65,21 @@
"Node name for S&R": "LoadImage" "Node name for S&R": "LoadImage"
}, },
"widgets_values": [ "widgets_values": [
"A_bottle_of_perfume.png", "CAR.png",
"image" "image"
] ]
}, },
{ {
"id": 39, "id": 2,
"type": "LoadImage", "type": "LoadImage",
"pos": { "pos": [
"0": 600, 369.0213623046875,
"1": 988 415.34893798828125
}, ],
"size": { "size": [
"0": 315, 450.83685302734375,
"1": 314 314.0000305175781
}, ],
"flags": {}, "flags": {},
"order": 1, "order": 1,
"mode": 0, "mode": 0,
@@ -61,9 +89,8 @@
"name": "IMAGE", "name": "IMAGE",
"type": "IMAGE", "type": "IMAGE",
"links": [ "links": [
59 4
], ]
"slot_index": 0
}, },
{ {
"name": "MASK", "name": "MASK",
@@ -75,21 +102,21 @@
"Node name for S&R": "LoadImage" "Node name for S&R": "LoadImage"
}, },
"widgets_values": [ "widgets_values": [
"A_red_studio_with_a_shelf__close_up.png", "seed_713360865.png",
"image" "image"
] ]
}, },
{ {
"id": 15, "id": 5,
"type": "Note", "type": "Note",
"pos": { "pos": [
"0": 995.4524536132812, 951.4669189453125,
"1": 601.5353393554688 5.085720062255859
}, ],
"size": { "size": [
"0": 306.28387451171875, 210,
"1": 58 88
}, ],
"flags": {}, "flags": {},
"order": 2, "order": 2,
"mode": 0, "mode": 0,
@@ -103,40 +130,14 @@
"bgcolor": "#653" "bgcolor": "#653"
}, },
{ {
"id": 40, "id": 7,
"type": "PreviewImage", "type": "PreviewImage",
"pos": { "pos": [
"0": 1408, 1368.5450439453125,
"1": 623 340.4889221191406
},
"size": [
210,
246
], ],
"flags": {},
"order": 5,
"mode": 0,
"inputs": [
{
"name": "images",
"type": "IMAGE",
"link": 62
}
],
"outputs": [],
"properties": {
"Node name for S&R": "PreviewImage"
}
},
{
"id": 41,
"type": "PreviewImage",
"pos": {
"0": 1408,
"1": 951
},
"size": [ "size": [
210, 387.0335693359375,
246 246
], ],
"flags": {}, "flags": {},
@@ -146,25 +147,26 @@
{ {
"name": "images", "name": "images",
"type": "IMAGE", "type": "IMAGE",
"link": 63 "link": 6
} }
], ],
"outputs": [], "outputs": [],
"properties": { "properties": {
"Node name for S&R": "PreviewImage" "Node name for S&R": "PreviewImage"
} },
"widgets_values": []
}, },
{ {
"id": 36, "id": 3,
"type": "ShotByTextNode", "type": "ShotByTextOriginal",
"pos": { "pos": [
"0": 996, 941.8839111328125,
"1": 736 149.14889526367188
}, ],
"size": { "size": [
"0": 315, 273.388671875,
"1": 106 202
}, ],
"flags": {}, "flags": {},
"order": 3, "order": 3,
"mode": 0, "mode": 0,
@@ -172,7 +174,7 @@
{ {
"name": "image", "name": "image",
"type": "IMAGE", "type": "IMAGE",
"link": 64 "link": 1
} }
], ],
"outputs": [ "outputs": [
@@ -180,31 +182,34 @@
"name": "output_image", "name": "output_image",
"type": "IMAGE", "type": "IMAGE",
"links": [ "links": [
62 5
], ]
"slot_index": 0
} }
], ],
"properties": { "properties": {
"Node name for S&R": "ShotByTextNode" "Node name for S&R": "ShotByTextOriginal"
}, },
"widgets_values": [ "widgets_values": [
"a beautiful sunset", "BRIA_API_TOKEN",
1, "sea",
"fast",
false,
false,
true,
"" ""
] ]
}, },
{ {
"id": 37, "id": 4,
"type": "ShotByImageNode", "type": "ShotByImageOriginal",
"pos": { "pos": [
"0": 999, 945.0781860351562,
"1": 932 441.9688415527344
}, ],
"size": { "size": [
"0": 315, 272.0703125,
"1": 102 174
}, ],
"flags": {}, "flags": {},
"order": 4, "order": 4,
"mode": 0, "mode": 0,
@@ -212,12 +217,12 @@
{ {
"name": "image", "name": "image",
"type": "IMAGE", "type": "IMAGE",
"link": 65 "link": 3
}, },
{ {
"name": "ref_image", "name": "ref_image",
"type": "IMAGE", "type": "IMAGE",
"link": 59 "link": 4
} }
], ],
"outputs": [ "outputs": [
@@ -225,58 +230,60 @@
"name": "output_image", "name": "output_image",
"type": "IMAGE", "type": "IMAGE",
"links": [ "links": [
63 6
], ]
"slot_index": 0
} }
], ],
"properties": { "properties": {
"Node name for S&R": "ShotByImageNode" "Node name for S&R": "ShotByImageOriginal"
}, },
"widgets_values": [ "widgets_values": [
0, "BRIA_API_TOKEN",
"" false,
false,
true,
1
] ]
} }
], ],
"links": [ "links": [
[ [
59, 1,
39, 1,
0, 0,
37, 3,
0,
"IMAGE"
],
[
3,
1,
0,
4,
0,
"IMAGE"
],
[
4,
2,
0,
4,
1, 1,
"IMAGE" "IMAGE"
], ],
[ [
62, 5,
36, 3,
0, 0,
40, 6,
0, 0,
"IMAGE" "IMAGE"
], ],
[ [
63, 6,
37, 4,
0, 0,
41, 7,
0,
"IMAGE"
],
[
64,
42,
0,
36,
0,
"IMAGE"
],
[
65,
42,
0,
37,
0, 0,
"IMAGE" "IMAGE"
] ]
@@ -285,12 +292,13 @@
"config": {}, "config": {},
"extra": { "extra": {
"ds": { "ds": {
"scale": 0.9849732675807669, "scale": 0.7513148009015777,
"offset": [ "offset": [
-339.6686422794803, 11.112206386364164,
-496.4354678014682 66.47311795454547
] ]
} },
"frontendVersion": "1.25.11"
}, },
"version": 0.4 "version": 0.4
} }