This commit is contained in:
guill 2026-07-06 17:34:00 +08:00 committed by GitHub
commit b78377b6f7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 93 additions and 6 deletions

View File

@ -251,7 +251,12 @@ class BriaRemoveVideoBackground(IO.ComfyNode):
node_id="BriaRemoveVideoBackground", node_id="BriaRemoveVideoBackground",
display_name="Bria Remove Video Background", display_name="Bria Remove Video Background",
category="partner/video/Bria", 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. 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=[ inputs=[
IO.Video.Input("video"), IO.Video.Input("video"),
IO.Combo.Input( IO.Combo.Input(
@ -267,8 +272,13 @@ class BriaRemoveVideoBackground(IO.ComfyNode):
"Cyan", "Cyan",
"Magenta", "Magenta",
"Orange", "Orange",
"Transparent",
], ],
tooltip="Background color for the output video.", 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( IO.Int.Input(
"seed", "seed",
@ -301,13 +311,18 @@ class BriaRemoveVideoBackground(IO.ComfyNode):
seed: int, seed: int,
) -> IO.NodeOutput: ) -> IO.NodeOutput:
validate_video_duration(video, max_duration=60.0) validate_video_duration(video, max_duration=60.0)
# Bria returns 422 if background_color="Transparent" is paired with mp4_h264
# (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( response = await sync_op(
cls, cls,
ApiEndpoint(path="/proxy/bria/v2/video/edit/remove_background", method="POST"), ApiEndpoint(path="/proxy/bria/v2/video/edit/remove_background", method="POST"),
data=BriaRemoveVideoBackgroundRequest( data=BriaRemoveVideoBackgroundRequest(
video=await upload_video_to_comfyapi(cls, video), video=await upload_video_to_comfyapi(cls, video),
background_color=background_color, background_color=background_color,
output_container_and_codec="mp4_h264", output_container_and_codec=output_container_and_codec,
seed=seed, seed=seed,
), ),
response_model=BriaStatusResponse, response_model=BriaStatusResponse,
@ -507,8 +522,10 @@ class BriaTransparentVideoBackground(IO.ComfyNode):
display_name="Bria Remove Video Background (Transparent)", display_name="Bria Remove Video Background (Transparent)",
category="partner/video/Bria", category="partner/video/Bria",
description="Remove the background from a video using Bria and return the cut-out frames " 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 " "plus an alpha mask, ready for per-frame compositing or feeding into Save WEBM to "
"write a transparent video.", "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=[ inputs=[
IO.Video.Input("video"), IO.Video.Input("video"),
IO.Int.Input( IO.Int.Input(

View File

@ -5,10 +5,13 @@ import os
import av import av
import io import io
from fractions import Fraction from fractions import Fraction
import numpy as np
from comfy_api.input_impl.video_types import VideoFromFile, VideoFromComponents 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 comfy_api.input.basic_types import AudioInput
from av.error import InvalidDataError from av.error import InvalidDataError
from av.codec import CodecContext
EPSILON = 0.0001 EPSILON = 0.0001
@ -237,3 +240,70 @@ def test_duration_consistency(video_components):
manual_duration = float(components.images.shape[0] / components.frame_rate) manual_duration = float(components.images.shape[0] / components.frame_rate)
assert duration == pytest.approx(manual_duration) 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()))
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_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"
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}"
)