File size: 5,025 Bytes
2b97acb
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
"""Bernini in-context conditioning (context_latents) support for Wan S2V models.

Adds a "Wan S2V-Bernini Patch" node (MODEL -> MODEL). Insert it after the model
loader when running a Bernini trunk with the Wan2.2-S2V module stacked on (e.g.
via DiffusionModelLoaderKJ with extra_state_dict), and set conditioning with the
native BerniniConditioning node (optionally combined with WanSoundImageToVideo
for audio).

Uses only official ComfyUI extension points, no monkey patching:
- WanModel._forward (inherited by S2V) already pads context_latents and appends
  their rope freqs; the native S2V forward_orig just never appends the tokens,
  which crashes attention on the freqs length mismatch.
- A DIFFUSION_MODEL wrapper patch-embeds the context latents (it can reach the
  model via executor.class_obj) and stashes the tokens in transformer_options.
- A dit block-0 replace patch inserts the tokens right after the main video
  tokens, before the first attention runs, restoring the token/freqs alignment.
  They ride through all blocks (audio injection writes only x[:, :seq_len]) and
  unpatchify() drops them at the output.

When context_latents cannot be honored (non-S2V model, wrong latent channels),
they are removed from the call with a warning instead of crashing attention.
Models with native context_latents support (base WanModel, or a future S2V
implementation) are passed through untouched.
"""

import inspect
import logging

import torch

import comfy.ldm.common_dit
from comfy.ldm.wan import model as wan
from comfy.patcher_extension import WrappersMP

_TOKENS = "_wan_s2v_bernini_tokens"
_MAIN_LEN = "_wan_s2v_bernini_main_len"

_native_support = {}


def _has_native_support(cls):
    if cls not in _native_support:
        try:
            _native_support[cls] = "context_latents" in inspect.getsource(cls.forward_orig)
        except (OSError, TypeError):
            _native_support[cls] = False
    return _native_support[cls]


def _drop_reason(model, context_latents):
    cls = type(model)
    if cls is not wan.WanModel_S2V:
        return f"{cls.__name__} is not supported"
    if cls._forward is not wan.WanModel._forward:
        return "WanModel_S2V no longer inherits WanModel._forward"
    ch = model.patch_embedding.weight.shape[1]
    if any(lat.shape[1] != ch for lat in context_latents):
        return f"model expects {ch}-channel latents"
    return None


def _embed_context_latents(executor, x, timestep, context, clip_fea=None, time_dim_concat=None, transformer_options={}, **kwargs):
    model = executor.class_obj
    context_latents = kwargs.get("context_latents", None)
    transformer_options.pop(_TOKENS, None)
    if context_latents is not None and not _has_native_support(type(model)):
        reason = _drop_reason(model, context_latents)
        if reason is not None:
            logging.warning(f"wan_s2v_module_patch: dropping context_latents: {reason}")
            kwargs = {k: v for k, v in kwargs.items() if k != "context_latents"}
        else:
            p = model.patch_size
            xp = comfy.ldm.common_dit.pad_to_patch_size(x, p)
            t_len = xp.shape[-3]
            if time_dim_concat is not None:
                t_len += comfy.ldm.common_dit.pad_to_patch_size(time_dim_concat, p).shape[-3]
            transformer_options[_MAIN_LEN] = (t_len // p[0]) * (xp.shape[-2] // p[1]) * (xp.shape[-1] // p[2])
            tokens = []
            for lat in context_latents:
                lat = comfy.ldm.common_dit.pad_to_patch_size(lat, p)
                tokens.append(model.patch_embedding(lat.float().to(x.device)).flatten(2).transpose(1, 2))
            transformer_options[_TOKENS] = torch.cat(tokens, dim=1).to(x.dtype)
    return executor(x, timestep, context, clip_fea, time_dim_concat, transformer_options, **kwargs)


def _insert_tokens_block0(args, extra):
    transformer_options = args["transformer_options"]
    tokens = transformer_options.get(_TOKENS, None)
    if tokens is not None:
        n = transformer_options[_MAIN_LEN]
        img = args["img"]
        args = {**args, "img": torch.cat([img[:, :n], tokens.to(img.dtype), img[:, n:]], dim=1)}
    return extra["original_block"](args)


class WanS2VBerniniPatch:
    @classmethod
    def INPUT_TYPES(cls):
        return {"required": {"model": ("MODEL",)}}

    RETURN_TYPES = ("MODEL",)
    FUNCTION = "patch"
    CATEGORY = "model_patches/video"
    DESCRIPTION = "Enables Bernini context_latents conditioning (BerniniConditioning node) on Wan S2V models, e.g. a Bernini trunk with the S2V module stacked on. No effect on other models or without BerniniConditioning."

    def patch(self, model):
        m = model.clone()
        m.add_wrapper_with_key(WrappersMP.DIFFUSION_MODEL, "wan_s2v_bernini", _embed_context_latents)
        m.set_model_patch_replace(_insert_tokens_block0, "dit", "double_block", 0)
        return (m,)


NODE_CLASS_MAPPINGS = {"WanS2VBerniniPatch": WanS2VBerniniPatch}
NODE_DISPLAY_NAME_MAPPINGS = {"WanS2VBerniniPatch": "Wan S2V-Bernini Patch"}