diff --git a/__init__.py b/__init__.py index 2950882..24b7d98 100644 --- a/__init__.py +++ b/__init__.py @@ -30,7 +30,15 @@ from .nodes import ( ShotByTextManualPlacementNode, ShotByTextManualPaddingNode, ShotByTextCustomCoordinatesNode, - AttributionByImageNode + AttributionByImageNode, + RemoveVideoBackgroundNode, + VideoSolidColorBackgroundNode, + VideoMaskByPromptNode, + VideoMaskByKeyPointsNode, + VideoIncreaseResolutionNode, + VideoEraseElementsNode, + LoadVideoFramesNode, + PreviewVideoURLNode ) # Map the node class to a name used internally by ComfyUI @@ -67,6 +75,14 @@ NODE_CLASS_MAPPINGS = { "RefineImageLiteNodeV2": RefineImageLiteNodeV2, "GenerateStructuredPromptNodeV2": GenerateStructuredPromptNodeV2, "GenerateStructuredPromptLiteNodeV2": GenerateStructuredPromptLiteNodeV2, + "RemoveVideoBackgroundNode":RemoveVideoBackgroundNode, + "VideoSolidColorBackgroundNode":VideoSolidColorBackgroundNode, + "VideoMaskByPromptNode":VideoMaskByPromptNode, + "VideoMaskByKeyPointsNode":VideoMaskByKeyPointsNode, + "VideoIncreaseResolutionNode":VideoIncreaseResolutionNode, + "VideoEraseElementsNode":VideoEraseElementsNode, + "LoadVideoFramesNode":LoadVideoFramesNode, + "PreviewVideoURLNode":PreviewVideoURLNode } # Map the node display name to the one shown in the ComfyUI node interface NODE_DISPLAY_NAME_MAPPINGS = { @@ -102,6 +118,14 @@ NODE_DISPLAY_NAME_MAPPINGS = { "RefineImageLiteNodeV2": "Refine Image - Lite", "GenerateStructuredPromptNodeV2": "Generate Structured Prompt", "GenerateStructuredPromptLiteNodeV2": "Generate Structured Prompt - Lite", + "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", + "LoadVideoFramesNode":"Bria Load Video", + "PreviewVideoURLNode":"Bria Preview Video" } diff --git a/nodes/__init__.py b/nodes/__init__.py index 2e7d9c5..c9619f0 100644 --- a/nodes/__init__.py +++ b/nodes/__init__.py @@ -32,3 +32,12 @@ 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 +from .video_nodes.load_video import LoadVideoFramesNode +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 new file mode 100644 index 0000000..f98877d --- /dev/null +++ b/nodes/video_nodes/load_video.py @@ -0,0 +1,52 @@ +import os +import folder_paths + +class LoadVideoFramesNode: + """ + Load a video file from the input folder or upload. + + Parameters: + video (str): Selected or uploaded video filename. + + Returns: + video_path (STRING): Absolute path to the video file. + """ + + @classmethod + def INPUT_TYPES(cls): + input_dir = folder_paths.get_input_directory() + files = [f for f in os.listdir(input_dir) if os.path.isfile(os.path.join(input_dir, f))] + files = folder_paths.filter_files_content_types(files, ["video"]) + + return { + "required": { + "video": (sorted(files), {"video_upload": True}), + } + } + + RETURN_TYPES = ("STRING",) + RETURN_NAMES = ("video_path",) + FUNCTION = "load_video" + CATEGORY = "API Nodes" + + def load_video(self, video): + video_path = folder_paths.get_annotated_filepath(video) + if not os.path.exists(video_path): + raise FileNotFoundError(f"Video file not found: {video_path}") + + return (video_path,) + + @classmethod + def IS_CHANGED(cls, video, **kwargs): + """Force re-execution when video file changes""" + video_path = folder_paths.get_annotated_filepath(video) + if os.path.exists(video_path): + return os.path.getmtime(video_path) + return float("nan") + + @classmethod + def VALIDATE_INPUTS(cls, video, **kwargs): + """Validate that the video file exists""" + if not folder_paths.exists_annotated_filepath(video): + return f"Invalid video file: {video}" + return True \ 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 new file mode 100644 index 0000000..66e35e0 --- /dev/null +++ b/nodes/video_nodes/remove_video_background_node.py @@ -0,0 +1,119 @@ +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 upload_video_to_s3 + +class RemoveVideoBackgroundNode(): + """ + Removes the background from a video using the Bria API. + + Parameters: + api_key (str): Your Bria API key. + video_url (str): Local path or URL of the video to process. + preserve_audio (bool, optional): Whether to keep the audio track. Default is True. + output_container_and_codec (str, optional): Desired output format and codec. Default is "webm_vp9". + + Returns: + result_video_url (STRING): URL of the video with background removed. + """ + @classmethod + def INPUT_TYPES(self): + return { + "required": { + "api_key": ("STRING", {"default": "BRIA_API_TOKEN"}), + "video_url": ("STRING", { + "default": "", + "tooltip": "URL of video to process (provide either frames or video_url)" + }), + }, + "optional": { + "preserve_audio": ("BOOLEAN", {"default": True}), + "output_container_and_codec": ([ + "mp4_h264", + "mp4_h265", + "webm_vp9", + "mov_h265", + "mov_proresks", + "mkv_h264", + "mkv_h265", + "mkv_vp9", + "gif" + ], {"default": "webm_vp9"}), + } + } + + RETURN_TYPES = ("STRING",) + RETURN_NAMES = ("result_video_url",) + CATEGORY = "API Nodes" + FUNCTION = "execute" + + def __init__(self): + self.api_url = "https://engine.prod.bria-api.com/v2/video/edit/remove_background" + + def execute(self, api_key, video_url, preserve_audio=True, output_container_and_codec="webm_vp9",): + 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 + + input_video_url = "" + if video_url and video_url.strip() != "": + if os.path.exists(video_url): + filename = f"{ str(uuid.uuid4())}_{os.path.basename(video_url)}" + input_video_url = upload_video_to_s3(video_url, filename, api_key) + if video_url.startswith(folder_paths.get_temp_directory()): + video_path = None + else: + input_video_url = video_url + + try: + + print("Step 3: Calling Bria API for background removal...") + payload = { + "video": input_video_url, + "preserve_audio": preserve_audio, + "output_container_and_codec": output_container_and_codec + } + + 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: + print('Initial Video RMBG request successful, polling for completion...') + response_dict = response.json() + + status_url = response_dict.get('status_url') + request_id = response_dict.get('request_id') + + if not status_url: + raise Exception("No status_url returned from API") + + print(f"Request ID: {request_id}, Status URL: {status_url}") + + final_response = poll_status_until_completed(status_url, api_key, timeout=3600, check_interval=5) + + result_video_url = final_response['result']['video_url'] + + print(f"Video processing completed. Result URL: {result_video_url}") + print(f"Background removal complete! Use Preview Video URL node to view the result.") + + return (result_video_url,) + else: + raise Exception(f"Error: API request failed with status code {response.status_code} {response.text}") + + except Exception as e: + raise Exception(f"{e}") + finally: + if video_path: + try: + if os.path.exists(video_path): + os.unlink(video_path) + except: + pass + 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..2680978 --- /dev/null +++ b/nodes/video_nodes/video_erase_elements_node.py @@ -0,0 +1,127 @@ +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 upload_video_to_s3 + +class VideoEraseElementsNode(): + """ + Erase elements from a video using the Bria API. + + Parameters: + api_key (str): Your Bria API key. + video_url (str): Local path or URL of the video to process. + mask_url (str, optional): URL of a mask video for selective erasing. + output_container_and_codec (str, optional): Desired output format and codec. Default is "mp4_h264". + preserve_audio (bool, optional): Whether to keep the audio track. Default is True. + + Returns: + result_video_url (STRING): URL of the processed video with elements erased. + """ + @classmethod + def INPUT_TYPES(self): + return { + "required": { + "api_key": ("STRING", {"default": "BRIA_API_TOKEN"}), + "video_url": ("STRING", { + "default": "", + "tooltip": "URL of video to process (provide either frames or video_url)" + }), + }, + "optional": { + "mask_url": ("STRING", { + "default": "", + "tooltip": "URL of mask video (optional)" + }), + "output_container_and_codec": ([ + "mp4_h264", + "mp4_h265", + "webm_vp9", + "mov_h265", + "mov_proresks", + "mkv_h264", + "mkv_h265", + "mkv_vp9", + "gif" + ], {"default": "mp4_h264"}), + "preserve_audio": ("BOOLEAN", {"default": True}), + } + } + + RETURN_TYPES = ("STRING",) + RETURN_NAMES = ("result_video_url",) + CATEGORY = "API Nodes" + FUNCTION = "execute" + + def __init__(self): + self.api_url = "https://engine.prod.bria-api.com/v2/video/edit/erase" + + def execute(self, api_key, video_url, mask_url="", output_container_and_codec="mp4_h264", preserve_audio=True): + if api_key.strip() == "" or api_key.strip() == "BRIA_API_TOKEN": + raise Exception("Please insert a valid API key.") + api_key = deserialize_and_get_comfy_key(api_key) + video_path = None + + if video_url and video_url.strip() != "": + # Check if video_url is a local file path or a URL + if os.path.exists(video_url): + filename = f"{ str(uuid.uuid4())}_{os.path.basename(video_url)}" + input_video_url = upload_video_to_s3(video_url, filename, api_key) + + if not input_video_url or not (input_video_url.startswith('http://') or input_video_url.startswith('https://')): + raise Exception(f"Failed to upload video to S3. Got: {input_video_url}") + if video_url.startswith(folder_paths.get_temp_directory()): + video_path = None + else: + input_video_url = video_url + + try: + + print("Step 3: Calling Bria API for element erasure...") + payload = { + "video": input_video_url, + "mask": mask_url, + "output_container_and_codec": output_container_and_codec, + "preserve_audio": preserve_audio + } + + headers = { + "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: + print('Initial Video Erase Elements request successful, polling for completion...') + response_dict = response.json() + + status_url = response_dict.get('status_url') + request_id = response_dict.get('request_id') + + if not status_url: + raise Exception("No status_url returned from API") + + print(f"Request ID: {request_id}, Status URL: {status_url}") + + final_response = poll_status_until_completed(status_url, api_key, timeout=3600, check_interval=5) + + result_video_url = final_response['result']['video_url'] + + print(f"Video processing completed. Result URL: {result_video_url}") + print(f"Element erasure complete! Use Preview Video URL node to view the result.") + + return (result_video_url,) + else: + raise Exception(f"Error: API request failed with status code {response.status_code} {response.text}") + + except Exception as e: + raise Exception(f"{e}") + finally: + if video_path: + try: + if os.path.exists(video_path): + os.unlink(video_path) + except: + pass \ 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..46829af --- /dev/null +++ b/nodes/video_nodes/video_increase_resolution_node.py @@ -0,0 +1,124 @@ +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 upload_video_to_s3 + +class VideoIncreaseResolutionNode(): + """ + Increase the resolution of a video using the Bria API. + + Parameters: + api_key (str): Your Bria API key. + video_url (str): Local path or URL of the video to process. + desired_increase (str, optional): Resolution increase factor, '2' or '4'. Default is '2'. + output_container_and_codec (str, optional): Desired output format and codec. Default is "mp4_h264". + preserve_audio (bool, optional): Whether to keep the audio track. Default is True. + + Returns: + result_video_url (STRING): URL of the processed video with increased resolution. + """ + @classmethod + def INPUT_TYPES(self): + return { + "required": { + "api_key": ("STRING", {"default": "BRIA_API_TOKEN"}), + "video_url": ("STRING", { + "default": "", + "tooltip": "URL of video to process (provide either frames or video_url)" + }), + }, + "optional": { + "desired_increase": (['2', '4'], {"default": '2'}), + "output_container_and_codec": ([ + "mp4_h264", + "mp4_h265", + "webm_vp9", + "mov_h265", + "mov_proresks", + "mkv_h264", + "mkv_h265", + "mkv_vp9", + "gif" + ], {"default": "mp4_h264"}), + "preserve_audio": ("BOOLEAN", {"default": True}), + } + } + + RETURN_TYPES = ("STRING",) + RETURN_NAMES = ("result_video_url",) + CATEGORY = "API Nodes" + FUNCTION = "execute" + + def __init__(self): + self.api_url = "https://engine.prod.bria-api.com/v2/video/edit/increase_resolution" + + def execute(self, api_key, video_url, desired_increase='2', output_container_and_codec="mp4_h264", preserve_audio=True): + if api_key.strip() == "" or api_key.strip() == "BRIA_API_TOKEN": + raise Exception("Please insert a valid API key.") + api_key = deserialize_and_get_comfy_key(api_key) + video_path = None + + if video_url and video_url.strip() != "": + if os.path.exists(video_url): + filename = f"{ str(uuid.uuid4())}_{os.path.basename(video_url)}" + input_video_url = upload_video_to_s3(video_url, filename, api_key) + + if not input_video_url or not (input_video_url.startswith('http://') or input_video_url.startswith('https://')): + raise Exception(f"Failed to upload video to S3. Got: {input_video_url}") + + if video_url.startswith(folder_paths.get_temp_directory()): + video_path = None + else: + input_video_url = video_url + + try: + + print("Step 3: Calling Bria API for resolution increase...") + payload = { + "video": input_video_url, + "desired_increase": desired_increase, + "output_container_and_codec": output_container_and_codec, + "preserve_audio": preserve_audio + } + + headers = { + "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: + print('Initial Video Increase Resolution request successful, polling for completion...') + response_dict = response.json() + + status_url = response_dict.get('status_url') + request_id = response_dict.get('request_id') + + if not status_url: + raise Exception("No status_url returned from API") + + print(f"Request ID: {request_id}, Status URL: {status_url}") + + final_response = poll_status_until_completed(status_url, api_key, timeout=3600, check_interval=5) + + result_video_url = final_response['result']['video_url'] + + print(f"Video processing completed. Result URL: {result_video_url}") + print(f"Resolution increase complete! Use Preview Video URL node to view the result.") + + return (result_video_url,) + else: + raise Exception(f"Error: API request failed with status code {response.status_code} {response.text}") + + except Exception as e: + raise Exception(f"{e}") + finally: + if video_path: + try: + if os.path.exists(video_path): + os.unlink(video_path) + except: + pass \ 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..e83c189 --- /dev/null +++ b/nodes/video_nodes/video_mask_by_key_points_node.py @@ -0,0 +1,132 @@ +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 upload_video_to_s3 +import json + +class VideoMaskByKeyPointsNode(): + """ + Generate a video mask using key points with the Bria API. + + Parameters: + key_points (str): JSON string of key points for masking. + api_key (str): Your Bria API key. + video_url (str): Local path or URL of the video to process. + output_container_and_codec (str, optional): Desired output format and codec. Default is "mp4_h264". + preserve_audio (bool, optional): Whether to keep the audio track. Default is True. + + Returns: + mask_url (STRING): URL of the generated video mask. + """ + @classmethod + def INPUT_TYPES(self): + return { + "required": { + "key_points": ("STRING", {"default": "[]", "multiline": True}), + "api_key": ("STRING", {"default": "BRIA_API_TOKEN"}), + "video_url": ("STRING", { + "default": "", + "tooltip": "URL of video to process (provide either frames or video_url)" + }), + }, + "optional": { + "output_container_and_codec": ([ + "mp4_h264", + "mp4_h265", + "webm_vp9", + "mov_h265", + "mov_proresks", + "mkv_h264", + "mkv_h265", + "mkv_vp9", + "gif" + ], {"default": "mp4_h264"}), + "preserve_audio": ("BOOLEAN", {"default": True}), + } + } + + RETURN_TYPES = ("STRING",) + RETURN_NAMES = ("mask_url",) + CATEGORY = "API Nodes" + FUNCTION = "execute" + + def __init__(self): + self.api_url = "https://engine.prod.bria-api.com/v2/video/segment/mask_by_key_points" + + def execute(self, key_points, api_key, video_url, output_container_and_codec="mp4_h264", preserve_audio=True): + if api_key.strip() == "" or api_key.strip() == "BRIA_API_TOKEN": + raise Exception("Please insert a valid API key.") + 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}") + + video_path = None + + if video_url and video_url.strip() != "": + if os.path.exists(video_url): + filename = f"{ str(uuid.uuid4())}_{os.path.basename(video_url)}" + input_video_url = upload_video_to_s3(video_url, filename, api_key) + + if not input_video_url or not (input_video_url.startswith('http://') or input_video_url.startswith('https://')): + raise Exception(f"Failed to upload video to S3. Got: {input_video_url}") + + + if video_url.startswith(folder_paths.get_temp_directory()): + video_path = None + else: + input_video_url = video_url + + try: + + print("Step 3: Calling Bria API for video mask generation by key points...") + payload = { + "video": input_video_url, + "key_points": key_points_array, + "output_container_and_codec": output_container_and_codec, + "preserve_audio": preserve_audio + } + + headers = { + "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: + print('Initial Video Mask by Key Points request successful, polling for completion...') + response_dict = response.json() + + status_url = response_dict.get('status_url') + request_id = response_dict.get('request_id') + + if not status_url: + raise Exception("No status_url returned from API") + + print(f"Request ID: {request_id}, Status URL: {status_url}") + + final_response = poll_status_until_completed(status_url, api_key, timeout=3600, check_interval=5) + + result_mask_url = final_response['result']['mask_url'] + + print(f"Video mask processing completed. Result URL: {result_mask_url}") + print(f"Video mask generation complete! Use Preview Video URL node to view the result.") + + return (result_mask_url,) + else: + raise Exception(f"Error: API request failed with status code {response.status_code} {response.text}") + + except Exception as e: + raise Exception(f"{e}") + finally: + if video_path: + try: + if os.path.exists(video_path): + os.unlink(video_path) + except: + pass \ 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..00ccdb6 --- /dev/null +++ b/nodes/video_nodes/video_mask_by_prompt_node.py @@ -0,0 +1,126 @@ +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 upload_video_to_s3 + +class VideoMaskByPromptNode(): + """ + Generate a video mask using a text prompt with the Bria API. + + Parameters: + prompt (str): Text prompt describing what to mask in the video. + api_key (str): Your Bria API key. + video_url (str): Local path or URL of the video to process. + output_container_and_codec (str, optional): Desired output format and codec. Default is "mp4_h264". + preserve_audio (bool, optional): Whether to keep the audio track. Default is True. + + Returns: + mask_url (STRING): URL of the generated video mask. + """ + @classmethod + def INPUT_TYPES(self): + return { + "required": { + "prompt": ("STRING", {"default": ""}), + "api_key": ("STRING", {"default": "BRIA_API_TOKEN"}), + "video_url": ("STRING", { + "default": "", + "tooltip": "URL of video to process (provide either frames or video_url)" + }), + }, + "optional": { + "output_container_and_codec": ([ + "mp4_h264", + "mp4_h265", + "webm_vp9", + "mov_h265", + "mov_proresks", + "mkv_h264", + "mkv_h265", + "mkv_vp9", + "gif" + ], {"default": "mp4_h264"}), + "preserve_audio": ("BOOLEAN", {"default": True}), + } + } + + RETURN_TYPES = ("STRING",) + RETURN_NAMES = ("mask_url",) + CATEGORY = "API Nodes" + FUNCTION = "execute" + + def __init__(self): + self.api_url = "https://engine.prod.bria-api.com/v2/video/segment/mask_by_prompt" + + def execute(self, prompt, api_key, video_url, output_container_and_codec="mp4_h264", preserve_audio=True): + if api_key.strip() == "" or api_key.strip() == "BRIA_API_TOKEN": + raise Exception("Please insert a valid API key.") + api_key = deserialize_and_get_comfy_key(api_key) + + video_path = None + + if video_url and video_url.strip() != "": + if os.path.exists(video_url): + filename = f"{ str(uuid.uuid4())}_{os.path.basename(video_url)}" + input_video_url = upload_video_to_s3(video_url, filename, api_key) + + if not input_video_url or not (input_video_url.startswith('http://') or input_video_url.startswith('https://')): + raise Exception(f"Failed to upload video to S3. Got: {input_video_url}") + + + if video_url.startswith(folder_paths.get_temp_directory()): + video_path = None + else: + input_video_url = video_url + + try: + + print("Step 3: Calling Bria API for video mask generation...") + payload = { + "video": input_video_url, + "prompt": prompt, + "output_container_and_codec": output_container_and_codec, + "preserve_audio": preserve_audio + } + + headers = { + "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: + print('Initial Video Mask by Prompt request successful, polling for completion...') + response_dict = response.json() + + status_url = response_dict.get('status_url') + request_id = response_dict.get('request_id') + + if not status_url: + raise Exception("No status_url returned from API") + + print(f"Request ID: {request_id}, Status URL: {status_url}") + + final_response = poll_status_until_completed(status_url, api_key, timeout=3600, check_interval=5) + + result_mask_url = final_response['result']['mask_url'] + + print(f"Video mask processing completed. Result URL: {result_mask_url}") + print(f"Video mask generation complete! Use Preview Video URL node to view the result.") + + return (result_mask_url,) + else: + raise Exception(f"Error: API request failed with status code {response.status_code} {response.text}") + + except Exception as e: + raise Exception(f"{e}") + finally: + if video_path: + try: + if os.path.exists(video_path): + os.unlink(video_path) + except: + pass \ 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..3e2e401 --- /dev/null +++ b/nodes/video_nodes/video_solid_color_background_node.py @@ -0,0 +1,137 @@ +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 upload_video_to_s3 + +class VideoSolidColorBackgroundNode(): + """ + Apply a solid color background to a video using the Bria API. + + Parameters: + api_key (str): Your Bria API key. + video_url (str): Local path or URL of the video to process. + background_color (str, optional): Color to apply as background. Default is "Transparent". + output_container_and_codec (str, optional): Desired output format and codec. Default is "mp4_h264". + preserve_audio (bool, optional): Whether to keep the audio track. Default is True. + + Returns: + result_video_url (STRING): URL of the video with the solid color background applied. + """ + @classmethod + def INPUT_TYPES(self): + return { + "required": { + "api_key": ("STRING", {"default": "BRIA_API_TOKEN"}), + "video_url": ("STRING", { + "default": "", + "tooltip": "URL of video to process (provide either frames or video_url)" + }), + }, + "optional": { + "background_color": ([ + "Transparent", + "Black", + "White", + "Gray", + "Red", + "Green", + "Blue", + "Yellow", + "Cyan", + "Magenta", + "Orange" + ], {"default": "Transparent"}), + "output_container_and_codec": ([ + "mp4_h264", + "mp4_h265", + "webm_vp9", + "mov_h265", + "mov_proresks", + "mkv_h264", + "mkv_h265", + "mkv_vp9", + "gif" + ], {"default": "webm_vp9"}), + "preserve_audio": ("BOOLEAN", {"default": True}), + } + } + + RETURN_TYPES = ("STRING",) + RETURN_NAMES = ("result_video_url",) + CATEGORY = "API Nodes" + FUNCTION = "execute" + + def __init__(self): + self.api_url = "https://engine.prod.bria-api.com/v2/video/edit/remove_background" + + def execute(self, api_key, video_url, background_color="Transparent", output_container_and_codec="webm_vp9", preserve_audio=True): + if api_key.strip() == "" or api_key.strip() == "BRIA_API_TOKEN": + raise Exception("Please insert a valid API key.") + api_key = deserialize_and_get_comfy_key(api_key) + video_path = None + + if video_url and video_url.strip() != "": + if os.path.exists(video_url): + filename = f"{ str(uuid.uuid4())}_{os.path.basename(video_url)}" + input_video_url = upload_video_to_s3(video_url, filename, api_key) + + if not input_video_url or not (input_video_url.startswith('http://') or input_video_url.startswith('https://')): + raise Exception(f"Failed to upload video to S3. Got: {input_video_url}") + + + if video_url.startswith(folder_paths.get_temp_directory()): + video_path = None + else: + input_video_url = video_url + + try: + + print("Step 3: Calling Bria API for solid color background...") + payload = { + "video": input_video_url, + "background_color": background_color, + "output_container_and_codec": output_container_and_codec, + "preserve_audio": preserve_audio + } + + headers = { + "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: + print('Initial Video Solid Color Background request successful, polling for completion...') + response_dict = response.json() + + status_url = response_dict.get('status_url') + request_id = response_dict.get('request_id') + + if not status_url: + raise Exception("No status_url returned from API") + + print(f"Request ID: {request_id}, Status URL: {status_url}") + + final_response = poll_status_until_completed(status_url, api_key, timeout=3600, check_interval=5) + + result_video_url = final_response['result']['video_url'] + + print(f"Video processing completed. Result URL: {result_video_url}") + print(f"Solid color background processing complete! Use Preview Video URL node to view the result.") + + return (result_video_url,) + else: + raise Exception(f"Error: API request failed with status code {response.status_code} {response.text}") + + except Exception as e: + raise Exception(f"{e}") + finally: + if video_path: + try: + if os.path.exists(video_path): + os.unlink(video_path) + except: + pass \ 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..1bbf755 --- /dev/null +++ b/nodes/video_nodes/video_utils.py @@ -0,0 +1,69 @@ +import os +import requests + + +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)}") + 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