hanxxing's picture
Duplicate from rzgar/Bernini-R-S2V
832fee5
Raw
History Blame Contribute Delete
6.93 kB
import torch
from typing_extensions import override
import comfy.model_management
import comfy.utils
import node_helpers
from comfy_api.latest import ComfyExtension, io
from .audio_mask import build_timeline_audio_inject_mask
from .wan_audio import apply_timeline_audio_conditioning, resolve_timeline_segment_ranges
def _resize_long_edge(image, max_size, stride=16):
h, w = image.shape[1], image.shape[2]
scale = min(max_size / max(h, w), 1.0)
nh = max(stride, round(h * scale / stride) * stride)
nw = max(stride, round(w * scale / stride) * stride)
return comfy.utils.common_upscale(image[:, :, :, :3].movedim(-1, 1), nw, nh, "area", "disabled").movedim(1, -1)
def _build_context_latents(vae, width, height, length, source_video=None, reference_video=None, reference_images=None, ref_max_size=848):
context = []
if source_video is not None:
vid = comfy.utils.common_upscale(source_video[:length, :, :, :3].movedim(-1, 1), width, height, "area", "center").movedim(1, -1)
context.append(vae.encode(vid[:, :, :, :3]))
if reference_video is not None:
ref_vid = _resize_long_edge(reference_video[:length], ref_max_size)
context.append(vae.encode(ref_vid[:, :, :, :3]))
if reference_images:
for name in sorted(reference_images):
imgs = reference_images[name]
if imgs is None:
continue
for i in range(imgs.shape[0]):
img = _resize_long_edge(imgs[i:i + 1], ref_max_size)
context.append(vae.encode(img[:, :, :, :3]))
return context
class BerniniS2VConditioningV2(io.ComfyNode):
@classmethod
def define_schema(cls):
return io.Schema(
node_id="BerniniS2VConditioningV2",
display_name="Bernini S2V Conditioning v2",
category="model/conditioning/bernini",
description="Bernini in-context conditioning with masked S2V audio for one or two speakers. Requires a Wan 2.2 S2V grafted Bernini-R model. Paint speaker masks on the output frame; reference_image_0 maps to image0 in prompts.",
inputs=[
io.Conditioning.Input("positive"),
io.Conditioning.Input("negative"),
io.Vae.Input("vae"),
io.Int.Input("width", default=832, min=16, max=8192, step=16),
io.Int.Input("height", default=480, min=16, max=8192, step=16),
io.Int.Input("length", default=81, min=1, max=8192, step=4),
io.Int.Input("batch_size", default=1, min=1, max=4096),
io.AudioEncoderOutput.Input("audio_1"),
io.Mask.Input("mask_1", tooltip="White = speaker 1 lip-sync region on the output frame."),
io.AudioEncoderOutput.Input("audio_2", optional=True),
io.Mask.Input("mask_2", optional=True, tooltip="Required when audio_2 is connected."),
io.Int.Input("speaker_2_start_frame", default=-1, min=-1, max=8192, step=1,
tooltip="-1 auto-starts speaker 2 when speaker 1 audio ends."),
io.Image.Input("source_video", optional=True),
io.Image.Input("reference_video", optional=True),
io.Autogrow.Input("reference_images", optional=True,
template=io.Autogrow.TemplatePrefix(
input=io.Image.Input("reference_image"),
prefix="reference_image_", min=0, max=8)),
io.Int.Input("ref_max_size", default=848, min=16, max=8192, step=16, optional=True),
io.Int.Input("mask_crossfade_frames", default=4, min=0, max=64, step=1,
tooltip="Softens the mask handoff between speakers. 0 = hard cut."),
io.Float.Input("audio_inject_scale", default=1.0, min=0.0, max=10.0, step=0.01),
],
outputs=[
io.Conditioning.Output(display_name="positive"),
io.Conditioning.Output(display_name="negative"),
io.Latent.Output(display_name="latent"),
],
)
@classmethod
def execute(
cls,
positive,
negative,
vae,
width,
height,
length,
batch_size,
audio_1,
mask_1,
audio_2=None,
mask_2=None,
speaker_2_start_frame=-1,
source_video=None,
reference_video=None,
reference_images=None,
ref_max_size=848,
mask_crossfade_frames=4,
audio_inject_scale=1.0,
) -> io.NodeOutput:
if audio_1 is None:
raise ValueError("Bernini S2V Conditioning v2 requires audio_1.")
if mask_1 is None:
raise ValueError("Bernini S2V Conditioning v2 requires mask_1.")
if audio_2 is not None and mask_2 is None:
raise ValueError("mask_2 is required when audio_2 is connected.")
segments = [{"audio_encoder_output": audio_1, "start_frame": 0, "mask_image": mask_1}]
if audio_2 is not None:
segments.append({
"audio_encoder_output": audio_2,
"start_frame": speaker_2_start_frame,
"mask_image": mask_2,
})
latent = torch.zeros(
[batch_size, 16, ((length - 1) // 4) + 1, height // 8, width // 8],
device=comfy.model_management.intermediate_device())
context = _build_context_latents(vae, width, height, length, source_video, reference_video, reference_images, ref_max_size)
if context:
positive = node_helpers.conditioning_set_values(positive, {"context_latents": context})
negative = node_helpers.conditioning_set_values(negative, {"context_latents": context})
positive, negative = apply_timeline_audio_conditioning(positive, negative, length, segments)
resolved_segments = resolve_timeline_segment_ranges(length, segments)
cond_values = {
"audio_inject_scale": audio_inject_scale,
"audio_inject_mask": build_timeline_audio_inject_mask(
width, height, length, resolved_segments,
crossfade_frames=mask_crossfade_frames,
device=comfy.model_management.intermediate_device(),
),
}
positive = node_helpers.conditioning_set_values(positive, cond_values)
negative_values = dict(cond_values)
negative_values["audio_inject_mask"] = negative_values["audio_inject_mask"] * 0.0
negative = node_helpers.conditioning_set_values(negative, negative_values)
return io.NodeOutput(positive, negative, {"samples": latent})
class WanBerniniS2VV2Extension(ComfyExtension):
@override
async def get_node_list(self) -> list[type[io.ComfyNode]]:
return [BerniniS2VConditioningV2]
async def comfy_entrypoint() -> WanBerniniS2VV2Extension:
return WanBerniniS2VV2Extension()