hal9000ace commited on
Commit
25368c3
·
verified ·
1 Parent(s): 0d5d1a9

Upload H3Loopsampler.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. H3Loopsampler.py +374 -0
H3Loopsampler.py ADDED
@@ -0,0 +1,374 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import copy
2
+ import torch
3
+ import comfy.sample
4
+ import comfy.utils
5
+ import comfy.model_management
6
+ import latent_preview
7
+ from comfy.nested_tensor import NestedTensor
8
+
9
+
10
+ class H3LoopingSampler:
11
+ @classmethod
12
+ def INPUT_TYPES(s):
13
+ return {
14
+ "required": {
15
+ "noise": ("NOISE",),
16
+ "guider": ("GUIDER",),
17
+ "sampler": ("SAMPLER",),
18
+ "sigmas": ("SIGMAS",),
19
+ "latent_image": ("LATENT",),
20
+ "temporal_tile_size": (
21
+ "INT",
22
+ {
23
+ "default": 81,
24
+ "min": 17,
25
+ "max": 257,
26
+ "step": 4,
27
+ "tooltip": "Tamanho do tile temporal em frames de latente (vídeo)",
28
+ },
29
+ ),
30
+ "temporal_overlap": (
31
+ "INT",
32
+ {
33
+ "default": 17,
34
+ "min": 5,
35
+ "max": 65,
36
+ "step": 4,
37
+ },
38
+ ),
39
+ "temporal_overlap_strength": (
40
+ "FLOAT",
41
+ {
42
+ "default": 0.65,
43
+ "min": 0.0,
44
+ "max": 1.0,
45
+ "step": 0.01,
46
+ "tooltip": "Força da nova chunk na zona de overlap (0 = mantém só o anterior, 1 = blend total)",
47
+ },
48
+ ),
49
+ "horizontal_tiles": ("INT", {"default": 1, "min": 1, "max": 4}),
50
+ "vertical_tiles": ("INT", {"default": 1, "min": 1, "max": 4}),
51
+ "spatial_overlap": (
52
+ "INT",
53
+ {
54
+ "default": 8,
55
+ "min": 0,
56
+ "max": 32,
57
+ },
58
+ ),
59
+ },
60
+ "optional": {
61
+ "adain_factor": (
62
+ "FLOAT",
63
+ {
64
+ "default": 0.15,
65
+ "min": 0.0,
66
+ "max": 1.0,
67
+ "step": 0.01,
68
+ },
69
+ ),
70
+ },
71
+ }
72
+
73
+ RETURN_TYPES = ("LATENT", "LATENT")
74
+ RETURN_NAMES = ("output", "denoised_output")
75
+ FUNCTION = "sample"
76
+ CATEGORY = "sampling/H3"
77
+ DESCRIPTION = "H3 Looping / Tiled Sampler com output + denoised_output (compatível com SplitSigmas) ComfyGuy9000"
78
+
79
+ def _is_nested(self, samples):
80
+ return isinstance(samples, NestedTensor) or getattr(samples, "is_nested", False)
81
+
82
+ def _get_tensors(self, samples):
83
+ if self._is_nested(samples):
84
+ if hasattr(samples, "tensors"):
85
+ return list(samples.tensors)
86
+ return list(samples.unbind())
87
+ return [samples]
88
+
89
+ def _make_nested(self, tensors):
90
+ if len(tensors) == 1:
91
+ return tensors[0]
92
+ return NestedTensor(tensors)
93
+
94
+ def _get_video(self, samples):
95
+ return self._get_tensors(samples)[0]
96
+
97
+ def _slice_video_temporal(self, video, start, end):
98
+ return video[:, :, start:end].clone()
99
+
100
+ def _slice_video_spatial(self, video, v_start, v_end, h_start, h_end):
101
+ return video[:, :, :, v_start:v_end, h_start:h_end].clone()
102
+
103
+ def _create_spatial_weights(self, shape, v, h, vertical_tiles, horizontal_tiles, spatial_overlap, device, dtype):
104
+ weights = torch.ones(shape, device=device, dtype=dtype)
105
+ if spatial_overlap > 0:
106
+ if h > 0:
107
+ blend = torch.linspace(0, 1, spatial_overlap, device=device, dtype=dtype)
108
+ weights[..., :spatial_overlap] *= blend.view(1, 1, 1, 1, -1)
109
+ if h < horizontal_tiles - 1:
110
+ blend = torch.linspace(1, 0, spatial_overlap, device=device, dtype=dtype)
111
+ weights[..., -spatial_overlap:] *= blend.view(1, 1, 1, 1, -1)
112
+ if v > 0:
113
+ blend = torch.linspace(0, 1, spatial_overlap, device=device, dtype=dtype)
114
+ weights[..., :spatial_overlap, :] *= blend.view(1, 1, 1, -1, 1)
115
+ if v < vertical_tiles - 1:
116
+ blend = torch.linspace(1, 0, spatial_overlap, device=device, dtype=dtype)
117
+ weights[..., -spatial_overlap:, :] *= blend.view(1, 1, 1, -1, 1)
118
+ return weights
119
+
120
+ def _adain(self, source, target, factor):
121
+ if factor <= 0.0:
122
+ return source
123
+ src_mean = source.mean(dim=(2, 3, 4), keepdim=True)
124
+ src_std = source.std(dim=(2, 3, 4), keepdim=True) + 1e-5
125
+ tgt_mean = target.mean(dim=(2, 3, 4), keepdim=True)
126
+ tgt_std = target.std(dim=(2, 3, 4), keepdim=True) + 1e-5
127
+ normalized = (source - src_mean) / src_std
128
+ stylized = normalized * tgt_std + tgt_mean
129
+ return source * (1.0 - factor) + stylized * factor
130
+
131
+ def sample(
132
+ self,
133
+ noise,
134
+ guider,
135
+ sampler,
136
+ sigmas,
137
+ latent_image,
138
+ temporal_tile_size,
139
+ temporal_overlap,
140
+ temporal_overlap_strength,
141
+ horizontal_tiles,
142
+ vertical_tiles,
143
+ spatial_overlap,
144
+ adain_factor=0.15,
145
+ ):
146
+ original_latent = latent_image
147
+ samples = latent_image["samples"]
148
+
149
+ video = self._get_video(samples)
150
+ if video.ndim != 5:
151
+ raise ValueError(f"Expected video [B,C,T,H,W], got {tuple(video.shape)}")
152
+
153
+ B, C, T, H, W = video.shape
154
+ print(f"\n========== H3LoopingSampler ComfyGuy9000 ==========")
155
+ print(f"Input video latent: {video.shape}")
156
+ print(f"Tiles: {vertical_tiles}x{horizontal_tiles} | spatial_overlap={spatial_overlap}")
157
+ print(f"Temporal tile={temporal_tile_size} | overlap={temporal_overlap} | strength={temporal_overlap_strength}")
158
+
159
+ original_tensors = self._get_tensors(samples)
160
+ has_audio = len(original_tensors) > 1
161
+ full_audio = original_tensors[1] if has_audio else None
162
+
163
+ temporal_tile_size = min(temporal_tile_size, T)
164
+ temporal_overlap = min(temporal_overlap, max(4, temporal_tile_size - 4))
165
+
166
+ if vertical_tiles > 1:
167
+ base_tile_h = (H + (vertical_tiles - 1) * spatial_overlap) // vertical_tiles
168
+ else:
169
+ base_tile_h = H
170
+ if horizontal_tiles > 1:
171
+ base_tile_w = (W + (horizontal_tiles - 1) * spatial_overlap) // horizontal_tiles
172
+ else:
173
+ base_tile_w = W
174
+
175
+ print(f"Base tile size (latent): {base_tile_h} x {base_tile_w}")
176
+
177
+ final_video = None
178
+ final_denoised_video = None
179
+ weights = None
180
+ first_seed = noise.seed
181
+ disable_pbar = not comfy.utils.PROGRESS_BAR_ENABLED
182
+
183
+ tile_count = 0
184
+ for v in range(vertical_tiles):
185
+ for h in range(horizontal_tiles):
186
+ v_start = v * (base_tile_h - spatial_overlap)
187
+ h_start = h * (base_tile_w - spatial_overlap)
188
+ v_end = min(v_start + base_tile_h, H) if v < vertical_tiles - 1 else H
189
+ h_end = min(h_start + base_tile_w, W) if h < horizontal_tiles - 1 else W
190
+
191
+ tile_count += 1
192
+ print(f"\n>>> Spatial tile {tile_count}/{vertical_tiles*horizontal_tiles} ({v},{h})")
193
+ print(f" H[{v_start}:{v_end}] W[{h_start}:{h_end}]")
194
+
195
+ spatial_video = self._slice_video_spatial(video, v_start, v_end, h_start, h_end)
196
+
197
+ tile_out_video = None
198
+ tile_denoised_video = None
199
+ first_chunk_ref = None
200
+
201
+ step = max(1, temporal_tile_size - temporal_overlap)
202
+ starts = list(range(0, max(1, T - temporal_overlap), step))
203
+
204
+ for i, start in enumerate(starts):
205
+ end = min(start + temporal_tile_size, T)
206
+ print(f" Temporal chunk {i}: [{start}:{end}]")
207
+
208
+ chunk_video = self._slice_video_temporal(spatial_video, start, end)
209
+
210
+ if has_audio:
211
+ chunk_samples = self._make_nested([chunk_video, full_audio])
212
+ else:
213
+ chunk_samples = chunk_video
214
+
215
+ chunk_latent = {"samples": chunk_samples}
216
+ if "noise_mask" in latent_image:
217
+ chunk_latent["noise_mask"] = latent_image["noise_mask"]
218
+
219
+ noise.seed = first_seed + start * (vertical_tiles * horizontal_tiles) + v * horizontal_tiles + h
220
+
221
+ # === Captura do x0 (denoised) ===
222
+ x0_output = {}
223
+ callback = latent_preview.prepare_callback(
224
+ guider.model_patcher, sigmas.shape[-1] - 1, x0_output
225
+ )
226
+
227
+ noise_mask = chunk_latent.get("noise_mask", None)
228
+
229
+ out_samples = guider.sample(
230
+ noise.generate_noise(chunk_latent),
231
+ chunk_samples,
232
+ sampler,
233
+ sigmas,
234
+ denoise_mask=noise_mask,
235
+ callback=callback,
236
+ disable_pbar=disable_pbar,
237
+ seed=noise.seed,
238
+ )
239
+
240
+ out_samples = out_samples.to(comfy.model_management.intermediate_device())
241
+ chunk_out_video = self._get_video(out_samples)
242
+
243
+ # Pega a versão denoised (x0) se disponível
244
+ if "x0" in x0_output:
245
+ x0 = x0_output["x0"]
246
+ if self._is_nested(out_samples) and not self._is_nested(x0):
247
+ try:
248
+ latent_shapes = [t.shape for t in self._get_tensors(out_samples)]
249
+ x0 = NestedTensor(comfy.utils.unpack_latents(x0, latent_shapes))
250
+ except:
251
+ pass
252
+ chunk_denoised_video = self._get_video(x0)
253
+ try:
254
+ chunk_denoised_video = guider.model_patcher.model.process_latent_out(
255
+ chunk_denoised_video.cpu()
256
+ ).to(chunk_out_video.device)
257
+ except:
258
+ chunk_denoised_video = chunk_denoised_video.to(chunk_out_video.device)
259
+ else:
260
+ chunk_denoised_video = chunk_out_video
261
+
262
+ # AdaIN (aplica nos dois)
263
+ if first_chunk_ref is None:
264
+ first_chunk_ref = chunk_out_video.detach()
265
+ else:
266
+ ref = first_chunk_ref
267
+ if ref.shape[2] != chunk_out_video.shape[2]:
268
+ ref = first_chunk_ref[:, :, :1].expand_as(chunk_out_video)
269
+ else:
270
+ ref = first_chunk_ref[:, :, :chunk_out_video.shape[2]]
271
+ chunk_out_video = self._adain(chunk_out_video, ref, adain_factor)
272
+ chunk_denoised_video = self._adain(chunk_denoised_video, ref, adain_factor)
273
+
274
+ # === Blend temporal (CORRIGIDO) ===
275
+ if tile_out_video is None:
276
+ tile_out_video = chunk_out_video
277
+ tile_denoised_video = chunk_denoised_video
278
+ else:
279
+ overlap = temporal_overlap
280
+ if overlap > 0 and tile_out_video.shape[2] >= overlap:
281
+ alpha = torch.linspace(
282
+ 1.0, 0.0, overlap,
283
+ device=tile_out_video.device,
284
+ dtype=tile_out_video.dtype
285
+ ).view(1, 1, -1, 1, 1)
286
+
287
+ # Fórmula corrigida:
288
+ # strength = 0.0 → mantém só o anterior
289
+ # strength = 1.0 → blend linear normal
290
+ prev = tile_out_video[:, :, -overlap:]
291
+ new = chunk_out_video[:, :, :overlap]
292
+ blended = prev * (1.0 - (1.0 - alpha) * temporal_overlap_strength) + \
293
+ new * (1.0 - alpha) * temporal_overlap_strength
294
+
295
+ tile_out_video = torch.cat(
296
+ [tile_out_video[:, :, :-overlap], blended, chunk_out_video[:, :, overlap:]],
297
+ dim=2
298
+ )
299
+
300
+ # Blend denoised
301
+ prev_d = tile_denoised_video[:, :, -overlap:]
302
+ new_d = chunk_denoised_video[:, :, :overlap]
303
+ blended_d = prev_d * (1.0 - (1.0 - alpha) * temporal_overlap_strength) + \
304
+ new_d * (1.0 - alpha) * temporal_overlap_strength
305
+
306
+ tile_denoised_video = torch.cat(
307
+ [tile_denoised_video[:, :, :-overlap], blended_d, chunk_denoised_video[:, :, overlap:]],
308
+ dim=2
309
+ )
310
+ else:
311
+ tile_out_video = torch.cat([tile_out_video, chunk_out_video], dim=2)
312
+ tile_denoised_video = torch.cat([tile_denoised_video, chunk_denoised_video], dim=2)
313
+
314
+ # Acumula spatial
315
+ if final_video is None:
316
+ out_T = tile_out_video.shape[2]
317
+ final_video = torch.zeros(B, C, out_T, H, W, device=tile_out_video.device, dtype=tile_out_video.dtype)
318
+ final_denoised_video = torch.zeros_like(final_video)
319
+ weights = torch.zeros_like(final_video)
320
+
321
+ if tile_out_video.shape[2] != final_video.shape[2]:
322
+ if tile_out_video.shape[2] > final_video.shape[2]:
323
+ tile_out_video = tile_out_video[:, :, :final_video.shape[2]]
324
+ tile_denoised_video = tile_denoised_video[:, :, :final_video.shape[2]]
325
+ else:
326
+ pad = final_video.shape[2] - tile_out_video.shape[2]
327
+ tile_out_video = torch.nn.functional.pad(tile_out_video, (0, 0, 0, 0, 0, pad))
328
+ tile_denoised_video = torch.nn.functional.pad(tile_denoised_video, (0, 0, 0, 0, 0, pad))
329
+
330
+ w = self._create_spatial_weights(
331
+ tile_out_video.shape, v, h, vertical_tiles, horizontal_tiles,
332
+ spatial_overlap, tile_out_video.device, tile_out_video.dtype
333
+ )
334
+
335
+ tile_out_video = tile_out_video.to(final_video.device)
336
+ tile_denoised_video = tile_denoised_video.to(final_video.device)
337
+ w = w.to(final_video.device)
338
+
339
+ final_video[:, :, :, v_start:v_end, h_start:h_end] += tile_out_video * w
340
+ final_denoised_video[:, :, :, v_start:v_end, h_start:h_end] += tile_denoised_video * w
341
+ weights[:, :, :, v_start:v_end, h_start:h_end] += w
342
+
343
+ final_video = final_video / (weights + 1e-8)
344
+ final_denoised_video = final_denoised_video / (weights + 1e-8)
345
+ noise.seed = first_seed
346
+
347
+ # Monta NestedTensor para as duas saídas
348
+ def make_output_latent(video_tensor):
349
+ out_tensors = [video_tensor]
350
+ if has_audio:
351
+ out_tensors.append(full_audio.to(video_tensor.device))
352
+ out_samples = self._make_nested(out_tensors)
353
+ out_latent = copy.deepcopy(original_latent)
354
+ out_latent["samples"] = out_samples
355
+ return out_latent
356
+
357
+ output_latent = make_output_latent(final_video)
358
+ denoised_latent = make_output_latent(final_denoised_video)
359
+
360
+ print(f"\n[H3LoopingSampler] Final video shape: {final_video.shape}")
361
+ print(f"Total spatial tiles: {tile_count}")
362
+ print("Saídas: output + denoised_output")
363
+ print("========================================\n")
364
+
365
+ return (output_latent, denoised_latent)
366
+
367
+
368
+ NODE_CLASS_MAPPINGS = {
369
+ "H3LoopingSampler": H3LoopingSampler
370
+ }
371
+
372
+ NODE_DISPLAY_NAME_MAPPINGS = {
373
+ "H3LoopingSampler": "H3 Looping / Tiled Sampler (ComfyGuy9000)"
374
+ }