Publish NativeLooper

This commit is contained in:
Matthew
2026-04-08 17:53:22 -07:00
commit 6d098c794d
9 changed files with 687 additions and 0 deletions
+7
View File
@@ -0,0 +1,7 @@
__pycache__/
*.pyc
*.pyo
.DS_Store
*.egg-info/
dist/
build/
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+38
View File
@@ -0,0 +1,38 @@
# ComfyUI-NativeLooper
Multi-loop video generation with **sharp output** and character consistency.
## Features
- **Multi-loop generation** — up to 10 segments, chained for seamless long videos
- **Per-loop prompts** — different prompt for each segment
- **Per-loop frame counts** — customize duration per segment
- **Per-loop LoRA** — different LoRA per segment
- **Memory efficient** — segments saved to disk between loops, VRAM cleaned between iterations
## Installation
### Option 1: Clone into custom_nodes
```bash
cd ComfyUI/custom_nodes
git clone https://github.com/YOUR_USERNAME/ComfyUI-NativeLooper.git
```
### Option 2: Manual
Copy the `nodes.py` and `__init__.py` files into `ComfyUI/custom_nodes/ComfyUI-NativeLooper/`.
No additional dependencies required — uses only ComfyUI built-in modules.
## How It Works
1. For each loop:
- Encodes prompt and CLIP Vision features for the current start image
- Builds WanImageToVideo conditioning
- Optionally applies a per-loop LoRA
- Samples with KSampler, decodes with VAE
- Saves segment to disk; uses last frame as start image for the next loop
2. Concatenates all segments into the final video.
## License
MIT
+16
View File
@@ -0,0 +1,16 @@
from .nodes import NODE_CLASS_MAPPINGS as NCM1, NODE_DISPLAY_NAME_MAPPINGS as NDM1
from .nodes_highlow import I2VLooperHL, I2VLoopHL
NODE_CLASS_MAPPINGS = {
**NCM1,
"I2VLooperHL": I2VLooperHL,
"I2VLoopHL": I2VLoopHL,
}
NODE_DISPLAY_NAME_MAPPINGS = {
**NDM1,
"I2VLooperHL": "I2V Looper High/Low",
"I2VLoopHL": "I2V Loop High/Low",
}
__all__ = ["NODE_CLASS_MAPPINGS", "NODE_DISPLAY_NAME_MAPPINGS"]
+261
View File
@@ -0,0 +1,261 @@
"""
Native Looper - Multi-loop video generation using WanImageToVideo conditioning.
Each loop uses the last frame from the previous loop as the start image,
chaining I2V generation for seamless long videos.
"""
import torch
import gc
import os
import tempfile
import shutil
import comfy.model_management
import comfy.utils
import comfy.latent_formats
import comfy.sd
import folder_paths
import nodes
import node_helpers
class NativeLoop:
"""Per-loop config: prompt, frames, LoRA."""
@classmethod
def INPUT_TYPES(cls):
lora_list = ["None"] + folder_paths.get_filename_list("loras")
return {
"required": {
"prompt": ("STRING", {"default": "", "multiline": True}),
"frames": ("INT", {"default": 0, "min": 0, "max": 200, "tooltip": "Frames for this loop (0 = use 81)"}),
"lora_name": (lora_list, {"default": "None"}),
"lora_strength": ("FLOAT", {"default": 0.8, "min": 0.0, "max": 2.0, "step": 0.05}),
},
}
RETURN_TYPES = ("NATIVE_LOOP",)
RETURN_NAMES = ("loop_config",)
FUNCTION = "build"
CATEGORY = "video/native"
def build(self, prompt, frames, lora_name, lora_strength):
return ({
"prompt": prompt,
"frames": frames,
"lora_name": lora_name if lora_name != "None" else None,
"lora_strength": lora_strength,
},)
class NativeLooper:
"""
Multi-loop video generation using WanImageToVideo.
Last frame of each loop becomes start image for the next.
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"model": ("MODEL",),
"vae": ("VAE",),
"clip": ("CLIP",),
"clip_vision": ("CLIP_VISION", {"tooltip": "CLIP Vision model (e.g. clip_vision_h.safetensors)"}),
"start_image": ("IMAGE", {"tooltip": "Start image for loop 1"}),
"width": ("INT", {"default": 832, "min": 16, "max": 4096, "step": 16}),
"height": ("INT", {"default": 480, "min": 16, "max": 4096, "step": 16}),
"steps": ("INT", {"default": 20, "min": 1, "max": 100}),
"cfg": ("FLOAT", {"default": 4.0, "min": 0.0, "max": 30.0, "step": 0.1}),
"sampler_name": (["uni_pc", "euler", "dpm++_sde", "dpm++_2m", "ddim"], {"default": "uni_pc"}),
"scheduler": (["normal", "simple", "karras", "sgm_uniform"], {"default": "normal"}),
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
},
"optional": {
"positive_prompt": ("STRING", {"forceInput": True, "tooltip": "Base/fallback prompt"}),
"negative_prompt": ("STRING", {"forceInput": True, "tooltip": "Global negative prompt"}),
"loop_1": ("NATIVE_LOOP",),
"loop_2": ("NATIVE_LOOP",),
"loop_3": ("NATIVE_LOOP",),
"loop_4": ("NATIVE_LOOP",),
"loop_5": ("NATIVE_LOOP",),
"loop_6": ("NATIVE_LOOP",),
"loop_7": ("NATIVE_LOOP",),
"loop_8": ("NATIVE_LOOP",),
"loop_9": ("NATIVE_LOOP",),
"loop_10": ("NATIVE_LOOP",),
}
}
RETURN_TYPES = ("IMAGE", "STRING",)
RETURN_NAMES = ("full_video", "used_prompts",)
FUNCTION = "generate"
CATEGORY = "video/native"
def generate(self, model, vae, clip, clip_vision, start_image,
width, height,
steps, cfg, sampler_name, scheduler, seed,
positive_prompt="", negative_prompt="low quality, blurry, glitch, distortion",
loop_1=None, loop_2=None, loop_3=None, loop_4=None, loop_5=None,
loop_6=None, loop_7=None, loop_8=None, loop_9=None, loop_10=None):
from comfy_extras.nodes_wan import WanImageToVideo
loop_configs = [loop_1, loop_2, loop_3, loop_4, loop_5,
loop_6, loop_7, loop_8, loop_9, loop_10]
used_prompts_log = []
segment_dir = tempfile.mkdtemp(prefix="native_i2v_segments_")
print(f"\n{'='*60}")
print("NATIVE I2V LOOPER")
print(f"{'='*60}")
extension_loops = sum(1 for c in loop_configs if c is not None)
if extension_loops == 0:
extension_loops = 1
print(f"Loops: {extension_loops} (auto-detected)")
current_start_image = comfy.utils.common_upscale(
start_image[:1].movedim(-1, 1), width, height, "bilinear", "center"
).movedim(1, -1)
segment_paths = []
current_seed = seed
for loop_idx in range(10):
loop_id = loop_idx + 1
comfy.model_management.soft_empty_cache()
if torch.cuda.is_available():
torch.cuda.empty_cache()
gc.collect()
config = loop_configs[loop_idx] if loop_idx < len(loop_configs) else None
if config is None:
print(f"\n{'─'*60}")
print(f"LOOP {loop_id}/{extension_loops} — SKIPPED")
print(f"{'─'*60}")
used_prompts_log.append(f"Loop {loop_id}: SKIPPED")
current_seed += 1
continue
loop_frame_count = config.get("frames", 0)
if loop_frame_count <= 0:
loop_frame_count = 81
active_prompt = config.get("prompt", "").strip() or positive_prompt
loop_lora_name = config.get("lora_name")
loop_lora_strength = config.get("lora_strength", 0.8)
print(f"\n{'─'*60}")
print(f"LOOP {loop_id}/{extension_loops} ({loop_frame_count} frames)")
if loop_lora_name:
print(f"LoRA: {loop_lora_name} @ {loop_lora_strength}")
print(f"{'─'*60}")
print(f"Prompt: {active_prompt[:80]}...")
used_prompts_log.append(f"Loop {loop_id} ({loop_frame_count}f): {active_prompt[:80]}")
# Encode text
tokens_pos = clip.tokenize(active_prompt)
cond_pos, pooled_pos = clip.encode_from_tokens(tokens_pos, return_pooled=True)
c_pos = [[cond_pos, {"pooled_output": pooled_pos}]]
tokens_neg = clip.tokenize(negative_prompt)
cond_neg, pooled_neg = clip.encode_from_tokens(tokens_neg, return_pooled=True)
c_neg = [[cond_neg, {"pooled_output": pooled_neg}]]
# CLIP Vision encode
cv_result = nodes.CLIPVisionEncode().encode(clip_vision, current_start_image, "none")
cv_out = cv_result[0]
# WanImageToVideo conditioning
i2v_result = WanImageToVideo.execute(
positive=c_pos, negative=c_neg, vae=vae,
clip_vision_output=cv_out, start_image=current_start_image,
width=width, height=height, length=loop_frame_count, batch_size=1,
)
c_sample_pos = i2v_result[0]
c_sample_neg = i2v_result[1]
latent_dict = i2v_result[2]
# Apply LoRA
loop_model = model
if loop_lora_name:
lora_path = folder_paths.get_full_path("loras", loop_lora_name)
if lora_path:
print(f"Loading LoRA: {loop_lora_name}...")
lora = comfy.utils.load_torch_file(lora_path, safe_load=True)
loop_model, _ = comfy.sd.load_lora_for_models(model, clip, lora, loop_lora_strength, loop_lora_strength)
del lora
# Sample
print(f"Sampling ({steps} steps, {sampler_name}, cfg={cfg})...")
ksampler = nodes.KSampler()
result = ksampler.sample(
model=loop_model, seed=current_seed, steps=steps, cfg=cfg,
sampler_name=sampler_name, scheduler=scheduler,
positive=c_sample_pos, negative=c_sample_neg,
latent_image=latent_dict, denoise=1.0
)
sampled = result[0]["samples"]
# Decode
print("VAE decoding...")
decoded = vae.decode(sampled)
while decoded.ndim > 4:
decoded = decoded.squeeze(0)
if decoded.shape[1] != height or decoded.shape[2] != width:
decoded = comfy.utils.common_upscale(
decoded.movedim(-1, 1), width, height, "bilinear", "center"
).movedim(1, -1)
print(f"Decoded: {decoded.shape[0]} frames")
seg_path = os.path.join(segment_dir, f"segment_{loop_id:03d}.pt")
torch.save(decoded.cpu(), seg_path)
segment_paths.append(seg_path)
# Last frame becomes next loop's start image
current_start_image = decoded[-1:].clone()
if loop_lora_name and loop_model is not model:
del loop_model
del decoded, sampled, c_pos, c_neg
comfy.model_management.soft_empty_cache()
if torch.cuda.is_available():
torch.cuda.empty_cache()
gc.collect()
current_seed += 1
# Combine
print(f"\n{'='*60}")
print("Combining segments...")
full_video = None
for seg_path in segment_paths:
seg = torch.load(seg_path, map_location="cpu")
full_video = seg if full_video is None else torch.cat([full_video, seg], dim=0)
del seg
gc.collect()
print(f" Total: {full_video.shape[0]} frames")
if full_video is None:
full_video = torch.zeros((1, height, width, 3))
try:
shutil.rmtree(segment_dir)
except:
pass
print(f"\nFinal video: {full_video.shape[0]} frames")
return (full_video, "\n".join(used_prompts_log))
NODE_CLASS_MAPPINGS = {
"I2VLooper": NativeLooper,
"I2VLoop": NativeLoop,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"I2VLooper": "I2V Looper",
"I2VLoop": "I2V Loop",
}
+329
View File
@@ -0,0 +1,329 @@
"""
I2V Looper High/Low - Multi-loop I2V video generation with dual High/Low noise models.
Each loop uses WanImageToVideo conditioning with the last frame from the
previous loop as the start image. Matches APP VIDEO workflow approach.
"""
import torch
import gc
import os
import tempfile
import shutil
import comfy.model_management
import comfy.utils
import comfy.latent_formats
import comfy.sd
import folder_paths
import nodes
import node_helpers
class I2VLoopHL:
"""Per-loop config with High/Low LoRA support."""
@classmethod
def INPUT_TYPES(cls):
lora_list = ["None"] + folder_paths.get_filename_list("loras")
return {
"required": {
"prompt": ("STRING", {"default": "", "multiline": True}),
"frames": ("INT", {"default": 81, "min": 1, "max": 200}),
},
"optional": {
"lora_high": (lora_list, {"default": "None"}),
"lora_high_strength": ("FLOAT", {"default": 0.8, "min": 0.0, "max": 2.0, "step": 0.05}),
"lora_low": (lora_list, {"default": "None"}),
"lora_low_strength": ("FLOAT", {"default": 0.8, "min": 0.0, "max": 2.0, "step": 0.05}),
},
}
RETURN_TYPES = ("I2V_LOOP_HL",)
RETURN_NAMES = ("loop_config",)
FUNCTION = "build"
CATEGORY = "video/i2v_looper"
def build(self, prompt, frames, lora_high="None", lora_high_strength=0.8,
lora_low="None", lora_low_strength=0.8):
return ({
"prompt": prompt,
"frames": frames,
"lora_high": lora_high if lora_high != "None" else None,
"lora_high_strength": lora_high_strength,
"lora_low": lora_low if lora_low != "None" else None,
"lora_low_strength": lora_low_strength,
},)
class I2VLooperHL:
"""
Multi-loop I2V with High/Low noise models.
Last frame of each loop becomes start image for the next.
Matches APP VIDEO dual-pass KSamplerAdvanced approach.
"""
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"model_high": ("MODEL", {"tooltip": "High-noise model"}),
"model_low": ("MODEL", {"tooltip": "Low-noise model"}),
"vae": ("VAE",),
"clip": ("CLIP",),
"clip_vision": ("CLIP_VISION", {"tooltip": "CLIP Vision model (e.g. clip_vision_h.safetensors)"}),
"start_image": ("IMAGE", {"tooltip": "Start image for loop 1"}),
"width": ("INT", {"default": 832, "min": 16, "max": 4096, "step": 16}),
"height": ("INT", {"default": 480, "min": 16, "max": 4096, "step": 16}),
"steps": ("INT", {"default": 8, "min": 1, "max": 100}),
"cfg": ("FLOAT", {"default": 1.0, "min": 0.0, "max": 30.0, "step": 0.1}),
"split_step": ("INT", {"default": 4, "min": 1, "max": 50, "tooltip": "Step where high switches to low model"}),
"sampler_name": (["euler", "uni_pc", "dpm++_sde", "dpm++_2m", "ddim"], {"default": "euler"}),
"scheduler": (["simple", "normal", "karras", "sgm_uniform"], {"default": "simple"}),
"seed": ("INT", {"default": 0, "min": 0, "max": 0xffffffffffffffff}),
},
"optional": {
"positive_prompt": ("STRING", {"forceInput": True, "tooltip": "Base/fallback prompt"}),
"negative_prompt": ("STRING", {"forceInput": True, "tooltip": "Global negative prompt"}),
"loop_1": ("I2V_LOOP_HL",),
"loop_2": ("I2V_LOOP_HL",),
"loop_3": ("I2V_LOOP_HL",),
"loop_4": ("I2V_LOOP_HL",),
"loop_5": ("I2V_LOOP_HL",),
"loop_6": ("I2V_LOOP_HL",),
"loop_7": ("I2V_LOOP_HL",),
"loop_8": ("I2V_LOOP_HL",),
"loop_9": ("I2V_LOOP_HL",),
"loop_10": ("I2V_LOOP_HL",),
}
}
RETURN_TYPES = ("IMAGE", "STRING",)
RETURN_NAMES = ("full_video", "used_prompts",)
FUNCTION = "generate"
CATEGORY = "video/i2v_looper"
def _load_lora(self, model, clip, lora_name, strength):
lora_path = folder_paths.get_full_path("loras", lora_name)
if lora_path:
lora = comfy.utils.load_torch_file(lora_path, safe_load=True)
patched_model, patched_clip = comfy.sd.load_lora_for_models(
model, clip, lora, strength, strength
)
del lora
return patched_model, patched_clip
print(f"WARNING: LoRA not found: {lora_name}")
return model, clip
def generate(self, model_high, model_low, vae, clip, clip_vision, start_image,
width, height,
steps, cfg, split_step, sampler_name, scheduler, seed,
positive_prompt="", negative_prompt="low quality, blurry, glitch, distortion",
loop_1=None, loop_2=None, loop_3=None, loop_4=None, loop_5=None,
loop_6=None, loop_7=None, loop_8=None, loop_9=None, loop_10=None):
from comfy_extras.nodes_wan import WanImageToVideo
loop_configs = [loop_1, loop_2, loop_3, loop_4, loop_5,
loop_6, loop_7, loop_8, loop_9, loop_10]
# Count connected loops
extension_loops = sum(1 for c in loop_configs if c is not None)
if extension_loops == 0:
extension_loops = 1 # At least 1 loop
used_prompts_log = []
segment_dir = tempfile.mkdtemp(prefix="i2v_hl_segments_")
print(f"\n{'='*60}")
print("I2V LOOPER HIGH/LOW")
print(f"{'='*60}")
print(f"Loops: {extension_loops} (auto-detected from connected loop nodes)")
print(f"Steps: {steps} (high 0-{split_step}, low {split_step}-end)")
# Resize start image
current_start_image = comfy.utils.common_upscale(
start_image[:1].movedim(-1, 1), width, height, "bilinear", "center"
).movedim(1, -1)
segment_paths = []
current_seed = seed
for loop_idx in range(10):
loop_id = loop_idx + 1
comfy.model_management.soft_empty_cache()
if torch.cuda.is_available():
torch.cuda.empty_cache()
gc.collect()
config = loop_configs[loop_idx] if loop_idx < len(loop_configs) else None
if config is None:
print(f"\n{'─'*60}")
print(f"LOOP {loop_id}/{extension_loops} — SKIPPED")
print(f"{'─'*60}")
used_prompts_log.append(f"Loop {loop_id}: SKIPPED")
current_seed += 1
continue
loop_frame_count = config.get("frames", 81)
if loop_frame_count <= 0:
loop_frame_count = 81
active_prompt = config.get("prompt", "").strip() or positive_prompt
lora_high_name = config.get("lora_high")
lora_high_str = config.get("lora_high_strength", 0.8)
lora_low_name = config.get("lora_low")
lora_low_str = config.get("lora_low_strength", 0.8)
print(f"\n{'─'*60}")
print(f"LOOP {loop_id}/{extension_loops} ({loop_frame_count} frames)")
if lora_high_name:
print(f"LoRA HIGH: {lora_high_name} @ {lora_high_str}")
if lora_low_name:
print(f"LoRA LOW: {lora_low_name} @ {lora_low_str}")
print(f"{'─'*60}")
print(f"Prompt: {active_prompt[:80]}...")
used_prompts_log.append(f"Loop {loop_id} ({loop_frame_count}f): {active_prompt[:80]}")
# Encode text
tokens_pos = clip.tokenize(active_prompt)
cond_pos, pooled_pos = clip.encode_from_tokens(tokens_pos, return_pooled=True)
c_pos = [[cond_pos, {"pooled_output": pooled_pos}]]
tokens_neg = clip.tokenize(negative_prompt)
cond_neg, pooled_neg = clip.encode_from_tokens(tokens_neg, return_pooled=True)
c_neg = [[cond_neg, {"pooled_output": pooled_neg}]]
# CLIP Vision encode — use CLIPVisionEncode node (same as APP VIDEO)
print("CLIP Vision encoding...")
cv_result = nodes.CLIPVisionEncode().encode(clip_vision, current_start_image, "none")
cv_out = cv_result[0] # CLIP_VISION_OUTPUT
# WanImageToVideo conditioning (same as APP VIDEO)
print("WanImageToVideo conditioning...")
i2v_result = WanImageToVideo.execute(
positive=c_pos, negative=c_neg, vae=vae,
clip_vision_output=cv_out, start_image=current_start_image,
width=width, height=height, length=loop_frame_count, batch_size=1,
)
c_sample_pos = i2v_result[0]
c_sample_neg = i2v_result[1]
latent_dict = i2v_result[2]
# Apply per-loop LoRAs
high_model = model_high
low_model = model_low
if lora_high_name:
print(f"Loading HIGH LoRA...")
high_model, _ = self._load_lora(model_high, clip, lora_high_name, lora_high_str)
if lora_low_name:
print(f"Loading LOW LoRA...")
low_model, _ = self._load_lora(model_low, clip, lora_low_name, lora_low_str)
# TWO-PASS SAMPLING (matching APP VIDEO exactly)
ksampler = nodes.KSamplerAdvanced()
# Pass 1: High-noise model (steps 0 to split_step)
# APP VIDEO: add_noise=enable, return_with_leftover_noise=enable
print(f"Sampling HIGH (steps 0-{split_step})...")
high_out = ksampler.sample(
model=high_model,
add_noise="enable",
noise_seed=current_seed,
steps=steps,
cfg=cfg,
sampler_name=sampler_name,
scheduler=scheduler,
positive=c_sample_pos,
negative=c_sample_neg,
latent_image=latent_dict,
start_at_step=0,
end_at_step=split_step,
return_with_leftover_noise="enable",
)
comfy.model_management.soft_empty_cache()
if torch.cuda.is_available():
torch.cuda.empty_cache()
gc.collect()
# Pass 2: Low-noise model (split_step to end)
# APP VIDEO: add_noise=disable, noise_seed=0, end_at_step=1000, return_with_leftover_noise=disable
print(f"Sampling LOW (steps {split_step}-end)...")
low_out = ksampler.sample(
model=low_model,
add_noise="disable",
noise_seed=0,
steps=steps,
cfg=cfg,
sampler_name=sampler_name,
scheduler=scheduler,
positive=c_sample_pos,
negative=c_sample_neg,
latent_image={"samples": high_out[0]["samples"]},
start_at_step=split_step,
end_at_step=1000,
return_with_leftover_noise="disable",
)
sampled = low_out[0]["samples"]
# Decode
print("VAE decoding...")
decoded = vae.decode(sampled)
while decoded.ndim > 4:
decoded = decoded.squeeze(0)
print(f"Decoded: {decoded.shape[0]} frames")
if decoded.shape[1] != height or decoded.shape[2] != width:
decoded = comfy.utils.common_upscale(
decoded.movedim(-1, 1), width, height, "bilinear", "center"
).movedim(1, -1)
# Save segment
seg_path = os.path.join(segment_dir, f"segment_{loop_id:03d}.pt")
torch.save(decoded.cpu(), seg_path)
segment_paths.append(seg_path)
# Last frame becomes next loop's start image
current_start_image = decoded[-1:].clone()
print(f"Next start image from frame {decoded.shape[0]-1}")
# Cleanup
if lora_high_name and high_model is not model_high:
del high_model
if lora_low_name and low_model is not model_low:
del low_model
del decoded, sampled, c_pos, c_neg
comfy.model_management.soft_empty_cache()
if torch.cuda.is_available():
torch.cuda.empty_cache()
gc.collect()
current_seed += 1
# Combine all segments
print(f"\n{'='*60}")
print("Combining segments...")
print(f"{'='*60}")
full_video = None
for seg_path in segment_paths:
seg = torch.load(seg_path, map_location="cpu")
full_video = seg if full_video is None else torch.cat([full_video, seg], dim=0)
del seg
gc.collect()
print(f" Total: {full_video.shape[0]} frames")
if full_video is None:
full_video = torch.zeros((1, height, width, 3))
try:
shutil.rmtree(segment_dir)
except:
pass
print(f"\nFinal video: {full_video.shape[0]} frames")
return (full_video, "\n".join(used_prompts_log))
+13
View File
@@ -0,0 +1,13 @@
[project]
name = "comfyui-native-looper"
description = "Multi-loop video generation with sharp output and character consistency"
version = "1.0.0"
license = "MIT"
[project.urls]
Repository = "https://github.com/YOUR_USERNAME/ComfyUI-NativeLooper"
[tool.comfy]
PublisherId = ""
DisplayName = "Native Looper"
Icon = ""
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long