File size: 5,708 Bytes
9c98083
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
import torch
import comfy.model_management
import comfy.utils
import node_helpers


class WanI2VContinuity_Dolphin:
    """
    Wan2.2 I2V ๋…ธ๋“œ โ€” ํด๋ฆฝ ์ด์–ด๋ถ™์ด๊ธฐ(์—ฐ์†์„ฑ)์šฉ.

    ๊ธฐ์กด ์ ‘๊ทผ(WanImageToVideoSVIPro)์˜ ๋ฌธ์ œ: anchor ์ด๋ฏธ์ง€๋ฅผ ๋”ฐ๋กœ ์ธ์ฝ”๋”ฉํ•œ latent์™€,
    ์ด์ „ ํด๋ฆฝ latent์˜ ๋’ท๋ถ€๋ถ„์„ ๊ทธ๋ƒฅ ์ž˜๋ผ๋‚ธ ์กฐ๊ฐ(motion_latent)์„ ์ด์–ด๋ถ™์˜€๋‹ค. WAN VAE๋Š”
    ์ธ๊ณผ์ (causal) ์‹œ๊ฐ„ ์••์ถ•์ด๋ผ ๊ฐ latent ํ”„๋ ˆ์ž„์ด "๊ทธ ์‹œ์ ๊นŒ์ง€์˜ ์‹œํ€€์Šค ๋ฌธ๋งฅ"์„ ์ „์ œ๋กœ
    ์ธ์ฝ”๋”ฉ๋˜๋Š”๋ฐ, ์„œ๋กœ ๋‹ค๋ฅธ ๋ฌธ๋งฅ์—์„œ ๋‚˜์˜จ latent ์กฐ๊ฐ์„ ์ด์–ด๋ถ™์ด๋ฉด ํ†ต๊ณ„์ ์œผ๋กœ ๋ถˆ์—ฐ์†์ด๋ผ
    ํ™”์งˆ์ด ํŠ€๊ณ  ๋™์ž‘ ์—ฐ์†์„ฑ๋„ ๋ชป ์‚ด๋ ธ๋‹ค.

    ์ด ๋…ธ๋“œ๋Š” ComfyUI ์ฝ”์–ด `WanImageToVideo`์˜ ๊ฒ€์ฆ๋œ ๋ฐฉ์‹์„ ๋”ฐ๋ฅธ๋‹ค:
    ์ด์ „ ํด๋ฆฝ์—์„œ ๋””์ฝ”๋”ฉํ•œ ๋งˆ์ง€๋ง‰ Nํ”„๋ ˆ์ž„(ํ”ฝ์…€)์„ start_image๋กœ ๋ฐ›์•„, ํšŒ์ƒ‰(0.5) filler์™€
    ํ•จ๊ป˜ ํ•˜๋‚˜์˜ ์‹œํ€€์Šค๋กœ ๋งŒ๋“ค์–ด **ํ•œ ๋ฒˆ์˜ ํ†ต์งœ VAE ์ธ์ฝ”๋”ฉ**์„ ํ•œ๋‹ค. ๊ทธ๋ž˜์„œ known ๊ตฌ๊ฐ„๊ณผ
    ์ƒ์„ฑ ๊ตฌ๊ฐ„์˜ latent ๋ฌธ๋งฅ์ด ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ ์ด์–ด์ง„๋‹ค. ๋งˆ์Šคํฌ๋„ ํ”„๋ ˆ์ž„์ˆ˜โ†’latent์ˆ˜ ๋ณ€ํ™˜์„
    ์ •ํ™•ํžˆ ํ•ด์„œ known ํ”„๋ ˆ์ž„ ์ „์ฒด๋ฅผ ๊ณ ์ •ํ•œ๋‹ค.

    ์—ฌ๊ธฐ์— PainterI2V์˜ motion_amplitude(์Šฌ๋กœ์šฐ๋ชจ์…˜ ๋ฐฉ์ง€)๋ฅผ ์ด์‹ํ–ˆ๋‹ค. ๋‹จ known ํ”„๋ ˆ์ž„์€
    ๊ฑด๋“œ๋ฆฌ์ง€ ์•Š๊ณ , ๋’ค์— ๋ถ™๋Š” filler(์ƒ์„ฑ ๋Œ€์ƒ) latent๋งŒ ๋งˆ์ง€๋ง‰ known latent ๋Œ€๋น„ ์ฐจ์ด๋ฅผ
    ์ฆํญํ•œ๋‹ค. motion_amplitude=1.0์ด๋ฉด ์ฝ”์–ด WanImageToVideo์™€ ๋™์ผํ•˜๊ฒŒ ๋™์ž‘ํ•œ๋‹ค.
    """

    @classmethod
    def INPUT_TYPES(cls):
        return {
            "required": {
                "positive": ("CONDITIONING",),
                "negative": ("CONDITIONING",),
                "vae": ("VAE",),
                "width": ("INT", {"default": 832, "min": 16, "max": 4096, "step": 16}),
                "height": ("INT", {"default": 480, "min": 16, "max": 4096, "step": 16}),
                "length": ("INT", {"default": 81, "min": 1, "max": 9999, "step": 4}),
                "batch_size": ("INT", {"default": 1, "min": 1, "max": 4096}),
                "motion_amplitude": ("FLOAT", {"default": 1.0, "min": 1.0, "max": 2.0, "step": 0.05,
                                                 "tooltip": "1.0=์ฝ”์–ด WanImageToVideo์™€ ๋™์ผ. >1.0์ด๋ฉด filler ๊ตฌ๊ฐ„์— ์Šฌ๋กœ์šฐ๋ชจ์…˜ ๋ฐฉ์ง€ ์ฆํญ."}),
            },
            "optional": {
                "clip_vision_output": ("CLIP_VISION_OUTPUT",),
                "start_image": ("IMAGE", {"tooltip": "known ํ”„๋ ˆ์ž„(๋“ค). ์ด์ „ ํด๋ฆฝ์˜ ๋””์ฝ”๋”ฉ๋œ ๋งˆ์ง€๋ง‰ Nํ”„๋ ˆ์ž„์„ ๋„ฃ์œผ๋ฉด ์—ฐ์†์„ฑ์ด ์ƒ๊น€. 1์žฅ์ด๋ฉด ์ผ๋ฐ˜ I2V."}),
            },
        }

    RETURN_TYPES = ("CONDITIONING", "CONDITIONING", "LATENT")
    RETURN_NAMES = ("positive", "negative", "latent")
    FUNCTION = "encode"
    CATEGORY = "Dolphin Node/Wan Video"

    def encode(self, positive, negative, vae, width, height, length, batch_size,
               motion_amplitude=1.0, clip_vision_output=None, start_image=None):
        latent = torch.zeros(
            [batch_size, 16, ((length - 1) // 4) + 1, height // 8, width // 8],
            device=comfy.model_management.intermediate_device(),
        )

        if start_image is not None:
            start_image = comfy.utils.common_upscale(
                start_image[:length].movedim(-1, 1), width, height, "bilinear", "center"
            ).movedim(1, -1)
            n_known_frames = start_image.shape[0]

            # known ํ”„๋ ˆ์ž„ ๋’ค๋ฅผ ํšŒ์ƒ‰(0.5) filler๋กœ ์ฑ„์šด ๋’ค ํ†ต์งœ๋กœ ์ธ์ฝ”๋”ฉ (์ฝ”์–ด ๋ฐฉ์‹)
            image = torch.ones(
                (length, height, width, start_image.shape[-1]),
                device=start_image.device, dtype=start_image.dtype,
            ) * 0.5
            image[:n_known_frames] = start_image

            concat_latent_image = vae.encode(image[:, :, :, :3])

            # ํ”„๋ ˆ์ž„์ˆ˜ -> latent์ˆ˜ (WAN VAE 4x ์‹œ๊ฐ„ ์••์ถ•)
            n_known_latents = ((n_known_frames - 1) // 4) + 1

            # motion_amplitude: filler(์ƒ์„ฑ ๋Œ€์ƒ) latent๋งŒ ์ฆํญ. known ํ”„๋ ˆ์ž„์€ ๊ทธ๋Œ€๋กœ.
            if motion_amplitude > 1.0 and concat_latent_image.shape[2] > n_known_latents:
                base_latent = concat_latent_image[:, :, n_known_latents - 1:n_known_latents]
                filler = concat_latent_image[:, :, n_known_latents:]
                diff = filler - base_latent
                diff_mean = diff.mean(dim=(1, 3, 4), keepdim=True)
                diff_centered = diff - diff_mean
                scaled = base_latent + diff_centered * motion_amplitude + diff_mean
                scaled = torch.clamp(scaled, -6, 6)
                concat_latent_image = torch.cat(
                    [concat_latent_image[:, :, :n_known_latents], scaled], dim=2
                )

            mask = torch.ones(
                (1, 1, latent.shape[2], concat_latent_image.shape[-2], concat_latent_image.shape[-1]),
                device=start_image.device, dtype=start_image.dtype,
            )
            mask[:, :, :n_known_latents] = 0.0

            positive = node_helpers.conditioning_set_values(
                positive, {"concat_latent_image": concat_latent_image, "concat_mask": mask}
            )
            negative = node_helpers.conditioning_set_values(
                negative, {"concat_latent_image": concat_latent_image, "concat_mask": mask}
            )

        if clip_vision_output is not None:
            positive = node_helpers.conditioning_set_values(positive, {"clip_vision_output": clip_vision_output})
            negative = node_helpers.conditioning_set_values(negative, {"clip_vision_output": clip_vision_output})

        return (positive, negative, {"samples": latent})