From 1abab1d81e6f1c898850ffc5f6376e9af4db5b73 Mon Sep 17 00:00:00 2001 From: Glary-Bot Date: Tue, 9 Jun 2026 17:10:27 +0000 Subject: [PATCH 1/3] [Partner Nodes] feat(bria): add Transparent option to Remove Video Background Bria's /remove_background endpoint accepts background_color='Transparent' when paired with an alpha-capable container/codec; pass output_container_and_codec= 'webm_vp9' in that case (otherwise mp4_h264, unchanged). The returned WebM flows through ComfyUI's VIDEO type as a file reference, so Save Video with the default auto/auto settings stream-copies it byte-for-byte with the alpha plane intact (covered by new test_save_to_auto_preserves_vp9_alpha_via_stream_copy). BriaTransparentVideoBackground is kept as-is because it remains the supported path for the IMAGE+MASK workflow: Get Video Components currently cannot expose alpha for VP9 streams (PyAV's default vp9 decoder drops the side alpha layer), so the per-frame compositing workflow still needs the dedicated node that decodes via libvpx-vp9 itself. --- comfy_api_nodes/nodes_bria.py | 24 +++++-- tests-unit/comfy_api_test/video_types_test.py | 65 ++++++++++++++++++- 2 files changed, 83 insertions(+), 6 deletions(-) diff --git a/comfy_api_nodes/nodes_bria.py b/comfy_api_nodes/nodes_bria.py index e138fafa9..9fa76cccd 100644 --- a/comfy_api_nodes/nodes_bria.py +++ b/comfy_api_nodes/nodes_bria.py @@ -251,12 +251,17 @@ class BriaRemoveVideoBackground(IO.ComfyNode): node_id="BriaRemoveVideoBackground", display_name="Bria Remove Video Background", category="partner/video/Bria", - description="Remove the background from a video using Bria. ", + description="Remove the background from a video using Bria. Pick a color to composite " + "the foreground over a solid background (returns an MP4), or pick 'Transparent' to " + "get a WebM/VP9 video with a real alpha channel. Connect Transparent output directly " + "to Save Video to write the alpha-channel WebM to disk; for per-frame compositing " + "(images + mask sockets), use 'Bria Remove Video Background (Transparent)' instead.", inputs=[ IO.Video.Input("video"), IO.Combo.Input( "background_color", options=[ + "Transparent", "Black", "White", "Gray", @@ -268,7 +273,11 @@ class BriaRemoveVideoBackground(IO.ComfyNode): "Magenta", "Orange", ], - tooltip="Background color for the output video.", + tooltip="Background color for the output video. 'Transparent' returns a " + "WebM/VP9 video with an alpha channel (best paired with Save Video); other " + "options composite the foreground over the chosen solid color and return an MP4. " + "For per-frame compositing (images + mask sockets) use 'Bria Remove Video " + "Background (Transparent)' instead.", ), IO.Int.Input( "seed", @@ -301,13 +310,17 @@ class BriaRemoveVideoBackground(IO.ComfyNode): seed: int, ) -> IO.NodeOutput: validate_video_duration(video, max_duration=60.0) + # Bria returns 422 if background_color="Transparent" is paired with mp4_h264 + # (no alpha plane). VP9-in-WebM carries the alpha; downstream VIDEO handling + # preserves it as long as Save Video uses auto/auto (stream-copy) settings. + output_container_and_codec = "webm_vp9" if background_color == "Transparent" else "mp4_h264" response = await sync_op( cls, ApiEndpoint(path="/proxy/bria/v2/video/edit/remove_background", method="POST"), data=BriaRemoveVideoBackgroundRequest( video=await upload_video_to_comfyapi(cls, video), background_color=background_color, - output_container_and_codec="mp4_h264", + output_container_and_codec=output_container_and_codec, seed=seed, ), response_model=BriaStatusResponse, @@ -504,8 +517,9 @@ class BriaTransparentVideoBackground(IO.ComfyNode): display_name="Bria Remove Video Background (Transparent)", category="partner/video/Bria", description="Remove the background from a video using Bria and return the cut-out frames " - "plus an alpha mask. Connect both to a compositing node, or feed them to Save WEBM to " - "write a transparent video.", + "plus an alpha mask, ready for per-frame compositing or feeding into Save WEBM. Use this " + "when you need IMAGE + MASK sockets; if you only need to save a transparent video file, " + "use 'Bria Remove Video Background' with background_color='Transparent' instead.", inputs=[ IO.Video.Input("video"), IO.Int.Input( diff --git a/tests-unit/comfy_api_test/video_types_test.py b/tests-unit/comfy_api_test/video_types_test.py index b25fcb1ca..b2729bf50 100644 --- a/tests-unit/comfy_api_test/video_types_test.py +++ b/tests-unit/comfy_api_test/video_types_test.py @@ -5,10 +5,13 @@ import os import av import io from fractions import Fraction +import numpy as np + from comfy_api.input_impl.video_types import VideoFromFile, VideoFromComponents -from comfy_api.util.video_types import VideoComponents +from comfy_api.util.video_types import VideoComponents, VideoContainer, VideoCodec from comfy_api.input.basic_types import AudioInput from av.error import InvalidDataError +from av.codec import CodecContext EPSILON = 0.0001 @@ -237,3 +240,63 @@ def test_duration_consistency(video_components): manual_duration = float(components.images.shape[0] / components.frame_rate) assert duration == pytest.approx(manual_duration) + + +def _make_webm_vp9_with_alpha(width=32, height=32, frames=5, fps=10) -> bytes: + buf = io.BytesIO() + with av.open(buf, mode="w", format="webm") as container: + stream = container.add_stream("libvpx-vp9", rate=fps) + stream.width = width + stream.height = height + stream.pix_fmt = "yuva420p" + for i in range(frames): + rgba = np.zeros((height, width, 4), dtype=np.uint8) + rgba[..., 0] = 200 + rgba[..., 1] = 50 + i * 30 + rgba[..., 2] = 80 + rgba[..., 3] = (i + 1) * 50 + frame = av.VideoFrame.from_ndarray(rgba, format="rgba") + frame = frame.reformat(format="yuva420p") + for packet in stream.encode(frame): + container.mux(packet) + for packet in stream.encode(None): + container.mux(packet) + return buf.getvalue() + + +def _per_frame_alpha_means_libvpx(source) -> list: + means = [] + with av.open(source, mode="r") as container: + stream = container.streams.video[0] + decoder = CodecContext.create("libvpx-vp9", "r") + for packet in container.demux(stream): + for frame in decoder.decode(packet): + rgba = frame.to_ndarray(format="rgba") + means.append(float(rgba[..., 3].mean())) + return means + + +def test_save_to_auto_preserves_vp9_alpha_via_stream_copy(): + """Save Video with format=auto, codec=auto stream-copies a WebM/VP9+alpha file + without touching the alpha plane. This is the assumption that the consolidated + 'Bria Remove Video Background' node relies on when background_color='Transparent' + (which makes Bria return a webm_vp9 url with a real alpha channel).""" + raw = _make_webm_vp9_with_alpha() + original_means = _per_frame_alpha_means_libvpx(io.BytesIO(raw)) + assert len(original_means) > 0, "test fixture failed to produce frames" + assert original_means == sorted(original_means), ( + "test fixture alpha did not encode as monotonically increasing" + ) + + video = VideoFromFile(io.BytesIO(raw)) + out_buf = io.BytesIO() + video.save_to(out_buf, format=VideoContainer.AUTO, codec=VideoCodec.AUTO) + + out_buf.seek(0) + saved_means = _per_frame_alpha_means_libvpx(out_buf) + + assert len(saved_means) == len(original_means) + for original, saved in zip(original_means, saved_means): + assert original == pytest.approx(saved, abs=EPSILON), ( + f"alpha lost across stream-copy round-trip: {original} -> {saved}" + ) From ab952395d793ce95050eab9a8a354ee671c0fedd Mon Sep 17 00:00:00 2001 From: Glary-Bot Date: Tue, 9 Jun 2026 17:39:58 +0000 Subject: [PATCH 2/3] fixup: address review feedback (default=Black, honest Save Video caveat) - Move 'Transparent' to the end of the dropdown and set explicit default='Black' so newly-added nodes preserve prior behavior (review warning). - Reword node + tooltip descriptions to be honest that Save Video drops the alpha plane when its default .mp4 extension is used; direct users to the IMAGE+MASK node for any workflow that needs to persist alpha to disk today. - Update the dispatch comment with the same caveat. --- comfy_api_nodes/nodes_bria.py | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/comfy_api_nodes/nodes_bria.py b/comfy_api_nodes/nodes_bria.py index 9fa76cccd..9b4a5b34e 100644 --- a/comfy_api_nodes/nodes_bria.py +++ b/comfy_api_nodes/nodes_bria.py @@ -252,16 +252,16 @@ class BriaRemoveVideoBackground(IO.ComfyNode): display_name="Bria Remove Video Background", category="partner/video/Bria", description="Remove the background from a video using Bria. Pick a color to composite " - "the foreground over a solid background (returns an MP4), or pick 'Transparent' to " - "get a WebM/VP9 video with a real alpha channel. Connect Transparent output directly " - "to Save Video to write the alpha-channel WebM to disk; for per-frame compositing " - "(images + mask sockets), use 'Bria Remove Video Background (Transparent)' instead.", + "the foreground over a solid background (returns an MP4), or pick 'Transparent' to get " + "a WebM/VP9 video with a real alpha channel. For per-frame compositing (images + mask " + "sockets) or saving the transparent result to disk with alpha preserved, use 'Bria " + "Remove Video Background (Transparent)' instead -- Save Video currently writes to .mp4 " + "by default, which drops the VP9 alpha plane.", inputs=[ IO.Video.Input("video"), IO.Combo.Input( "background_color", options=[ - "Transparent", "Black", "White", "Gray", @@ -272,12 +272,13 @@ class BriaRemoveVideoBackground(IO.ComfyNode): "Cyan", "Magenta", "Orange", + "Transparent", ], - tooltip="Background color for the output video. 'Transparent' returns a " - "WebM/VP9 video with an alpha channel (best paired with Save Video); other " - "options composite the foreground over the chosen solid color and return an MP4. " - "For per-frame compositing (images + mask sockets) use 'Bria Remove Video " - "Background (Transparent)' instead.", + default="Black", + tooltip="Background color for the output video. Colors composite the " + "foreground over a solid background and return an MP4. 'Transparent' returns " + "a WebM/VP9 video with an alpha channel; for per-frame compositing or saving " + "the alpha to disk, use 'Bria Remove Video Background (Transparent)' instead.", ), IO.Int.Input( "seed", @@ -311,8 +312,9 @@ class BriaRemoveVideoBackground(IO.ComfyNode): ) -> IO.NodeOutput: validate_video_duration(video, max_duration=60.0) # Bria returns 422 if background_color="Transparent" is paired with mp4_h264 - # (no alpha plane). VP9-in-WebM carries the alpha; downstream VIDEO handling - # preserves it as long as Save Video uses auto/auto (stream-copy) settings. + # (no alpha plane), so request webm_vp9 instead. VP9 carries the alpha in a side + # layer; the bytes pass through ComfyUI's VIDEO type as a file reference but + # SaveVideo's default .mp4 extension currently drops it on mux -- see PR notes. output_container_and_codec = "webm_vp9" if background_color == "Transparent" else "mp4_h264" response = await sync_op( cls, From 320fcfa29b68a0421bd02ed9f5f2d4c196f65436 Mon Sep 17 00:00:00 2001 From: Glary-Bot Date: Tue, 9 Jun 2026 17:47:29 +0000 Subject: [PATCH 3/3] fixup: address review feedback round 2 - Rename test to reflect that it only validates the in-memory BytesIO stream-copy path; documents that the .mp4 file-path case (SaveVideo's default) drops alpha. - Flush the libvpx-vp9 decoder with decode(None) in the helper. - Rework BriaTransparentVideoBackground description to make it explicit that this is currently the only way to persist the alpha channel to disk. --- comfy_api_nodes/nodes_bria.py | 7 ++++--- tests-unit/comfy_api_test/video_types_test.py | 17 ++++++++++++----- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/comfy_api_nodes/nodes_bria.py b/comfy_api_nodes/nodes_bria.py index 9b4a5b34e..4296a3a3d 100644 --- a/comfy_api_nodes/nodes_bria.py +++ b/comfy_api_nodes/nodes_bria.py @@ -519,9 +519,10 @@ class BriaTransparentVideoBackground(IO.ComfyNode): display_name="Bria Remove Video Background (Transparent)", category="partner/video/Bria", description="Remove the background from a video using Bria and return the cut-out frames " - "plus an alpha mask, ready for per-frame compositing or feeding into Save WEBM. Use this " - "when you need IMAGE + MASK sockets; if you only need to save a transparent video file, " - "use 'Bria Remove Video Background' with background_color='Transparent' instead.", + "plus an alpha mask, ready for per-frame compositing or feeding into Save WEBM to " + "persist a transparent video to disk. This is currently the only way to save the " + "alpha channel: Save Video on the consolidated 'Bria Remove Video Background' node's " + "Transparent output writes a .mp4 by default and drops the VP9 alpha plane on mux.", inputs=[ IO.Video.Input("video"), IO.Int.Input( diff --git a/tests-unit/comfy_api_test/video_types_test.py b/tests-unit/comfy_api_test/video_types_test.py index b2729bf50..84f963517 100644 --- a/tests-unit/comfy_api_test/video_types_test.py +++ b/tests-unit/comfy_api_test/video_types_test.py @@ -273,14 +273,21 @@ def _per_frame_alpha_means_libvpx(source) -> list: for frame in decoder.decode(packet): rgba = frame.to_ndarray(format="rgba") means.append(float(rgba[..., 3].mean())) + try: + for frame in decoder.decode(None): + rgba = frame.to_ndarray(format="rgba") + means.append(float(rgba[..., 3].mean())) + except EOFError: + pass return means -def test_save_to_auto_preserves_vp9_alpha_via_stream_copy(): - """Save Video with format=auto, codec=auto stream-copies a WebM/VP9+alpha file - without touching the alpha plane. This is the assumption that the consolidated - 'Bria Remove Video Background' node relies on when background_color='Transparent' - (which makes Bria return a webm_vp9 url with a real alpha channel).""" +def test_video_from_file_save_to_bytesio_stream_copies_vp9_alpha(): + """VideoFromFile.save_to(BytesIO, format=AUTO, codec=AUTO) stream-copies the source + container/codec without re-encoding, so a WebM/VP9 file with an alpha plane + round-trips unchanged. Note: this covers ONLY the in-memory BytesIO path. Saving + to a .mp4 file (SaveVideo's default extension) muxes VP9 into MP4 and drops the + alpha plane -- intentionally not covered here; see PR notes.""" raw = _make_webm_vp9_with_alpha() original_means = _per_frame_alpha_means_libvpx(io.BytesIO(raw)) assert len(original_means) > 0, "test fixture failed to produce frames"