File size: 9,572 Bytes
bc50ea0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
"""Regression coverage for the hidden Motion Context fallback engine.

The public 0.5 node normally delegates compatible scenes to seitanism's
upstream implementation.  These tests keep the internal fallback honest for
the features that still require it: arbitrary visual lengths, interior Guide
placement, exact 40 Hz PCM alignment, and coexistence with other anchors.
"""

import importlib.util
import os
import sys
import types

import torch
import torch.nn.functional as functional


_TESTS_DIR = os.path.dirname(os.path.abspath(__file__))
_PKG_DIR = os.path.dirname(_TESTS_DIR)
_PACKAGE = "h3_internal_update_unit"


class Nested:
    def __init__(self, parts):
        self.parts = parts

    def unbind(self):
        return list(self.parts)


def _install_stubs():
    comfy = types.ModuleType("comfy")
    comfy_utils = types.ModuleType("comfy.utils")

    def common_upscale(samples, width, height, method, crop):
        del method, crop
        return functional.interpolate(
            samples, size=(height, width), mode="bilinear",
            align_corners=False)

    comfy_utils.common_upscale = common_upscale
    comfy.utils = comfy_utils
    sys.modules["comfy"] = comfy
    sys.modules["comfy.utils"] = comfy_utils

    folder_paths = types.ModuleType("folder_paths")
    folder_paths.get_output_directory = lambda: "/tmp"
    sys.modules["folder_paths"] = folder_paths

    node_helpers = types.ModuleType("node_helpers")

    def conditioning_set_values(conditioning, values, append=False):
        output = []
        for embedding, extra in conditioning:
            metadata = extra.copy()
            for key, incoming in values.items():
                value = incoming
                if append and metadata.get(key) is not None:
                    value = metadata[key] + incoming
                metadata[key] = value
            output.append([embedding, metadata])
        return output

    node_helpers.conditioning_set_values = conditioning_set_values
    sys.modules["node_helpers"] = node_helpers

    package = types.ModuleType(_PACKAGE)
    package.__path__ = [_PKG_DIR]
    sys.modules[_PACKAGE] = package

    timing = types.ModuleType(_PACKAGE + ".av_timing")
    timing.AUDIO_TRIM_FRAMES_KEY = "_audio_trim_frames"
    timing.AUDIO_WITH_OVERLAP_FRAMES_KEY = "_audio_overlap_frames"
    timing.AUDIO_WITH_OVERLAP_WAVEFORM_KEY = "_audio_overlap_waveform"
    timing.conform_waveform_length = lambda waveform, length, label: (
        waveform[..., :length])
    sys.modules[timing.__name__] = timing

    layout = types.ModuleType(_PACKAGE + ".patch_layout")
    layout.MC_KEY = "_motion_context_frame"
    layout.MC_AUDIO_KEY = "_motion_context_audio_end"
    layout.apply_patch = lambda: True
    layout.claim_patch_ownership = lambda: (True, "test")
    layout.is_applied = lambda: True
    layout.native_guides_available = lambda: True
    sys.modules[layout.__name__] = layout

    payload = types.ModuleType(_PACKAGE + ".patch_payload")
    payload.CHAIN_AUDIO_KEY = "h3_chain_context_audio"
    payload.apply_patch = lambda: True
    payload.claim_patch_ownership = lambda **kwargs: (True, "test")
    payload.is_applied = lambda: True
    payload.native_payload_merge_status = lambda: {
        "native_keyframe_ref_merge": True,
        "native_keyframe_ref_audio_merge": True,
    }
    sys.modules[payload.__name__] = payload


def _load_nodes():
    _install_stubs()
    spec = importlib.util.spec_from_file_location(
        _PACKAGE + ".nodes", os.path.join(_PKG_DIR, "nodes.py"))
    module = importlib.util.module_from_spec(spec)
    sys.modules[spec.name] = module
    spec.loader.exec_module(module)
    return module


def _video_latent(frame_count=124):
    assert frame_count == 124
    video = torch.zeros((1, 16, 37, 1, 1), dtype=torch.float32)
    audio = torch.zeros((1, 8, 2, 207), dtype=torch.float32)
    return {"samples": Nested([video, audio])}


def _conditioning(keyframes=None):
    metadata = {
        "minimax_frame_count": 124,
        "minimax_keyframes": list(keyframes or []),
    }
    return [[torch.zeros((1, 1), dtype=torch.float32), metadata]]


