Compare commits
45
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8585ab54e8 | ||
|
|
b38d15efe4 | ||
|
|
513fec79b5 | ||
|
|
ddbf7d0695 | ||
|
|
3befc0a2ac | ||
|
|
af6ef2a829 | ||
|
|
bc5dacb9ef | ||
|
|
fc8aa8b6a7 | ||
|
|
9960a93044 | ||
|
|
8d3eef85ca | ||
|
|
bb4108b6c4 | ||
|
|
18a5ffbcca | ||
|
|
4f3b7fb77a | ||
|
|
c3b5fea335 | ||
|
|
b8e40e90bc | ||
|
|
9b3f15b7bd | ||
|
|
f7c9ed12b0 | ||
|
|
bdd053a81b | ||
|
|
2913a9eb6b | ||
|
|
080c17f4a4 | ||
|
|
711080ffb0 | ||
|
|
69f111bfe9 | ||
|
|
011f728b0d | ||
|
|
ac9678cd23 | ||
|
|
daede982ac | ||
|
|
e716e585df | ||
|
|
1296b6a27c | ||
|
|
19f15f4ba4 | ||
|
|
30d0154caf | ||
|
|
283665f654 | ||
|
|
03e3bec952 | ||
|
|
f1793ba9ad | ||
|
|
eda093ea66 | ||
|
|
165a103f0c | ||
|
|
ad2f4a5853 | ||
|
|
43f858dca4 | ||
|
|
538cd53ac8 | ||
|
|
bae0ed3842 | ||
|
|
a164f8ec45 | ||
|
|
ebe9e2e6b1 | ||
|
|
429c51ac6d | ||
|
|
aed4832984 | ||
|
|
a78aff0fb2 | ||
|
|
6edfc55109 | ||
|
|
a4855c1a1a |
@@ -7,17 +7,19 @@ on:
|
||||
paths:
|
||||
- "pyproject.toml"
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
publish-node:
|
||||
name: Publish Custom Node to registry
|
||||
runs-on: ubuntu-latest
|
||||
# if this is a forked repository. Skipping the workflow.
|
||||
if: github.event.repository.fork == false
|
||||
if: ${{ github.repository_owner == 'Bria-AI' }}
|
||||
steps:
|
||||
- name: Check out code
|
||||
uses: actions/checkout@v4
|
||||
- name: Publish Custom Node
|
||||
uses: Comfy-Org/publish-node-action@main
|
||||
uses: Comfy-Org/publish-node-action@v1
|
||||
with:
|
||||
## Add your own personal access token to your Github Repository secrets and reference it here.
|
||||
personal_access_token: ${{ secrets.REGISTRY_ACCESS_TOKEN }}
|
||||
|
||||
+2
-1
@@ -1 +1,2 @@
|
||||
*.pyc
|
||||
*.pyc
|
||||
.idea
|
||||
@@ -13,7 +13,7 @@ An API token is required to use the nodes in your workflows. Get started quickly
|
||||
<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.
|
||||
|
||||
To load a workflow, import the compatible workflow.json files from this [folder](workflows).
|
||||
@@ -45,7 +45,8 @@ These nodes use pre-trained tailored models to generate images that faithfully r
|
||||
| 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 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
|
||||
These nodes modify specific parts of images, enabling adjustments while maintaining the integrity of the rest of the image.
|
||||
@@ -67,6 +68,15 @@ These nodes create high-quality product images for eCommerce workflows.
|
||||
| **ShotByText** | Modifies an image's background by providing a text prompt. Powered by BRIA's ControlNet Background-Generation. |
|
||||
| **ShotByImage** | Modifies an image's background by providing a reference image. Uses BRIA's ControlNet Background-Generation and Image-Prompt. |
|
||||
|
||||
## 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) |
|
||||
|
||||
|
||||
|
||||
|
||||
# Installation
|
||||
There are two methods to install the BRIA ComfyUI API nodes:
|
||||
|
||||
|
||||
+56
-8
@@ -1,7 +1,32 @@
|
||||
from nodes.tailored_portrait_node import TailoredPortraitNode
|
||||
from .nodes import (EraserNode, GenFillNode, ImageExpansionNode, ReplaceBgNode, RmbgNode, RemoveForegroundNode, ShotByTextNode, ShotByImageNode, TailoredGenNode,
|
||||
TailoredModelInfoNode, Text2ImageBaseNode, Text2ImageFastNode, Text2ImageHDNode,
|
||||
ReimagineNode)
|
||||
from .nodes import (
|
||||
EraserNode,
|
||||
GenFillNode,
|
||||
ImageExpansionNode,
|
||||
ReplaceBgNode,
|
||||
RmbgNode,
|
||||
RemoveForegroundNode,
|
||||
ShotByTextOriginalNode,
|
||||
ShotByImageOriginalNode,
|
||||
TailoredGenNode,
|
||||
TailoredModelInfoNode,
|
||||
Text2ImageBaseNode,
|
||||
Text2ImageFastNode,
|
||||
Text2ImageHDNode,
|
||||
TailoredPortraitNode,
|
||||
ReimagineNode,
|
||||
ShotByTextAutomaticNode,
|
||||
ShotByImageManualPaddingNode,
|
||||
ShotByImageAutomaticAspectRatioNode,
|
||||
ShotByImageCustomCoordinatesNode,
|
||||
ShotByImageManualPlacementNode,
|
||||
ShotByImageAutomaticNode,
|
||||
ShotByTextAutomaticAspectRatioNode,
|
||||
ShotByTextManualPlacementNode,
|
||||
ShotByTextManualPaddingNode,
|
||||
ShotByTextCustomCoordinatesNode,
|
||||
AttributionByImageNode
|
||||
)
|
||||
|
||||
# Map the node class to a name used internally by ComfyUI
|
||||
NODE_CLASS_MAPPINGS = {
|
||||
"BriaEraser": EraserNode, # Return the class, not an instance
|
||||
@@ -10,8 +35,18 @@ NODE_CLASS_MAPPINGS = {
|
||||
"ReplaceBgNode": ReplaceBgNode,
|
||||
"RmbgNode": RmbgNode,
|
||||
"RemoveForegroundNode": RemoveForegroundNode,
|
||||
"ShotByTextNode": ShotByTextNode,
|
||||
"ShotByImageNode": ShotByImageNode,
|
||||
"ShotByTextOriginal": ShotByTextOriginalNode,
|
||||
"ShotByImageOriginal": ShotByImageOriginalNode,
|
||||
"ShotByTextAutomatic": ShotByTextAutomaticNode,
|
||||
"ShotByTextManualPlacement": ShotByTextManualPlacementNode,
|
||||
"ShotByTextCustomCoordinates": ShotByTextCustomCoordinatesNode,
|
||||
"ShotByTextManualPadding": ShotByTextManualPaddingNode,
|
||||
"ShotByTextAutomaticAspectRatio": ShotByTextAutomaticAspectRatioNode,
|
||||
"ShotByImageAutomatic": ShotByImageAutomaticNode,
|
||||
"ShotByImageManualPlacement": ShotByImageManualPlacementNode,
|
||||
"ShotByImageCustomCoordinates": ShotByImageCustomCoordinatesNode,
|
||||
"ShotByImageManualPadding": ShotByImageManualPaddingNode,
|
||||
"ShotByImageAutomaticAspectRatio": ShotByImageAutomaticAspectRatioNode,
|
||||
"BriaTailoredGen": TailoredGenNode,
|
||||
"TailoredModelInfoNode": TailoredModelInfoNode,
|
||||
"TailoredPortraitNode": TailoredPortraitNode,
|
||||
@@ -19,6 +54,7 @@ NODE_CLASS_MAPPINGS = {
|
||||
"Text2ImageFastNode": Text2ImageFastNode,
|
||||
"Text2ImageHDNode": Text2ImageHDNode,
|
||||
"ReimagineNode": ReimagineNode,
|
||||
"AttributionByImageNode":AttributionByImageNode
|
||||
}
|
||||
# Map the node display name to the one shown in the ComfyUI node interface
|
||||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
@@ -28,12 +64,24 @@ NODE_DISPLAY_NAME_MAPPINGS = {
|
||||
"ReplaceBgNode": "Bria Replace Background",
|
||||
"RmbgNode": "Bria RMBG",
|
||||
"RemoveForegroundNode": "Bria Remove Foreground",
|
||||
"ShotByTextNode": "Bria Shot By Text",
|
||||
"ShotByImageNode": "Bria Shot By Image",
|
||||
"ShotByTextOriginal": "Shot by Text - Original",
|
||||
"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",
|
||||
"TailoredModelInfoNode": "Bria Tailored Model Info",
|
||||
"TailoredPortraitNode": "Bria Restyle Portrait",
|
||||
"Text2ImageBaseNode": "Bria Text2Image Base",
|
||||
"Text2ImageFastNode": "Bria Text2Image Fast",
|
||||
"Text2ImageHDNode": "Bria Text2Image HD",
|
||||
"ReimagineNode": "Bria Reimagine",
|
||||
"AttributionByImageNode":"Attribution By Image Node"
|
||||
}
|
||||
|
||||
+16
-3
@@ -4,12 +4,25 @@ from .image_expansion_node import ImageExpansionNode
|
||||
from .replace_bg_node import ReplaceBgNode
|
||||
from .rmbg_node import RmbgNode
|
||||
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_model_info_node import TailoredModelInfoNode
|
||||
from .tailored_portrait_node import TailoredPortraitNode
|
||||
from .text_2_image_base_node import Text2ImageBaseNode
|
||||
from .text_2_image_fast_node import Text2ImageFastNode
|
||||
from .text_2_image_hd_node import Text2ImageHDNode
|
||||
from .reimagine_node import ReimagineNode
|
||||
from .reimagine_node import ReimagineNode
|
||||
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
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
import requests
|
||||
import torch
|
||||
|
||||
from .common import preprocess_image, image_to_base64, poll_status_until_completed
|
||||
|
||||
class AttributionByImageNode():
|
||||
@classmethod
|
||||
def INPUT_TYPES(self):
|
||||
return {
|
||||
"required": {
|
||||
"image": ("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"
|
||||
|
||||
# Define the execute method as expected by ComfyUI
|
||||
def execute(self, image, model_version, api_key):
|
||||
if api_key.strip() == "" or api_key.strip() == "BRIA_API_TOKEN":
|
||||
raise Exception("Please insert a valid API key.")
|
||||
|
||||
# Check if image is tensor, if so, convert to NumPy array
|
||||
if isinstance(image, torch.Tensor):
|
||||
image = preprocess_image(image)
|
||||
|
||||
# Convert image to base64 for the new API format
|
||||
image_base64 = image_to_base64(image)
|
||||
payload = {
|
||||
"image": image_base64,
|
||||
"model_version": model_version,
|
||||
}
|
||||
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"api_token": f"{api_key}"
|
||||
}
|
||||
|
||||
try:
|
||||
response = requests.post(self.api_url, json=payload, headers=headers)
|
||||
|
||||
if response.status_code == 200 or response.status_code == 202:
|
||||
print('Initial Attribution via Images API 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)
|
||||
return (str(final_response.get("result",{}).get("content")),)
|
||||
else:
|
||||
raise Exception(f"Error: API request failed with status code {response.status_code} {response.text}")
|
||||
except Exception as e:
|
||||
raise Exception(f"{e}")
|
||||
+67
-12
@@ -5,6 +5,7 @@ import torch
|
||||
import base64
|
||||
from torchvision.transforms import ToPILImage
|
||||
import requests
|
||||
import time
|
||||
|
||||
def postprocess_image(image):
|
||||
result_image = Image.open(io.BytesIO(image))
|
||||
@@ -44,7 +45,7 @@ def preprocess_mask(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":
|
||||
raise Exception("Please insert a valid API key.")
|
||||
|
||||
@@ -58,10 +59,12 @@ def process_request(api_url, image, mask, api_key):
|
||||
image_base64 = image_to_base64(image)
|
||||
mask_base64 = image_to_base64(mask)
|
||||
|
||||
# Prepare the API request payload
|
||||
# Prepare the API request payload for v2 API
|
||||
payload = {
|
||||
"file": f"{image_base64}",
|
||||
"mask_file": f"{mask_base64}"
|
||||
"image": image_base64,
|
||||
"mask": mask_base64,
|
||||
"visual_input_content_moderation":visual_input_content_moderation,
|
||||
"visual_output_content_moderation":visual_output_content_moderation
|
||||
}
|
||||
|
||||
headers = {
|
||||
@@ -70,13 +73,23 @@ def process_request(api_url, image, mask, api_key):
|
||||
}
|
||||
|
||||
try:
|
||||
response = requests.post(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 = requests.post(api_url, json=payload, headers=headers)
|
||||
if response.status_code == 200 or response.status_code == 202:
|
||||
print('Initial request successful, polling for completion...')
|
||||
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 = result_image.convert("RGBA")
|
||||
result_image = np.array(result_image).astype(np.float32) / 255.0
|
||||
@@ -84,9 +97,51 @@ def process_request(api_url, image, mask, api_key):
|
||||
# image_tensor = image_tensor = ToTensor()(output_image)
|
||||
# image_tensor = image_tensor.permute(1, 2, 0) / 255.0 # Shape now becomes [1, 2200, 1548, 3]
|
||||
# print(f"output tensor shape is: {image_tensor.shape}")
|
||||
return (result_image,)
|
||||
return (result_image,)
|
||||
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:
|
||||
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 = {"api_token": 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")
|
||||
|
||||
@@ -8,6 +8,10 @@ class EraserNode():
|
||||
"image": ("IMAGE",), # Input image from another node
|
||||
"mask": ("MASK",), # Binary mask input
|
||||
"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
|
||||
|
||||
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
|
||||
def execute(self, image, mask, api_key):
|
||||
return process_request(self.api_url, 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, visual_input_content_moderation, visual_output_content_moderation)
|
||||
|
||||
@@ -4,7 +4,7 @@ from PIL import Image
|
||||
import io
|
||||
import torch
|
||||
|
||||
from .common import image_to_base64, preprocess_image, preprocess_mask
|
||||
from .common import preprocess_image, preprocess_mask, image_to_base64, poll_status_until_completed
|
||||
|
||||
|
||||
class GenFillNode():
|
||||
@@ -18,7 +18,12 @@ class GenFillNode():
|
||||
"api_key": ("STRING", {"default": "BRIA_API_TOKEN"}), # API Key input with a default value
|
||||
},
|
||||
"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,10 +33,10 @@ class GenFillNode():
|
||||
FUNCTION = "execute" # This is the method that will be executed
|
||||
|
||||
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
|
||||
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":
|
||||
raise Exception("Please insert a valid API key.")
|
||||
|
||||
@@ -47,12 +52,14 @@ class GenFillNode():
|
||||
|
||||
# Prepare the API request payload
|
||||
payload = {
|
||||
"file": f"{image_base64}",
|
||||
"mask_file": f"{mask_base64}",
|
||||
"image": image_base64,
|
||||
"mask": mask_base64,
|
||||
"prompt": prompt,
|
||||
"negative_prompt": "blurry",
|
||||
"sync": True,
|
||||
"seed": seed,
|
||||
"prompt_content_moderation":prompt_content_moderation,
|
||||
"visual_input_content_moderation":visual_input_content_moderation,
|
||||
"visual_output_content_moderation":visual_output_content_moderation
|
||||
}
|
||||
|
||||
headers = {
|
||||
@@ -61,20 +68,30 @@ class GenFillNode():
|
||||
}
|
||||
|
||||
try:
|
||||
# Send initial request to get status URL
|
||||
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
|
||||
|
||||
if response.status_code == 200 or response.status_code == 202:
|
||||
print('Initial genfill request successful, polling for completion...')
|
||||
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 = result_image.convert("RGB")
|
||||
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}")
|
||||
raise Exception(f"Error: API request failed with status code {response.status_code} {response.text}")
|
||||
|
||||
except Exception as e:
|
||||
raise Exception(f"{e}")
|
||||
|
||||
@@ -4,7 +4,7 @@ from PIL import Image
|
||||
import io
|
||||
import torch
|
||||
|
||||
from .common import image_to_base64, preprocess_image
|
||||
from .common import image_to_base64, preprocess_image, poll_status_until_completed
|
||||
|
||||
|
||||
class ImageExpansionNode():
|
||||
@@ -13,17 +13,21 @@ class ImageExpansionNode():
|
||||
return {
|
||||
"required": {
|
||||
"image": ("IMAGE",), # Input image from another node
|
||||
"original_image_size": ("STRING",),
|
||||
"original_image_location": ("STRING",),
|
||||
"api_key": ("STRING", {"default": "BRIA_API_TOKEN"}), # API Key input with a default value
|
||||
},
|
||||
|
||||
"optional": {
|
||||
"original_image_size": ("STRING",),
|
||||
"original_image_location": ("STRING",),
|
||||
"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": ""}),
|
||||
"seed": ("INT", {"default": 681794}),
|
||||
"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}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,27 +37,28 @@ class ImageExpansionNode():
|
||||
FUNCTION = "execute" # This is the method that will be executed
|
||||
|
||||
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" # Image Expansion API URL
|
||||
|
||||
# Define the execute method as expected by ComfyUI
|
||||
def execute(self, image,
|
||||
original_image_size,
|
||||
original_image_location,
|
||||
canvas_size,
|
||||
aspect_ratio,
|
||||
prompt,
|
||||
seed,
|
||||
negative_prompt,
|
||||
content_moderation,
|
||||
prompt_content_moderation,
|
||||
preserve_alpha,
|
||||
visual_input_content_moderation,
|
||||
visual_output_content_moderation,
|
||||
api_key):
|
||||
if api_key.strip() == "" or api_key.strip() == "BRIA_API_TOKEN":
|
||||
raise Exception("Please insert a valid API key.")
|
||||
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 ()
|
||||
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(",")]
|
||||
original_image_location = [int(x.strip()) for x in original_image_location.split(",")]
|
||||
canvas_size = [int(x.strip()) for x in canvas_size.split(",")]
|
||||
|
||||
if prompt == "":
|
||||
prompt = None
|
||||
if negative_prompt == "":
|
||||
negative_prompt = " " # hack to avoid error in triton which expects non-empty string
|
||||
|
||||
@@ -63,18 +68,32 @@ class ImageExpansionNode():
|
||||
|
||||
# Convert the image directly to Base64 string
|
||||
image_base64 = image_to_base64(image)
|
||||
|
||||
# Prepare the API request payload
|
||||
payload = {
|
||||
"file": f"{image_base64}",
|
||||
"original_image_size": original_image_size,
|
||||
"original_image_location": original_image_location,
|
||||
"canvas_size": canvas_size,
|
||||
if aspect_ratio and aspect_ratio != "None":
|
||||
payload = {
|
||||
"image": image_base64,
|
||||
"aspect_ratio": aspect_ratio,
|
||||
"prompt": prompt,
|
||||
"negative_prompt": negative_prompt,
|
||||
"seed": seed,
|
||||
"content_moderation": content_moderation
|
||||
"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
|
||||
}
|
||||
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,
|
||||
"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 = {
|
||||
"Content-Type": "application/json",
|
||||
@@ -83,19 +102,34 @@ class ImageExpansionNode():
|
||||
|
||||
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
|
||||
|
||||
if response.status_code == 200 or response.status_code == 202:
|
||||
print('Initial image expansion request successful, polling for completion...')
|
||||
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}")
|
||||
|
||||
# Poll status URL until completion
|
||||
final_response = poll_status_until_completed(status_url, api_key)
|
||||
|
||||
# Get the result image URL
|
||||
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 = result_image.convert("RGB")
|
||||
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}")
|
||||
raise Exception(f"Error: API request failed with status code {response.status_code}: {response.text}")
|
||||
|
||||
except Exception as e:
|
||||
raise Exception(f"{e}")
|
||||
|
||||
@@ -4,7 +4,7 @@ from PIL import Image
|
||||
import io
|
||||
import torch
|
||||
|
||||
from .common import preprocess_image, image_to_base64
|
||||
from .common import preprocess_image, image_to_base64, poll_status_until_completed
|
||||
|
||||
|
||||
class RemoveForegroundNode():
|
||||
@@ -16,7 +16,9 @@ class RemoveForegroundNode():
|
||||
"api_key": ("STRING", {"default": "BRIA_API_TOKEN"}), # API Key input with a default value
|
||||
},
|
||||
"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}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,10 +28,10 @@ class RemoveForegroundNode():
|
||||
FUNCTION = "execute" # This is the method that will be executed
|
||||
|
||||
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" # remove foreground API URL
|
||||
|
||||
# Define the execute method as expected by ComfyUI
|
||||
def execute(self, image, content_moderation, api_key):
|
||||
def execute(self, image, visual_input_content_moderation, visual_output_content_moderation, preserve_alpha, api_key):
|
||||
if api_key.strip() == "" or api_key.strip() == "BRIA_API_TOKEN":
|
||||
raise Exception("Please insert a valid API key.")
|
||||
|
||||
@@ -44,7 +46,12 @@ class RemoveForegroundNode():
|
||||
|
||||
# files=[('file',('temp_img.jpeg', open(temp_img_path, 'rb'),'image/jpeg'))
|
||||
# ]
|
||||
payload = {"file": image_to_base64(image), "content_moderation": content_moderation}
|
||||
payload = {
|
||||
"image": image_to_base64(image),
|
||||
"visual_input_content_moderation": visual_input_content_moderation,
|
||||
"visual_output_content_moderation":visual_output_content_moderation,
|
||||
"preserve_alpha": preserve_alpha
|
||||
}
|
||||
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
@@ -53,18 +60,31 @@ class RemoveForegroundNode():
|
||||
|
||||
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
|
||||
|
||||
if response.status_code == 200 or response.status_code == 202:
|
||||
print('Initial request successful, polling for completion...')
|
||||
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}")
|
||||
|
||||
# Poll status URL until completion
|
||||
final_response = poll_status_until_completed(status_url, api_key)
|
||||
|
||||
# Get the result image URL
|
||||
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 = 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}")
|
||||
raise Exception(f"Error: API request failed with status code {response.status_code} {response.text}")
|
||||
|
||||
except Exception as e:
|
||||
raise Exception(f"{e}")
|
||||
|
||||
+53
-35
@@ -4,7 +4,7 @@ from PIL import Image
|
||||
import io
|
||||
import torch
|
||||
|
||||
from .common import image_to_base64, preprocess_image, preprocess_mask
|
||||
from .common import image_to_base64, preprocess_image, preprocess_mask, poll_status_until_completed
|
||||
|
||||
|
||||
class ReplaceBgNode():
|
||||
@@ -16,16 +16,17 @@ class ReplaceBgNode():
|
||||
"api_key": ("STRING", {"default": "BRIA_API_TOKEN"}), # API Key input with a default value
|
||||
},
|
||||
"optional": {
|
||||
"fast": ("BOOLEAN", {"default": True}),
|
||||
"bg_prompt": ("STRING",),
|
||||
"ref_image": ("IMAGE",), # Input ref image from another node
|
||||
"mode": (["base", "fast", "high_control"], {"default": "base"}),
|
||||
"prompt": ("STRING",),
|
||||
"ref_images": ("IMAGE",),
|
||||
"refine_prompt": ("BOOLEAN", {"default": True}),
|
||||
"enhance_ref_image": ("BOOLEAN", {"default": True}),
|
||||
"enhance_ref_images": ("BOOLEAN", {"default": True}),
|
||||
"original_quality": ("BOOLEAN", {"default": False}),
|
||||
"force_rmbg": ("BOOLEAN", {"default": False}),
|
||||
"negative_prompt": ("STRING", {"default": None}),
|
||||
"seed": ("INT", {"default": 681794}),
|
||||
"content_moderation": ("BOOLEAN", {"default": False}),
|
||||
"visual_output_content_moderation": ("BOOLEAN", {"default": False}),
|
||||
"prompt_content_moderation": ("BOOLEAN", {"default": False}),
|
||||
"force_background_detection": ("BOOLEAN", {"default": False}),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,20 +36,21 @@ class ReplaceBgNode():
|
||||
FUNCTION = "execute" # This is the method that will be executed
|
||||
|
||||
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" # Replace BG API URL
|
||||
|
||||
# Define the execute method as expected by ComfyUI
|
||||
def execute(self, image, fast,
|
||||
def execute(self, image, mode,
|
||||
refine_prompt,
|
||||
enhance_ref_image,
|
||||
original_quality,
|
||||
force_rmbg,
|
||||
negative_prompt,
|
||||
seed,
|
||||
api_key,
|
||||
content_moderation,
|
||||
bg_prompt=None,
|
||||
ref_image=None,):
|
||||
visual_output_content_moderation,
|
||||
prompt_content_moderation,
|
||||
enhance_ref_images,
|
||||
force_background_detection,
|
||||
prompt=None,
|
||||
ref_images=None,):
|
||||
if api_key.strip() == "" or api_key.strip() == "BRIA_API_TOKEN":
|
||||
raise Exception("Please insert a valid API key.")
|
||||
|
||||
@@ -56,28 +58,29 @@ class ReplaceBgNode():
|
||||
if isinstance(image, torch.Tensor):
|
||||
image = preprocess_image(image)
|
||||
|
||||
# Convert the image and mask directly to Base64 strings
|
||||
# Convert the image to Base64 string
|
||||
image_base64 = image_to_base64(image)
|
||||
ref_image_file = None # initialization, will be updated if it is supplied
|
||||
if ref_image is not None:
|
||||
ref_image = preprocess_image(ref_image)
|
||||
ref_image_file = image_to_base64(ref_image)
|
||||
|
||||
if ref_images is not None:
|
||||
ref_images = preprocess_image(ref_images)
|
||||
ref_images = [image_to_base64(ref_images)]
|
||||
else:
|
||||
ref_images=[]
|
||||
|
||||
# Prepare the API request payload
|
||||
# Prepare the API request payload for v2 API
|
||||
payload = {
|
||||
"file": f"{image_base64}",
|
||||
"fast": fast,
|
||||
"bg_prompt": bg_prompt,
|
||||
"ref_image_file": ref_image_file,
|
||||
"image": image_base64,
|
||||
"mode": mode,
|
||||
"prompt": prompt,
|
||||
"ref_images":ref_images,
|
||||
"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
|
||||
"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 = {
|
||||
@@ -87,19 +90,34 @@ class ReplaceBgNode():
|
||||
|
||||
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
|
||||
|
||||
if response.status_code == 200 or response.status_code == 202:
|
||||
print('Initial replace background request successful, polling for completion...')
|
||||
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')
|
||||
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}")
|
||||
|
||||
# Poll status URL until completion
|
||||
final_response = poll_status_until_completed(status_url, api_key)
|
||||
|
||||
# Get the result image URL
|
||||
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 = result_image.convert("RGB")
|
||||
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}")
|
||||
raise Exception(f"Error: API request failed with status code {response.status_code}{response.text}")
|
||||
|
||||
except Exception as e:
|
||||
raise Exception(f"{e}")
|
||||
|
||||
+41
-21
@@ -4,8 +4,7 @@ from PIL import Image
|
||||
import io
|
||||
import torch
|
||||
|
||||
from .common import preprocess_image
|
||||
from io import BytesIO
|
||||
from .common import preprocess_image, image_to_base64, poll_status_until_completed
|
||||
|
||||
class RmbgNode():
|
||||
@classmethod
|
||||
@@ -16,7 +15,10 @@ class RmbgNode():
|
||||
"api_key": ("STRING", {"default": "BRIA_API_TOKEN"}), # API Key input with a default value
|
||||
},
|
||||
"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}),
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,10 +28,10 @@ class RmbgNode():
|
||||
FUNCTION = "execute" # This is the method that will be executed
|
||||
|
||||
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" # RMBG API URL
|
||||
|
||||
# Define the execute method as expected by ComfyUI
|
||||
def execute(self, image, content_moderation, api_key):
|
||||
def execute(self, image, visual_input_content_moderation, visual_output_content_moderation, preserve_alpha, api_key):
|
||||
if api_key.strip() == "" or api_key.strip() == "BRIA_API_TOKEN":
|
||||
raise Exception("Please insert a valid API key.")
|
||||
|
||||
@@ -37,31 +39,49 @@ class RmbgNode():
|
||||
if isinstance(image, torch.Tensor):
|
||||
image = preprocess_image(image)
|
||||
|
||||
# Prepare the API request payload
|
||||
image_buffer = BytesIO()
|
||||
image.save(image_buffer, format="JPEG")
|
||||
# Convert image to base64 for the new API format
|
||||
image_base64 = image_to_base64(image)
|
||||
payload = {
|
||||
"image": image_base64,
|
||||
"visual_input_content_moderation": visual_input_content_moderation,
|
||||
"visual_output_content_moderation":visual_output_content_moderation,
|
||||
"preserve_alpha":preserve_alpha
|
||||
}
|
||||
|
||||
# Get binary data from buffer
|
||||
image_buffer.seek(0) # Move cursor to the start of the buffer
|
||||
binary_data = image_buffer.read()
|
||||
|
||||
files=[('file',('temp_img.jpeg', BytesIO(binary_data),'image/jpeg'))]
|
||||
payload = {"content_moderation": content_moderation}
|
||||
headers = {
|
||||
"Content-Type": "application/json",
|
||||
"api_token": f"{api_key}"
|
||||
}
|
||||
|
||||
try:
|
||||
response = requests.post(self.api_url, data=payload, headers={"api_token": api_key}, files=files)
|
||||
# Check for successful response
|
||||
if response.status_code == 200:
|
||||
print('response is 200')
|
||||
# Process the output image from API response
|
||||
response = requests.post(self.api_url, json=payload, headers=headers)
|
||||
|
||||
if response.status_code == 200 or response.status_code == 202:
|
||||
print('Initial RMBG request successful, polling for completion...')
|
||||
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)
|
||||
|
||||
# Get the result image URL
|
||||
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 = 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}")
|
||||
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,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)
|
||||
@@ -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)
|
||||
+41
-72
@@ -1,72 +1,41 @@
|
||||
import requests
|
||||
import torch
|
||||
|
||||
from .common import postprocess_image, preprocess_image, image_to_base64
|
||||
|
||||
class ShotByImageNode():
|
||||
@classmethod
|
||||
def INPUT_TYPES(self):
|
||||
return {
|
||||
"required": {
|
||||
"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_NAMES = ("output_image",)
|
||||
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/v1/product/lifestyle_shot_by_image" # Eraser API URL
|
||||
|
||||
# Define the execute method as expected by ComfyUI
|
||||
def execute(self, image, ref_image, api_key, enhance_ref_image, content_moderation):
|
||||
if api_key.strip() == "" or api_key.strip() == "BRIA_API_TOKEN":
|
||||
raise Exception("Please insert a valid API key.")
|
||||
|
||||
# Check if image and mask are tensors, if so, convert to NumPy arrays
|
||||
if isinstance(image, torch.Tensor):
|
||||
image = preprocess_image(image)
|
||||
if isinstance(ref_image, torch.Tensor):
|
||||
ref_image = preprocess_image(ref_image)
|
||||
|
||||
# Convert the image and mask directly to Base64 strings
|
||||
image_base64 = image_to_base64(image)
|
||||
ref_image_base64 = image_to_base64(ref_image)
|
||||
enhance_ref_image = bool(enhance_ref_image)
|
||||
|
||||
payload = {
|
||||
"file": image_base64,
|
||||
"ref_image_file": ref_image_base64,
|
||||
"enhance_ref_image": enhance_ref_image,
|
||||
"placement_type": "original",
|
||||
"original_quality": True,
|
||||
"sync": True,
|
||||
"content_moderation": content_moderation
|
||||
}
|
||||
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}")
|
||||
from .utils.shot_utils import get_image_input_types, create_image_payload, make_api_request, shot_by_image_api_url, PlacementType
|
||||
|
||||
|
||||
class ShotByImageOriginalNode:
|
||||
@classmethod
|
||||
def INPUT_TYPES(self):
|
||||
input_types = get_image_input_types()
|
||||
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,
|
||||
api_key,
|
||||
sync=True,
|
||||
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.ORIGINAL.value,
|
||||
original_quality=True,
|
||||
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,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)
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
+42
-69
@@ -1,69 +1,42 @@
|
||||
import requests
|
||||
import torch
|
||||
|
||||
from .common import postprocess_image, preprocess_image, image_to_base64
|
||||
|
||||
class ShotByTextNode():
|
||||
@classmethod
|
||||
def INPUT_TYPES(self):
|
||||
return {
|
||||
"required": {
|
||||
"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_NAMES = ("output_image",)
|
||||
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/v1/product/lifestyle_shot_by_text" # Eraser API URL
|
||||
|
||||
# Define the execute method as expected by ComfyUI
|
||||
def execute(self, image, api_key, scene_description, optimize_description, content_moderation):
|
||||
if api_key.strip() == "" or api_key.strip() == "BRIA_API_TOKEN":
|
||||
raise Exception("Please insert a valid API key.")
|
||||
|
||||
# Check if image and mask are tensors, if so, convert to NumPy arrays
|
||||
if isinstance(image, torch.Tensor):
|
||||
image = preprocess_image(image)
|
||||
|
||||
optimize_description = bool(optimize_description)
|
||||
image_base64 = image_to_base64(image)
|
||||
payload = {
|
||||
"file": image_base64,
|
||||
"scene_description": scene_description,
|
||||
"optimize_description": optimize_description,
|
||||
"placement_type": "original",
|
||||
"original_quality": True,
|
||||
"sync": True,
|
||||
"content_moderation": content_moderation
|
||||
|
||||
}
|
||||
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}")
|
||||
|
||||
from .utils.shot_utils import get_text_input_types, create_text_payload, make_api_request, shot_by_text_api_url, PlacementType
|
||||
|
||||
|
||||
class ShotByTextOriginalNode:
|
||||
@classmethod
|
||||
def INPUT_TYPES(self):
|
||||
input_types = get_text_input_types()
|
||||
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,
|
||||
api_key,
|
||||
sync=True,
|
||||
optimize_description=True,
|
||||
exclude_elements="",
|
||||
force_rmbg=False,
|
||||
content_moderation=False,
|
||||
):
|
||||
payload = create_text_payload(
|
||||
image,
|
||||
api_key,
|
||||
scene_description,
|
||||
mode,
|
||||
PlacementType.ORIGINAL.value,
|
||||
original_quality=True,
|
||||
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)
|
||||
|
||||
@@ -4,7 +4,7 @@ from PIL import Image
|
||||
import io
|
||||
import torch
|
||||
|
||||
from .common import image_to_base64
|
||||
from .common import image_to_base64, preprocess_image
|
||||
|
||||
class TailoredPortraitNode():
|
||||
@classmethod
|
||||
@@ -35,7 +35,10 @@ class TailoredPortraitNode():
|
||||
if api_key.strip() == "" or api_key.strip() == "BRIA_API_TOKEN":
|
||||
raise Exception("Please insert a valid API key.")
|
||||
|
||||
# Convert the image and mask directly to Base64 strings
|
||||
# Convert the image and mask directly to if isinstance(image, torch.Tensor):
|
||||
if isinstance(image, torch.Tensor):
|
||||
image = preprocess_image(image)
|
||||
|
||||
image_base64 = image_to_base64(image)
|
||||
|
||||
# Prepare the API request payload
|
||||
|
||||
@@ -38,7 +38,7 @@ class Text2ImageBaseNode():
|
||||
FUNCTION = "execute" # This is the method that will be executed
|
||||
|
||||
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(
|
||||
self, api_key, prompt, aspect_ratio, seed, negative_prompt,
|
||||
|
||||
@@ -0,0 +1,204 @@
|
||||
import requests
|
||||
import torch
|
||||
from ..common import postprocess_image, preprocess_image, image_to_base64
|
||||
|
||||
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"""
|
||||
headers = {"Content-Type": "application/json", "api_token": f"{api_key}"}
|
||||
|
||||
try:
|
||||
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
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
[project]
|
||||
name = "comfyui-bria-api"
|
||||
description = "Custom nodes for ComfyUI using BRIA's API."
|
||||
version = "2.0.3"
|
||||
version = "2.1.3"
|
||||
license = {file = "LICENSE"}
|
||||
|
||||
[project.urls]
|
||||
|
||||
+144
-136
@@ -1,18 +1,47 @@
|
||||
{
|
||||
"last_node_id": 42,
|
||||
"last_link_id": 65,
|
||||
"id": "1cdd7d4c-58b5-4047-947b-1977ad36d364",
|
||||
"revision": 0,
|
||||
"last_node_id": 14,
|
||||
"last_link_id": 11,
|
||||
"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",
|
||||
"pos": {
|
||||
"0": 591,
|
||||
"1": 593
|
||||
},
|
||||
"size": {
|
||||
"0": 315,
|
||||
"1": 314
|
||||
},
|
||||
"pos": [
|
||||
383.92852783203125,
|
||||
38.40964889526367
|
||||
],
|
||||
"size": [
|
||||
397.5969543457031,
|
||||
314
|
||||
],
|
||||
"flags": {},
|
||||
"order": 0,
|
||||
"mode": 0,
|
||||
@@ -22,10 +51,9 @@
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": [
|
||||
64,
|
||||
65
|
||||
],
|
||||
"slot_index": 0
|
||||
1,
|
||||
3
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "MASK",
|
||||
@@ -37,21 +65,21 @@
|
||||
"Node name for S&R": "LoadImage"
|
||||
},
|
||||
"widgets_values": [
|
||||
"A_bottle_of_perfume.png",
|
||||
"CAR.png",
|
||||
"image"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 39,
|
||||
"id": 2,
|
||||
"type": "LoadImage",
|
||||
"pos": {
|
||||
"0": 600,
|
||||
"1": 988
|
||||
},
|
||||
"size": {
|
||||
"0": 315,
|
||||
"1": 314
|
||||
},
|
||||
"pos": [
|
||||
369.0213623046875,
|
||||
415.34893798828125
|
||||
],
|
||||
"size": [
|
||||
450.83685302734375,
|
||||
314.0000305175781
|
||||
],
|
||||
"flags": {},
|
||||
"order": 1,
|
||||
"mode": 0,
|
||||
@@ -61,9 +89,8 @@
|
||||
"name": "IMAGE",
|
||||
"type": "IMAGE",
|
||||
"links": [
|
||||
59
|
||||
],
|
||||
"slot_index": 0
|
||||
4
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "MASK",
|
||||
@@ -75,21 +102,21 @@
|
||||
"Node name for S&R": "LoadImage"
|
||||
},
|
||||
"widgets_values": [
|
||||
"A_red_studio_with_a_shelf__close_up.png",
|
||||
"seed_713360865.png",
|
||||
"image"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 15,
|
||||
"id": 5,
|
||||
"type": "Note",
|
||||
"pos": {
|
||||
"0": 995.4524536132812,
|
||||
"1": 601.5353393554688
|
||||
},
|
||||
"size": {
|
||||
"0": 306.28387451171875,
|
||||
"1": 58
|
||||
},
|
||||
"pos": [
|
||||
951.4669189453125,
|
||||
5.085720062255859
|
||||
],
|
||||
"size": [
|
||||
210,
|
||||
88
|
||||
],
|
||||
"flags": {},
|
||||
"order": 2,
|
||||
"mode": 0,
|
||||
@@ -103,40 +130,14 @@
|
||||
"bgcolor": "#653"
|
||||
},
|
||||
{
|
||||
"id": 40,
|
||||
"id": 7,
|
||||
"type": "PreviewImage",
|
||||
"pos": {
|
||||
"0": 1408,
|
||||
"1": 623
|
||||
},
|
||||
"size": [
|
||||
210,
|
||||
246
|
||||
"pos": [
|
||||
1368.5450439453125,
|
||||
340.4889221191406
|
||||
],
|
||||
"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": [
|
||||
210,
|
||||
387.0335693359375,
|
||||
246
|
||||
],
|
||||
"flags": {},
|
||||
@@ -146,25 +147,26 @@
|
||||
{
|
||||
"name": "images",
|
||||
"type": "IMAGE",
|
||||
"link": 63
|
||||
"link": 6
|
||||
}
|
||||
],
|
||||
"outputs": [],
|
||||
"properties": {
|
||||
"Node name for S&R": "PreviewImage"
|
||||
}
|
||||
},
|
||||
"widgets_values": []
|
||||
},
|
||||
{
|
||||
"id": 36,
|
||||
"type": "ShotByTextNode",
|
||||
"pos": {
|
||||
"0": 996,
|
||||
"1": 736
|
||||
},
|
||||
"size": {
|
||||
"0": 315,
|
||||
"1": 106
|
||||
},
|
||||
"id": 3,
|
||||
"type": "ShotByTextOriginal",
|
||||
"pos": [
|
||||
941.8839111328125,
|
||||
149.14889526367188
|
||||
],
|
||||
"size": [
|
||||
273.388671875,
|
||||
202
|
||||
],
|
||||
"flags": {},
|
||||
"order": 3,
|
||||
"mode": 0,
|
||||
@@ -172,7 +174,7 @@
|
||||
{
|
||||
"name": "image",
|
||||
"type": "IMAGE",
|
||||
"link": 64
|
||||
"link": 1
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
@@ -180,31 +182,34 @@
|
||||
"name": "output_image",
|
||||
"type": "IMAGE",
|
||||
"links": [
|
||||
62
|
||||
],
|
||||
"slot_index": 0
|
||||
5
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "ShotByTextNode"
|
||||
"Node name for S&R": "ShotByTextOriginal"
|
||||
},
|
||||
"widgets_values": [
|
||||
"a beautiful sunset",
|
||||
1,
|
||||
"BRIA_API_TOKEN",
|
||||
"sea",
|
||||
"fast",
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
""
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": 37,
|
||||
"type": "ShotByImageNode",
|
||||
"pos": {
|
||||
"0": 999,
|
||||
"1": 932
|
||||
},
|
||||
"size": {
|
||||
"0": 315,
|
||||
"1": 102
|
||||
},
|
||||
"id": 4,
|
||||
"type": "ShotByImageOriginal",
|
||||
"pos": [
|
||||
945.0781860351562,
|
||||
441.9688415527344
|
||||
],
|
||||
"size": [
|
||||
272.0703125,
|
||||
174
|
||||
],
|
||||
"flags": {},
|
||||
"order": 4,
|
||||
"mode": 0,
|
||||
@@ -212,12 +217,12 @@
|
||||
{
|
||||
"name": "image",
|
||||
"type": "IMAGE",
|
||||
"link": 65
|
||||
"link": 3
|
||||
},
|
||||
{
|
||||
"name": "ref_image",
|
||||
"type": "IMAGE",
|
||||
"link": 59
|
||||
"link": 4
|
||||
}
|
||||
],
|
||||
"outputs": [
|
||||
@@ -225,58 +230,60 @@
|
||||
"name": "output_image",
|
||||
"type": "IMAGE",
|
||||
"links": [
|
||||
63
|
||||
],
|
||||
"slot_index": 0
|
||||
6
|
||||
]
|
||||
}
|
||||
],
|
||||
"properties": {
|
||||
"Node name for S&R": "ShotByImageNode"
|
||||
"Node name for S&R": "ShotByImageOriginal"
|
||||
},
|
||||
"widgets_values": [
|
||||
0,
|
||||
""
|
||||
"BRIA_API_TOKEN",
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
1
|
||||
]
|
||||
}
|
||||
],
|
||||
"links": [
|
||||
[
|
||||
59,
|
||||
39,
|
||||
1,
|
||||
1,
|
||||
0,
|
||||
37,
|
||||
3,
|
||||
0,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
3,
|
||||
1,
|
||||
0,
|
||||
4,
|
||||
0,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
4,
|
||||
2,
|
||||
0,
|
||||
4,
|
||||
1,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
62,
|
||||
36,
|
||||
5,
|
||||
3,
|
||||
0,
|
||||
40,
|
||||
6,
|
||||
0,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
63,
|
||||
37,
|
||||
6,
|
||||
4,
|
||||
0,
|
||||
41,
|
||||
0,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
64,
|
||||
42,
|
||||
0,
|
||||
36,
|
||||
0,
|
||||
"IMAGE"
|
||||
],
|
||||
[
|
||||
65,
|
||||
42,
|
||||
0,
|
||||
37,
|
||||
7,
|
||||
0,
|
||||
"IMAGE"
|
||||
]
|
||||
@@ -285,12 +292,13 @@
|
||||
"config": {},
|
||||
"extra": {
|
||||
"ds": {
|
||||
"scale": 0.9849732675807669,
|
||||
"scale": 0.7513148009015777,
|
||||
"offset": [
|
||||
-339.6686422794803,
|
||||
-496.4354678014682
|
||||
11.112206386364164,
|
||||
66.47311795454547
|
||||
]
|
||||
}
|
||||
},
|
||||
"frontendVersion": "1.25.11"
|
||||
},
|
||||
"version": 0.4
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user