From 6a94e8b91b8e95d3c8b10e2ca806998b5f23ceb4 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Wed, 3 Dec 2025 10:23:07 +0000 Subject: [PATCH 1/6] WAI-4253 --- __init__.py | 20 +++- nodes/__init__.py | 6 + .../remove_video_background_node.py | 97 ++++++++++++++++ .../video_nodes/video_erase_elements_node.py | 97 ++++++++++++++++ .../video_increase_resolution_node.py | 95 ++++++++++++++++ .../video_mask_by_key_points_node.py | 101 +++++++++++++++++ .../video_nodes/video_mask_by_prompt_node.py | 95 ++++++++++++++++ .../video_solid_color_background_node.py | 107 ++++++++++++++++++ 8 files changed, 617 insertions(+), 1 deletion(-) create mode 100644 nodes/video_nodes/remove_video_background_node.py create mode 100644 nodes/video_nodes/video_erase_elements_node.py create mode 100644 nodes/video_nodes/video_increase_resolution_node.py create mode 100644 nodes/video_nodes/video_mask_by_key_points_node.py create mode 100644 nodes/video_nodes/video_mask_by_prompt_node.py create mode 100644 nodes/video_nodes/video_solid_color_background_node.py diff --git a/__init__.py b/__init__.py index 0a6e81d..7b6dc9d 100644 --- a/__init__.py +++ b/__init__.py @@ -26,7 +26,13 @@ from .nodes import ( ShotByTextManualPlacementNode, ShotByTextManualPaddingNode, ShotByTextCustomCoordinatesNode, - AttributionByImageNode + AttributionByImageNode, + RemoveVideoBackgroundNode, + VideoSolidColorBackgroundNode, + VideoMaskByPromptNode, + VideoMaskByKeyPointsNode, + VideoIncreaseResolutionNode, + VideoEraseElementsNode, ) # Map the node class to a name used internally by ComfyUI @@ -59,6 +65,12 @@ NODE_CLASS_MAPPINGS = { "AttributionByImageNode": AttributionByImageNode, "GenerateImageNodeV2": GenerateImageNodeV2, "RefineImageNodeV2": RefineImageNodeV2, + "RemoveVideoBackgroundNode":RemoveVideoBackgroundNode, + "VideoSolidColorBackgroundNode":VideoSolidColorBackgroundNode, + "VideoMaskByPromptNode":VideoMaskByPromptNode, + "VideoMaskByKeyPointsNode":VideoMaskByKeyPointsNode, + "VideoIncreaseResolutionNode":VideoIncreaseResolutionNode, + "VideoEraseElementsNode":VideoEraseElementsNode, } # Map the node display name to the one shown in the ComfyUI node interface NODE_DISPLAY_NAME_MAPPINGS = { @@ -90,4 +102,10 @@ NODE_DISPLAY_NAME_MAPPINGS = { "AttributionByImageNode": "Attribution By Image Node", "GenerateImageNodeV2": "Generate Image", "RefineImageNodeV2": "Refine and Regenerate Image", + "RemoveVideoBackgroundNode": "Bria Remove Video 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", } diff --git a/nodes/__init__.py b/nodes/__init__.py index 5350502..d20292a 100644 --- a/nodes/__init__.py +++ b/nodes/__init__.py @@ -28,3 +28,9 @@ 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.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 diff --git a/nodes/video_nodes/remove_video_background_node.py b/nodes/video_nodes/remove_video_background_node.py new file mode 100644 index 0000000..1f7519d --- /dev/null +++ b/nodes/video_nodes/remove_video_background_node.py @@ -0,0 +1,97 @@ +import requests +from ..common import deserialize_and_get_comfy_key, poll_status_until_completed + +class RemoveVideoBackgroundNode(): + """ + Bria Remove Video Background Node + + This node removes the background from videos using the Bria API. + It accepts a publicly accessible video URL and returns a processed video URL + with the background removed. + + Supported input resolution: up to 16000x16000 (16K) + + Parameters: + - video_url: Publicly accessible URL of the input video + - api_key: Your Bria API token + - output_container_and_codec: Output video format and codec (default: webm_vp9) + """ + @classmethod + def INPUT_TYPES(self): + return { + "required": { + "video_url": ("STRING", {"default": ""}), + "api_key": ("STRING", {"default": "BRIA_API_TOKEN"}), + }, + "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"}), + } + } + + RETURN_TYPES = ("STRING",) + RETURN_NAMES = ("video_url_response",) + CATEGORY = "API Nodes" + FUNCTION = "execute" + + def __init__(self): + self.api_url = "https://engine.prod.bria-api.com/v2/video/edit/remove_background" # Video RMBG API URL + + # Define the execute method as expected by ComfyUI + def execute(self, video_url, api_key, preserve_audio, output_container_and_codec="webm_vp9"): + if api_key.strip() == "" or api_key.strip() == "BRIA_API_TOKEN": + raise Exception("Please insert a valid API key.") + api_key = deserialize_and_get_comfy_key(api_key) + + # Prepare the API request payload + payload = { + "video": video_url, + "preserve_audio": preserve_audio, + "output_container_and_codec": output_container_and_codec + } + + 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 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}") + + + + 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}") + diff --git a/nodes/video_nodes/video_erase_elements_node.py b/nodes/video_nodes/video_erase_elements_node.py new file mode 100644 index 0000000..4a32dd4 --- /dev/null +++ b/nodes/video_nodes/video_erase_elements_node.py @@ -0,0 +1,97 @@ +import requests +from ..common import deserialize_and_get_comfy_key, poll_status_until_completed + +class VideoEraseElementsNode(): + """ + Bria Video Erase Elements Node + + This node erases specific elements from videos based on a text prompt using the Bria API. + It accepts a publicly accessible video URL and a text instruction describing the object + to be masked and removed. + + Parameters: + - video_url: Publicly accessible URL of the input video + - api_key: Your Bria API token + - prompt: Text instruction describing the object to be masked + - output_container_and_codec: Output video format and codec (default: mp4_h264) + - preserve_audio: Audio preservation (default: True) + """ + @classmethod + def INPUT_TYPES(self): + return { + "required": { + "video_url": ("STRING", {"default": ""}), + "mask_url": ("STRING", {"default": ""}), + "prompt": ("STRING", {"default": ""}), + "api_key": ("STRING", {"default": "BRIA_API_TOKEN"}), + }, + "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 = ("video_url_response",) + CATEGORY = "API Nodes" + FUNCTION = "execute" + + def __init__(self): + self.api_url = "https://engine.prod.bria-api.com/v2/video/edit/erase" + + def execute(self, video_url, mask_url, prompt, api_key, 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.") + api_key = deserialize_and_get_comfy_key(api_key) + + # Prepare the API request payload + payload = { + "video": video_url, + "mask":mask_url, + "prompt": prompt, + "output_container_and_codec": output_container_and_codec, + "preserve_audio": preserve_audio + } + + 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 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}") + + 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}") \ No newline at end of file diff --git a/nodes/video_nodes/video_increase_resolution_node.py b/nodes/video_nodes/video_increase_resolution_node.py new file mode 100644 index 0000000..d02cbfb --- /dev/null +++ b/nodes/video_nodes/video_increase_resolution_node.py @@ -0,0 +1,95 @@ +import requests +from ..common import deserialize_and_get_comfy_key, poll_status_until_completed + +class VideoIncreaseResolutionNode(): + """ + Bria Video Increase Resolution Node + + This node increases the resolution of videos using the Bria API. + It accepts a publicly accessible video URL and returns a processed video URL + with increased resolution. + + Parameters: + - video_url: Publicly accessible URL of the input video + - api_key: Your Bria API token + - desired_increase: Integer scale factor for upscaling (2 or 4) + - output_container_and_codec: Output video format and codec (default: mp4_h264) + - preserve_audio: Audio preservation (default: True) + """ + @classmethod + def INPUT_TYPES(self): + return { + "required": { + "video_url": ("STRING", {"default": ""}), + "api_key": ("STRING", {"default": "BRIA_API_TOKEN"}), + }, + "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 = ("video_url_response",) + 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, video_url, api_key, desired_increase, 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.") + api_key = deserialize_and_get_comfy_key(api_key) + + # Prepare the API request payload + payload = { + "video": video_url, + "desired_increase": desired_increase, + "output_container_and_codec": output_container_and_codec, + "preserve_audio": preserve_audio + } + + 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 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}") + + 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}") \ No newline at end of file diff --git a/nodes/video_nodes/video_mask_by_key_points_node.py b/nodes/video_nodes/video_mask_by_key_points_node.py new file mode 100644 index 0000000..7ecf46f --- /dev/null +++ b/nodes/video_nodes/video_mask_by_key_points_node.py @@ -0,0 +1,101 @@ +import requests +from ..common import deserialize_and_get_comfy_key, poll_status_until_completed +import json + +class VideoMaskByKeyPointsNode(): + """ + Bria Video Mask by Key Points Node + + This node generates a mask for specific elements in videos based on key point coordinates + using the Bria API. It accepts a publicly accessible video URL and an array of coordinate + objects defining the mask hints. + + Parameters: + - video_url: Publicly accessible URL of the input video + - api_key: Your Bria API token + - key_points: Array of coordinate objects defining the mask hints (JSON format) + - output_container_and_codec: Output video format and codec (default: mp4_h264) + - preserve_audio: Audio preservation (default: True) + """ + @classmethod + def INPUT_TYPES(self): + return { + "required": { + "video_url": ("STRING", {"default": ""}), + "key_points": ("STRING", {"default": "[]", "multiline": True}), + "api_key": ("STRING", {"default": "BRIA_API_TOKEN"}), + }, + "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_response",) + 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, video_url, key_points, api_key, 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.") + api_key = deserialize_and_get_comfy_key(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}") + + # Prepare the API request payload + payload = { + "video": video_url, + "key_points": key_points_array, + "output_container_and_codec": output_container_and_codec, + "preserve_audio": preserve_audio + } + + 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 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}") + + 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}") \ No newline at end of file diff --git a/nodes/video_nodes/video_mask_by_prompt_node.py b/nodes/video_nodes/video_mask_by_prompt_node.py new file mode 100644 index 0000000..ef43333 --- /dev/null +++ b/nodes/video_nodes/video_mask_by_prompt_node.py @@ -0,0 +1,95 @@ +import requests +from ..common import deserialize_and_get_comfy_key, poll_status_until_completed + +class VideoMaskByPromptNode(): + """ + Bria Video Mask by Prompt Node + + This node generates a mask for specific elements in videos based on a text prompt + using the Bria API. It accepts a publicly accessible video URL and a text instruction + describing the object to be masked. + + Parameters: + - video_url: Publicly accessible URL of the input video + - api_key: Your Bria API token + - prompt: Text instruction describing the object to be masked + - output_container_and_codec: Output video format and codec (default: mp4_h264) + - preserve_audio: Audio preservation (default: True) + """ + @classmethod + def INPUT_TYPES(self): + return { + "required": { + "video_url": ("STRING", {"default": ""}), + "prompt": ("STRING", {"default": ""}), + "api_key": ("STRING", {"default": "BRIA_API_TOKEN"}), + }, + "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_response",) + 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, video_url, prompt, api_key, 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.") + api_key = deserialize_and_get_comfy_key(api_key) + + # Prepare the API request payload + payload = { + "video": video_url, + "prompt": prompt, + "output_container_and_codec": output_container_and_codec, + "preserve_audio": preserve_audio + } + + 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 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}") + + 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}") \ No newline at end of file diff --git a/nodes/video_nodes/video_solid_color_background_node.py b/nodes/video_nodes/video_solid_color_background_node.py new file mode 100644 index 0000000..b38d13d --- /dev/null +++ b/nodes/video_nodes/video_solid_color_background_node.py @@ -0,0 +1,107 @@ +import requests +from ..common import deserialize_and_get_comfy_key, poll_status_until_completed + +class VideoSolidColorBackgroundNode(): + """ + Bria Video Solid Color Background Node + + This node removes the background from videos and replaces it with a solid color + using the Bria API. It accepts a publicly accessible video URL and returns a + processed video URL with the specified background color. + + Parameters: + - video_url: Publicly accessible URL of the input video + - api_key: Your Bria API token + - background_color: Predefined color string (default: Transparent) + - output_container_and_codec: Output video format and codec (default: mp4_h264) + - preserve_audio: Audio preservation (default: True) + """ + @classmethod + def INPUT_TYPES(self): + return { + "required": { + "video_url": ("STRING", {"default": ""}), + "api_key": ("STRING", {"default": "BRIA_API_TOKEN"}), + }, + "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": "mp4_h264"}), + "preserve_audio": ("BOOLEAN", {"default": True}), + } + } + + RETURN_TYPES = ("STRING",) + RETURN_NAMES = ("video_url_response",) + 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, video_url, api_key, background_color="Transparent", 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.") + api_key = deserialize_and_get_comfy_key(api_key) + + # Prepare the API request payload + payload = { + "video": video_url, + "background_color": background_color, + "output_container_and_codec": output_container_and_codec, + "preserve_audio": preserve_audio + } + + 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 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}") + + 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}") \ No newline at end of file From a611bddb9f20c87f7307cef87ceb86ade9a91458 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Mon, 8 Dec 2025 11:08:19 +0000 Subject: [PATCH 2/6] support load, preview videos nodes --- __init__.py | 6 + nodes/__init__.py | 2 + nodes/video_nodes/load_video.py | 133 ++++++++ nodes/video_nodes/preview_video.py | 284 ++++++++++++++++++ .../remove_video_background_node.py | 79 +++-- .../video_nodes/video_erase_elements_node.py | 92 ++++-- .../video_increase_resolution_node.py | 84 ++++-- .../video_mask_by_key_points_node.py | 84 ++++-- .../video_nodes/video_mask_by_prompt_node.py | 84 ++++-- .../video_solid_color_background_node.py | 79 +++-- nodes/video_nodes/video_utils.py | 230 ++++++++++++++ 11 files changed, 1012 insertions(+), 145 deletions(-) create mode 100644 nodes/video_nodes/load_video.py create mode 100644 nodes/video_nodes/preview_video.py create mode 100644 nodes/video_nodes/video_utils.py diff --git a/__init__.py b/__init__.py index 7b6dc9d..28698f4 100644 --- a/__init__.py +++ b/__init__.py @@ -33,6 +33,8 @@ from .nodes import ( VideoMaskByKeyPointsNode, VideoIncreaseResolutionNode, VideoEraseElementsNode, + LoadVideoFramesNode, + PreviewVideoFramesNode ) # Map the node class to a name used internally by ComfyUI @@ -71,6 +73,8 @@ NODE_CLASS_MAPPINGS = { "VideoMaskByKeyPointsNode":VideoMaskByKeyPointsNode, "VideoIncreaseResolutionNode":VideoIncreaseResolutionNode, "VideoEraseElementsNode":VideoEraseElementsNode, + "LoadVideoFramesNode":LoadVideoFramesNode, + "PreviewVideoFramesNode":PreviewVideoFramesNode } # Map the node display name to the one shown in the ComfyUI node interface NODE_DISPLAY_NAME_MAPPINGS = { @@ -108,4 +112,6 @@ NODE_DISPLAY_NAME_MAPPINGS = { "VideoMaskByKeyPointsNode":"Bria Video Mask By Key Points", "VideoIncreaseResolutionNode":"Bria Video Increase Resolution", "VideoEraseElementsNode":"Bria Video Erase Elements", + "LoadVideoFramesNode":"Load Video Node", + "PreviewVideoFramesNode":"Preview Video Node" } diff --git a/nodes/__init__.py b/nodes/__init__.py index d20292a..ae04659 100644 --- a/nodes/__init__.py +++ b/nodes/__init__.py @@ -34,3 +34,5 @@ from .video_nodes.video_solid_color_background_node import VideoSolidColorBackgr 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 import PreviewVideoFramesNode diff --git a/nodes/video_nodes/load_video.py b/nodes/video_nodes/load_video.py new file mode 100644 index 0000000..56e15bd --- /dev/null +++ b/nodes/video_nodes/load_video.py @@ -0,0 +1,133 @@ +import os +import torch +import numpy as np +import folder_paths +import av + +class LoadVideoFramesNode: + """ + Bria Load Video Frames Node + + This node loads a video file and extracts all frames as images. + The user can upload a video and it will be converted into a batch of image tensors + that can be used with other nodes in the pipeline. + + Parameters: + - video: Video file to load from input directory + + Returns: + - frames: Batch of images (IMAGE tensor format) + - frame_count: Total number of frames extracted + - fps: Frames per second of the video + """ + + @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}), + }, + "optional": { + "max_frames": ("INT", { + "default": 0, + "min": 0, + "max": 10000, + "step": 1, + "tooltip": "Maximum number of frames to extract (0 = all frames)" + }), + } + } + + RETURN_TYPES = ("IMAGE", "INT", "FLOAT", "STRING") + RETURN_NAMES = ("frames", "frame_count", "fps", "video_format") + FUNCTION = "load_video" + CATEGORY = "API Nodes" + DESCRIPTION = "Loads a video file and extracts frames as a batch of images." + + def load_video(self, video, max_frames=0): + video_path = folder_paths.get_annotated_filepath(video) + video_format = os.path.splitext(video)[1].lower().lstrip('.') + + if not os.path.exists(video_path): + raise FileNotFoundError(f"Video file not found: {video_path}") + + frames = [] + frame_index = 0 + extracted_count = 0 + + try: + # Open video using PyAV + container = av.open(video_path) + + # Get video stream + video_stream = None + for stream in container.streams: + if stream.type == 'video': + video_stream = stream + break + + if video_stream is None: + raise ValueError(f"No video stream found in {video}") + + # Get FPS + if video_stream.average_rate is not None: + fps = float(video_stream.average_rate) + + print(f"Loading video: {video}") + print(f"Video FPS: {fps}") + print(f"Video resolution: {video_stream.width}x{video_stream.height}") + + # Decode frames + for frame in container.decode(video_stream): + # Convert frame to RGB PIL Image + img = frame.to_image() + + # Convert PIL Image to numpy array + img_array = np.array(img.convert('RGB')).astype(np.float32) / 255.0 + + # Convert to torch tensor with shape [H, W, C] + img_tensor = torch.from_numpy(img_array) + + frames.append(img_tensor) + extracted_count += 1 + frame_index += 1 + + # Check if we've reached max_frames + if max_frames > 0 and extracted_count >= max_frames: + break + + container.close() + + if len(frames) == 0: + raise ValueError(f"No frames could be extracted from {video}") + + # Stack frames into a single tensor with shape [B, H, W, C] + frames_tensor = torch.stack(frames, dim=0) + + print(f"Extracted {extracted_count} frames from video") + print(f"Output tensor shape: {frames_tensor.shape}") + print(f"Video format: {video_format}") + + return (frames_tensor, extracted_count, fps, video_format) + + except Exception as e: + raise Exception(f"Error loading video {video}: {str(e)}") + + @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 \ No newline at end of file diff --git a/nodes/video_nodes/preview_video.py b/nodes/video_nodes/preview_video.py new file mode 100644 index 0000000..d79c3e5 --- /dev/null +++ b/nodes/video_nodes/preview_video.py @@ -0,0 +1,284 @@ +import os +import json +import random +import torch +import numpy as np +from PIL import Image +import folder_paths +from comfy.cli_args import args +import av +from fractions import Fraction + +class PreviewVideoFramesNode: + """ + Bria Preview Video Frames Node + + This node takes a batch of image frames and creates an actual video file + that can be played as animation in the ComfyUI interface. + + Parameters: + - frames: Batch of image tensors to preview + - fps: Frames per second for animation display + - format: Video format (webp or mp4) + """ + + def __init__(self): + self.output_dir = folder_paths.get_temp_directory() + self.type = "temp" + self.prefix_append = "_video_preview_" + ''.join(random.choice("abcdefghijklmnopqrstupvxyz") for _ in range(5)) + + @classmethod + def INPUT_TYPES(cls): + return { + "required": { + "frames": ("IMAGE", {"tooltip": "Batch of image frames to preview"}), + }, + "optional": { + "fps": ("FLOAT", { + "default": 30.0, + "min": 1.0, + "max": 120.0, + "step": 0.1, + "tooltip": "Frames per second for animation playback" + }), + "format": ([ + "mp4_h264", + "mp4_h265", + "webm_vp9", + "mov_h265", + "mov_proresks", + "mkv_h264", + "mkv_h265", + "mkv_vp9", + "gif", + "webp" + ], { + "default": "mp4_h264", + "tooltip": "Output video format - matches Bria API formats" + }), + "quality": (["high", "medium", "low"], { + "default": "medium", + "tooltip": "Video quality (affects file size)" + }), + "max_preview_frames": ("INT", { + "default": 0, + "min": 0, + "max": 1000, + "step": 1, + "tooltip": "Maximum frames to preview (0 = all frames)" + }), + }, + "hidden": { + "prompt": "PROMPT", + "extra_pnginfo": "EXTRA_PNGINFO" + }, + } + + RETURN_TYPES = () + FUNCTION = "preview_frames" + OUTPUT_NODE = True + CATEGORY = "API Nodes" + DESCRIPTION = "Previews video frames as an animated video in the ComfyUI interface." + + def preview_frames(self, frames, fps=30.0, format="mp4_h264", quality="medium", max_preview_frames=0, prompt=None, extra_pnginfo=None): + """ + Preview video frames as animated video + + Args: + frames: Batch of image tensors [B, H, W, C] + fps: Frames per second for display + format: Output format (supports all Bria API formats) + quality: Video quality (high, medium, low) + max_preview_frames: Maximum number of frames to preview (0 = all) + prompt: Hidden parameter for ComfyUI workflow + extra_pnginfo: Hidden parameter for ComfyUI metadata + + Returns: + dict: UI output with video file for animation + """ + filename_prefix = "VideoPreview" + filename_prefix += self.prefix_append + + # Limit frames if specified + total_frames = frames.shape[0] + if max_preview_frames > 0 and total_frames > max_preview_frames: + # Sample frames evenly across the video + indices = torch.linspace(0, total_frames - 1, max_preview_frames).long() + frames = frames[indices] + print(f"Preview limited to {max_preview_frames} frames (sampled from {total_frames} total frames)") + else: + print(f"Previewing {total_frames} frames as animated video (format: {format})") + + # Get save path + full_output_folder, filename, counter, subfolder, filename_prefix = folder_paths.get_save_image_path( + filename_prefix, + self.output_dir, + frames[0].shape[1], # width + frames[0].shape[0] # height + ) + + # Choose format and create video + if format == "webp": + result_file = self._save_as_webp(frames, full_output_folder, filename, counter, fps, quality, prompt, extra_pnginfo) + elif format == "gif": + result_file = self._save_as_gif(frames, full_output_folder, filename, counter, fps, quality, prompt, extra_pnginfo) + else: + # All video formats (mp4, webm, mov, mkv) + result_file = self._save_as_video(frames, full_output_folder, filename, counter, fps, format, quality, prompt, extra_pnginfo) + + print(f"Saved animated video: {result_file}") + + # Return UI output with animation + return { + "ui": { + "images": [{ + "filename": result_file, + "subfolder": subfolder, + "type": self.type + }], + "animated": (True,) + } + } + + def _get_format_config(self, format_name): + """ + Get container, codec, and file extension for a given format + + Returns: (container, codec, extension, pixel_format) + """ + format_map = { + "mp4_h264": ("mp4", "libx264", "mp4", "yuv420p"), + "mp4_h265": ("mp4", "libx265", "mp4", "yuv420p"), + "webm_vp9": ("webm", "libvpx-vp9", "webm", "yuv420p"), + "mov_h265": ("mov", "libx265", "mov", "yuv420p"), + "mov_proresks": ("mov", "prores_ks", "mov", "yuv422p10le"), + "mkv_h264": ("matroska", "libx264", "mkv", "yuv420p"), + "mkv_h265": ("matroska", "libx265", "mkv", "yuv420p"), + "mkv_vp9": ("matroska", "libvpx-vp9", "mkv", "yuv420p"), + } + return format_map.get(format_name, ("mp4", "libx264", "mp4", "yuv420p")) + + + def _save_as_webp(self, frames, output_folder, filename, counter, fps, quality, prompt, extra_pnginfo): + """Save frames as animated WEBP""" + file = f"{filename}_{counter:05}_.webp" + + # Convert frames to PIL images + pil_images = [] + for frame in frames: + frame_array = 255.0 * frame.cpu().numpy() + img = Image.fromarray(np.clip(frame_array, 0, 255).astype(np.uint8)) + pil_images.append(img) + + # Add metadata to first frame + metadata = pil_images[0].getexif() + if not args.disable_metadata: + if prompt is not None: + metadata[0x0110] = "prompt:{}".format(json.dumps(prompt)) + if extra_pnginfo is not None: + inital_exif = 0x010f + for x in extra_pnginfo: + metadata[inital_exif] = "{}:{}".format(x, json.dumps(extra_pnginfo[x])) + inital_exif -= 1 + + # Quality mapping + quality_values = {"high": 95, "medium": 85, "low": 70} + webp_quality = quality_values.get(quality, 85) + + # Save as animated WEBP + duration = int(1000.0 / fps) # duration in milliseconds + pil_images[0].save( + os.path.join(output_folder, file), + save_all=True, + duration=duration, + append_images=pil_images[1:], + exif=metadata, + lossless=False, + quality=webp_quality, + method=4 + ) + + return file + + def _save_as_gif(self, frames, output_folder, filename, counter, fps, quality, prompt, extra_pnginfo): + """Save frames as animated GIF""" + file = f"{filename}_{counter:05}_.gif" + + # Convert frames to PIL images + pil_images = [] + for frame in frames: + frame_array = 255.0 * frame.cpu().numpy() + img = Image.fromarray(np.clip(frame_array, 0, 255).astype(np.uint8)) + pil_images.append(img) + + # Save as animated GIF + duration = int(1000.0 / fps) # duration in milliseconds + optimize = quality == "low" # Optimize for low quality to reduce size + pil_images[0].save( + os.path.join(output_folder, file), + save_all=True, + duration=duration, + append_images=pil_images[1:], + loop=0, + optimize=optimize + ) + + return file + + def _save_as_video(self, frames, output_folder, filename, counter, fps, format_name, quality, prompt, extra_pnginfo): + """ + Save frames as video using PyAV with support for all Bria API formats + + Supports: mp4_h264, mp4_h265, webm_vp9, mov_h265, mov_proresks, mkv_h264, mkv_h265, mkv_vp9 + """ + # Get format configuration + container_format, codec, extension, pix_fmt = self._get_format_config(format_name) + + file = f"{filename}_{counter:05}_.{extension}" + filepath = os.path.join(output_folder, file) + + print(f"Encoding video: container={container_format}, codec={codec}, quality={quality}") + + # Open video container + container = av.open(filepath, mode="w", format=container_format) + + # Add metadata if enabled + if not args.disable_metadata: + if prompt is not None: + container.metadata["prompt"] = json.dumps(prompt) + if extra_pnginfo is not None: + for x in extra_pnginfo: + container.metadata[x] = json.dumps(extra_pnginfo[x]) + + # Create video stream + stream = container.add_stream(codec, rate=Fraction(round(fps * 1000), 1000)) + stream.width = frames.shape[2] # width + stream.height = frames.shape[1] # height + stream.pix_fmt = pix_fmt + + # Encode frames + for i, frame in enumerate(frames): + # Convert tensor to numpy array and scale to 0-255 + frame_array = torch.clamp(frame * 255, min=0, max=255) + frame_np = frame_array.to(device=torch.device("cpu"), dtype=torch.uint8).numpy() + + # Create video frame + video_frame = av.VideoFrame.from_ndarray(frame_np, format="rgb24") + + # Encode frame + for packet in stream.encode(video_frame): + container.mux(packet) + + # Progress update for long videos + if (i + 1) % 100 == 0: + print(f"Encoded {i + 1}/{frames.shape[0]} frames...") + + # Flush remaining packets + for packet in stream.encode(): + container.mux(packet) + + container.close() + + print(f"Video encoding complete: {file}") + + return file \ No newline at end of file diff --git a/nodes/video_nodes/remove_video_background_node.py b/nodes/video_nodes/remove_video_background_node.py index 1f7519d..19adbb4 100644 --- a/nodes/video_nodes/remove_video_background_node.py +++ b/nodes/video_nodes/remove_video_background_node.py @@ -1,18 +1,20 @@ +import os import requests from ..common import deserialize_and_get_comfy_key, poll_status_until_completed +from .video_utils import frames_to_video, video_to_frames, upload_video_to_s3 class RemoveVideoBackgroundNode(): """ Bria Remove Video Background Node This node removes the background from videos using the Bria API. - It accepts a publicly accessible video URL and returns a processed video URL - with the background removed. + It accepts video frames from the Load Video node, uploads to S3, + processes via API, and returns the processed frames for preview. Supported input resolution: up to 16000x16000 (16K) Parameters: - - video_url: Publicly accessible URL of the input video + - frames: Batch of image frames from Load Video node - api_key: Your Bria API token - output_container_and_codec: Output video format and codec (default: webm_vp9) """ @@ -20,11 +22,11 @@ class RemoveVideoBackgroundNode(): def INPUT_TYPES(self): return { "required": { - "video_url": ("STRING", {"default": ""}), + "frames": ("IMAGE", {"tooltip": "Batch of video frames"}), "api_key": ("STRING", {"default": "BRIA_API_TOKEN"}), }, "optional": { - "preserve_audio": ("BOOLEAN", {"default":True}), + "preserve_audio": ("BOOLEAN", {"default": False}), "output_container_and_codec": ([ "mp4_h264", "mp4_h265", @@ -36,36 +38,56 @@ class RemoveVideoBackgroundNode(): "mkv_vp9", "gif" ], {"default": "webm_vp9"}), + "video_format": ("STRING", { + "default": "mp4", + "tooltip": "Original video format from Load Video node" + }), + "fbs": ("FLOAT", { + "default": "30", + "tooltip": "Original video format from Load Video node" + }), } } - RETURN_TYPES = ("STRING",) - RETURN_NAMES = ("video_url_response",) + RETURN_TYPES = ("IMAGE", "INT", "FLOAT") + RETURN_NAMES = ("frames", "frame_count", "fps") CATEGORY = "API Nodes" FUNCTION = "execute" def __init__(self): - self.api_url = "https://engine.prod.bria-api.com/v2/video/edit/remove_background" # Video RMBG API URL + self.api_url = "https://engine.prod.bria-api.com/v2/video/edit/remove_background" # Define the execute method as expected by ComfyUI - def execute(self, video_url, api_key, preserve_audio, output_container_and_codec="webm_vp9"): + def execute(self, frames, api_key, fbs, video_format, preserve_audio=False, output_container_and_codec="webm_vp9"): if api_key.strip() == "" or api_key.strip() == "BRIA_API_TOKEN": raise Exception("Please insert a valid API key.") api_key = deserialize_and_get_comfy_key(api_key) - # Prepare the API request payload - payload = { - "video": video_url, - "preserve_audio": preserve_audio, - "output_container_and_codec": output_container_and_codec - } - - headers = { - "Content-Type": "application/json", - "api_token": f"{api_key}" - } - + print(f"Processing {frames.shape[0]} frames for background removal...") + + # Step 1: Convert frames to video + print("Step 1: Converting frames to video...") + video_path = frames_to_video(frames, fbs, video_format=video_format) + try: + # Step 2: Upload video to S3 + print("Step 2: Uploading video to S3...") + filename = f"input_video_{os.path.basename(video_path)}" + video_url = upload_video_to_s3(video_path, filename,api_key) + + # Step 3: Call Bria API for background removal + print("Step 3: Calling Bria API for background removal...") + payload = { + "video": video_url, + "preserve_audio": preserve_audio, + "output_container_and_codec": output_container_and_codec + } + + headers = { + "Content-Type": "application/json", + "api_token": f"{api_key}" + } + response = requests.post(self.api_url, json=payload, headers=headers) if response.status_code == 200 or response.status_code == 202: @@ -86,12 +108,23 @@ class RemoveVideoBackgroundNode(): print(f"Video processing completed. Result URL: {result_video_url}") + # Step 4: Download and convert processed video to frames + print("Step 4: Downloading and converting processed video to frames...") + result_frames, result_frame_count, result_fps = video_to_frames(result_video_url) - - return (result_video_url,) + print(f"Background removal complete! Processed {result_frame_count} frames.") + + return (result_frames, result_frame_count, result_fps) 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: + # Clean up temporary video file + try: + if os.path.exists(video_path): + os.unlink(video_path) + except: + pass diff --git a/nodes/video_nodes/video_erase_elements_node.py b/nodes/video_nodes/video_erase_elements_node.py index 4a32dd4..2e3f5ef 100644 --- a/nodes/video_nodes/video_erase_elements_node.py +++ b/nodes/video_nodes/video_erase_elements_node.py @@ -1,31 +1,37 @@ +import os import requests from ..common import deserialize_and_get_comfy_key, poll_status_until_completed +from .video_utils import frames_to_video, video_to_frames, upload_video_to_s3 class VideoEraseElementsNode(): """ Bria Video Erase Elements Node This node erases specific elements from videos based on a text prompt using the Bria API. - It accepts a publicly accessible video URL and a text instruction describing the object - to be masked and removed. + It accepts video frames from the Load Video node, uploads to S3, processes via API, + and returns the processed frames for preview. Parameters: - - video_url: Publicly accessible URL of the input video + - frames: Batch of image frames from Load Video node - api_key: Your Bria API token + - mask_url: Publicly accessible URL of the mask video (optional) - prompt: Text instruction describing the object to be masked - output_container_and_codec: Output video format and codec (default: mp4_h264) - - preserve_audio: Audio preservation (default: True) + - preserve_audio: Audio preservation (default: False) """ @classmethod def INPUT_TYPES(self): return { "required": { - "video_url": ("STRING", {"default": ""}), - "mask_url": ("STRING", {"default": ""}), + "frames": ("IMAGE", {"tooltip": "Batch of video frames"}), "prompt": ("STRING", {"default": ""}), "api_key": ("STRING", {"default": "BRIA_API_TOKEN"}), }, "optional": { + "mask_url": ("STRING", { + "default": "", + "tooltip": "URL of mask video (optional)" + }), "output_container_and_codec": ([ "mp4_h264", "mp4_h265", @@ -37,38 +43,59 @@ class VideoEraseElementsNode(): "mkv_vp9", "gif" ], {"default": "mp4_h264"}), - "preserve_audio": ("BOOLEAN", {"default": True}), + "preserve_audio": ("BOOLEAN", {"default": False}), + "video_format": ("STRING", { + "default": "mp4", + "tooltip": "Original video format from Load Video node" + }), + "fbs": ("FLOAT", { + "default": "30", + "tooltip": "Original video format from Load Video node" + }), } } - RETURN_TYPES = ("STRING",) - RETURN_NAMES = ("video_url_response",) + RETURN_TYPES = ("IMAGE", "INT", "FLOAT") + RETURN_NAMES = ("frames", "frame_count", "fps") CATEGORY = "API Nodes" FUNCTION = "execute" def __init__(self): self.api_url = "https://engine.prod.bria-api.com/v2/video/edit/erase" - def execute(self, video_url, mask_url, prompt, api_key, output_container_and_codec="mp4_h264", preserve_audio=True): + def execute(self, frames, prompt, api_key, fbs, video_format, mask_url="", output_container_and_codec="mp4_h264", + preserve_audio=False): if api_key.strip() == "" or api_key.strip() == "BRIA_API_TOKEN": raise Exception("Please insert a valid API key.") api_key = deserialize_and_get_comfy_key(api_key) - # Prepare the API request payload - payload = { - "video": video_url, - "mask":mask_url, - "prompt": prompt, - "output_container_and_codec": output_container_and_codec, - "preserve_audio": preserve_audio - } - - headers = { - "Content-Type": "application/json", - "api_token": f"{api_key}" - } - + print(f"Processing {frames.shape[0]} frames for element erasure...") + + # Step 1: Convert frames to video + print("Step 1: Converting frames to video...") + video_path = frames_to_video(frames, fbs, video_format=video_format) + try: + # Step 2: Upload video to S3 + print("Step 2: Uploading video to S3...") + filename = f"input_video_{os.path.basename(video_path)}" + video_url = upload_video_to_s3(video_path, filename, api_key) + + # Step 3: Call Bria API for element erasure + print("Step 3: Calling Bria API for element erasure...") + payload = { + "video": video_url, + "mask": mask_url, + "prompt": prompt, + "output_container_and_codec": output_container_and_codec, + "preserve_audio": preserve_audio + } + + headers = { + "Content-Type": "application/json", + "api_token": f"{api_key}" + } + response = requests.post(self.api_url, json=payload, headers=headers) if response.status_code == 200 or response.status_code == 202: @@ -89,9 +116,22 @@ class VideoEraseElementsNode(): print(f"Video processing completed. Result URL: {result_video_url}") - return (result_video_url,) + # Step 4: Download and convert processed video to frames + print("Step 4: Downloading and converting processed video to frames...") + result_frames, result_frame_count, result_fps = video_to_frames(result_video_url) + + print(f"Element erasure complete! Processed {result_frame_count} frames.") + + return (result_frames, result_frame_count, result_fps) else: raise Exception(f"Error: API request failed with status code {response.status_code} {response.text}") except Exception as e: - raise Exception(f"{e}") \ No newline at end of file + raise Exception(f"{e}") + finally: + # Clean up temporary video file + try: + if os.path.exists(video_path): + os.unlink(video_path) + except: + pass \ No newline at end of file diff --git a/nodes/video_nodes/video_increase_resolution_node.py b/nodes/video_nodes/video_increase_resolution_node.py index d02cbfb..9810679 100644 --- a/nodes/video_nodes/video_increase_resolution_node.py +++ b/nodes/video_nodes/video_increase_resolution_node.py @@ -1,26 +1,28 @@ +import os import requests from ..common import deserialize_and_get_comfy_key, poll_status_until_completed +from .video_utils import frames_to_video, video_to_frames, upload_video_to_s3 class VideoIncreaseResolutionNode(): """ Bria Video Increase Resolution Node This node increases the resolution of videos using the Bria API. - It accepts a publicly accessible video URL and returns a processed video URL - with increased resolution. + It accepts video frames from the Load Video node, uploads to S3, + processes via API, and returns the processed frames for preview. Parameters: - - video_url: Publicly accessible URL of the input video + - frames: Batch of image frames from Load Video node - api_key: Your Bria API token - desired_increase: Integer scale factor for upscaling (2 or 4) - output_container_and_codec: Output video format and codec (default: mp4_h264) - - preserve_audio: Audio preservation (default: True) + - preserve_audio: Audio preservation (default: False) """ @classmethod def INPUT_TYPES(self): return { "required": { - "video_url": ("STRING", {"default": ""}), + "frames": ("IMAGE", {"tooltip": "Batch of video frames"}), "api_key": ("STRING", {"default": "BRIA_API_TOKEN"}), }, "optional": { @@ -36,37 +38,58 @@ class VideoIncreaseResolutionNode(): "mkv_vp9", "gif" ], {"default": "mp4_h264"}), - "preserve_audio": ("BOOLEAN", {"default": True}), + "preserve_audio": ("BOOLEAN", {"default": False}), + "video_format": ("STRING", { + "default": "mp4", + "tooltip": "Original video format from Load Video node" + }), + "fbs": ("FLOAT", { + "default": "30", + "tooltip": "Original video format from Load Video node" + }), } } - RETURN_TYPES = ("STRING",) - RETURN_NAMES = ("video_url_response",) + RETURN_TYPES = ("IMAGE", "INT", "FLOAT") + RETURN_NAMES = ("frames", "frame_count", "fps") 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, video_url, api_key, desired_increase, output_container_and_codec="mp4_h264", preserve_audio=True): + def execute(self, frames, api_key, video_format, fbs, desired_increase='2', output_container_and_codec="mp4_h264", + preserve_audio=False): if api_key.strip() == "" or api_key.strip() == "BRIA_API_TOKEN": raise Exception("Please insert a valid API key.") api_key = deserialize_and_get_comfy_key(api_key) - # Prepare the API request payload - payload = { - "video": video_url, - "desired_increase": desired_increase, - "output_container_and_codec": output_container_and_codec, - "preserve_audio": preserve_audio - } - - headers = { - "Content-Type": "application/json", - "api_token": f"{api_key}" - } - + print(f"Processing {frames.shape[0]} frames for resolution increase...") + + # Step 1: Convert frames to video + print("Step 1: Converting frames to video...") + video_path = frames_to_video(frames, fbs, video_format=video_format) + try: + # Step 2: Upload video to S3 + print("Step 2: Uploading video to S3...") + filename = f"input_video_{os.path.basename(video_path)}" + video_url = upload_video_to_s3(video_path, filename, api_key) + + # Step 3: Call Bria API for resolution increase + print("Step 3: Calling Bria API for resolution increase...") + payload = { + "video": video_url, + "desired_increase": desired_increase, + "output_container_and_codec": output_container_and_codec, + "preserve_audio": preserve_audio + } + + headers = { + "Content-Type": "application/json", + "api_token": f"{api_key}" + } + response = requests.post(self.api_url, json=payload, headers=headers) if response.status_code == 200 or response.status_code == 202: @@ -87,9 +110,22 @@ class VideoIncreaseResolutionNode(): print(f"Video processing completed. Result URL: {result_video_url}") - return (result_video_url,) + # Step 4: Download and convert processed video to frames + print("Step 4: Downloading and converting processed video to frames...") + result_frames, result_frame_count, result_fps = video_to_frames(result_video_url) + + print(f"Resolution increase complete! Processed {result_frame_count} frames.") + + return (result_frames, result_frame_count, result_fps) else: raise Exception(f"Error: API request failed with status code {response.status_code} {response.text}") except Exception as e: - raise Exception(f"{e}") \ No newline at end of file + raise Exception(f"{e}") + finally: + # Clean up temporary video file + try: + if os.path.exists(video_path): + os.unlink(video_path) + except: + pass \ No newline at end of file diff --git a/nodes/video_nodes/video_mask_by_key_points_node.py b/nodes/video_nodes/video_mask_by_key_points_node.py index 7ecf46f..24d85c5 100644 --- a/nodes/video_nodes/video_mask_by_key_points_node.py +++ b/nodes/video_nodes/video_mask_by_key_points_node.py @@ -1,5 +1,7 @@ +import os import requests from ..common import deserialize_and_get_comfy_key, poll_status_until_completed +from .video_utils import frames_to_video, video_to_frames, upload_video_to_s3 import json class VideoMaskByKeyPointsNode(): @@ -7,21 +9,21 @@ class VideoMaskByKeyPointsNode(): Bria Video Mask by Key Points Node This node generates a mask for specific elements in videos based on key point coordinates - using the Bria API. It accepts a publicly accessible video URL and an array of coordinate - objects defining the mask hints. + using the Bria API. It accepts video frames from the Load Video node, uploads to S3, + processes via API, and returns the processed mask frames for preview. Parameters: - - video_url: Publicly accessible URL of the input video + - frames: Batch of image frames from Load Video node - api_key: Your Bria API token - key_points: Array of coordinate objects defining the mask hints (JSON format) - output_container_and_codec: Output video format and codec (default: mp4_h264) - - preserve_audio: Audio preservation (default: True) + - preserve_audio: Audio preservation (default: False) """ @classmethod def INPUT_TYPES(self): return { "required": { - "video_url": ("STRING", {"default": ""}), + "frames": ("IMAGE", {"tooltip": "Batch of video frames"}), "key_points": ("STRING", {"default": "[]", "multiline": True}), "api_key": ("STRING", {"default": "BRIA_API_TOKEN"}), }, @@ -37,19 +39,28 @@ class VideoMaskByKeyPointsNode(): "mkv_vp9", "gif" ], {"default": "mp4_h264"}), - "preserve_audio": ("BOOLEAN", {"default": True}), + "preserve_audio": ("BOOLEAN", {"default": False}), + "video_format": ("STRING", { + "default": "mp4", + "tooltip": "Original video format from Load Video node" + }), + "fbs": ("FLOAT", { + "default": "30", + "tooltip": "Original video format from Load Video node" + }), } } - RETURN_TYPES = ("STRING",) - RETURN_NAMES = ("mask_url_response",) + RETURN_TYPES = ("IMAGE", "INT","STRING", "FLOAT",) + RETURN_NAMES = ("mask_frames",'mask_url', "frame_count", "fps",) 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, video_url, key_points, api_key, output_container_and_codec="mp4_h264", preserve_audio=True): + def execute(self, frames, key_points, api_key,video_format, fbs, output_container_and_codec="mp4_h264", + preserve_audio=False): if api_key.strip() == "" or api_key.strip() == "BRIA_API_TOKEN": raise Exception("Please insert a valid API key.") api_key = deserialize_and_get_comfy_key(api_key) @@ -59,20 +70,32 @@ class VideoMaskByKeyPointsNode(): except json.JSONDecodeError as e: raise Exception(f"Invalid JSON format for key_points: {e}") - # Prepare the API request payload - payload = { - "video": video_url, - "key_points": key_points_array, - "output_container_and_codec": output_container_and_codec, - "preserve_audio": preserve_audio - } - - headers = { - "Content-Type": "application/json", - "api_token": f"{api_key}" - } - + print(f"Processing {frames.shape[0]} frames for video mask generation by key points...") + + # Step 1: Convert frames to video + print("Step 1: Converting frames to video...") + video_path = frames_to_video(frames, fbs, video_format=video_format) + try: + # Step 2: Upload video to S3 + print("Step 2: Uploading video to S3...") + filename = f"input_video_{os.path.basename(video_path)}" + video_url = upload_video_to_s3(video_path, filename, api_key) + + # Step 3: Call Bria API for mask generation + print("Step 3: Calling Bria API for video mask generation by key points...") + payload = { + "video": video_url, + "key_points": key_points_array, + "output_container_and_codec": output_container_and_codec, + "preserve_audio": preserve_audio + } + + headers = { + "Content-Type": "application/json", + "api_token": f"{api_key}" + } + response = requests.post(self.api_url, json=payload, headers=headers) if response.status_code == 200 or response.status_code == 202: @@ -93,9 +116,22 @@ class VideoMaskByKeyPointsNode(): print(f"Video mask processing completed. Result URL: {result_mask_url}") - return (result_mask_url,) + # Step 4: Download and convert processed mask video to frames + print("Step 4: Downloading and converting processed mask video to frames...") + result_frames, result_frame_count, result_fps = video_to_frames(result_mask_url) + + print(f"Video mask generation complete! Processed {result_frame_count} frames.") + + return (result_frames, result_mask_url, result_frame_count, result_fps,) else: raise Exception(f"Error: API request failed with status code {response.status_code} {response.text}") except Exception as e: - raise Exception(f"{e}") \ No newline at end of file + raise Exception(f"{e}") + finally: + # Clean up temporary video file + try: + if os.path.exists(video_path): + os.unlink(video_path) + except: + pass \ No newline at end of file diff --git a/nodes/video_nodes/video_mask_by_prompt_node.py b/nodes/video_nodes/video_mask_by_prompt_node.py index ef43333..1cd5273 100644 --- a/nodes/video_nodes/video_mask_by_prompt_node.py +++ b/nodes/video_nodes/video_mask_by_prompt_node.py @@ -1,26 +1,28 @@ +import os import requests from ..common import deserialize_and_get_comfy_key, poll_status_until_completed +from .video_utils import frames_to_video, video_to_frames, upload_video_to_s3 class VideoMaskByPromptNode(): """ Bria Video Mask by Prompt Node This node generates a mask for specific elements in videos based on a text prompt - using the Bria API. It accepts a publicly accessible video URL and a text instruction - describing the object to be masked. + using the Bria API. It accepts video frames from the Load Video node, uploads to S3, + processes via API, and returns the processed mask frames for preview. Parameters: - - video_url: Publicly accessible URL of the input video + - frames: Batch of image frames from Load Video node - api_key: Your Bria API token - prompt: Text instruction describing the object to be masked - output_container_and_codec: Output video format and codec (default: mp4_h264) - - preserve_audio: Audio preservation (default: True) + - preserve_audio: Audio preservation (default: False) """ @classmethod def INPUT_TYPES(self): return { "required": { - "video_url": ("STRING", {"default": ""}), + "frames": ("IMAGE", {"tooltip": "Batch of video frames"}), "prompt": ("STRING", {"default": ""}), "api_key": ("STRING", {"default": "BRIA_API_TOKEN"}), }, @@ -36,37 +38,58 @@ class VideoMaskByPromptNode(): "mkv_vp9", "gif" ], {"default": "mp4_h264"}), - "preserve_audio": ("BOOLEAN", {"default": True}), + "preserve_audio": ("BOOLEAN", {"default": False}), + "video_format": ("STRING", { + "default": "mp4", + "tooltip": "Original video format from Load Video node" + }), + "fbs": ("FLOAT", { + "default": "30", + "tooltip": "Original video format from Load Video node" + }), } } - RETURN_TYPES = ("STRING",) - RETURN_NAMES = ("mask_url_response",) + RETURN_TYPES = ("IMAGE", "STRING", "INT", "FLOAT", ) + RETURN_NAMES = ("mask_frames", "mask_url", "frame_count", "fps") 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, video_url, prompt, api_key, output_container_and_codec="mp4_h264", preserve_audio=True): + def execute(self, frames, prompt, api_key, video_format, fbs, output_container_and_codec="mp4_h264", + preserve_audio=False): if api_key.strip() == "" or api_key.strip() == "BRIA_API_TOKEN": raise Exception("Please insert a valid API key.") api_key = deserialize_and_get_comfy_key(api_key) - # Prepare the API request payload - payload = { - "video": video_url, - "prompt": prompt, - "output_container_and_codec": output_container_and_codec, - "preserve_audio": preserve_audio - } - - headers = { - "Content-Type": "application/json", - "api_token": f"{api_key}" - } - + print(f"Processing {frames.shape[0]} frames for video mask generation...") + + # Step 1: Convert frames to video + print("Step 1: Converting frames to video...") + video_path = frames_to_video(frames, fbs, video_format=video_format) + try: + # Step 2: Upload video to S3 + print("Step 2: Uploading video to S3...") + filename = f"input_video_{os.path.basename(video_path)}" + video_url = upload_video_to_s3(video_path, filename, api_key) + + # Step 3: Call Bria API for mask generation + print("Step 3: Calling Bria API for video mask generation...") + payload = { + "video": video_url, + "prompt": prompt, + "output_container_and_codec": output_container_and_codec, + "preserve_audio": preserve_audio + } + + headers = { + "Content-Type": "application/json", + "api_token": f"{api_key}" + } + response = requests.post(self.api_url, json=payload, headers=headers) if response.status_code == 200 or response.status_code == 202: @@ -87,9 +110,22 @@ class VideoMaskByPromptNode(): print(f"Video mask processing completed. Result URL: {result_mask_url}") - return (result_mask_url,) + # Step 4: Download and convert processed mask video to frames + print("Step 4: Downloading and converting processed mask video to frames...") + result_frames, result_frame_count, result_fps = video_to_frames(result_mask_url) + + print(f"Video mask generation complete! Processed {result_frame_count} frames.") + + return (result_frames, result_mask_url, result_frame_count, result_fps,) else: raise Exception(f"Error: API request failed with status code {response.status_code} {response.text}") except Exception as e: - raise Exception(f"{e}") \ No newline at end of file + raise Exception(f"{e}") + finally: + # Clean up temporary video file + try: + if os.path.exists(video_path): + os.unlink(video_path) + except: + pass \ No newline at end of file diff --git a/nodes/video_nodes/video_solid_color_background_node.py b/nodes/video_nodes/video_solid_color_background_node.py index b38d13d..92a15d7 100644 --- a/nodes/video_nodes/video_solid_color_background_node.py +++ b/nodes/video_nodes/video_solid_color_background_node.py @@ -1,26 +1,28 @@ +import os import requests from ..common import deserialize_and_get_comfy_key, poll_status_until_completed +from .video_utils import frames_to_video, video_to_frames, upload_video_to_s3 class VideoSolidColorBackgroundNode(): """ Bria Video Solid Color Background Node This node removes the background from videos and replaces it with a solid color - using the Bria API. It accepts a publicly accessible video URL and returns a - processed video URL with the specified background color. + using the Bria API. It accepts video frames from the Load Video node, uploads to S3, + processes via API, and returns the processed frames for preview. Parameters: - - video_url: Publicly accessible URL of the input video + - frames: Batch of image frames from Load Video node - api_key: Your Bria API token - background_color: Predefined color string (default: Transparent) - output_container_and_codec: Output video format and codec (default: mp4_h264) - - preserve_audio: Audio preservation (default: True) + - preserve_audio: Audio preservation (default: False) """ @classmethod def INPUT_TYPES(self): return { "required": { - "video_url": ("STRING", {"default": ""}), + "frames": ("IMAGE", {"tooltip": "Batch of video frames"}), "api_key": ("STRING", {"default": "BRIA_API_TOKEN"}), }, "optional": { @@ -48,37 +50,55 @@ class VideoSolidColorBackgroundNode(): "mkv_vp9", "gif" ], {"default": "mp4_h264"}), - "preserve_audio": ("BOOLEAN", {"default": True}), + "preserve_audio": ("BOOLEAN", {"default": False}), + "video_format": ("STRING", { + "default": "mp4", + "tooltip": "Original video format from Load Video node" + }), + "fbs": ("FLOAT", { + "default": "30", + "tooltip": "Original video format from Load Video node" + }), + } } - RETURN_TYPES = ("STRING",) - RETURN_NAMES = ("video_url_response",) + RETURN_TYPES = ("IMAGE", "INT", "FLOAT") + RETURN_NAMES = ("frames", "frame_count", "fps") 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, video_url, api_key, background_color="Transparent", output_container_and_codec="mp4_h264", preserve_audio=True): + def execute(self, frames, api_key, video_format,fbs, background_color="Transparent", output_container_and_codec="mp4_h264", + preserve_audio=False): if api_key.strip() == "" or api_key.strip() == "BRIA_API_TOKEN": raise Exception("Please insert a valid API key.") api_key = deserialize_and_get_comfy_key(api_key) - # Prepare the API request payload - payload = { - "video": video_url, - "background_color": background_color, - "output_container_and_codec": output_container_and_codec, - "preserve_audio": preserve_audio - } - - headers = { - "Content-Type": "application/json", - "api_token": f"{api_key}" - } - + print(f"Processing {frames.shape[0]} frames for solid color background...") + + print("Step 1: Converting frames to video...") + video_path = frames_to_video(frames, fbs, video_format=video_format) try: + print("Step 2: Uploading video to S3...") + filename = f"input_video_{os.path.basename(video_path)}" + video_url = upload_video_to_s3(video_path, filename, api_key) + + print("Step 3: Calling Bria API for solid color background...") + payload = { + "video": video_url, + "background_color": background_color, + "output_container_and_codec": output_container_and_codec, + "preserve_audio": preserve_audio + } + + headers = { + "Content-Type": "application/json", + "api_token": f"{api_key}" + } + response = requests.post(self.api_url, json=payload, headers=headers) if response.status_code == 200 or response.status_code == 202: @@ -99,9 +119,20 @@ class VideoSolidColorBackgroundNode(): print(f"Video processing completed. Result URL: {result_video_url}") - return (result_video_url,) + print("Step 4: Downloading and converting processed video to frames...") + result_frames, result_frame_count, result_fps = video_to_frames(result_video_url) + + print(f"Solid color background processing complete! Processed {result_frame_count} frames.") + + return (result_frames, result_frame_count, result_fps) else: raise Exception(f"Error: API request failed with status code {response.status_code} {response.text}") except Exception as e: - raise Exception(f"{e}") \ No newline at end of file + raise Exception(f"{e}") + finally: + try: + if os.path.exists(video_path): + os.unlink(video_path) + except: + pass \ No newline at end of file diff --git a/nodes/video_nodes/video_utils.py b/nodes/video_nodes/video_utils.py new file mode 100644 index 0000000..181d556 --- /dev/null +++ b/nodes/video_nodes/video_utils.py @@ -0,0 +1,230 @@ +import mimetypes +import os +import torch +import numpy as np +import av +import requests +import tempfile +from fractions import Fraction + +def get_video_config(ext): + ext = ext.lower().replace(".", "") + + configs = { + "mp4": ("mp4", "libx264", "yuv420p"), + "mov": ("mov", "libx264", "yuv420p"), + "mkv": ("matroska", "libx264", "yuv420p"), + "webm": ("webm", "libvpx-vp9", "yuv420p"), + "gif": ("gif", "gif", "pal8"), + } + + if ext in configs: + return configs[ext] + + print(f"[WARNING] Unknown video format '{ext}', falling back to mp4/h264.") + return ("mp4", "libx264", "yuv420p") + + + +def frames_to_video(frames, fps, video_format="mp4"): + """ + Convert a batch of image frames to a video file. + Automatically selects correct container/codec/pixel-format based on extension. + """ + # Pick container, codec and pix_fmt + container_format, codec, pix_fmt = get_video_config(video_format) + + # Create temp file + temp_file = tempfile.NamedTemporaryFile(suffix=f".{video_format}", delete=False) + filepath = temp_file.name + temp_file.close() + + print(f"Converting {frames.shape[0]} frames → {video_format}") + print(f"Container={container_format}, Codec={codec}, PixFmt={pix_fmt}") + + # Open container with correct format + container = av.open(filepath, mode="w", format=container_format) + + # Create video stream + stream = container.add_stream(codec, rate=Fraction(round(fps * 1000), 1000)) + stream.width = frames.shape[2] + stream.height = frames.shape[1] + stream.pix_fmt = pix_fmt + + # Encode frames + for i, frame in enumerate(frames): + frame_array = torch.clamp(frame * 255, 0, 255) + frame_np = frame_array.to(dtype=torch.uint8, device="cpu").numpy() + + + video_frame = av.VideoFrame.from_ndarray(frame_np, format="rgb24") + + for packet in stream.encode(video_frame): + container.mux(packet) + + if (i + 1) % 100 == 0: + print(f"Encoded {i + 1}/{frames.shape[0]} frames...") + + # Flush remaining packets + for packet in stream.encode(): + container.mux(packet) + + container.close() + return filepath + + +def video_to_frames(video_path): + """ + Convert a video file to a batch of image frames + + Args: + video_path: Path to video file (local or URL) + + Returns: + tuple: (frames_tensor, frame_count, fps) + """ + # Download if it's a URL + if video_path.startswith("http://") or video_path.startswith("https://"): + print(f"Downloading video from: {video_path}") + response = requests.get(video_path, stream=True) + if response.status_code != 200: + raise Exception(f"Failed to download video: {response.status_code}") + + # Save to temporary file + temp_file = tempfile.NamedTemporaryFile(suffix=".mp4", delete=False) + for chunk in response.iter_content(chunk_size=8192): + temp_file.write(chunk) + temp_file.close() + video_path = temp_file.name + print(f"Video downloaded to: {video_path}") + + if not os.path.exists(video_path): + raise FileNotFoundError(f"Video file not found: {video_path}") + + frames = [] + fps = 24.0 # Default FPS + + try: + # Open video using PyAV + container = av.open(video_path) + + # Get video stream + video_stream = None + for stream in container.streams: + if stream.type == 'video': + video_stream = stream + break + + if video_stream is None: + raise ValueError(f"No video stream found in video") + + # Get FPS + if video_stream.average_rate is not None: + fps = float(video_stream.average_rate) + + print(f"Loading video: {video_path}") + print(f"Video FPS: {fps}") + print(f"Video resolution: {video_stream.width}x{video_stream.height}") + + # Decode frames + for frame in container.decode(video_stream): + # Convert frame to RGB PIL Image + img = frame.to_image() + + # Convert PIL Image to numpy array + img_array = np.array(img.convert('RGB')).astype(np.float32) / 255.0 + + # Convert to torch tensor with shape [H, W, C] + img_tensor = torch.from_numpy(img_array) + + frames.append(img_tensor) + + container.close() + + if len(frames) == 0: + raise ValueError(f"No frames could be extracted from video") + + # Stack frames into a single tensor with shape [B, H, W, C] + frames_tensor = torch.stack(frames, dim=0) + + print(f"Extracted {len(frames)} frames from video") + print(f"Output tensor shape: {frames_tensor.shape}") + + # Clean up temporary file if downloaded + if video_path.startswith(tempfile.gettempdir()): + try: + os.unlink(video_path) + except: + pass + + return (frames_tensor, len(frames), fps) + + except Exception as e: + raise Exception(f"Error loading video: {str(e)}") + + +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" + } + 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)}") + From 0e0e59bbd46d339fc6149ae746fefea04b5a46a0 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Mon, 8 Dec 2025 11:25:00 +0000 Subject: [PATCH 3/6] fix default value --- nodes/video_nodes/remove_video_background_node.py | 2 +- nodes/video_nodes/video_erase_elements_node.py | 2 +- nodes/video_nodes/video_increase_resolution_node.py | 2 +- nodes/video_nodes/video_mask_by_key_points_node.py | 4 ++-- nodes/video_nodes/video_mask_by_prompt_node.py | 2 +- nodes/video_nodes/video_solid_color_background_node.py | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/nodes/video_nodes/remove_video_background_node.py b/nodes/video_nodes/remove_video_background_node.py index 19adbb4..029d823 100644 --- a/nodes/video_nodes/remove_video_background_node.py +++ b/nodes/video_nodes/remove_video_background_node.py @@ -26,7 +26,7 @@ class RemoveVideoBackgroundNode(): "api_key": ("STRING", {"default": "BRIA_API_TOKEN"}), }, "optional": { - "preserve_audio": ("BOOLEAN", {"default": False}), + "preserve_audio": ("BOOLEAN", {"default": True}), "output_container_and_codec": ([ "mp4_h264", "mp4_h265", diff --git a/nodes/video_nodes/video_erase_elements_node.py b/nodes/video_nodes/video_erase_elements_node.py index 2e3f5ef..c2f2b97 100644 --- a/nodes/video_nodes/video_erase_elements_node.py +++ b/nodes/video_nodes/video_erase_elements_node.py @@ -43,7 +43,7 @@ class VideoEraseElementsNode(): "mkv_vp9", "gif" ], {"default": "mp4_h264"}), - "preserve_audio": ("BOOLEAN", {"default": False}), + "preserve_audio": ("BOOLEAN", {"default": True}), "video_format": ("STRING", { "default": "mp4", "tooltip": "Original video format from Load Video node" diff --git a/nodes/video_nodes/video_increase_resolution_node.py b/nodes/video_nodes/video_increase_resolution_node.py index 9810679..827581e 100644 --- a/nodes/video_nodes/video_increase_resolution_node.py +++ b/nodes/video_nodes/video_increase_resolution_node.py @@ -38,7 +38,7 @@ class VideoIncreaseResolutionNode(): "mkv_vp9", "gif" ], {"default": "mp4_h264"}), - "preserve_audio": ("BOOLEAN", {"default": False}), + "preserve_audio": ("BOOLEAN", {"default": True}), "video_format": ("STRING", { "default": "mp4", "tooltip": "Original video format from Load Video node" diff --git a/nodes/video_nodes/video_mask_by_key_points_node.py b/nodes/video_nodes/video_mask_by_key_points_node.py index 24d85c5..6dbbb34 100644 --- a/nodes/video_nodes/video_mask_by_key_points_node.py +++ b/nodes/video_nodes/video_mask_by_key_points_node.py @@ -39,7 +39,7 @@ class VideoMaskByKeyPointsNode(): "mkv_vp9", "gif" ], {"default": "mp4_h264"}), - "preserve_audio": ("BOOLEAN", {"default": False}), + "preserve_audio": ("BOOLEAN", {"default": True}), "video_format": ("STRING", { "default": "mp4", "tooltip": "Original video format from Load Video node" @@ -51,7 +51,7 @@ class VideoMaskByKeyPointsNode(): } } - RETURN_TYPES = ("IMAGE", "INT","STRING", "FLOAT",) + RETURN_TYPES = ("IMAGE", "STRING", "INT", "FLOAT",) RETURN_NAMES = ("mask_frames",'mask_url', "frame_count", "fps",) CATEGORY = "API Nodes" FUNCTION = "execute" diff --git a/nodes/video_nodes/video_mask_by_prompt_node.py b/nodes/video_nodes/video_mask_by_prompt_node.py index 1cd5273..7f2e72b 100644 --- a/nodes/video_nodes/video_mask_by_prompt_node.py +++ b/nodes/video_nodes/video_mask_by_prompt_node.py @@ -38,7 +38,7 @@ class VideoMaskByPromptNode(): "mkv_vp9", "gif" ], {"default": "mp4_h264"}), - "preserve_audio": ("BOOLEAN", {"default": False}), + "preserve_audio": ("BOOLEAN", {"default": True}), "video_format": ("STRING", { "default": "mp4", "tooltip": "Original video format from Load Video node" diff --git a/nodes/video_nodes/video_solid_color_background_node.py b/nodes/video_nodes/video_solid_color_background_node.py index 92a15d7..d05dc46 100644 --- a/nodes/video_nodes/video_solid_color_background_node.py +++ b/nodes/video_nodes/video_solid_color_background_node.py @@ -50,7 +50,7 @@ class VideoSolidColorBackgroundNode(): "mkv_vp9", "gif" ], {"default": "mp4_h264"}), - "preserve_audio": ("BOOLEAN", {"default": False}), + "preserve_audio": ("BOOLEAN", {"default": True}), "video_format": ("STRING", { "default": "mp4", "tooltip": "Original video format from Load Video node" From 862403893fc3fb6fd80657cbf7c581d3fbb252f1 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Tue, 9 Dec 2025 08:31:08 +0000 Subject: [PATCH 4/6] fix default value --- nodes/video_nodes/remove_video_background_node.py | 2 +- nodes/video_nodes/video_erase_elements_node.py | 9 +++------ nodes/video_nodes/video_increase_resolution_node.py | 2 +- nodes/video_nodes/video_mask_by_key_points_node.py | 2 +- nodes/video_nodes/video_mask_by_prompt_node.py | 2 +- nodes/video_nodes/video_solid_color_background_node.py | 2 +- 6 files changed, 8 insertions(+), 11 deletions(-) diff --git a/nodes/video_nodes/remove_video_background_node.py b/nodes/video_nodes/remove_video_background_node.py index 029d823..ddaf59f 100644 --- a/nodes/video_nodes/remove_video_background_node.py +++ b/nodes/video_nodes/remove_video_background_node.py @@ -43,7 +43,7 @@ class RemoveVideoBackgroundNode(): "tooltip": "Original video format from Load Video node" }), "fbs": ("FLOAT", { - "default": "30", + "default": 25, "tooltip": "Original video format from Load Video node" }), } diff --git a/nodes/video_nodes/video_erase_elements_node.py b/nodes/video_nodes/video_erase_elements_node.py index c2f2b97..5ed9245 100644 --- a/nodes/video_nodes/video_erase_elements_node.py +++ b/nodes/video_nodes/video_erase_elements_node.py @@ -7,7 +7,7 @@ class VideoEraseElementsNode(): """ Bria Video Erase Elements Node - This node erases specific elements from videos based on a text prompt using the Bria API. + This node erases specific elements from videos using the Bria API. It accepts video frames from the Load Video node, uploads to S3, processes via API, and returns the processed frames for preview. @@ -15,7 +15,6 @@ class VideoEraseElementsNode(): - frames: Batch of image frames from Load Video node - api_key: Your Bria API token - mask_url: Publicly accessible URL of the mask video (optional) - - prompt: Text instruction describing the object to be masked - output_container_and_codec: Output video format and codec (default: mp4_h264) - preserve_audio: Audio preservation (default: False) """ @@ -24,7 +23,6 @@ class VideoEraseElementsNode(): return { "required": { "frames": ("IMAGE", {"tooltip": "Batch of video frames"}), - "prompt": ("STRING", {"default": ""}), "api_key": ("STRING", {"default": "BRIA_API_TOKEN"}), }, "optional": { @@ -49,7 +47,7 @@ class VideoEraseElementsNode(): "tooltip": "Original video format from Load Video node" }), "fbs": ("FLOAT", { - "default": "30", + "default": 25, "tooltip": "Original video format from Load Video node" }), } @@ -63,7 +61,7 @@ class VideoEraseElementsNode(): def __init__(self): self.api_url = "https://engine.prod.bria-api.com/v2/video/edit/erase" - def execute(self, frames, prompt, api_key, fbs, video_format, mask_url="", output_container_and_codec="mp4_h264", + def execute(self, frames, api_key, fbs, video_format, mask_url="", output_container_and_codec="mp4_h264", preserve_audio=False): if api_key.strip() == "" or api_key.strip() == "BRIA_API_TOKEN": raise Exception("Please insert a valid API key.") @@ -86,7 +84,6 @@ class VideoEraseElementsNode(): payload = { "video": video_url, "mask": mask_url, - "prompt": prompt, "output_container_and_codec": output_container_and_codec, "preserve_audio": preserve_audio } diff --git a/nodes/video_nodes/video_increase_resolution_node.py b/nodes/video_nodes/video_increase_resolution_node.py index 827581e..a72153b 100644 --- a/nodes/video_nodes/video_increase_resolution_node.py +++ b/nodes/video_nodes/video_increase_resolution_node.py @@ -44,7 +44,7 @@ class VideoIncreaseResolutionNode(): "tooltip": "Original video format from Load Video node" }), "fbs": ("FLOAT", { - "default": "30", + "default": 25, "tooltip": "Original video format from Load Video node" }), } diff --git a/nodes/video_nodes/video_mask_by_key_points_node.py b/nodes/video_nodes/video_mask_by_key_points_node.py index 6dbbb34..b2cd38d 100644 --- a/nodes/video_nodes/video_mask_by_key_points_node.py +++ b/nodes/video_nodes/video_mask_by_key_points_node.py @@ -45,7 +45,7 @@ class VideoMaskByKeyPointsNode(): "tooltip": "Original video format from Load Video node" }), "fbs": ("FLOAT", { - "default": "30", + "default": 25, "tooltip": "Original video format from Load Video node" }), } diff --git a/nodes/video_nodes/video_mask_by_prompt_node.py b/nodes/video_nodes/video_mask_by_prompt_node.py index 7f2e72b..e4ef19f 100644 --- a/nodes/video_nodes/video_mask_by_prompt_node.py +++ b/nodes/video_nodes/video_mask_by_prompt_node.py @@ -44,7 +44,7 @@ class VideoMaskByPromptNode(): "tooltip": "Original video format from Load Video node" }), "fbs": ("FLOAT", { - "default": "30", + "default": 25, "tooltip": "Original video format from Load Video node" }), } diff --git a/nodes/video_nodes/video_solid_color_background_node.py b/nodes/video_nodes/video_solid_color_background_node.py index d05dc46..e38499e 100644 --- a/nodes/video_nodes/video_solid_color_background_node.py +++ b/nodes/video_nodes/video_solid_color_background_node.py @@ -56,7 +56,7 @@ class VideoSolidColorBackgroundNode(): "tooltip": "Original video format from Load Video node" }), "fbs": ("FLOAT", { - "default": "30", + "default": 25, "tooltip": "Original video format from Load Video node" }), From 1cfe5758f1b86994849ee9c06029a2684127ff3c Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Wed, 10 Dec 2025 13:34:36 +0000 Subject: [PATCH 5/6] output the resukt url --- nodes/video_nodes/remove_video_background_node.py | 6 +++--- nodes/video_nodes/video_erase_elements_node.py | 6 +++--- nodes/video_nodes/video_increase_resolution_node.py | 6 +++--- nodes/video_nodes/video_solid_color_background_node.py | 6 +++--- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/nodes/video_nodes/remove_video_background_node.py b/nodes/video_nodes/remove_video_background_node.py index ddaf59f..0c751e9 100644 --- a/nodes/video_nodes/remove_video_background_node.py +++ b/nodes/video_nodes/remove_video_background_node.py @@ -49,8 +49,8 @@ class RemoveVideoBackgroundNode(): } } - RETURN_TYPES = ("IMAGE", "INT", "FLOAT") - RETURN_NAMES = ("frames", "frame_count", "fps") + RETURN_TYPES = ("IMAGE", "INT", "FLOAT","STRING") + RETURN_NAMES = ("frames", "frame_count", "fps", "result_video_url") CATEGORY = "API Nodes" FUNCTION = "execute" @@ -114,7 +114,7 @@ class RemoveVideoBackgroundNode(): print(f"Background removal complete! Processed {result_frame_count} frames.") - return (result_frames, result_frame_count, result_fps) + return (result_frames, result_frame_count, result_fps, result_video_url,) else: raise Exception(f"Error: API request failed with status code {response.status_code} {response.text}") diff --git a/nodes/video_nodes/video_erase_elements_node.py b/nodes/video_nodes/video_erase_elements_node.py index 5ed9245..56884e6 100644 --- a/nodes/video_nodes/video_erase_elements_node.py +++ b/nodes/video_nodes/video_erase_elements_node.py @@ -53,8 +53,8 @@ class VideoEraseElementsNode(): } } - RETURN_TYPES = ("IMAGE", "INT", "FLOAT") - RETURN_NAMES = ("frames", "frame_count", "fps") + RETURN_TYPES = ("IMAGE", "INT", "FLOAT", "STRING") + RETURN_NAMES = ("frames", "frame_count", "fps", "result_video_url") CATEGORY = "API Nodes" FUNCTION = "execute" @@ -119,7 +119,7 @@ class VideoEraseElementsNode(): print(f"Element erasure complete! Processed {result_frame_count} frames.") - return (result_frames, result_frame_count, result_fps) + return (result_frames, result_frame_count, result_fps, result_video_url,) else: raise Exception(f"Error: API request failed with status code {response.status_code} {response.text}") diff --git a/nodes/video_nodes/video_increase_resolution_node.py b/nodes/video_nodes/video_increase_resolution_node.py index a72153b..3dffc28 100644 --- a/nodes/video_nodes/video_increase_resolution_node.py +++ b/nodes/video_nodes/video_increase_resolution_node.py @@ -50,8 +50,8 @@ class VideoIncreaseResolutionNode(): } } - RETURN_TYPES = ("IMAGE", "INT", "FLOAT") - RETURN_NAMES = ("frames", "frame_count", "fps") + RETURN_TYPES = ("IMAGE", "INT", "FLOAT", "STRING") + RETURN_NAMES = ("frames", "frame_count", "fps", "result_video_url") CATEGORY = "API Nodes" FUNCTION = "execute" @@ -116,7 +116,7 @@ class VideoIncreaseResolutionNode(): print(f"Resolution increase complete! Processed {result_frame_count} frames.") - return (result_frames, result_frame_count, result_fps) + return (result_frames, result_frame_count, result_fps, result_video_url,) else: raise Exception(f"Error: API request failed with status code {response.status_code} {response.text}") diff --git a/nodes/video_nodes/video_solid_color_background_node.py b/nodes/video_nodes/video_solid_color_background_node.py index e38499e..6bbf0b0 100644 --- a/nodes/video_nodes/video_solid_color_background_node.py +++ b/nodes/video_nodes/video_solid_color_background_node.py @@ -63,8 +63,8 @@ class VideoSolidColorBackgroundNode(): } } - RETURN_TYPES = ("IMAGE", "INT", "FLOAT") - RETURN_NAMES = ("frames", "frame_count", "fps") + RETURN_TYPES = ("IMAGE", "INT", "FLOAT", "STRING") + RETURN_NAMES = ("frames", "frame_count", "fps", "result_video_url") CATEGORY = "API Nodes" FUNCTION = "execute" @@ -124,7 +124,7 @@ class VideoSolidColorBackgroundNode(): print(f"Solid color background processing complete! Processed {result_frame_count} frames.") - return (result_frames, result_frame_count, result_fps) + return (result_frames, result_frame_count, result_fps, result_video_url,) else: raise Exception(f"Error: API request failed with status code {response.status_code} {response.text}") From 82f31ddf72ed5d52063334b223b2ce5969b28fc5 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Mon, 15 Dec 2025 11:23:41 +0000 Subject: [PATCH 6/6] refactor video flow --- __init__.py | 8 +- nodes/__init__.py | 3 +- nodes/video_nodes/load_video.py | 103 +--- nodes/video_nodes/preview_video.py | 284 ---------- .../preview_video_node_from_url.py | 135 +++++ .../remove_video_background_node.py | 89 ++-- .../video_nodes/video_erase_elements_node.py | 93 ++-- .../video_increase_resolution_node.py | 91 ++-- .../video_mask_by_key_points_node.py | 93 ++-- .../video_nodes/video_mask_by_prompt_node.py | 93 ++-- .../video_solid_color_background_node.py | 91 ++-- nodes/video_nodes/video_utils.py | 161 ------ workflows/Video_Editig_Workflow.json | 495 ++++++++++++++++++ 13 files changed, 904 insertions(+), 835 deletions(-) delete mode 100644 nodes/video_nodes/preview_video.py create mode 100644 nodes/video_nodes/preview_video_node_from_url.py create mode 100644 workflows/Video_Editig_Workflow.json diff --git a/__init__.py b/__init__.py index 28698f4..5a0c73a 100644 --- a/__init__.py +++ b/__init__.py @@ -34,7 +34,7 @@ from .nodes import ( VideoIncreaseResolutionNode, VideoEraseElementsNode, LoadVideoFramesNode, - PreviewVideoFramesNode + PreviewVideoURLNode ) # Map the node class to a name used internally by ComfyUI @@ -74,7 +74,7 @@ NODE_CLASS_MAPPINGS = { "VideoIncreaseResolutionNode":VideoIncreaseResolutionNode, "VideoEraseElementsNode":VideoEraseElementsNode, "LoadVideoFramesNode":LoadVideoFramesNode, - "PreviewVideoFramesNode":PreviewVideoFramesNode + "PreviewVideoURLNode":PreviewVideoURLNode } # Map the node display name to the one shown in the ComfyUI node interface NODE_DISPLAY_NAME_MAPPINGS = { @@ -112,6 +112,6 @@ NODE_DISPLAY_NAME_MAPPINGS = { "VideoMaskByKeyPointsNode":"Bria Video Mask By Key Points", "VideoIncreaseResolutionNode":"Bria Video Increase Resolution", "VideoEraseElementsNode":"Bria Video Erase Elements", - "LoadVideoFramesNode":"Load Video Node", - "PreviewVideoFramesNode":"Preview Video Node" + "LoadVideoFramesNode":"Bria Load Video", + "PreviewVideoURLNode":"Bria Preview Video" } diff --git a/nodes/__init__.py b/nodes/__init__.py index ae04659..986b50c 100644 --- a/nodes/__init__.py +++ b/nodes/__init__.py @@ -35,4 +35,5 @@ 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 import PreviewVideoFramesNode +from .video_nodes.preview_video_node_from_url import PreviewVideoURLNode + diff --git a/nodes/video_nodes/load_video.py b/nodes/video_nodes/load_video.py index 56e15bd..f98877d 100644 --- a/nodes/video_nodes/load_video.py +++ b/nodes/video_nodes/load_video.py @@ -1,24 +1,15 @@ import os -import torch -import numpy as np import folder_paths -import av class LoadVideoFramesNode: """ - Bria Load Video Frames Node - - This node loads a video file and extracts all frames as images. - The user can upload a video and it will be converted into a batch of image tensors - that can be used with other nodes in the pipeline. - + Load a video file from the input folder or upload. + Parameters: - - video: Video file to load from input directory - + video (str): Selected or uploaded video filename. + Returns: - - frames: Batch of images (IMAGE tensor format) - - frame_count: Total number of frames extracted - - fps: Frames per second of the video + video_path (STRING): Absolute path to the video file. """ @classmethod @@ -30,92 +21,20 @@ class LoadVideoFramesNode: return { "required": { "video": (sorted(files), {"video_upload": True}), - }, - "optional": { - "max_frames": ("INT", { - "default": 0, - "min": 0, - "max": 10000, - "step": 1, - "tooltip": "Maximum number of frames to extract (0 = all frames)" - }), } } - RETURN_TYPES = ("IMAGE", "INT", "FLOAT", "STRING") - RETURN_NAMES = ("frames", "frame_count", "fps", "video_format") + RETURN_TYPES = ("STRING",) + RETURN_NAMES = ("video_path",) FUNCTION = "load_video" CATEGORY = "API Nodes" - DESCRIPTION = "Loads a video file and extracts frames as a batch of images." - def load_video(self, video, max_frames=0): - video_path = folder_paths.get_annotated_filepath(video) - video_format = os.path.splitext(video)[1].lower().lstrip('.') - + 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}") - - frames = [] - frame_index = 0 - extracted_count = 0 - - try: - # Open video using PyAV - container = av.open(video_path) - - # Get video stream - video_stream = None - for stream in container.streams: - if stream.type == 'video': - video_stream = stream - break - - if video_stream is None: - raise ValueError(f"No video stream found in {video}") - - # Get FPS - if video_stream.average_rate is not None: - fps = float(video_stream.average_rate) - - print(f"Loading video: {video}") - print(f"Video FPS: {fps}") - print(f"Video resolution: {video_stream.width}x{video_stream.height}") - - # Decode frames - for frame in container.decode(video_stream): - # Convert frame to RGB PIL Image - img = frame.to_image() - - # Convert PIL Image to numpy array - img_array = np.array(img.convert('RGB')).astype(np.float32) / 255.0 - - # Convert to torch tensor with shape [H, W, C] - img_tensor = torch.from_numpy(img_array) - - frames.append(img_tensor) - extracted_count += 1 - frame_index += 1 - - # Check if we've reached max_frames - if max_frames > 0 and extracted_count >= max_frames: - break - - container.close() - - if len(frames) == 0: - raise ValueError(f"No frames could be extracted from {video}") - - # Stack frames into a single tensor with shape [B, H, W, C] - frames_tensor = torch.stack(frames, dim=0) - - print(f"Extracted {extracted_count} frames from video") - print(f"Output tensor shape: {frames_tensor.shape}") - print(f"Video format: {video_format}") - - return (frames_tensor, extracted_count, fps, video_format) - - except Exception as e: - raise Exception(f"Error loading video {video}: {str(e)}") + + return (video_path,) @classmethod def IS_CHANGED(cls, video, **kwargs): diff --git a/nodes/video_nodes/preview_video.py b/nodes/video_nodes/preview_video.py deleted file mode 100644 index d79c3e5..0000000 --- a/nodes/video_nodes/preview_video.py +++ /dev/null @@ -1,284 +0,0 @@ -import os -import json -import random -import torch -import numpy as np -from PIL import Image -import folder_paths -from comfy.cli_args import args -import av -from fractions import Fraction - -class PreviewVideoFramesNode: - """ - Bria Preview Video Frames Node - - This node takes a batch of image frames and creates an actual video file - that can be played as animation in the ComfyUI interface. - - Parameters: - - frames: Batch of image tensors to preview - - fps: Frames per second for animation display - - format: Video format (webp or mp4) - """ - - def __init__(self): - self.output_dir = folder_paths.get_temp_directory() - self.type = "temp" - self.prefix_append = "_video_preview_" + ''.join(random.choice("abcdefghijklmnopqrstupvxyz") for _ in range(5)) - - @classmethod - def INPUT_TYPES(cls): - return { - "required": { - "frames": ("IMAGE", {"tooltip": "Batch of image frames to preview"}), - }, - "optional": { - "fps": ("FLOAT", { - "default": 30.0, - "min": 1.0, - "max": 120.0, - "step": 0.1, - "tooltip": "Frames per second for animation playback" - }), - "format": ([ - "mp4_h264", - "mp4_h265", - "webm_vp9", - "mov_h265", - "mov_proresks", - "mkv_h264", - "mkv_h265", - "mkv_vp9", - "gif", - "webp" - ], { - "default": "mp4_h264", - "tooltip": "Output video format - matches Bria API formats" - }), - "quality": (["high", "medium", "low"], { - "default": "medium", - "tooltip": "Video quality (affects file size)" - }), - "max_preview_frames": ("INT", { - "default": 0, - "min": 0, - "max": 1000, - "step": 1, - "tooltip": "Maximum frames to preview (0 = all frames)" - }), - }, - "hidden": { - "prompt": "PROMPT", - "extra_pnginfo": "EXTRA_PNGINFO" - }, - } - - RETURN_TYPES = () - FUNCTION = "preview_frames" - OUTPUT_NODE = True - CATEGORY = "API Nodes" - DESCRIPTION = "Previews video frames as an animated video in the ComfyUI interface." - - def preview_frames(self, frames, fps=30.0, format="mp4_h264", quality="medium", max_preview_frames=0, prompt=None, extra_pnginfo=None): - """ - Preview video frames as animated video - - Args: - frames: Batch of image tensors [B, H, W, C] - fps: Frames per second for display - format: Output format (supports all Bria API formats) - quality: Video quality (high, medium, low) - max_preview_frames: Maximum number of frames to preview (0 = all) - prompt: Hidden parameter for ComfyUI workflow - extra_pnginfo: Hidden parameter for ComfyUI metadata - - Returns: - dict: UI output with video file for animation - """ - filename_prefix = "VideoPreview" - filename_prefix += self.prefix_append - - # Limit frames if specified - total_frames = frames.shape[0] - if max_preview_frames > 0 and total_frames > max_preview_frames: - # Sample frames evenly across the video - indices = torch.linspace(0, total_frames - 1, max_preview_frames).long() - frames = frames[indices] - print(f"Preview limited to {max_preview_frames} frames (sampled from {total_frames} total frames)") - else: - print(f"Previewing {total_frames} frames as animated video (format: {format})") - - # Get save path - full_output_folder, filename, counter, subfolder, filename_prefix = folder_paths.get_save_image_path( - filename_prefix, - self.output_dir, - frames[0].shape[1], # width - frames[0].shape[0] # height - ) - - # Choose format and create video - if format == "webp": - result_file = self._save_as_webp(frames, full_output_folder, filename, counter, fps, quality, prompt, extra_pnginfo) - elif format == "gif": - result_file = self._save_as_gif(frames, full_output_folder, filename, counter, fps, quality, prompt, extra_pnginfo) - else: - # All video formats (mp4, webm, mov, mkv) - result_file = self._save_as_video(frames, full_output_folder, filename, counter, fps, format, quality, prompt, extra_pnginfo) - - print(f"Saved animated video: {result_file}") - - # Return UI output with animation - return { - "ui": { - "images": [{ - "filename": result_file, - "subfolder": subfolder, - "type": self.type - }], - "animated": (True,) - } - } - - def _get_format_config(self, format_name): - """ - Get container, codec, and file extension for a given format - - Returns: (container, codec, extension, pixel_format) - """ - format_map = { - "mp4_h264": ("mp4", "libx264", "mp4", "yuv420p"), - "mp4_h265": ("mp4", "libx265", "mp4", "yuv420p"), - "webm_vp9": ("webm", "libvpx-vp9", "webm", "yuv420p"), - "mov_h265": ("mov", "libx265", "mov", "yuv420p"), - "mov_proresks": ("mov", "prores_ks", "mov", "yuv422p10le"), - "mkv_h264": ("matroska", "libx264", "mkv", "yuv420p"), - "mkv_h265": ("matroska", "libx265", "mkv", "yuv420p"), - "mkv_vp9": ("matroska", "libvpx-vp9", "mkv", "yuv420p"), - } - return format_map.get(format_name, ("mp4", "libx264", "mp4", "yuv420p")) - - - def _save_as_webp(self, frames, output_folder, filename, counter, fps, quality, prompt, extra_pnginfo): - """Save frames as animated WEBP""" - file = f"{filename}_{counter:05}_.webp" - - # Convert frames to PIL images - pil_images = [] - for frame in frames: - frame_array = 255.0 * frame.cpu().numpy() - img = Image.fromarray(np.clip(frame_array, 0, 255).astype(np.uint8)) - pil_images.append(img) - - # Add metadata to first frame - metadata = pil_images[0].getexif() - if not args.disable_metadata: - if prompt is not None: - metadata[0x0110] = "prompt:{}".format(json.dumps(prompt)) - if extra_pnginfo is not None: - inital_exif = 0x010f - for x in extra_pnginfo: - metadata[inital_exif] = "{}:{}".format(x, json.dumps(extra_pnginfo[x])) - inital_exif -= 1 - - # Quality mapping - quality_values = {"high": 95, "medium": 85, "low": 70} - webp_quality = quality_values.get(quality, 85) - - # Save as animated WEBP - duration = int(1000.0 / fps) # duration in milliseconds - pil_images[0].save( - os.path.join(output_folder, file), - save_all=True, - duration=duration, - append_images=pil_images[1:], - exif=metadata, - lossless=False, - quality=webp_quality, - method=4 - ) - - return file - - def _save_as_gif(self, frames, output_folder, filename, counter, fps, quality, prompt, extra_pnginfo): - """Save frames as animated GIF""" - file = f"{filename}_{counter:05}_.gif" - - # Convert frames to PIL images - pil_images = [] - for frame in frames: - frame_array = 255.0 * frame.cpu().numpy() - img = Image.fromarray(np.clip(frame_array, 0, 255).astype(np.uint8)) - pil_images.append(img) - - # Save as animated GIF - duration = int(1000.0 / fps) # duration in milliseconds - optimize = quality == "low" # Optimize for low quality to reduce size - pil_images[0].save( - os.path.join(output_folder, file), - save_all=True, - duration=duration, - append_images=pil_images[1:], - loop=0, - optimize=optimize - ) - - return file - - def _save_as_video(self, frames, output_folder, filename, counter, fps, format_name, quality, prompt, extra_pnginfo): - """ - Save frames as video using PyAV with support for all Bria API formats - - Supports: mp4_h264, mp4_h265, webm_vp9, mov_h265, mov_proresks, mkv_h264, mkv_h265, mkv_vp9 - """ - # Get format configuration - container_format, codec, extension, pix_fmt = self._get_format_config(format_name) - - file = f"{filename}_{counter:05}_.{extension}" - filepath = os.path.join(output_folder, file) - - print(f"Encoding video: container={container_format}, codec={codec}, quality={quality}") - - # Open video container - container = av.open(filepath, mode="w", format=container_format) - - # Add metadata if enabled - if not args.disable_metadata: - if prompt is not None: - container.metadata["prompt"] = json.dumps(prompt) - if extra_pnginfo is not None: - for x in extra_pnginfo: - container.metadata[x] = json.dumps(extra_pnginfo[x]) - - # Create video stream - stream = container.add_stream(codec, rate=Fraction(round(fps * 1000), 1000)) - stream.width = frames.shape[2] # width - stream.height = frames.shape[1] # height - stream.pix_fmt = pix_fmt - - # Encode frames - for i, frame in enumerate(frames): - # Convert tensor to numpy array and scale to 0-255 - frame_array = torch.clamp(frame * 255, min=0, max=255) - frame_np = frame_array.to(device=torch.device("cpu"), dtype=torch.uint8).numpy() - - # Create video frame - video_frame = av.VideoFrame.from_ndarray(frame_np, format="rgb24") - - # Encode frame - for packet in stream.encode(video_frame): - container.mux(packet) - - # Progress update for long videos - if (i + 1) % 100 == 0: - print(f"Encoded {i + 1}/{frames.shape[0]} frames...") - - # Flush remaining packets - for packet in stream.encode(): - container.mux(packet) - - container.close() - - print(f"Video encoding complete: {file}") - - return file \ No newline at end of file diff --git a/nodes/video_nodes/preview_video_node_from_url.py b/nodes/video_nodes/preview_video_node_from_url.py new file mode 100644 index 0000000..377903e --- /dev/null +++ b/nodes/video_nodes/preview_video_node_from_url.py @@ -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' diff --git a/nodes/video_nodes/remove_video_background_node.py b/nodes/video_nodes/remove_video_background_node.py index 0c751e9..66e35e0 100644 --- a/nodes/video_nodes/remove_video_background_node.py +++ b/nodes/video_nodes/remove_video_background_node.py @@ -1,29 +1,32 @@ import os +import uuid import requests +import folder_paths from ..common import deserialize_and_get_comfy_key, poll_status_until_completed -from .video_utils import frames_to_video, video_to_frames, upload_video_to_s3 +from .video_utils import upload_video_to_s3 class RemoveVideoBackgroundNode(): """ - Bria Remove Video Background Node - - This node removes the background from videos using the Bria API. - It accepts video frames from the Load Video node, uploads to S3, - processes via API, and returns the processed frames for preview. - - Supported input resolution: up to 16000x16000 (16K) - + Removes the background from a video using the Bria API. + Parameters: - - frames: Batch of image frames from Load Video node - - api_key: Your Bria API token - - output_container_and_codec: Output video format and codec (default: webm_vp9) + 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". + + Returns: + result_video_url (STRING): URL of the video with background removed. """ @classmethod def INPUT_TYPES(self): return { "required": { - "frames": ("IMAGE", {"tooltip": "Batch of video frames"}), "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}), @@ -38,47 +41,38 @@ class RemoveVideoBackgroundNode(): "mkv_vp9", "gif" ], {"default": "webm_vp9"}), - "video_format": ("STRING", { - "default": "mp4", - "tooltip": "Original video format from Load Video node" - }), - "fbs": ("FLOAT", { - "default": 25, - "tooltip": "Original video format from Load Video node" - }), } } - RETURN_TYPES = ("IMAGE", "INT", "FLOAT","STRING") - RETURN_NAMES = ("frames", "frame_count", "fps", "result_video_url") + 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" - # Define the execute method as expected by ComfyUI - def execute(self, frames, api_key, fbs, video_format, preserve_audio=False, output_container_and_codec="webm_vp9"): + def execute(self, api_key, video_url, preserve_audio=True, output_container_and_codec="webm_vp9",): if api_key.strip() == "" or api_key.strip() == "BRIA_API_TOKEN": raise Exception("Please insert a valid API key.") api_key = deserialize_and_get_comfy_key(api_key) - - print(f"Processing {frames.shape[0]} frames for background removal...") - - # Step 1: Convert frames to video - print("Step 1: Converting frames to video...") - video_path = frames_to_video(frames, fbs, video_format=video_format) + 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: - # Step 2: Upload video to S3 - print("Step 2: Uploading video to S3...") - filename = f"input_video_{os.path.basename(video_path)}" - video_url = upload_video_to_s3(video_path, filename,api_key) - # Step 3: Call Bria API for background removal print("Step 3: Calling Bria API for background removal...") payload = { - "video": video_url, + "video": input_video_url, "preserve_audio": preserve_audio, "output_container_and_codec": output_container_and_codec } @@ -107,24 +101,19 @@ class RemoveVideoBackgroundNode(): 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.") - # Step 4: Download and convert processed video to frames - print("Step 4: Downloading and converting processed video to frames...") - result_frames, result_frame_count, result_fps = video_to_frames(result_video_url) - - print(f"Background removal complete! Processed {result_frame_count} frames.") - - return (result_frames, result_frame_count, result_fps, result_video_url,) + 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: - # Clean up temporary video file - try: - if os.path.exists(video_path): - os.unlink(video_path) - except: - pass + if video_path: + try: + if os.path.exists(video_path): + os.unlink(video_path) + except: + pass diff --git a/nodes/video_nodes/video_erase_elements_node.py b/nodes/video_nodes/video_erase_elements_node.py index 56884e6..2680978 100644 --- a/nodes/video_nodes/video_erase_elements_node.py +++ b/nodes/video_nodes/video_erase_elements_node.py @@ -1,31 +1,35 @@ import os +import uuid import requests +import folder_paths from ..common import deserialize_and_get_comfy_key, poll_status_until_completed -from .video_utils import frames_to_video, video_to_frames, upload_video_to_s3 +from .video_utils import upload_video_to_s3 class VideoEraseElementsNode(): """ - Bria Video Erase Elements Node - - This node erases specific elements from videos using the Bria API. - It accepts video frames from the Load Video node, uploads to S3, processes via API, - and returns the processed frames for preview. - + Erase elements from a video using the Bria API. + Parameters: - - frames: Batch of image frames from Load Video node - - api_key: Your Bria API token - - mask_url: Publicly accessible URL of the mask video (optional) - - output_container_and_codec: Output video format and codec (default: mp4_h264) - - preserve_audio: Audio preservation (default: False) + 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": { - "frames": ("IMAGE", {"tooltip": "Batch of video frames"}), "api_key": ("STRING", {"default": "BRIA_API_TOKEN"}), + "video_url": ("STRING", { + "default": "", + "tooltip": "URL of video to process (provide either frames or video_url)" + }), }, - "optional": { + "optional": { "mask_url": ("STRING", { "default": "", "tooltip": "URL of mask video (optional)" @@ -42,47 +46,41 @@ class VideoEraseElementsNode(): "gif" ], {"default": "mp4_h264"}), "preserve_audio": ("BOOLEAN", {"default": True}), - "video_format": ("STRING", { - "default": "mp4", - "tooltip": "Original video format from Load Video node" - }), - "fbs": ("FLOAT", { - "default": 25, - "tooltip": "Original video format from Load Video node" - }), } } - RETURN_TYPES = ("IMAGE", "INT", "FLOAT", "STRING") - RETURN_NAMES = ("frames", "frame_count", "fps", "result_video_url") + 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, frames, api_key, fbs, video_format, mask_url="", output_container_and_codec="mp4_h264", - preserve_audio=False): + 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.") api_key = deserialize_and_get_comfy_key(api_key) + video_path = None - print(f"Processing {frames.shape[0]} frames for element erasure...") - - # Step 1: Convert frames to video - print("Step 1: Converting frames to video...") - video_path = frames_to_video(frames, fbs, video_format=video_format) + 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: - # Step 2: Upload video to S3 - print("Step 2: Uploading video to S3...") - filename = f"input_video_{os.path.basename(video_path)}" - video_url = upload_video_to_s3(video_path, filename, api_key) - # Step 3: Call Bria API for element erasure print("Step 3: Calling Bria API for element erasure...") payload = { - "video": video_url, + "video": input_video_url, "mask": mask_url, "output_container_and_codec": output_container_and_codec, "preserve_audio": preserve_audio @@ -112,23 +110,18 @@ class VideoEraseElementsNode(): 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.") - # Step 4: Download and convert processed video to frames - print("Step 4: Downloading and converting processed video to frames...") - result_frames, result_frame_count, result_fps = video_to_frames(result_video_url) - - print(f"Element erasure complete! Processed {result_frame_count} frames.") - - return (result_frames, result_frame_count, result_fps, result_video_url,) + 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: - # Clean up temporary video file - try: - if os.path.exists(video_path): - os.unlink(video_path) - except: - pass \ No newline at end of file + if video_path: + try: + if os.path.exists(video_path): + os.unlink(video_path) + except: + pass \ No newline at end of file diff --git a/nodes/video_nodes/video_increase_resolution_node.py b/nodes/video_nodes/video_increase_resolution_node.py index 3dffc28..46829af 100644 --- a/nodes/video_nodes/video_increase_resolution_node.py +++ b/nodes/video_nodes/video_increase_resolution_node.py @@ -1,29 +1,33 @@ import os +import uuid import requests +import folder_paths from ..common import deserialize_and_get_comfy_key, poll_status_until_completed -from .video_utils import frames_to_video, video_to_frames, upload_video_to_s3 +from .video_utils import upload_video_to_s3 class VideoIncreaseResolutionNode(): """ - Bria Video Increase Resolution Node - - This node increases the resolution of videos using the Bria API. - It accepts video frames from the Load Video node, uploads to S3, - processes via API, and returns the processed frames for preview. - + Increase the resolution of a video using the Bria API. + Parameters: - - frames: Batch of image frames from Load Video node - - api_key: Your Bria API token - - desired_increase: Integer scale factor for upscaling (2 or 4) - - output_container_and_codec: Output video format and codec (default: mp4_h264) - - preserve_audio: Audio preservation (default: False) + 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": { - "frames": ("IMAGE", {"tooltip": "Batch of video frames"}), "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'}), @@ -39,47 +43,41 @@ class VideoIncreaseResolutionNode(): "gif" ], {"default": "mp4_h264"}), "preserve_audio": ("BOOLEAN", {"default": True}), - "video_format": ("STRING", { - "default": "mp4", - "tooltip": "Original video format from Load Video node" - }), - "fbs": ("FLOAT", { - "default": 25, - "tooltip": "Original video format from Load Video node" - }), } } - RETURN_TYPES = ("IMAGE", "INT", "FLOAT", "STRING") - RETURN_NAMES = ("frames", "frame_count", "fps", "result_video_url") + 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, frames, api_key, video_format, fbs, desired_increase='2', output_container_and_codec="mp4_h264", - preserve_audio=False): + 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.") api_key = deserialize_and_get_comfy_key(api_key) + video_path = None - print(f"Processing {frames.shape[0]} frames for resolution increase...") - - # Step 1: Convert frames to video - print("Step 1: Converting frames to video...") - video_path = frames_to_video(frames, fbs, video_format=video_format) + 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: - # Step 2: Upload video to S3 - print("Step 2: Uploading video to S3...") - filename = f"input_video_{os.path.basename(video_path)}" - video_url = upload_video_to_s3(video_path, filename, api_key) - # Step 3: Call Bria API for resolution increase print("Step 3: Calling Bria API for resolution increase...") payload = { - "video": video_url, + "video": input_video_url, "desired_increase": desired_increase, "output_container_and_codec": output_container_and_codec, "preserve_audio": preserve_audio @@ -109,23 +107,18 @@ class VideoIncreaseResolutionNode(): 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.") - # Step 4: Download and convert processed video to frames - print("Step 4: Downloading and converting processed video to frames...") - result_frames, result_frame_count, result_fps = video_to_frames(result_video_url) - - print(f"Resolution increase complete! Processed {result_frame_count} frames.") - - return (result_frames, result_frame_count, result_fps, result_video_url,) + 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: - # Clean up temporary video file - try: - if os.path.exists(video_path): - os.unlink(video_path) - except: - pass \ No newline at end of file + if video_path: + try: + if os.path.exists(video_path): + os.unlink(video_path) + except: + pass \ No newline at end of file diff --git a/nodes/video_nodes/video_mask_by_key_points_node.py b/nodes/video_nodes/video_mask_by_key_points_node.py index b2cd38d..e83c189 100644 --- a/nodes/video_nodes/video_mask_by_key_points_node.py +++ b/nodes/video_nodes/video_mask_by_key_points_node.py @@ -1,31 +1,35 @@ import os +import uuid import requests +import folder_paths from ..common import deserialize_and_get_comfy_key, poll_status_until_completed -from .video_utils import frames_to_video, video_to_frames, upload_video_to_s3 +from .video_utils import upload_video_to_s3 import json class VideoMaskByKeyPointsNode(): """ - Bria Video Mask by Key Points Node - - This node generates a mask for specific elements in videos based on key point coordinates - using the Bria API. It accepts video frames from the Load Video node, uploads to S3, - processes via API, and returns the processed mask frames for preview. - + Generate a video mask using key points with the Bria API. + Parameters: - - frames: Batch of image frames from Load Video node - - api_key: Your Bria API token - - key_points: Array of coordinate objects defining the mask hints (JSON format) - - output_container_and_codec: Output video format and codec (default: mp4_h264) - - preserve_audio: Audio preservation (default: False) + 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": { - "frames": ("IMAGE", {"tooltip": "Batch of video frames"}), "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": ([ @@ -40,27 +44,18 @@ class VideoMaskByKeyPointsNode(): "gif" ], {"default": "mp4_h264"}), "preserve_audio": ("BOOLEAN", {"default": True}), - "video_format": ("STRING", { - "default": "mp4", - "tooltip": "Original video format from Load Video node" - }), - "fbs": ("FLOAT", { - "default": 25, - "tooltip": "Original video format from Load Video node" - }), } } - RETURN_TYPES = ("IMAGE", "STRING", "INT", "FLOAT",) - RETURN_NAMES = ("mask_frames",'mask_url', "frame_count", "fps",) + 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, frames, key_points, api_key,video_format, fbs, output_container_and_codec="mp4_h264", - preserve_audio=False): + 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.") api_key = deserialize_and_get_comfy_key(api_key) @@ -69,23 +64,28 @@ class VideoMaskByKeyPointsNode(): 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 - print(f"Processing {frames.shape[0]} frames for video mask generation by key points...") - - # Step 1: Convert frames to video - print("Step 1: Converting frames to video...") - video_path = frames_to_video(frames, fbs, video_format=video_format) + 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: - # Step 2: Upload video to S3 - print("Step 2: Uploading video to S3...") - filename = f"input_video_{os.path.basename(video_path)}" - video_url = upload_video_to_s3(video_path, filename, api_key) - # Step 3: Call Bria API for mask generation print("Step 3: Calling Bria API for video mask generation by key points...") payload = { - "video": video_url, + "video": input_video_url, "key_points": key_points_array, "output_container_and_codec": output_container_and_codec, "preserve_audio": preserve_audio @@ -115,23 +115,18 @@ class VideoMaskByKeyPointsNode(): 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.") - # Step 4: Download and convert processed mask video to frames - print("Step 4: Downloading and converting processed mask video to frames...") - result_frames, result_frame_count, result_fps = video_to_frames(result_mask_url) - - print(f"Video mask generation complete! Processed {result_frame_count} frames.") - - return (result_frames, result_mask_url, result_frame_count, result_fps,) + 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: - # Clean up temporary video file - try: - if os.path.exists(video_path): - os.unlink(video_path) - except: - pass \ No newline at end of file + if video_path: + try: + if os.path.exists(video_path): + os.unlink(video_path) + except: + pass \ No newline at end of file diff --git a/nodes/video_nodes/video_mask_by_prompt_node.py b/nodes/video_nodes/video_mask_by_prompt_node.py index e4ef19f..00ccdb6 100644 --- a/nodes/video_nodes/video_mask_by_prompt_node.py +++ b/nodes/video_nodes/video_mask_by_prompt_node.py @@ -1,30 +1,34 @@ import os +import uuid import requests +import folder_paths from ..common import deserialize_and_get_comfy_key, poll_status_until_completed -from .video_utils import frames_to_video, video_to_frames, upload_video_to_s3 +from .video_utils import upload_video_to_s3 class VideoMaskByPromptNode(): """ - Bria Video Mask by Prompt Node - - This node generates a mask for specific elements in videos based on a text prompt - using the Bria API. It accepts video frames from the Load Video node, uploads to S3, - processes via API, and returns the processed mask frames for preview. - + Generate a video mask using a text prompt with the Bria API. + Parameters: - - frames: Batch of image frames from Load Video node - - api_key: Your Bria API token - - prompt: Text instruction describing the object to be masked - - output_container_and_codec: Output video format and codec (default: mp4_h264) - - preserve_audio: Audio preservation (default: False) + 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": { - "frames": ("IMAGE", {"tooltip": "Batch of video frames"}), "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": ([ @@ -39,47 +43,43 @@ class VideoMaskByPromptNode(): "gif" ], {"default": "mp4_h264"}), "preserve_audio": ("BOOLEAN", {"default": True}), - "video_format": ("STRING", { - "default": "mp4", - "tooltip": "Original video format from Load Video node" - }), - "fbs": ("FLOAT", { - "default": 25, - "tooltip": "Original video format from Load Video node" - }), } } - RETURN_TYPES = ("IMAGE", "STRING", "INT", "FLOAT", ) - RETURN_NAMES = ("mask_frames", "mask_url", "frame_count", "fps") + 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, frames, prompt, api_key, video_format, fbs, output_container_and_codec="mp4_h264", - preserve_audio=False): + 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.") api_key = deserialize_and_get_comfy_key(api_key) + + video_path = None - print(f"Processing {frames.shape[0]} frames for video mask generation...") - - # Step 1: Convert frames to video - print("Step 1: Converting frames to video...") - video_path = frames_to_video(frames, fbs, video_format=video_format) + 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: - # Step 2: Upload video to S3 - print("Step 2: Uploading video to S3...") - filename = f"input_video_{os.path.basename(video_path)}" - video_url = upload_video_to_s3(video_path, filename, api_key) - # Step 3: Call Bria API for mask generation print("Step 3: Calling Bria API for video mask generation...") payload = { - "video": video_url, + "video": input_video_url, "prompt": prompt, "output_container_and_codec": output_container_and_codec, "preserve_audio": preserve_audio @@ -109,23 +109,18 @@ class VideoMaskByPromptNode(): 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.") - # Step 4: Download and convert processed mask video to frames - print("Step 4: Downloading and converting processed mask video to frames...") - result_frames, result_frame_count, result_fps = video_to_frames(result_mask_url) - - print(f"Video mask generation complete! Processed {result_frame_count} frames.") - - return (result_frames, result_mask_url, result_frame_count, result_fps,) + 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: - # Clean up temporary video file - try: - if os.path.exists(video_path): - os.unlink(video_path) - except: - pass \ No newline at end of file + if video_path: + try: + if os.path.exists(video_path): + os.unlink(video_path) + except: + pass \ No newline at end of file diff --git a/nodes/video_nodes/video_solid_color_background_node.py b/nodes/video_nodes/video_solid_color_background_node.py index 6bbf0b0..3e2e401 100644 --- a/nodes/video_nodes/video_solid_color_background_node.py +++ b/nodes/video_nodes/video_solid_color_background_node.py @@ -1,29 +1,33 @@ import os +import uuid import requests +import folder_paths from ..common import deserialize_and_get_comfy_key, poll_status_until_completed -from .video_utils import frames_to_video, video_to_frames, upload_video_to_s3 +from .video_utils import upload_video_to_s3 class VideoSolidColorBackgroundNode(): """ - Bria Video Solid Color Background Node - - This node removes the background from videos and replaces it with a solid color - using the Bria API. It accepts video frames from the Load Video node, uploads to S3, - processes via API, and returns the processed frames for preview. - + Apply a solid color background to a video using the Bria API. + Parameters: - - frames: Batch of image frames from Load Video node - - api_key: Your Bria API token - - background_color: Predefined color string (default: Transparent) - - output_container_and_codec: Output video format and codec (default: mp4_h264) - - preserve_audio: Audio preservation (default: False) + 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": { - "frames": ("IMAGE", {"tooltip": "Batch of video frames"}), "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": ([ @@ -49,46 +53,44 @@ class VideoSolidColorBackgroundNode(): "mkv_h265", "mkv_vp9", "gif" - ], {"default": "mp4_h264"}), - "preserve_audio": ("BOOLEAN", {"default": True}), - "video_format": ("STRING", { - "default": "mp4", - "tooltip": "Original video format from Load Video node" - }), - "fbs": ("FLOAT", { - "default": 25, - "tooltip": "Original video format from Load Video node" - }), - + ], {"default": "webm_vp9"}), + "preserve_audio": ("BOOLEAN", {"default": True}), } } - RETURN_TYPES = ("IMAGE", "INT", "FLOAT", "STRING") - RETURN_NAMES = ("frames", "frame_count", "fps", "result_video_url") + 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, frames, api_key, video_format,fbs, background_color="Transparent", output_container_and_codec="mp4_h264", - preserve_audio=False): + 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.") api_key = deserialize_and_get_comfy_key(api_key) + video_path = None - print(f"Processing {frames.shape[0]} frames for solid color background...") + 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 - print("Step 1: Converting frames to video...") - video_path = frames_to_video(frames, fbs, video_format=video_format) try: - print("Step 2: Uploading video to S3...") - filename = f"input_video_{os.path.basename(video_path)}" - video_url = upload_video_to_s3(video_path, filename, api_key) print("Step 3: Calling Bria API for solid color background...") payload = { - "video": video_url, + "video": input_video_url, "background_color": background_color, "output_container_and_codec": output_container_and_codec, "preserve_audio": preserve_audio @@ -118,21 +120,18 @@ class VideoSolidColorBackgroundNode(): 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.") - print("Step 4: Downloading and converting processed video to frames...") - result_frames, result_frame_count, result_fps = video_to_frames(result_video_url) - - print(f"Solid color background processing complete! Processed {result_frame_count} frames.") - - return (result_frames, result_frame_count, result_fps, result_video_url,) + 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: - try: - if os.path.exists(video_path): - os.unlink(video_path) - except: - pass \ No newline at end of file + if video_path: + try: + if os.path.exists(video_path): + os.unlink(video_path) + except: + pass \ No newline at end of file diff --git a/nodes/video_nodes/video_utils.py b/nodes/video_nodes/video_utils.py index 181d556..1bbf755 100644 --- a/nodes/video_nodes/video_utils.py +++ b/nodes/video_nodes/video_utils.py @@ -1,166 +1,5 @@ -import mimetypes import os -import torch -import numpy as np -import av import requests -import tempfile -from fractions import Fraction - -def get_video_config(ext): - ext = ext.lower().replace(".", "") - - configs = { - "mp4": ("mp4", "libx264", "yuv420p"), - "mov": ("mov", "libx264", "yuv420p"), - "mkv": ("matroska", "libx264", "yuv420p"), - "webm": ("webm", "libvpx-vp9", "yuv420p"), - "gif": ("gif", "gif", "pal8"), - } - - if ext in configs: - return configs[ext] - - print(f"[WARNING] Unknown video format '{ext}', falling back to mp4/h264.") - return ("mp4", "libx264", "yuv420p") - - - -def frames_to_video(frames, fps, video_format="mp4"): - """ - Convert a batch of image frames to a video file. - Automatically selects correct container/codec/pixel-format based on extension. - """ - # Pick container, codec and pix_fmt - container_format, codec, pix_fmt = get_video_config(video_format) - - # Create temp file - temp_file = tempfile.NamedTemporaryFile(suffix=f".{video_format}", delete=False) - filepath = temp_file.name - temp_file.close() - - print(f"Converting {frames.shape[0]} frames → {video_format}") - print(f"Container={container_format}, Codec={codec}, PixFmt={pix_fmt}") - - # Open container with correct format - container = av.open(filepath, mode="w", format=container_format) - - # Create video stream - stream = container.add_stream(codec, rate=Fraction(round(fps * 1000), 1000)) - stream.width = frames.shape[2] - stream.height = frames.shape[1] - stream.pix_fmt = pix_fmt - - # Encode frames - for i, frame in enumerate(frames): - frame_array = torch.clamp(frame * 255, 0, 255) - frame_np = frame_array.to(dtype=torch.uint8, device="cpu").numpy() - - - video_frame = av.VideoFrame.from_ndarray(frame_np, format="rgb24") - - for packet in stream.encode(video_frame): - container.mux(packet) - - if (i + 1) % 100 == 0: - print(f"Encoded {i + 1}/{frames.shape[0]} frames...") - - # Flush remaining packets - for packet in stream.encode(): - container.mux(packet) - - container.close() - return filepath - - -def video_to_frames(video_path): - """ - Convert a video file to a batch of image frames - - Args: - video_path: Path to video file (local or URL) - - Returns: - tuple: (frames_tensor, frame_count, fps) - """ - # Download if it's a URL - if video_path.startswith("http://") or video_path.startswith("https://"): - print(f"Downloading video from: {video_path}") - response = requests.get(video_path, stream=True) - if response.status_code != 200: - raise Exception(f"Failed to download video: {response.status_code}") - - # Save to temporary file - temp_file = tempfile.NamedTemporaryFile(suffix=".mp4", delete=False) - for chunk in response.iter_content(chunk_size=8192): - temp_file.write(chunk) - temp_file.close() - video_path = temp_file.name - print(f"Video downloaded to: {video_path}") - - if not os.path.exists(video_path): - raise FileNotFoundError(f"Video file not found: {video_path}") - - frames = [] - fps = 24.0 # Default FPS - - try: - # Open video using PyAV - container = av.open(video_path) - - # Get video stream - video_stream = None - for stream in container.streams: - if stream.type == 'video': - video_stream = stream - break - - if video_stream is None: - raise ValueError(f"No video stream found in video") - - # Get FPS - if video_stream.average_rate is not None: - fps = float(video_stream.average_rate) - - print(f"Loading video: {video_path}") - print(f"Video FPS: {fps}") - print(f"Video resolution: {video_stream.width}x{video_stream.height}") - - # Decode frames - for frame in container.decode(video_stream): - # Convert frame to RGB PIL Image - img = frame.to_image() - - # Convert PIL Image to numpy array - img_array = np.array(img.convert('RGB')).astype(np.float32) / 255.0 - - # Convert to torch tensor with shape [H, W, C] - img_tensor = torch.from_numpy(img_array) - - frames.append(img_tensor) - - container.close() - - if len(frames) == 0: - raise ValueError(f"No frames could be extracted from video") - - # Stack frames into a single tensor with shape [B, H, W, C] - frames_tensor = torch.stack(frames, dim=0) - - print(f"Extracted {len(frames)} frames from video") - print(f"Output tensor shape: {frames_tensor.shape}") - - # Clean up temporary file if downloaded - if video_path.startswith(tempfile.gettempdir()): - try: - os.unlink(video_path) - except: - pass - - return (frames_tensor, len(frames), fps) - - except Exception as e: - raise Exception(f"Error loading video: {str(e)}") def upload_video_to_s3(video_path, filename, api_token): diff --git a/workflows/Video_Editig_Workflow.json b/workflows/Video_Editig_Workflow.json new file mode 100644 index 0000000..7fe68d3 --- /dev/null +++ b/workflows/Video_Editig_Workflow.json @@ -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 +} \ No newline at end of file