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()
|