Co-authored-by: naxci1 <206254294+naxci1@users.noreply.github.com>
1607 lines
72 KiB
Python
1607 lines
72 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""
|
||
FlashVSR ComfyUI Node - Video Super Resolution
|
||
===============================================
|
||
Supports 5 VAE models: Wan2.1, Wan2.2, LightVAE_W2.1, TAE_W2.2, LightTAE_HY1.5
|
||
|
||
Key Fixes Applied:
|
||
- FIX 1: Merged VAE selection into single 'vae_model' dropdown (5 options)
|
||
- FIX 2: STRICT file path mapping - each VAE loads its own distinct file
|
||
- FIX 3: Black border fix - crop ONLY AFTER full VAE decode is complete
|
||
- FIX 4: Lossless resize uses NEAREST for integer scaling
|
||
- FIX 5: VRAM optimization - 95% threshold before triggering OOM recovery
|
||
- FIX 6: Auto-download with CORRECT HuggingFace URLs
|
||
- FIX 7: Explicit VAE class instantiation - no guessing from state_dict
|
||
- FIX 8: Summary logging at end of processing
|
||
"""
|
||
|
||
import os, gc
|
||
import math
|
||
import torch
|
||
import folder_paths
|
||
import comfy.utils
|
||
import time
|
||
import sys
|
||
import psutil
|
||
|
||
import numpy as np
|
||
import torch.nn.functional as F
|
||
|
||
from einops import rearrange
|
||
from huggingface_hub import snapshot_download, hf_hub_download
|
||
try:
|
||
from .src import ModelManager, FlashVSRFullPipeline, FlashVSRTinyPipeline, FlashVSRTinyLongPipeline
|
||
from .src.models.TCDecoder import build_tcdecoder
|
||
from .src.models.utils import clean_vram, get_device_list, Buffer_LQ4x_Proj, Causal_LQ4x_Proj
|
||
from .src.models import wan_video_dit
|
||
from .src.models.wan_video_vae import (
|
||
WanVideoVAE, Wan22VideoVAE, LightX2VVAE, create_video_vae,
|
||
VAE_FULL_DIM, VAE_LIGHT_DIM, VAE_Z_DIM
|
||
)
|
||
except ImportError:
|
||
from src import ModelManager, FlashVSRFullPipeline, FlashVSRTinyPipeline, FlashVSRTinyLongPipeline
|
||
from src.models.TCDecoder import build_tcdecoder
|
||
from src.models.utils import clean_vram, get_device_list, Buffer_LQ4x_Proj, Causal_LQ4x_Proj
|
||
from src.models import wan_video_dit
|
||
from src.models.wan_video_vae import (
|
||
WanVideoVAE, Wan22VideoVAE, LightX2VVAE, create_video_vae,
|
||
VAE_FULL_DIM, VAE_LIGHT_DIM, VAE_Z_DIM
|
||
)
|
||
|
||
try:
|
||
import safetensors.torch
|
||
except ImportError:
|
||
pass
|
||
|
||
# =============================================================================
|
||
# FIX 1: Unified VAE model selection dropdown - ALL 5 OPTIONS
|
||
# =============================================================================
|
||
VAE_MODEL_OPTIONS = ["Wan2.1", "Wan2.2", "LightVAE_W2.1", "TAE_W2.2", "LightTAE_HY1.5"]
|
||
|
||
# =============================================================================
|
||
# FIX 2 & 7: STRICT file path mapping with EXPLICIT class instantiation
|
||
# Each VAE selection loads a DISTINCT file and uses EXPLICIT class (no guessing)
|
||
# =============================================================================
|
||
VAE_MODEL_MAP = {
|
||
"Wan2.1": {
|
||
"class": WanVideoVAE,
|
||
"file": "Wan2.1_VAE.pth",
|
||
"internal_name": "wan2.1",
|
||
"url": "https://huggingface.co/lightx2v/Autoencoders/resolve/main/Wan2.1_VAE.pth",
|
||
"dim": VAE_FULL_DIM,
|
||
"z_dim": VAE_Z_DIM,
|
||
"use_full_arch": False
|
||
},
|
||
"Wan2.2": {
|
||
"class": Wan22VideoVAE,
|
||
"file": "Wan2.2_VAE.pth",
|
||
"internal_name": "wan2.2",
|
||
"url": "https://huggingface.co/lightx2v/Autoencoders/resolve/main/Wan2.2_VAE.pth",
|
||
"dim": VAE_FULL_DIM,
|
||
"z_dim": VAE_Z_DIM,
|
||
"use_full_arch": False
|
||
},
|
||
"LightVAE_W2.1": {
|
||
"class": LightX2VVAE,
|
||
"file": "lightvaew2_1.pth",
|
||
"internal_name": "lightx2v",
|
||
"url": "https://huggingface.co/lightx2v/Autoencoders/resolve/main/lightvaew2_1.pth",
|
||
"dim": VAE_LIGHT_DIM,
|
||
"z_dim": VAE_Z_DIM,
|
||
"use_full_arch": True
|
||
},
|
||
"TAE_W2.2": {
|
||
"class": Wan22VideoVAE, # TAE uses same base as Wan2.2
|
||
"file": "taew2_2.safetensors",
|
||
"internal_name": "tae_w2.2",
|
||
"url": "https://huggingface.co/lightx2v/Autoencoders/resolve/main/taew2_2.safetensors",
|
||
"dim": VAE_FULL_DIM,
|
||
"z_dim": VAE_Z_DIM,
|
||
"use_full_arch": False
|
||
},
|
||
"LightTAE_HY1.5": {
|
||
"class": LightX2VVAE, # LightTAE uses LightX2V architecture
|
||
"file": "lighttaehy1_5.pth",
|
||
"internal_name": "lighttae_hy1.5",
|
||
"url": "https://huggingface.co/lightx2v/Autoencoders/resolve/main/lighttaehy1_5.pth",
|
||
"dim": VAE_LIGHT_DIM,
|
||
"z_dim": VAE_Z_DIM,
|
||
"use_full_arch": True
|
||
},
|
||
}
|
||
|
||
# =============================================================================
|
||
# FIX 5: VRAM threshold for OOM recovery - set to 95%
|
||
# =============================================================================
|
||
VRAM_OOM_THRESHOLD = 0.95 # Only trigger OOM recovery when 95% VRAM is used
|
||
|
||
device_choices = get_device_list()
|
||
|
||
def log(message: str, message_type: str = 'normal', icon: str = "", end: str = "\n", in_place: bool = False):
|
||
if icon:
|
||
message = f"{icon} {message}"
|
||
|
||
if message_type == 'error':
|
||
message = '\033[1;41m' + message + '\033[m'
|
||
elif message_type == 'warning':
|
||
message = '\033[1;31m' + message + '\033[m'
|
||
elif message_type == 'finish':
|
||
message = '\033[1;32m' + message + '\033[m'
|
||
elif message_type == 'info':
|
||
message = '\033[1;33m' + message + '\033[m'
|
||
else:
|
||
message = message
|
||
|
||
if in_place:
|
||
# Clear line before printing
|
||
sys.stdout.write("\r\033[K" + message)
|
||
sys.stdout.flush()
|
||
else:
|
||
print(f"{message}", end=end, flush=True)
|
||
|
||
def get_vram_info():
|
||
if torch.cuda.is_available():
|
||
vram_used = torch.cuda.memory_allocated() / (1024 ** 3)
|
||
vram_reserved = torch.cuda.memory_reserved() / (1024 ** 3)
|
||
vram_total = torch.cuda.get_device_properties(0).total_memory / (1024 ** 3)
|
||
return vram_used, vram_reserved, vram_total
|
||
return 0, 0, 0
|
||
|
||
def log_resource_usage(prefix="Resource Usage", end="\n", in_place=False):
|
||
ram = psutil.virtual_memory()
|
||
ram_used = ram.used / (1024 ** 3)
|
||
ram_total = ram.total / (1024 ** 3)
|
||
|
||
msg = f"[{prefix}] RAM: {ram_used:.1f}/{ram_total:.1f}G"
|
||
|
||
if torch.cuda.is_available():
|
||
vram_used, vram_reserved, vram_total = get_vram_info()
|
||
msg += f" | VRAM: {vram_used:.1f}/{vram_reserved:.1f}/{vram_total:.1f}G"
|
||
|
||
log(msg, message_type='info', icon="📊", end=end, in_place=in_place)
|
||
|
||
|
||
# =============================================================================
|
||
# FIX 5 & 9: VRAM Estimation, Pre-Flight Resource Check & Settings Recommender
|
||
# Calculate approximate VRAM requirements and provide optimal settings
|
||
# =============================================================================
|
||
def estimate_vram_usage(width, height, num_frames, scale, tiled_vae=False, tiled_dit=False,
|
||
chunk_size=0, mode="full"):
|
||
"""
|
||
Estimate approximate VRAM usage for the given video parameters.
|
||
Returns estimated VRAM in GB. Enhanced to consider chunk_size and mode.
|
||
|
||
=============================================================================
|
||
FIX: Accurate VRAM Estimation with Safety Factor
|
||
=============================================================================
|
||
Previous estimates were ~4.5GB when actual usage was ~15GB.
|
||
This was because we ignored:
|
||
- Intermediate Activations: PyTorch stores outputs for every layer
|
||
- VAE Upscaling: VAE decoding expands data significantly
|
||
- Workspace Memory: CUDA context overhead
|
||
|
||
Solution: Apply Safety_Factor = 4.0 to the raw tensor calculations
|
||
to account for these overheads.
|
||
"""
|
||
# Safety factor to account for intermediate activations, VAE upscaling overhead,
|
||
# and CUDA workspace memory. Empirically determined from observed ~15GB actual
|
||
# usage when estimates were ~4.5GB.
|
||
SAFETY_FACTOR = 4.0
|
||
|
||
# Base model memory varies by mode
|
||
if mode == "full":
|
||
base_model_gb = 5.0 # Full VAE + DiT
|
||
elif mode == "tiny-long":
|
||
base_model_gb = 3.5 # TCDecoder is lighter than full VAE
|
||
else: # tiny
|
||
base_model_gb = 4.0
|
||
|
||
# Per-frame latent memory (scaled output resolution)
|
||
output_h, output_w = height * scale, width * scale
|
||
|
||
# Latent dimensions (8x downsampled)
|
||
latent_h, latent_w = output_h // 8, output_w // 8
|
||
|
||
# Frames to process at once (if chunked, use chunk_size)
|
||
effective_frames = chunk_size if chunk_size > 0 and chunk_size <= num_frames else num_frames
|
||
|
||
# Input tensor size - use 4 bytes to account for float32 intermediates during processing
|
||
# Even though final tensors are bf16/fp16, operations often use float32 internally
|
||
input_tensor_bytes = output_h * output_w * 3 * effective_frames * 4
|
||
input_tensor_gb = (input_tensor_bytes * SAFETY_FACTOR) / (1024 ** 3)
|
||
|
||
# Approximate memory per frame in latent space (16 channels, bf16)
|
||
bytes_per_frame = latent_h * latent_w * 16 * 2 # bf16 = 2 bytes
|
||
total_latent_gb = (bytes_per_frame * effective_frames * SAFETY_FACTOR) / (1024 ** 3)
|
||
|
||
# DiT attention memory (quadratic with sequence length)
|
||
seq_len = latent_h * latent_w * (effective_frames // 4)
|
||
attention_gb = (seq_len * seq_len * 2 * SAFETY_FACTOR) / (1024 ** 3) * 0.001 # Rough estimate
|
||
|
||
# VAE decode memory - this is where most intermediate activations live
|
||
vae_decode_gb = (output_h * output_w * 3 * effective_frames * 2 * SAFETY_FACTOR) / (1024 ** 3)
|
||
|
||
# Apply tiling reductions
|
||
if tiled_dit:
|
||
attention_gb *= 0.3 # Tiling reduces peak attention memory
|
||
if tiled_vae:
|
||
vae_decode_gb *= 0.4 # Tiling reduces peak VAE memory
|
||
|
||
total_estimated = base_model_gb + input_tensor_gb + total_latent_gb + attention_gb + vae_decode_gb
|
||
return total_estimated
|
||
|
||
|
||
def get_optimal_settings(width, height, num_frames, scale, available_vram_gb, mode="full"):
|
||
"""
|
||
Calculate optimal settings (chunk_size, resize_factor, tiling) based on VRAM.
|
||
|
||
Returns dict with recommended settings.
|
||
"""
|
||
# Target VRAM usage: 85% of available to leave headroom
|
||
target_vram = available_vram_gb * 0.85
|
||
|
||
# Start with default settings
|
||
recommended = {
|
||
"chunk_size": 0, # 0 = process all at once
|
||
"resize_factor": 1.0,
|
||
"tiled_vae": False,
|
||
"tiled_dit": False,
|
||
"warning": None
|
||
}
|
||
|
||
# Test current settings
|
||
estimated = estimate_vram_usage(width, height, num_frames, scale,
|
||
tiled_vae=False, tiled_dit=False,
|
||
chunk_size=0, mode=mode)
|
||
|
||
if estimated <= target_vram:
|
||
# Settings are fine
|
||
return recommended
|
||
|
||
# Try enabling tiled VAE first (least impact on quality)
|
||
estimated_tiled_vae = estimate_vram_usage(width, height, num_frames, scale,
|
||
tiled_vae=True, tiled_dit=False,
|
||
chunk_size=0, mode=mode)
|
||
if estimated_tiled_vae <= target_vram:
|
||
recommended["tiled_vae"] = True
|
||
return recommended
|
||
|
||
# Try enabling both tiling
|
||
estimated_both_tiled = estimate_vram_usage(width, height, num_frames, scale,
|
||
tiled_vae=True, tiled_dit=True,
|
||
chunk_size=0, mode=mode)
|
||
if estimated_both_tiled <= target_vram:
|
||
recommended["tiled_vae"] = True
|
||
recommended["tiled_dit"] = True
|
||
return recommended
|
||
|
||
# Need chunking - find optimal chunk size
|
||
recommended["tiled_vae"] = True
|
||
recommended["tiled_dit"] = True
|
||
|
||
for chunk in [100, 64, 32, 16, 8, 4]:
|
||
if chunk >= num_frames:
|
||
continue
|
||
estimated_chunked = estimate_vram_usage(width, height, num_frames, scale,
|
||
tiled_vae=True, tiled_dit=True,
|
||
chunk_size=chunk, mode=mode)
|
||
if estimated_chunked <= target_vram:
|
||
recommended["chunk_size"] = chunk
|
||
return recommended
|
||
|
||
# Still too high - recommend resize factor
|
||
for resize in [0.8, 0.6, 0.5, 0.4, 0.3]:
|
||
new_h, new_w = int(height * resize), int(width * resize)
|
||
estimated_resized = estimate_vram_usage(new_w, new_h, num_frames, scale,
|
||
tiled_vae=True, tiled_dit=True,
|
||
chunk_size=8, mode=mode)
|
||
if estimated_resized <= target_vram:
|
||
recommended["chunk_size"] = 8
|
||
recommended["resize_factor"] = resize
|
||
return recommended
|
||
|
||
# Even with max reduction still risky
|
||
recommended["chunk_size"] = 4
|
||
recommended["resize_factor"] = 0.3
|
||
recommended["warning"] = "VRAM critically low. Results may be unstable."
|
||
return recommended
|
||
|
||
|
||
def check_resources(width, height, num_frames, scale, chunk_size, resize_factor,
|
||
tiled_vae, tiled_dit, mode="full"):
|
||
"""
|
||
=============================================================================
|
||
FIX 9: Pre-Flight Resource Calculator
|
||
=============================================================================
|
||
|
||
Performs intelligent pre-flight check before loading heavy models.
|
||
|
||
1. Gets hardware stats (VRAM, RAM) using torch.cuda.mem_get_info()
|
||
2. Estimates required memory based on video parameters
|
||
3. Simulates if current settings will cause OOM
|
||
4. Provides optimal settings recommendations
|
||
|
||
Returns:
|
||
dict with keys:
|
||
- estimated_vram_gb: float
|
||
- available_vram_gb: float
|
||
- ram_used_gb: float
|
||
- ram_total_gb: float
|
||
- will_oom: bool
|
||
- recommended_settings: dict (if will_oom)
|
||
- message: str
|
||
"""
|
||
result = {
|
||
"estimated_vram_gb": 0.0,
|
||
"available_vram_gb": 0.0,
|
||
"ram_used_gb": 0.0,
|
||
"ram_total_gb": 0.0,
|
||
"will_oom": False,
|
||
"recommended_settings": None,
|
||
"message": ""
|
||
}
|
||
|
||
# Get RAM info
|
||
ram = psutil.virtual_memory()
|
||
result["ram_used_gb"] = ram.used / (1024 ** 3)
|
||
result["ram_total_gb"] = ram.total / (1024 ** 3)
|
||
|
||
# Get VRAM info
|
||
if torch.cuda.is_available():
|
||
vram_free, vram_total = torch.cuda.mem_get_info()
|
||
result["available_vram_gb"] = vram_free / (1024 ** 3)
|
||
vram_total_gb = vram_total / (1024 ** 3)
|
||
else:
|
||
result["message"] = "CUDA not available. Running on CPU may be very slow."
|
||
return result
|
||
|
||
# Calculate effective dimensions after resize
|
||
effective_h = int(height * resize_factor) if resize_factor < 1.0 else height
|
||
effective_w = int(width * resize_factor) if resize_factor < 1.0 else width
|
||
|
||
# Estimate VRAM usage
|
||
result["estimated_vram_gb"] = estimate_vram_usage(
|
||
effective_w, effective_h, num_frames, scale,
|
||
tiled_vae=tiled_vae, tiled_dit=tiled_dit,
|
||
chunk_size=chunk_size, mode=mode
|
||
)
|
||
|
||
# Check if OOM likely
|
||
if result["estimated_vram_gb"] > result["available_vram_gb"] * VRAM_OOM_THRESHOLD:
|
||
result["will_oom"] = True
|
||
result["recommended_settings"] = get_optimal_settings(
|
||
effective_w, effective_h, num_frames, scale,
|
||
result["available_vram_gb"], mode
|
||
)
|
||
|
||
# Build message
|
||
if result["will_oom"]:
|
||
rec = result["recommended_settings"]
|
||
msg_parts = []
|
||
if rec["chunk_size"] != chunk_size and rec["chunk_size"] > 0:
|
||
msg_parts.append(f"chunk_size={rec['chunk_size']}")
|
||
if rec["resize_factor"] != resize_factor:
|
||
msg_parts.append(f"resize_factor={rec['resize_factor']:.1f}")
|
||
if rec["tiled_vae"] and not tiled_vae:
|
||
msg_parts.append("tiled_vae=True")
|
||
if rec["tiled_dit"] and not tiled_dit:
|
||
msg_parts.append("tiled_dit=True")
|
||
|
||
if msg_parts:
|
||
result["message"] = f"⚠️ Current settings require ~{result['estimated_vram_gb']:.1f}GB but only {result['available_vram_gb']:.1f}GB available. Recommended: {', '.join(msg_parts)}"
|
||
else:
|
||
result["message"] = f"⚠️ VRAM critically low. Estimated ~{result['estimated_vram_gb']:.1f}GB needed, only {result['available_vram_gb']:.1f}GB available."
|
||
else:
|
||
result["message"] = f"✅ Safe to proceed. Estimated ~{result['estimated_vram_gb']:.1f}GB needed, {result['available_vram_gb']:.1f}GB available."
|
||
|
||
return result
|
||
|
||
|
||
def log_preflight_check(width, height, num_frames, scale, chunk_size, resize_factor,
|
||
tiled_vae, tiled_dit, mode="full"):
|
||
"""
|
||
Log pre-flight resource check results.
|
||
"""
|
||
result = check_resources(width, height, num_frames, scale, chunk_size, resize_factor,
|
||
tiled_vae, tiled_dit, mode)
|
||
|
||
log("=" * 60, message_type='info')
|
||
log("PRE-FLIGHT RESOURCE CHECK", message_type='info', icon="🔍")
|
||
log(f"RAM: {result['ram_used_gb']:.1f}GB / {result['ram_total_gb']:.1f}GB", message_type='info', icon="💻")
|
||
log(f"VRAM Available: {result['available_vram_gb']:.1f}GB", message_type='info', icon="💾")
|
||
log(f"Estimated VRAM Required: {result['estimated_vram_gb']:.1f}GB", message_type='info', icon="📊")
|
||
|
||
if result["will_oom"]:
|
||
log(result["message"], message_type='warning', icon="⚠️")
|
||
if result["recommended_settings"]:
|
||
rec = result["recommended_settings"]
|
||
log("Recommended Optimal Settings:", message_type='info', icon="💡")
|
||
if rec["chunk_size"] > 0:
|
||
log(f" • chunk_size = {rec['chunk_size']}", message_type='info')
|
||
if rec["resize_factor"] < 1.0:
|
||
log(f" • resize_factor = {rec['resize_factor']:.1f}", message_type='info')
|
||
if rec["tiled_vae"]:
|
||
log(f" • tiled_vae = True", message_type='info')
|
||
if rec["tiled_dit"]:
|
||
log(f" • tiled_dit = True", message_type='info')
|
||
if rec.get("warning"):
|
||
log(f" ⚠️ {rec['warning']}", message_type='warning')
|
||
else:
|
||
log(result["message"], message_type='finish', icon="✅")
|
||
|
||
log("=" * 60, message_type='info')
|
||
|
||
return result
|
||
|
||
|
||
def log_vram_advisory(width, height, num_frames, scale, tiled_vae, tiled_dit, mode="full"):
|
||
"""
|
||
Log advisory message about VRAM usage.
|
||
Enhanced to use the new pre-flight check.
|
||
"""
|
||
if not torch.cuda.is_available():
|
||
return
|
||
|
||
estimated_vram = estimate_vram_usage(width, height, num_frames, scale, tiled_vae, tiled_dit, mode=mode)
|
||
available_vram = torch.cuda.get_device_properties(0).total_memory / (1024 ** 3)
|
||
current_used = torch.cuda.memory_allocated() / (1024 ** 3)
|
||
free_vram = available_vram - current_used
|
||
|
||
log(f"VRAM Advisory: Estimated ~{estimated_vram:.1f}GB needed, Available: {free_vram:.1f}GB free of {available_vram:.1f}GB total",
|
||
message_type='info', icon="💡")
|
||
|
||
if estimated_vram > free_vram * 0.9:
|
||
log("⚠️ Warning: High VRAM usage expected. Recommend enabling Tiled VAE/DiT.", message_type='warning', icon="⚠️")
|
||
elif estimated_vram < free_vram * 0.5:
|
||
log("✅ Safe to proceed. VRAM usage should be comfortable.", message_type='info', icon="✅")
|
||
|
||
def model_download(model_name="JunhaoZhuang/FlashVSR"):
|
||
model_dir = os.path.join(folder_paths.models_dir, model_name.split("/")[-1])
|
||
if not os.path.exists(model_dir):
|
||
log(f"Downloading model '{model_name}' from huggingface...", message_type='info', icon="⬇️")
|
||
snapshot_download(repo_id=model_name, local_dir=model_dir, local_dir_use_symlinks=False, resume_download=True)
|
||
|
||
|
||
# =============================================================================
|
||
# FIX 6: Auto-download VAE models if missing - UPDATED URLs
|
||
# =============================================================================
|
||
def download_vae_if_missing(vae_file: str, model_path: str, vae_config: dict) -> str:
|
||
"""
|
||
Check if VAE file exists. If not, attempt to download it using the URL in vae_config.
|
||
|
||
Args:
|
||
vae_file: The filename of the VAE (e.g., 'Wan2.1_VAE.pth')
|
||
model_path: The directory where VAE should be saved
|
||
vae_config: The VAE configuration from VAE_MODEL_MAP (must contain 'url' key)
|
||
|
||
Returns:
|
||
Full path to the VAE file
|
||
"""
|
||
vae_path = os.path.join(model_path, vae_file)
|
||
|
||
if os.path.exists(vae_path):
|
||
log(f"VAE file found: {vae_file}", message_type='info', icon="✅")
|
||
return vae_path
|
||
|
||
log(f"VAE file '{vae_file}' not found. Attempting auto-download...", message_type='warning', icon="⬇️")
|
||
|
||
# Get URL from config (FIX 6: Use EXACT URLs from VAE_MODEL_MAP)
|
||
url = vae_config.get("url")
|
||
|
||
if url:
|
||
try:
|
||
log(f"Downloading from: {url}", message_type='info', icon="🌐")
|
||
# Ensure directory exists
|
||
os.makedirs(model_path, exist_ok=True)
|
||
torch.hub.download_url_to_file(url, vae_path, progress=True)
|
||
log(f"Successfully downloaded VAE: {vae_file}", message_type='finish', icon="✅")
|
||
return vae_path
|
||
except Exception as e:
|
||
log(f"Download failed: {e}", message_type='error', icon="❌")
|
||
|
||
raise RuntimeError(
|
||
f'VAE file "{vae_file}" not found and auto-download failed.\n'
|
||
f'Please manually download it and save to: {vae_path}\n'
|
||
f'Download URL: {url}'
|
||
)
|
||
|
||
|
||
# =============================================================================
|
||
# FIX 7: Fixed tensor2video for correct video output
|
||
# Ensures proper tensor permutation: VAE output (B, C, F, H, W) -> video (F, H, W, C)
|
||
# CRITICAL: This is called AFTER VAE decode is complete - no cropping here
|
||
# =============================================================================
|
||
def tensor2video(frames: torch.Tensor):
|
||
"""
|
||
Convert VAE output tensor to video format.
|
||
|
||
Input: (B, C, F, H, W) - Batch, Channels, Frames, Height, Width (VAE output)
|
||
Output: (F, H, W, C) - Frames, Height, Width, Channels (video format)
|
||
|
||
The tensor is normalized from [-1, 1] to [0, 1] for display.
|
||
|
||
NOTE: This function does NOT crop - cropping happens in process_chunk()
|
||
AFTER this conversion is complete.
|
||
"""
|
||
# Handle different input shapes
|
||
if frames.dim() == 5:
|
||
# Expected shape: (B, C, F, H, W)
|
||
video_squeezed = frames.squeeze(0) # (C, F, H, W)
|
||
video_permuted = video_squeezed.permute(1, 2, 3, 0) # (F, H, W, C)
|
||
elif frames.dim() == 4:
|
||
# Shape: (C, F, H, W) or (F, C, H, W) - need to detect
|
||
if frames.shape[0] == 3 or frames.shape[0] == 4:
|
||
# Likely (C, F, H, W)
|
||
video_permuted = frames.permute(1, 2, 3, 0) # (F, H, W, C)
|
||
else:
|
||
# Likely (F, C, H, W)
|
||
video_permuted = frames.permute(0, 2, 3, 1) # (F, H, W, C)
|
||
else:
|
||
raise ValueError(f"Unexpected tensor shape: {frames.shape}")
|
||
|
||
# Normalize from [-1, 1] to [0, 1]
|
||
video_final = (video_permuted.float() + 1.0) / 2.0
|
||
# Clamp to valid range to avoid visual artifacts
|
||
video_final = torch.clamp(video_final, 0.0, 1.0)
|
||
|
||
return video_final
|
||
|
||
def largest_8n1_leq(n): # 8n+1
|
||
return 0 if n < 1 else ((n - 1)//8)*8 + 1
|
||
|
||
def next_8n5(n): # next 8n+5
|
||
return 21 if n < 21 else ((n - 5 + 7) // 8) * 8 + 5
|
||
|
||
def compute_scaled_and_target_dims(w0: int, h0: int, scale: int = 4, multiple: int = 128):
|
||
"""
|
||
Compute scaled dimensions and target dimensions (aligned to multiple).
|
||
|
||
=============================================================================
|
||
FIX 3: Black Border Fix - Track original scaled dimensions
|
||
=============================================================================
|
||
Returns: sW, sH (actual scaled), tW, tH (padded to multiple), pad_left, pad_top
|
||
"""
|
||
if w0 <= 0 or h0 <= 0:
|
||
raise ValueError("invalid original size")
|
||
|
||
sW, sH = w0 * scale, h0 * scale
|
||
tW = math.ceil(sW / multiple) * multiple
|
||
tH = math.ceil(sH / multiple) * multiple
|
||
|
||
# Calculate padding offsets (centered padding)
|
||
pad_left = (tW - sW) // 2
|
||
pad_top = (tH - sH) // 2
|
||
|
||
return sW, sH, tW, tH, pad_left, pad_top
|
||
|
||
|
||
def tensor_upscale_then_center_crop(frame_tensor: torch.Tensor, scale: int, tW: int, tH: int, pad_left: int, pad_top: int) -> torch.Tensor:
|
||
"""
|
||
Upscale frame tensor and pad to target dimensions.
|
||
|
||
=============================================================================
|
||
FIX 3: Black Border Fix - Use consistent padding offsets
|
||
=============================================================================
|
||
"""
|
||
h0, w0, c = frame_tensor.shape
|
||
tensor_bchw = frame_tensor.permute(2, 0, 1).unsqueeze(0) # HWC -> CHW -> BCHW
|
||
|
||
sW, sH = w0 * scale, h0 * scale
|
||
upscaled_tensor = F.interpolate(tensor_bchw, size=(sH, sW), mode='bicubic', align_corners=False)
|
||
|
||
# Apply symmetric padding to reach target dimensions
|
||
if sW < tW or sH < tH:
|
||
pad_r = tW - sW - pad_left
|
||
pad_b = tH - sH - pad_top
|
||
# Pad order: (left, right, top, bottom)
|
||
# Use 'replicate' mode which is safer for small images than 'reflect'
|
||
# (reflect requires image size >= padding size on each dimension)
|
||
max_pad = max(pad_left, pad_r, pad_top, pad_b)
|
||
min_dim = min(upscaled_tensor.shape[2], upscaled_tensor.shape[3])
|
||
if min_dim >= max_pad:
|
||
upscaled_tensor = F.pad(upscaled_tensor, (pad_left, pad_r, pad_top, pad_b), mode='reflect')
|
||
else:
|
||
# Fall back to replicate mode for small images
|
||
upscaled_tensor = F.pad(upscaled_tensor, (pad_left, pad_r, pad_top, pad_b), mode='replicate')
|
||
|
||
# Center crop to target dimensions if needed (should be exact after padding)
|
||
l = max(0, (upscaled_tensor.shape[3] - tW) // 2)
|
||
t = max(0, (upscaled_tensor.shape[2] - tH) // 2)
|
||
cropped_tensor = upscaled_tensor[:, :, t:t + tH, l:l + tW]
|
||
|
||
return cropped_tensor.squeeze(0)
|
||
|
||
|
||
def prepare_input_tensor(image_tensor: torch.Tensor, device, scale: int = 4, dtype=torch.bfloat16):
|
||
"""
|
||
Prepare input tensor with proper padding tracking.
|
||
|
||
=============================================================================
|
||
FIX 3: Black Border Fix - Track padding for later cropping
|
||
=============================================================================
|
||
Returns: vid_final, tH, tW, F, original_sH, original_sW, pad_top, pad_left
|
||
"""
|
||
N0, h0, w0, _ = image_tensor.shape
|
||
|
||
multiple = 128 # Keep 128 alignment for VAE/DiT blocks
|
||
sW, sH, tW, tH, pad_left, pad_top = compute_scaled_and_target_dims(w0, h0, scale=scale, multiple=multiple)
|
||
num_frames_with_padding = N0 + 4
|
||
F = largest_8n1_leq(num_frames_with_padding)
|
||
|
||
if F == 0:
|
||
raise RuntimeError(f"Not enough frames after padding. Got {num_frames_with_padding}.")
|
||
|
||
frames = []
|
||
for i in range(F):
|
||
frame_idx = min(i, N0 - 1)
|
||
frame_slice = image_tensor[frame_idx].to(device)
|
||
tensor_chw = tensor_upscale_then_center_crop(
|
||
frame_slice, scale=scale, tW=tW, tH=tH,
|
||
pad_left=pad_left, pad_top=pad_top
|
||
).to('cpu').to(dtype) * 2.0 - 1.0
|
||
frames.append(tensor_chw)
|
||
del frame_slice
|
||
|
||
vid_stacked = torch.stack(frames, 0)
|
||
vid_final = vid_stacked.permute(1, 0, 2, 3).unsqueeze(0)
|
||
|
||
del vid_stacked
|
||
clean_vram()
|
||
|
||
# Return additional info for cropping output back to original dimensions
|
||
return vid_final, tH, tW, F, sH, sW, pad_top, pad_left
|
||
|
||
def calculate_tile_coords(height, width, tile_size, overlap):
|
||
coords = []
|
||
|
||
stride = tile_size - overlap
|
||
num_rows = math.ceil((height - overlap) / stride)
|
||
num_cols = math.ceil((width - overlap) / stride)
|
||
|
||
for r in range(num_rows):
|
||
for c in range(num_cols):
|
||
y1 = r * stride
|
||
x1 = c * stride
|
||
|
||
y2 = min(y1 + tile_size, height)
|
||
x2 = min(x1 + tile_size, width)
|
||
|
||
if y2 - y1 < tile_size:
|
||
y1 = max(0, y2 - tile_size)
|
||
if x2 - x1 < tile_size:
|
||
x1 = max(0, x2 - tile_size)
|
||
|
||
coords.append((x1, y1, x2, y2))
|
||
|
||
return coords
|
||
|
||
def create_feather_mask(size, overlap):
|
||
H, W = size
|
||
mask = torch.ones(1, 1, H, W)
|
||
ramp = torch.linspace(0, 1, overlap)
|
||
|
||
mask[:, :, :, :overlap] = torch.minimum(mask[:, :, :, :overlap], ramp.view(1, 1, 1, -1))
|
||
mask[:, :, :, -overlap:] = torch.minimum(mask[:, :, :, -overlap:], ramp.flip(0).view(1, 1, 1, -1))
|
||
|
||
mask[:, :, :overlap, :] = torch.minimum(mask[:, :, :overlap, :], ramp.view(1, 1, -1, 1))
|
||
mask[:, :, -overlap:, :] = torch.minimum(mask[:, :, -overlap:, :], ramp.flip(0).view(1, 1, -1, 1))
|
||
|
||
return mask
|
||
|
||
def init_pipeline(model, mode, device, dtype, vae_model="Wan2.1"):
|
||
"""
|
||
Initialize FlashVSR pipeline with specified model and VAE type.
|
||
|
||
=============================================================================
|
||
FIX 2 & 7: STRICT VAE file path mapping with EXPLICIT class instantiation
|
||
=============================================================================
|
||
- vae_model: Unified VAE selection from dropdown (5 options)
|
||
- Each VAE selection loads a DISTINCT file (no file reuse)
|
||
- EXPLICIT class instantiation based on selection - NO guessing from state_dict
|
||
|
||
File Mapping:
|
||
- "Wan2.1" -> Wan2.1_VAE.pth -> WanVideoVAE
|
||
- "Wan2.2" -> Wan2.2_VAE.pth -> Wan22VideoVAE
|
||
- "LightVAE_W2.1" -> lightvaew2_1.pth -> LightX2VVAE
|
||
- "TAE_W2.2" -> taew2_2.safetensors -> Wan22VideoVAE
|
||
- "LightTAE_HY1.5" -> lighttaehy1_5.pth -> LightX2VVAE
|
||
"""
|
||
model_download(model_name="JunhaoZhuang/"+model)
|
||
model_path = os.path.join(folder_paths.models_dir, model)
|
||
if not os.path.exists(model_path):
|
||
raise RuntimeError(f'Model directory does not exist!\nPlease save all weights to "{model_path}"')
|
||
ckpt_path = os.path.join(model_path, "diffusion_pytorch_model_streaming_dmd.safetensors")
|
||
if not os.path.exists(ckpt_path):
|
||
raise RuntimeError(f'"diffusion_pytorch_model_streaming_dmd.safetensors" does not exist!\nPlease save it to "{model_path}"')
|
||
|
||
# ==========================================================================
|
||
# FIX 2 & 7: VAE Model Loading - EXPLICIT mapping (no guessing!)
|
||
# ==========================================================================
|
||
if vae_model not in VAE_MODEL_MAP:
|
||
log(f"Unknown VAE model '{vae_model}', defaulting to Wan2.1", message_type='warning', icon="⚠️")
|
||
vae_model = "Wan2.1"
|
||
|
||
vae_config = VAE_MODEL_MAP[vae_model]
|
||
vae_class = vae_config["class"]
|
||
vae_file = vae_config["file"]
|
||
vae_dim = vae_config["dim"]
|
||
vae_z_dim = vae_config["z_dim"]
|
||
use_full_arch = vae_config["use_full_arch"]
|
||
|
||
# Debug logging - Show EXACTLY which file and class will be used
|
||
log(f"VAE Selection: '{vae_model}' -> File: '{vae_file}' -> Class: {vae_class.__name__}",
|
||
message_type='info', icon="🔍")
|
||
|
||
# ==========================================================================
|
||
# FIX 6: Auto-download VAE if missing
|
||
# ==========================================================================
|
||
vae_path = download_vae_if_missing(vae_file, model_path, vae_config)
|
||
|
||
log(f"VAE file path confirmed: {vae_path}", message_type='info', icon="📁")
|
||
|
||
lq_path = os.path.join(model_path, "LQ_proj_in.ckpt")
|
||
if not os.path.exists(lq_path):
|
||
raise RuntimeError(f'"LQ_proj_in.ckpt" does not exist!\nPlease save it to "{model_path}"')
|
||
tcd_path = os.path.join(model_path, "TCDecoder.ckpt")
|
||
if not os.path.exists(tcd_path):
|
||
raise RuntimeError(f'"TCDecoder.ckpt" does not exist!\nPlease save it to "{model_path}"')
|
||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||
prompt_path = os.path.join(current_dir, "posi_prompt.pth")
|
||
|
||
mm = ModelManager(torch_dtype=dtype, device="cpu")
|
||
if mode == "full":
|
||
mm.load_models([ckpt_path, vae_path])
|
||
pipe = FlashVSRFullPipeline.from_model_manager(mm, device=device)
|
||
|
||
# =======================================================================
|
||
# FIX 7: EXPLICIT VAE class instantiation - NO guessing from state_dict
|
||
# =======================================================================
|
||
log(f"Creating EXPLICIT VAE instance: {vae_class.__name__} (dim={vae_dim}, z_dim={vae_z_dim})",
|
||
message_type='info', icon="📦")
|
||
|
||
# Load weights from file
|
||
if vae_path.endswith(".safetensors"):
|
||
try:
|
||
import safetensors.torch
|
||
sd = safetensors.torch.load_file(vae_path)
|
||
except ImportError:
|
||
raise RuntimeError("safetensors library required to load .safetensors VAE file.")
|
||
else:
|
||
sd = torch.load(vae_path, map_location="cpu", weights_only=False)
|
||
|
||
# EXPLICIT class instantiation based on user selection (FIX 7)
|
||
# NO state_dict inspection - we trust the user's selection
|
||
if vae_class == LightX2VVAE:
|
||
pipe.vae = LightX2VVAE(z_dim=vae_z_dim, dim=vae_dim, use_full_arch=use_full_arch)
|
||
elif vae_class == Wan22VideoVAE:
|
||
pipe.vae = Wan22VideoVAE(z_dim=vae_z_dim, dim=vae_dim)
|
||
else: # WanVideoVAE (default)
|
||
pipe.vae = WanVideoVAE(z_dim=vae_z_dim, dim=vae_dim)
|
||
|
||
# Load state dict with logging for missing/unexpected keys
|
||
load_result = pipe.vae.load_state_dict(sd, strict=False)
|
||
if load_result.missing_keys:
|
||
log(f"VAE missing keys: {len(load_result.missing_keys)} (expected for Light* models)",
|
||
message_type='info', icon="ℹ️")
|
||
if load_result.unexpected_keys:
|
||
log(f"VAE unexpected keys: {len(load_result.unexpected_keys)}",
|
||
message_type='info', icon="ℹ️")
|
||
|
||
pipe.vae = pipe.vae.to(device=device, dtype=dtype)
|
||
|
||
log(f"Loaded VAE weights from: {vae_path}", message_type='info', icon="✅")
|
||
log(f"VAE Type Active: {type(pipe.vae).__name__}", message_type='info', icon="📦")
|
||
|
||
pipe.vae.model.encoder = None
|
||
pipe.vae.model.conv1 = None
|
||
|
||
# =======================================================================
|
||
# FIX: Load TCDecoder for Full Mode (official FlashVSR approach)
|
||
# The official FlashVSR uses TCDecoder for all modes with LQ conditioning.
|
||
# This is critical because TCDecoder.decode_video() takes a `cond` parameter
|
||
# that enables proper video super-resolution with low-quality guidance.
|
||
# =======================================================================
|
||
multi_scale_channels = [512, 256, 128, 128]
|
||
pipe.TCDecoder = build_tcdecoder(new_channels=multi_scale_channels, device=device, dtype=dtype, new_latent_channels=16+768)
|
||
mis = pipe.TCDecoder.load_state_dict(torch.load(tcd_path, map_location=device, weights_only=False), strict=False)
|
||
pipe.TCDecoder.clean_mem()
|
||
log(f"Loaded TCDecoder for Full Mode (official FlashVSR approach)", message_type='info', icon="✅")
|
||
else:
|
||
mm.load_models([ckpt_path])
|
||
if mode == "tiny":
|
||
pipe = FlashVSRTinyPipeline.from_model_manager(mm, device=device)
|
||
else:
|
||
pipe = FlashVSRTinyLongPipeline.from_model_manager(mm, device=device)
|
||
multi_scale_channels = [512, 256, 128, 128]
|
||
pipe.TCDecoder = build_tcdecoder(new_channels=multi_scale_channels, device=device, dtype=dtype, new_latent_channels=16+768)
|
||
mis = pipe.TCDecoder.load_state_dict(torch.load(tcd_path, map_location=device, weights_only=False), strict=False)
|
||
pipe.TCDecoder.clean_mem()
|
||
|
||
if model == "FlashVSR":
|
||
pipe.denoising_model().LQ_proj_in = Buffer_LQ4x_Proj(in_dim=3, out_dim=1536, layer_num=1).to(device, dtype=dtype)
|
||
else:
|
||
pipe.denoising_model().LQ_proj_in = Causal_LQ4x_Proj(in_dim=3, out_dim=1536, layer_num=1).to(device, dtype=dtype)
|
||
pipe.denoising_model().LQ_proj_in.load_state_dict(torch.load(lq_path, map_location="cpu", weights_only=False), strict=True)
|
||
pipe.denoising_model().LQ_proj_in.to(device)
|
||
pipe.to(device, dtype=dtype)
|
||
pipe.enable_vram_management(num_persistent_param_in_dit=None)
|
||
pipe.init_cross_kv(prompt_path=prompt_path)
|
||
pipe.load_models_to_device(["dit","vae"])
|
||
pipe.offload_model()
|
||
|
||
# Log final pipeline info with VAE confirmation
|
||
vae_info = f"VAE Model: {vae_model}"
|
||
if hasattr(pipe, 'vae') and pipe.vae is not None:
|
||
vae_info += f" ({type(pipe.vae).__name__})"
|
||
|
||
log(f"Pipeline Initialized: Mode={mode}, Device={device}, Dtype={dtype}, Attention={wan_video_dit.ATTENTION_MODE}", message_type='info', icon="🔧")
|
||
log(f"Model: {model}, {vae_info}", message_type='info', icon="📦")
|
||
|
||
return pipe
|
||
|
||
class cqdm:
|
||
def __init__(self, iterable=None, total=None, desc="Processing", enable_debug=False):
|
||
self.desc = desc
|
||
self.pbar = None
|
||
self.iterable = None
|
||
self.total = total
|
||
self.enable_debug = enable_debug
|
||
self.start_time = time.time()
|
||
self.step_idx = 0
|
||
|
||
if iterable is not None:
|
||
try:
|
||
self.total = len(iterable)
|
||
self.iterable = iter(iterable)
|
||
except TypeError:
|
||
if self.total is None:
|
||
raise ValueError("Total must be provided for iterables with no length.")
|
||
|
||
elif self.total is not None:
|
||
pass
|
||
|
||
else:
|
||
raise ValueError("Either iterable or total must be provided.")
|
||
|
||
def __iter__(self):
|
||
if self.iterable is None:
|
||
raise TypeError(f"'{type(self).__name__}' object is not iterable. Did you mean to use it with a 'with' statement?")
|
||
if self.pbar is None:
|
||
self.pbar = comfy.utils.ProgressBar(self.total)
|
||
return self
|
||
|
||
def __next__(self):
|
||
if self.iterable is None:
|
||
raise TypeError("Cannot call __next__ on a non-iterable cqdm object.")
|
||
try:
|
||
step_start = time.time()
|
||
val = next(self.iterable)
|
||
|
||
if self.pbar:
|
||
self.pbar.update(1)
|
||
|
||
self.step_idx += 1
|
||
|
||
# Show a text progress bar in the log (single line using \r)
|
||
perc = (self.step_idx / self.total) * 100
|
||
bar_len = 20
|
||
filled = int(bar_len * self.step_idx // self.total)
|
||
bar = '█' * filled + '░' * (bar_len - filled)
|
||
|
||
elapsed = time.time() - self.start_time
|
||
rate = self.step_idx / elapsed if elapsed > 0 else 0
|
||
|
||
msg = f"{self.desc}: {self.step_idx}/{self.total} |{bar}| {perc:.1f}%"
|
||
|
||
if self.enable_debug:
|
||
step_end = time.time()
|
||
step_time = step_end - step_start
|
||
msg += f" (Step: {step_time:.2f}s)"
|
||
# Pass in_place=True to log_resource_usage to keep it on one line if possible
|
||
# But note log_resource_usage prints Resource usage which is long.
|
||
log_resource_usage(prefix=msg, in_place=True)
|
||
else:
|
||
print(f"\r{msg}", end="", flush=True)
|
||
if self.step_idx == self.total:
|
||
print()
|
||
|
||
return val
|
||
except StopIteration:
|
||
total_time = time.time() - self.start_time
|
||
if self.enable_debug:
|
||
# Use print with newline here to finalize the log block
|
||
print(f"\n✅ Loop '{self.desc}' finished in {total_time:.2f}s", flush=True)
|
||
raise
|
||
|
||
def __enter__(self):
|
||
if self.pbar is None:
|
||
self.pbar = comfy.utils.ProgressBar(self.total)
|
||
return self.pbar
|
||
|
||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||
pass
|
||
|
||
def __len__(self):
|
||
return self.total
|
||
|
||
def process_chunk(pipe, frames, scale, color_fix, tiled_vae, tiled_dit, tile_size, tile_overlap, unload_dit, sparse_ratio, kv_ratio, local_range, seed, force_offload, enable_debug, is_single_frame_input=False):
|
||
"""
|
||
Processes a single chunk of frames.
|
||
|
||
=============================================================================
|
||
FIX 3: Black Border Fix - Proper cropping to remove padding
|
||
=============================================================================
|
||
"""
|
||
# Aggressive garbage collection before processing (FIX 5)
|
||
clean_vram()
|
||
if torch.cuda.is_available():
|
||
torch.cuda.empty_cache()
|
||
|
||
_frames = frames
|
||
_device = pipe.device
|
||
dtype = pipe.torch_dtype
|
||
|
||
# Store original dimensions for cropping (FIX 3)
|
||
original_H, original_W = frames.shape[1], frames.shape[2]
|
||
target_H, target_W = original_H * scale, original_W * scale
|
||
|
||
# Padding logic for the chunk (temporal padding)
|
||
add = next_8n5(frames.shape[0]) - frames.shape[0]
|
||
padding_frames = frames[-1:, :, :, :].repeat(add, 1, 1, 1)
|
||
_frames = torch.cat([frames, padding_frames], dim=0)
|
||
|
||
if tiled_dit:
|
||
N, H, W, C = _frames.shape
|
||
|
||
final_output_canvas = torch.zeros(
|
||
(N, H * scale, W * scale, C),
|
||
dtype=torch.float16,
|
||
device="cpu"
|
||
)
|
||
weight_sum_canvas = torch.zeros_like(final_output_canvas)
|
||
tile_coords = calculate_tile_coords(H, W, tile_size, tile_overlap)
|
||
|
||
log(f"Starting Tiled Processing: {len(tile_coords)} tiles", message_type='info', icon="🚀")
|
||
|
||
# Create progress bar wrapper for tiled pipeline processing
|
||
class cqdm_tile(cqdm):
|
||
def __init__(self, *args, **kwargs):
|
||
super().__init__(*args, **kwargs, enable_debug=enable_debug)
|
||
|
||
for i, (x1, y1, x2, y2) in enumerate(cqdm(tile_coords, desc="Processing Tiles", enable_debug=enable_debug)):
|
||
tile_start = time.time()
|
||
if enable_debug:
|
||
log(f"Processing tile {i+1}/{len(tile_coords)}: ({x1},{y1}) -> ({x2},{y2})", message_type='info', icon="🔄")
|
||
|
||
input_tile = _frames[:, y1:y2, x1:x2, :]
|
||
|
||
# Get tile dimensions including padding info (FIX 3)
|
||
LQ_tile, th, tw, F, tile_sH, tile_sW, tile_pad_top, tile_pad_left = prepare_input_tensor(
|
||
input_tile, _device, scale=scale, dtype=dtype
|
||
)
|
||
if not isinstance(pipe, FlashVSRTinyLongPipeline):
|
||
LQ_tile = LQ_tile.to(_device)
|
||
|
||
output_tile_gpu = pipe(
|
||
prompt="", negative_prompt="", cfg_scale=1.0, num_inference_steps=1, seed=seed, tiled=tiled_vae,
|
||
progress_bar_cmd=cqdm_tile, LQ_video=LQ_tile, num_frames=F, height=th, width=tw, is_full_block=False, if_buffer=True,
|
||
topk_ratio=sparse_ratio*768*1280/(th*tw), kv_ratio=kv_ratio, local_range=local_range,
|
||
color_fix=color_fix, unload_dit=unload_dit, force_offload=force_offload,
|
||
enable_debug_logging=enable_debug
|
||
)
|
||
|
||
processed_tile_cpu = tensor2video(output_tile_gpu).to("cpu")
|
||
|
||
# =================================================================
|
||
# FIX 3: Crop output tile to remove padding before blending
|
||
# =================================================================
|
||
# Bounds checking to avoid IndexError
|
||
max_crop_h = min(tile_pad_top + tile_sH, processed_tile_cpu.shape[1])
|
||
max_crop_w = min(tile_pad_left + tile_sW, processed_tile_cpu.shape[2])
|
||
actual_h = max_crop_h - tile_pad_top
|
||
actual_w = max_crop_w - tile_pad_left
|
||
|
||
if actual_h > 0 and actual_w > 0:
|
||
processed_tile_cpu = processed_tile_cpu[:, tile_pad_top:max_crop_h,
|
||
tile_pad_left:max_crop_w, :]
|
||
|
||
if enable_debug:
|
||
tile_end = time.time()
|
||
tile_time = tile_end - tile_start
|
||
log(f"Tile {i+1} completed in {tile_time:.2f}s", message_type='info', icon="⏱️")
|
||
|
||
mask_nchw = create_feather_mask(
|
||
(processed_tile_cpu.shape[1], processed_tile_cpu.shape[2]),
|
||
tile_overlap * scale
|
||
).to("cpu")
|
||
mask_nhwc = mask_nchw.permute(0, 2, 3, 1)
|
||
out_x1, out_y1 = x1 * scale, y1 * scale
|
||
|
||
tile_H_scaled = processed_tile_cpu.shape[1]
|
||
tile_W_scaled = processed_tile_cpu.shape[2]
|
||
out_x2, out_y2 = out_x1 + tile_W_scaled, out_y1 + tile_H_scaled
|
||
final_output_canvas[:, out_y1:out_y2, out_x1:out_x2, :] += processed_tile_cpu * mask_nhwc
|
||
weight_sum_canvas[:, out_y1:out_y2, out_x1:out_x2, :] += mask_nhwc
|
||
|
||
del LQ_tile, output_tile_gpu, processed_tile_cpu, input_tile
|
||
clean_vram()
|
||
if torch.cuda.is_available():
|
||
torch.cuda.empty_cache()
|
||
|
||
weight_sum_canvas[weight_sum_canvas == 0] = 1.0
|
||
final_output = final_output_canvas / weight_sum_canvas
|
||
else:
|
||
log("Preparing full frame processing...", message_type='info', icon="🎞️")
|
||
if enable_debug:
|
||
log_resource_usage(prefix="Pre-Preprocess")
|
||
|
||
# Get padding info for cropping (FIX 3)
|
||
LQ, th, tw, F, sH, sW, pad_top, pad_left = prepare_input_tensor(_frames, _device, scale=scale, dtype=dtype)
|
||
if not isinstance(pipe, FlashVSRTinyLongPipeline):
|
||
LQ = LQ.to(_device)
|
||
|
||
log(f"Processing {frames.shape[0]} frames...", message_type='info', icon="🚀")
|
||
|
||
process_start = time.time()
|
||
|
||
class cqdm_debug(cqdm):
|
||
def __init__(self, *args, **kwargs):
|
||
super().__init__(*args, **kwargs, enable_debug=enable_debug)
|
||
|
||
video = pipe(
|
||
prompt="", negative_prompt="", cfg_scale=1.0, num_inference_steps=1, seed=seed, tiled=tiled_vae,
|
||
progress_bar_cmd=cqdm_debug, LQ_video=LQ, num_frames=F, height=th, width=tw, is_full_block=False, if_buffer=True,
|
||
topk_ratio=sparse_ratio*768*1280/(th*tw), kv_ratio=kv_ratio, local_range=local_range,
|
||
color_fix = color_fix, unload_dit=unload_dit, force_offload=force_offload,
|
||
enable_debug_logging=enable_debug
|
||
)
|
||
|
||
process_end = time.time()
|
||
|
||
if enable_debug:
|
||
log(f"Inference completed in {process_end - process_start:.2f}s", message_type='info', icon="⏱️")
|
||
final_output_tensor = tensor2video(video).to('cpu')
|
||
|
||
# =====================================================================
|
||
# FIX 3: Crop output to remove padding - use stored padding offsets
|
||
# =====================================================================
|
||
# The output has dimensions (N, tH, tW, C) where tH/tW are padded
|
||
# We need to crop to actual scaled dimensions (sH, sW)
|
||
final_output = final_output_tensor[:, pad_top:pad_top + sH, pad_left:pad_left + sW, :]
|
||
|
||
if enable_debug:
|
||
log(f"Cropped output from ({final_output_tensor.shape[1]}, {final_output_tensor.shape[2]}) "
|
||
f"to ({final_output.shape[1]}, {final_output.shape[2]}) removing padding",
|
||
message_type='info', icon="✂️")
|
||
|
||
del video, LQ
|
||
clean_vram()
|
||
if torch.cuda.is_available():
|
||
torch.cuda.empty_cache()
|
||
|
||
if is_single_frame_input and frames.shape[0] == 1:
|
||
if frames.shape[0] == 1:
|
||
final_output = final_output.to("cpu")
|
||
stacked_image_tensor = torch.median(final_output, dim=0).values.unsqueeze(0).float()
|
||
del final_output
|
||
clean_vram()
|
||
return stacked_image_tensor
|
||
|
||
return final_output[:frames.shape[0], :, :, :]
|
||
|
||
def flashvsr(pipe, frames, scale, color_fix, tiled_vae, tiled_dit, tile_size, tile_overlap, unload_dit, sparse_ratio, kv_ratio, local_range, seed, force_offload, enable_debug=False, chunk_size=0, resize_factor=1.0, mode="full"):
|
||
"""
|
||
=============================================================================
|
||
FIX 9 & 10: Unified Processing Pipeline with Pre-Flight Check
|
||
=============================================================================
|
||
|
||
Main FlashVSR processing function.
|
||
- FIX 4: Lossless Resize - Use NEAREST for integer scaling factors
|
||
- FIX 5: VRAM Advisory Logging with 95% threshold
|
||
- FIX 9: Pre-Flight Resource Check before processing
|
||
- FIX 10: Unified processing logic applied across all modes
|
||
"""
|
||
# Aggressive garbage collection (FIX 5)
|
||
clean_vram()
|
||
if torch.cuda.is_available():
|
||
torch.cuda.reset_peak_memory_stats()
|
||
torch.cuda.ipc_collect()
|
||
torch.cuda.empty_cache()
|
||
|
||
# ==========================================================================
|
||
# FIX 9: Pre-Flight Resource Check (BEFORE loading heavy models/processing)
|
||
# ==========================================================================
|
||
preflight_result = log_preflight_check(
|
||
frames.shape[2], frames.shape[1], frames.shape[0], scale, chunk_size, resize_factor,
|
||
tiled_vae, tiled_dit, mode=mode
|
||
)
|
||
|
||
# If pre-flight check suggests OOM, optionally apply recommended settings
|
||
# (Currently just logs warnings - user can adjust settings manually)
|
||
|
||
# ==========================================================================
|
||
# FIX 4: Lossless Resize Factor
|
||
# Use NEAREST interpolation for integer-like factors, BICUBIC otherwise
|
||
# ==========================================================================
|
||
if resize_factor < 1.0 and resize_factor > 0:
|
||
log(f"Resizing input by factor {resize_factor}...", message_type='info', icon="📉")
|
||
orig_H, orig_W = frames.shape[1], frames.shape[2]
|
||
new_H, new_W = int(orig_H * resize_factor), int(orig_W * resize_factor)
|
||
|
||
# Check if resize factor results in integer scaling (lossless possible)
|
||
is_integer_scale = (orig_H % new_H == 0 and orig_W % new_W == 0) or (resize_factor in [0.5, 0.25, 0.125])
|
||
|
||
frames_permuted = frames.permute(0, 3, 1, 2)
|
||
if is_integer_scale:
|
||
# Use NEAREST for potentially lossless integer downscaling
|
||
frames_resized = F.interpolate(frames_permuted, size=(new_H, new_W), mode='nearest')
|
||
log(f"Using NEAREST interpolation (lossless for {resize_factor}x)", message_type='info', icon="🔍")
|
||
else:
|
||
# Use BICUBIC for non-integer factors
|
||
frames_resized = F.interpolate(frames_permuted, size=(new_H, new_W), mode='bicubic', align_corners=False)
|
||
log(f"Using BICUBIC interpolation for non-integer scaling", message_type='info', icon="🔍")
|
||
|
||
frames = frames_resized.permute(0, 2, 3, 1) # Back to NHWC
|
||
del frames_permuted, frames_resized
|
||
clean_vram()
|
||
|
||
start_time = time.time()
|
||
|
||
# Get current dimensions (after potential resize)
|
||
N, H, W, C = frames.shape
|
||
|
||
# ==========================================================================
|
||
# FIX 5 & 10: Unified Debug Logging (same for all modes)
|
||
# ==========================================================================
|
||
if enable_debug:
|
||
_device = pipe.device
|
||
log(f"Debug Mode: Enabled", message_type='info', icon="🐞")
|
||
log(f"Device: {_device}", message_type='info', icon="🖥️")
|
||
log(f"Processing Mode: {mode}", message_type='info', icon="⚙️")
|
||
if torch.cuda.is_available():
|
||
log(f"Total VRAM: {torch.cuda.get_device_properties(0).total_memory / 1024**3:.2f} GB", message_type='info', icon="💾")
|
||
log(f"Input Frames: {frames.shape}", message_type='info', icon="🎞️")
|
||
log(f"Chunk Size: {chunk_size}", message_type='info', icon="📦")
|
||
log(f"Tiled DiT: {tiled_dit}, Tiled VAE: {tiled_vae}", message_type='info', icon="🧩")
|
||
log_resource_usage(prefix="Start")
|
||
|
||
# VRAM Advisory (FIX 5) - Enhanced with mode
|
||
if torch.cuda.is_available():
|
||
log_vram_advisory(W, H, N, scale, tiled_vae, tiled_dit, mode=mode)
|
||
|
||
# VRAM check and warning - FIX 5: Use 95% threshold (RTX 5070 Ti target)
|
||
if torch.cuda.is_available():
|
||
vram_free, vram_total = torch.cuda.mem_get_info()
|
||
vram_used = vram_total - vram_free
|
||
vram_usage_ratio = vram_used / vram_total
|
||
|
||
# FIX 5: Only trigger OOM recovery at 95% threshold (not 90%)
|
||
if vram_usage_ratio > VRAM_OOM_THRESHOLD:
|
||
log(f"Warning: VRAM usage is very high ({vram_usage_ratio*100:.1f}% > {VRAM_OOM_THRESHOLD*100:.0f}%)! Enabling fallback options is recommended.",
|
||
message_type='warning', icon="⚠️")
|
||
|
||
# Store input resolution for summary (FIX 8)
|
||
input_resolution = f"{frames.shape[2]}x{frames.shape[1]}"
|
||
output_resolution = f"{frames.shape[2] * scale}x{frames.shape[1] * scale}"
|
||
|
||
# Chunking Logic
|
||
total_frames = frames.shape[0]
|
||
final_outputs = []
|
||
|
||
is_single_frame_input = (frames.shape[0] == 1)
|
||
|
||
if chunk_size > 0 and chunk_size < total_frames:
|
||
num_chunks = math.ceil(total_frames / chunk_size)
|
||
log(f"Splitting video into {num_chunks} chunks (size {chunk_size})...", message_type='info', icon="✂️")
|
||
|
||
for i in range(num_chunks):
|
||
chunk_start = i * chunk_size
|
||
chunk_end = min((i + 1) * chunk_size, total_frames)
|
||
|
||
if enable_debug:
|
||
log(f"Processing Chunk {i+1}/{num_chunks}: Frames {chunk_start}-{chunk_end}", message_type='info', icon="🎞️")
|
||
|
||
chunk_frames = frames[chunk_start:chunk_end]
|
||
|
||
# Auto-Fallback Logic - FIX 5: Uses VRAM_OOM_THRESHOLD (95%)
|
||
retry_count = 0
|
||
max_retries = 2
|
||
current_tiled_vae = tiled_vae
|
||
current_tiled_dit = tiled_dit
|
||
|
||
while retry_count <= max_retries:
|
||
try:
|
||
chunk_out = process_chunk(
|
||
pipe, chunk_frames, scale, color_fix, current_tiled_vae, current_tiled_dit,
|
||
tile_size, tile_overlap, unload_dit, sparse_ratio, kv_ratio,
|
||
local_range, seed, force_offload, enable_debug,
|
||
is_single_frame_input=is_single_frame_input
|
||
)
|
||
final_outputs.append(chunk_out.cpu())
|
||
del chunk_out
|
||
clean_vram()
|
||
break # Success
|
||
except torch.OutOfMemoryError as e:
|
||
retry_count += 1
|
||
clean_vram()
|
||
log(f"OOM detected in Chunk {i+1} (Attempt {retry_count}). Recovering...", message_type='warning', icon="🔄")
|
||
|
||
if not current_tiled_vae:
|
||
log("Auto-enabling Tiled VAE to prevent OOM (override)...", message_type='info', icon="🛡️")
|
||
current_tiled_vae = True
|
||
elif not current_tiled_dit:
|
||
log("Auto-enabling Tiled DiT to prevent OOM (override)...", message_type='info', icon="🛡️")
|
||
current_tiled_dit = True
|
||
else:
|
||
log("Both Tiled VAE and DiT enabled but still OOM. Cannot recover.", message_type='error', icon="❌")
|
||
raise e # Cannot recover further
|
||
|
||
final_output_tensor = torch.cat(final_outputs, dim=0)
|
||
else:
|
||
# Auto-Fallback Logic for single chunk/full video
|
||
retry_count = 0
|
||
max_retries = 2
|
||
current_tiled_vae = tiled_vae
|
||
current_tiled_dit = tiled_dit
|
||
|
||
while retry_count <= max_retries:
|
||
try:
|
||
final_output_tensor = process_chunk(
|
||
pipe, frames, scale, color_fix, current_tiled_vae, current_tiled_dit,
|
||
tile_size, tile_overlap, unload_dit, sparse_ratio, kv_ratio,
|
||
local_range, seed, force_offload, enable_debug,
|
||
is_single_frame_input=is_single_frame_input
|
||
)
|
||
break
|
||
except torch.OutOfMemoryError as e:
|
||
retry_count += 1
|
||
clean_vram()
|
||
log(f"OOM detected (Attempt {retry_count}). Recovering...", message_type='warning', icon="🔄")
|
||
|
||
if not current_tiled_vae:
|
||
log("Auto-enabling Tiled VAE to prevent OOM (override)...", message_type='info', icon="🛡️")
|
||
current_tiled_vae = True
|
||
elif not current_tiled_dit:
|
||
log("Auto-enabling Tiled DiT to prevent OOM (override)...", message_type='info', icon="🛡️")
|
||
current_tiled_dit = True
|
||
else:
|
||
log("Both Tiled VAE and DiT enabled but still OOM. Cannot recover.", message_type='error', icon="❌")
|
||
raise e
|
||
|
||
end_time = time.time()
|
||
total_time = end_time - start_time
|
||
fps = frames.shape[0] / total_time if total_time > 0 else 0
|
||
|
||
# ==========================================================================
|
||
# FIX 8: Summary logging at end of processing
|
||
# ==========================================================================
|
||
log("=" * 60, message_type='info')
|
||
log("PROCESSING SUMMARY", message_type='finish', icon="📊")
|
||
log(f"Total Processing Time: {total_time:.2f}s ({fps:.2f} FPS)", message_type='info', icon="⏱️")
|
||
log(f"Input Resolution: {input_resolution} ({frames.shape[0]} frames)", message_type='info', icon="📥")
|
||
log(f"Output Resolution: {output_resolution} ({final_output_tensor.shape[0]} frames)", message_type='info', icon="📤")
|
||
|
||
if torch.cuda.is_available():
|
||
peak_memory = torch.cuda.max_memory_reserved() / 1024**3
|
||
log(f"Peak VRAM Used: {peak_memory:.2f} GB", message_type='info', icon="📈")
|
||
|
||
log_resource_usage(prefix="Final")
|
||
log("=" * 60, message_type='info')
|
||
|
||
return final_output_tensor
|
||
|
||
|
||
class FlashVSRNodeInitPipe:
|
||
"""
|
||
=============================================================================
|
||
FIX 1: Unified VAE Selection - Merged vae_type and alt_vae into vae_model
|
||
=============================================================================
|
||
"""
|
||
@classmethod
|
||
def INPUT_TYPES(cls):
|
||
return {
|
||
"required": {
|
||
"model": (["FlashVSR", "FlashVSR-v1.1"], {
|
||
"default": "FlashVSR-v1.1",
|
||
"tooltip": "Select the FlashVSR model version. V1.1 is recommended for better stability."
|
||
}),
|
||
"mode": (["tiny", "tiny-long", "full"], {
|
||
"default": "tiny",
|
||
"tooltip": 'Operation mode. "tiny": faster, standard memory. "tiny-long": optimized for long videos (lower VRAM). "full": higher quality but max VRAM.'
|
||
}),
|
||
"vae_model": (VAE_MODEL_OPTIONS, {
|
||
"default": "Wan2.1",
|
||
"tooltip": 'VAE model: Wan2.1 (default), Wan2.2, LightVAE_W2.1 (50% less VRAM), TAE_W2.2, LightTAE_HY1.5. Auto-downloads if missing.'
|
||
}),
|
||
"force_offload": ("BOOLEAN", {
|
||
"default": True,
|
||
"tooltip": "If enabled, forces offloading of models to CPU RAM after execution to free up VRAM for other nodes."
|
||
}),
|
||
"precision": (["fp16", "bf16", "auto"], {
|
||
"default": "auto",
|
||
"tooltip": "Inference precision. 'auto' selects bf16 if supported (RTX 30/40/50 series), otherwise fp16. bf16 is recommended."
|
||
}),
|
||
"device": (device_choices, {
|
||
"default": device_choices[0],
|
||
"tooltip": "Select the computation device (CUDA GPU, CPU, etc.). 'auto' picks the best available."
|
||
}),
|
||
"attention_mode": (["sparse_sage_attention", "block_sparse_attention", "flash_attention_2", "sdpa"], {
|
||
"default": "sparse_sage_attention",
|
||
"tooltip": 'Attention mechanism backend. "sparse_sage"/"block_sparse" use efficient sparse attention. "flash_attention_2"/"sdpa" use dense attention (slower, more VRAM).'
|
||
}),
|
||
}
|
||
}
|
||
|
||
RETURN_TYPES = ("PIPE",)
|
||
RETURN_NAMES = ("pipe",)
|
||
FUNCTION = "main"
|
||
CATEGORY = "FlashVSR"
|
||
DESCRIPTION = 'Initializes the FlashVSR pipeline. 5 VAE options: Wan2.1, Wan2.2, LightVAE_W2.1, TAE_W2.2, LightTAE_HY1.5. Auto-downloads missing files.'
|
||
|
||
def main(self, model, mode, vae_model, force_offload, precision, device, attention_mode):
|
||
_device = device
|
||
if device == "auto":
|
||
_device = "cuda:0" if torch.cuda.is_available() else "mps" if torch.backends.mps.is_available() else device
|
||
if _device == "auto" or _device not in device_choices:
|
||
raise RuntimeError("No devices found to run FlashVSR!")
|
||
|
||
if _device.startswith("cuda"):
|
||
torch.cuda.set_device(_device)
|
||
|
||
wan_video_dit.ATTENTION_MODE = attention_mode
|
||
|
||
# Auto bfloat16 detection
|
||
if precision == "auto":
|
||
if torch.cuda.is_available() and torch.cuda.is_bf16_supported():
|
||
precision = "bf16"
|
||
log("Auto-detected bf16 support.", message_type='info', icon="⚙️")
|
||
else:
|
||
precision = "fp16"
|
||
log("Defaulting to fp16.", message_type='info', icon="⚙️")
|
||
|
||
dtype_map = {
|
||
"fp32": torch.float32,
|
||
"fp16": torch.float16,
|
||
"bf16": torch.bfloat16,
|
||
}
|
||
try:
|
||
dtype = dtype_map[precision]
|
||
except:
|
||
dtype = torch.bfloat16
|
||
|
||
# Use unified vae_model parameter
|
||
pipe = init_pipeline(model, mode, _device, dtype, vae_model=vae_model)
|
||
# FIX 10: Store mode with pipe for unified processing logic
|
||
return((pipe, force_offload, mode),)
|
||
|
||
class FlashVSRNodeAdv:
|
||
@classmethod
|
||
def INPUT_TYPES(cls):
|
||
return {
|
||
"required": {
|
||
"pipe": ("PIPE", {
|
||
"tooltip": "The initialized FlashVSR pipeline object from the Init node."
|
||
}),
|
||
"frames": ("IMAGE", {
|
||
"tooltip": "Input video frames to be upscaled. Batch of images (N, H, W, C)."
|
||
}),
|
||
"scale": ("INT", {
|
||
"default": 2,
|
||
"min": 2,
|
||
"max": 4,
|
||
"tooltip": "Upscaling factor. 2x or 4x. Higher scale requires more VRAM and compute."
|
||
}),
|
||
"color_fix": ("BOOLEAN", {
|
||
"default": True,
|
||
"tooltip": "Apply wavelet-based color correction to match the output colors with the input, preventing color shifts."
|
||
}),
|
||
"tiled_vae": ("BOOLEAN", {
|
||
"default": True,
|
||
"tooltip": "Enable spatial tiling for the VAE decoder. Reduces VRAM usage significantly but is slower. Recommended for high-res outputs."
|
||
}),
|
||
"tiled_dit": ("BOOLEAN", {
|
||
"default": True,
|
||
"tooltip": "Enable spatial tiling for the Diffusion Transformer (DiT). Crucial for saving VRAM on large inputs. Disabling it is faster but risky."
|
||
}),
|
||
"tile_size": ("INT", {
|
||
"default": 256,
|
||
"min": 32,
|
||
"max": 1024,
|
||
"step": 32,
|
||
"tooltip": "Size of the tiles for DiT processing. Smaller = less VRAM, more tiles, slower."
|
||
}),
|
||
"tile_overlap": ("INT", {
|
||
"default": 24,
|
||
"min": 8,
|
||
"max": 512,
|
||
"step": 8,
|
||
"tooltip": "Overlap pixels between tiles to blend seams. Higher overlap = smoother transitions but more computation."
|
||
}),
|
||
"unload_dit": ("BOOLEAN", {
|
||
"default": False,
|
||
"tooltip": "Unload the DiT model from VRAM before VAE decoding starts. Use this if VAE decode runs out of memory."
|
||
}),
|
||
"sparse_ratio": ("FLOAT", {
|
||
"default": 2.0,
|
||
"min": 1.5,
|
||
"max": 2.0,
|
||
"step": 0.1,
|
||
"display": "slider",
|
||
"tooltip": "Control for sparse attention. 1.5 is faster, 2.0 is more stable/quality. (For sparse backends only)"
|
||
}),
|
||
"kv_ratio": ("FLOAT", {
|
||
"default": 3.0,
|
||
"min": 1.0,
|
||
"max": 3.0,
|
||
"step": 0.1,
|
||
"display": "slider",
|
||
"tooltip": "Key/Value cache ratio. 1.0 uses less VRAM; 3.0 provides highest quality retention."
|
||
}),
|
||
"local_range": ("INT", {
|
||
"default": 11,
|
||
"min": 9,
|
||
"max": 11,
|
||
"step": 2,
|
||
"tooltip": "Local attention range window. 9 = sharper details; 11 = more stable/consistent results."
|
||
}),
|
||
"seed": ("INT", {
|
||
"default": 0,
|
||
"min": 0,
|
||
"max": 1125899906842624,
|
||
"tooltip": "Random seed for noise generation. Same seed + same settings = reproducible results."
|
||
}),
|
||
"frame_chunk_size": ("INT", {
|
||
"default": 0,
|
||
"min": 0,
|
||
"max": 10000,
|
||
"step": 1,
|
||
"tooltip": "Process video in chunks of N frames to prevent VRAM OOM. 0 = Process all frames at once. Results are merged on CPU."
|
||
}),
|
||
"enable_debug": ("BOOLEAN", {
|
||
"default": False,
|
||
"tooltip": "Enable verbose logging to console. Shows VRAM usage, step times, tile info, and detailed progress."
|
||
}),
|
||
"keep_models_on_cpu": ("BOOLEAN", {
|
||
"default": True,
|
||
"tooltip": "Move models to CPU RAM instead of keeping them in VRAM when not in use. Prevents VRAM fragmentation/OOM."
|
||
}),
|
||
"resize_factor": ("FLOAT", {
|
||
"default": 1.0,
|
||
"min": 0.1,
|
||
"max": 1.0,
|
||
"step": 0.1,
|
||
"tooltip": "Resize input frames before processing. Set to 0.5x for large 1080p+ videos to save VRAM."
|
||
}),
|
||
}
|
||
}
|
||
|
||
RETURN_TYPES = ("IMAGE",)
|
||
RETURN_NAMES = ("image",)
|
||
FUNCTION = "main"
|
||
CATEGORY = "FlashVSR"
|
||
|
||
def main(self, pipe, frames, scale, color_fix, tiled_vae, tiled_dit, tile_size, tile_overlap, unload_dit, sparse_ratio, kv_ratio, local_range, seed, frame_chunk_size, enable_debug, keep_models_on_cpu, resize_factor):
|
||
# FIX 10: Extract mode from pipe tuple for unified processing
|
||
# Pipe tuple structure: (pipeline_object, force_offload, mode)
|
||
# Backwards compatible with older 2-element tuples (pipeline, force_offload)
|
||
if len(pipe) >= 3:
|
||
_pipe, _, mode = pipe
|
||
else:
|
||
_pipe = pipe[0]
|
||
mode = "full" # Default fallback for backwards compatibility
|
||
output = flashvsr(_pipe, frames, scale, color_fix, tiled_vae, tiled_dit, tile_size, tile_overlap, unload_dit, sparse_ratio, kv_ratio, local_range, seed, keep_models_on_cpu, enable_debug, frame_chunk_size, resize_factor, mode=mode)
|
||
return(output,)
|
||
|
||
class FlashVSRNode:
|
||
"""
|
||
=============================================================================
|
||
FIX 1: Unified VAE Selection - Single vae_model dropdown
|
||
=============================================================================
|
||
"""
|
||
@classmethod
|
||
def INPUT_TYPES(cls):
|
||
return {
|
||
"required": {
|
||
"frames": ("IMAGE", {
|
||
"tooltip": "Input video frames to be upscaled. Batch of images (N, H, W, C)."
|
||
}),
|
||
"model": (["FlashVSR", "FlashVSR-v1.1"], {
|
||
"default": "FlashVSR-v1.1",
|
||
"tooltip": "Select the FlashVSR model version. V1.1 is recommended for better stability."
|
||
}),
|
||
"mode": (["tiny", "tiny-long", "full"], {
|
||
"default": "tiny",
|
||
"tooltip": 'Operation mode. "tiny": faster, standard memory. "tiny-long": optimized for long videos (lower VRAM). "full": higher quality but max VRAM.'
|
||
}),
|
||
"vae_model": (VAE_MODEL_OPTIONS, {
|
||
"default": "Wan2.1",
|
||
"tooltip": 'VAE model: Wan2.1 (default), Wan2.2, LightVAE_W2.1 (50% less VRAM), TAE_W2.2, LightTAE_HY1.5. Auto-downloads if missing.'
|
||
}),
|
||
"scale": ("INT", {
|
||
"default": 2,
|
||
"min": 2,
|
||
"max": 4,
|
||
"tooltip": "Upscaling factor. 2x or 4x."
|
||
}),
|
||
"tiled_vae": ("BOOLEAN", {
|
||
"default": True,
|
||
"tooltip": "Enable spatial tiling for the VAE decoder. Reduces VRAM usage significantly but is slower. Recommended for high-res outputs."
|
||
}),
|
||
"tiled_dit": ("BOOLEAN", {
|
||
"default": True,
|
||
"tooltip": "Enable spatial tiling for the Diffusion Transformer (DiT). Crucial for saving VRAM on large inputs."
|
||
}),
|
||
"unload_dit": ("BOOLEAN", {
|
||
"default": True,
|
||
"tooltip": "Unload the DiT model from VRAM before VAE decoding starts to free up memory. Recommended for 16GB VRAM."
|
||
}),
|
||
"seed": ("INT", {
|
||
"default": 0,
|
||
"min": 0,
|
||
"max": 1125899906842624,
|
||
"tooltip": "Random seed for noise generation."
|
||
}),
|
||
"frame_chunk_size": ("INT", {
|
||
"default": 0,
|
||
"min": 0,
|
||
"max": 10000,
|
||
"step": 1,
|
||
"tooltip": "Process video in chunks of N frames to prevent VRAM OOM. 0 = Process all frames at once."
|
||
}),
|
||
"attention_mode": (["sparse_sage_attention", "block_sparse_attention", "flash_attention_2", "sdpa"], {
|
||
"default": "sparse_sage_attention",
|
||
"tooltip": 'Attention mechanism backend. "sparse_sage" is recommended for speed/memory efficiency.'
|
||
}),
|
||
"enable_debug": ("BOOLEAN", {
|
||
"default": False,
|
||
"tooltip": "Enable extensive logging for debugging."
|
||
}),
|
||
"keep_models_on_cpu": ("BOOLEAN", {
|
||
"default": True,
|
||
"tooltip": "Move models to CPU RAM instead of keeping them in VRAM when not in use."
|
||
}),
|
||
"resize_factor": ("FLOAT", {
|
||
"default": 1.0,
|
||
"min": 0.1,
|
||
"max": 1.0,
|
||
"step": 0.1,
|
||
"tooltip": "Resize input frames before processing. Set to 0.5x for large 1080p+ videos to save VRAM."
|
||
}),
|
||
}
|
||
}
|
||
|
||
RETURN_TYPES = ("IMAGE",)
|
||
RETURN_NAMES = ("image",)
|
||
FUNCTION = "main"
|
||
CATEGORY = "FlashVSR"
|
||
DESCRIPTION = 'Single-node FlashVSR upscaling. 5 VAE options: Wan2.1, Wan2.2, LightVAE_W2.1, TAE_W2.2, LightTAE_HY1.5. Auto-downloads missing files.'
|
||
|
||
def main(self, model, frames, mode, vae_model, scale, tiled_vae, tiled_dit, unload_dit, seed, frame_chunk_size, attention_mode, enable_debug, keep_models_on_cpu, resize_factor):
|
||
_device = "cuda:0" if torch.cuda.is_available() else "mps" if torch.backends.mps.is_available() else "auto"
|
||
if _device == "auto" or _device not in device_choices:
|
||
raise RuntimeError("No devices found to run FlashVSR!")
|
||
|
||
if _device.startswith("cuda"):
|
||
torch.cuda.set_device(_device)
|
||
|
||
wan_video_dit.ATTENTION_MODE = attention_mode
|
||
|
||
# Use unified vae_model parameter
|
||
pipe = init_pipeline(model, mode, _device, torch.float16, vae_model=vae_model)
|
||
# FIX 10: Pass mode for unified processing logic
|
||
output = flashvsr(pipe, frames, scale, True, tiled_vae, tiled_dit, 256, 24, unload_dit, 2.0, 3.0, 11, seed, keep_models_on_cpu, enable_debug, frame_chunk_size, resize_factor, mode=mode)
|
||
return(output,)
|
||
|
||
NODE_CLASS_MAPPINGS = {
|
||
"FlashVSRNode": FlashVSRNode,
|
||
"FlashVSRNodeAdv": FlashVSRNodeAdv,
|
||
"FlashVSRInitPipe": FlashVSRNodeInitPipe,
|
||
}
|
||
|
||
NODE_DISPLAY_NAME_MAPPINGS = {
|
||
"FlashVSRNode": "FlashVSR Ultra-Fast",
|
||
"FlashVSRNodeAdv": "FlashVSR Ultra-Fast (Advanced)",
|
||
"FlashVSRInitPipe": "FlashVSR Init Pipeline",
|
||
}
|