class VideoVAE:
    def __init__(self):
        self.calls = []

    def encode(self, images):
        count = int(images.shape[0])
        self.calls.append(tuple(images.shape))
        if count == 1:
            latent_steps = 1
        else:
            assert count >= 5 and (count - 5) % 17 == 0
            latent_steps = 2 + 5 * ((count - 5) // 17)
        return torch.zeros(
            (1, 16, latent_steps, 1, 1), dtype=torch.float32)


class AudioVAE:
    audio_sample_rate = 32000

    def __init__(self):
        self.first_stage_model = types.SimpleNamespace(
            samples_per_latent=800)
        self.last_pcm = None

    def encode(self, waveform):
        self.last_pcm = waveform.clone()
        steps = int(waveform.shape[1]) // 800
        return torch.zeros((1, 8, 2, steps), dtype=torch.float32)


def _visual_keyframes(metadata):
    return [item for item in metadata["minimax_keyframes"]
            if item.get("latent") is not None]


def main():
    nodes = _load_nodes()
    engine = nodes.MiniMaxH3MotionContext()
    schema = engine.INPUT_TYPES()["required"]
    assert "target_start" in schema
    assert schema["context_length"][1]["max"] == 9999

    # Seven is not a native H3 video run. It must become exactly seven still
    # Guides at the requested interior location, not snap down to five.
    vae = VideoVAE()
    context = torch.zeros((7, 4, 4, 3), dtype=torch.float32)
    anchors = [
        {"resolved_frame_index": 0, "marker": "first"},
        {"resolved_frame_index": 33, "marker": "collision"},
        {"resolved_frame_index": 123, "marker": "last"},
    ]
    result, trim = engine.apply(
        _conditioning(anchors), vae, _video_latent(), context, 7,
        "video", "head", "disabled", audio_context_length=0,
        target_start=30)
    metadata = result[0][1]
    visual = _visual_keyframes(metadata)
    assert len(vae.calls) == 7
    assert [item["resolved_frame_index"] for item in visual] == list(
        range(30, 37))
    assert trim == 0
    markers = {item.get("marker") for item in metadata["minimax_keyframes"]}
    assert "first" in markers and "last" in markers
    assert "collision" not in markers

    # A native 22-frame run remains a single efficient multi-frame Guide and
    # still supports interior placement.
    vae = VideoVAE()
    context = torch.zeros((22, 4, 4, 3), dtype=torch.float32)
    result, trim = engine.apply(
        _conditioning(), vae, _video_latent(), context, 22,
        "video", "head", "disabled", audio_context_length=0,
        target_start=30)
    visual = _visual_keyframes(result[0][1])
    assert len(vae.calls) == 1
    assert len(visual) == 1
    assert visual[0]["resolved_frame_index"] == 30
    assert tuple(visual[0]["latent"].shape) == (1, 16, 7, 1, 1)
    assert trim == 0

    # A 56-frame picture boundary is 74,667 samples at 32 kHz, while its
    # nearest 40 Hz latent grid is exactly 74,400. The encoder must see the
    # final exact-grid window, beginning at sample 267, with no center shift.
    vae = VideoVAE()
    audio_vae = AudioVAE()
    context = torch.zeros((56, 4, 4, 3), dtype=torch.float32)
    picture_samples = round(56 / 24 * 32000)
    waveform = torch.arange(
        picture_samples, dtype=torch.float32).reshape(1, 1, -1)
    result, trim = engine.apply(
        _conditioning(), vae, _video_latent(), context, 56,
        "video", "head", "disabled", audio_context_length=56,
        audio_vae=audio_vae,
        context_audio={"waveform": waveform, "sample_rate": 32000})
    assert trim == 56
    assert tuple(audio_vae.last_pcm.shape) == (1, 74400, 1)
    assert float(audio_vae.last_pcm[0, 0, 0]) == 267.0
    assert float(audio_vae.last_pcm[0, -1, 0]) == 74666.0
    audio_guides = [item for item in result[0][1]["minimax_keyframes"]
                    if item.get("audio_latent") is not None]
    assert len(audio_guides) == 1
    assert tuple(audio_guides[0]["audio_latent"].shape) == (1, 8, 2, 93)

    # Conversely, 124 frames end 267 samples before the rounded 207-cell
    # audio grid. Only that partial older boundary cell is padded; the seam
    # remains the final real PCM sample.
    audio_vae = AudioVAE()
    picture_samples = round(124 / 24 * 32000)
    waveform = torch.arange(
        picture_samples, dtype=torch.float32).reshape(1, 1, -1)
    encoded, steps = nodes._encode_tail_audio(
        audio_vae, {"waveform": waveform, "sample_rate": 32000}, 124)
    assert steps == 207
    assert tuple(encoded.shape) == (1, 8, 2, 207)
    assert tuple(audio_vae.last_pcm.shape) == (1, 165600, 1)
    assert torch.count_nonzero(audio_vae.last_pcm[0, :267, 0]) == 0
    assert float(audio_vae.last_pcm[0, 268, 0]) == 1.0
    assert float(audio_vae.last_pcm[0, -1, 0]) == 165332.0

    # An interior Guide may not consume the entire target; H3 must retain
    # some frames for generation.
    try:
        engine.apply(
            _conditioning(), VideoVAE(), _video_latent(),
            torch.zeros((7, 4, 4, 3)), 7, "video", "head", "disabled",
            audio_context_length=0, target_start=117)
    except ValueError as exc:
        assert "must leave room" in str(exc)
    else:
        raise AssertionError("full-target interior Guide was accepted")

    print("internal engine: arbitrary Guide lengths and target_start pass")
    print("internal engine: exact 40 Hz PCM tail alignment and boundary pad pass")


if __name__ == "__main__":
    main()