Files
FillandClaude Opus 4.5 37aa6cf9ce Initial release - FL DiffVSR video super-resolution nodes
- 4x video upscaling with temporal coherence using Stream-DiffVSR
- Model Loader node with precision/device options and xformers support
- Upscaler node with chunked processing for memory efficiency
- Automatic model download from HuggingFace
- Text-guided upscaling support

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-05 22:09:14 -08:00

583 lines
26 KiB
Python

# Copyright 2023 The HuggingFace Team. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import inspect
from typing import Any, Callable, Dict, List, Optional, Tuple, Union
import numpy as np
import PIL.Image
import torch
import torch.nn.functional as F
import torchvision.transforms as T
from transformers import CLIPImageProcessor, CLIPTextModel, CLIPTokenizer
from diffusers.image_processor import PipelineImageInput, VaeImageProcessor
from diffusers.loaders import FromSingleFileMixin, LoraLoaderMixin, TextualInversionLoaderMixin
from diffusers.models import AutoencoderKL, ControlNetModel, UNet2DConditionModel
from diffusers.schedulers import KarrasDiffusionSchedulers
from diffusers.utils import (
deprecate,
is_accelerate_available,
is_accelerate_version,
logging,
)
from diffusers.utils.torch_utils import randn_tensor, is_compiled_module
from diffusers.pipelines import DiffusionPipeline
from diffusers.pipelines.controlnet import MultiControlNetModel
from diffusers.pipelines.stable_diffusion import StableDiffusionPipelineOutput, StableDiffusionSafetyChecker
from ..util import flow_utils as of
from ..temporal_autoencoder.autoencoder_tiny import TemporalAutoencoderTiny
from ..scheduler.ddim_scheduler import DDIMScheduler
logger = logging.get_logger(__name__)
class StreamDiffVSRPipeline(
DiffusionPipeline, TextualInversionLoaderMixin, LoraLoaderMixin, FromSingleFileMixin
):
"""
Stream-DiffVSR Pipeline for video super-resolution.
Args:
vae: Temporal VAE model for encoding/decoding
text_encoder: CLIP text encoder
tokenizer: CLIP tokenizer
unet: UNet2DConditionModel for denoising
controlnet: ControlNet for temporal conditioning
scheduler: DDIM scheduler
safety_checker: Optional safety checker
feature_extractor: Optional feature extractor
"""
_optional_components = ["safety_checker", "feature_extractor"]
def __init__(
self,
vae: Union[AutoencoderKL, TemporalAutoencoderTiny],
text_encoder: CLIPTextModel,
tokenizer: CLIPTokenizer,
unet: UNet2DConditionModel,
controlnet: Union[ControlNetModel, List[ControlNetModel], Tuple[ControlNetModel], MultiControlNetModel],
scheduler: Union[KarrasDiffusionSchedulers, DDIMScheduler],
safety_checker: StableDiffusionSafetyChecker,
feature_extractor: CLIPImageProcessor,
requires_safety_checker: bool = True,
):
super().__init__()
if safety_checker is None and requires_safety_checker:
logger.warning(
f"You have disabled the safety checker for {self.__class__} by passing `safety_checker=None`."
)
if safety_checker is not None and feature_extractor is None:
raise ValueError(
"Make sure to define a feature extractor when loading {self.__class__} if you want to use the safety checker."
)
if isinstance(controlnet, (list, tuple)):
controlnet = MultiControlNetModel(controlnet)
self.register_modules(
vae=vae,
text_encoder=text_encoder,
tokenizer=tokenizer,
unet=unet,
controlnet=controlnet,
scheduler=scheduler,
safety_checker=safety_checker,
feature_extractor=feature_extractor,
)
self.vae_scale_factor = 2 ** (len(self.vae.config.block_out_channels) - 1)
self.image_processor = VaeImageProcessor(vae_scale_factor=self.vae_scale_factor, do_convert_rgb=True)
self.control_image_processor = VaeImageProcessor(
vae_scale_factor=self.vae_scale_factor, do_convert_rgb=True, do_normalize=True
)
self.register_to_config(requires_safety_checker=requires_safety_checker)
def enable_vae_slicing(self):
self.vae.enable_slicing()
def disable_vae_slicing(self):
self.vae.disable_slicing()
def enable_vae_tiling(self):
self.vae.enable_tiling()
def disable_vae_tiling(self):
self.vae.disable_tiling()
def enable_model_cpu_offload(self, gpu_id=0):
if is_accelerate_available() and is_accelerate_version(">=", "0.17.0.dev0"):
from accelerate import cpu_offload_with_hook
else:
raise ImportError("`enable_model_cpu_offload` requires `accelerate v0.17.0` or higher.")
device = torch.device(f"cuda:{gpu_id}")
hook = None
for cpu_offloaded_model in [self.text_encoder, self.unet, self.vae]:
_, hook = cpu_offload_with_hook(cpu_offloaded_model, device, prev_module_hook=hook)
if self.safety_checker is not None:
_, hook = cpu_offload_with_hook(self.safety_checker, device, prev_module_hook=hook)
cpu_offload_with_hook(self.controlnet, device)
self.final_offload_hook = hook
def encode_prompt(
self,
prompt,
device,
num_images_per_prompt,
do_classifier_free_guidance,
negative_prompt=None,
prompt_embeds: Optional[torch.FloatTensor] = None,
negative_prompt_embeds: Optional[torch.FloatTensor] = None,
lora_scale: Optional[float] = None,
):
if lora_scale is not None and isinstance(self, LoraLoaderMixin):
self._lora_scale = lora_scale
if prompt is not None and isinstance(prompt, str):
batch_size = 1
elif prompt is not None and isinstance(prompt, list):
batch_size = len(prompt)
else:
batch_size = prompt_embeds.shape[0]
if prompt_embeds is None:
if isinstance(self, TextualInversionLoaderMixin):
prompt = self.maybe_convert_prompt(prompt, self.tokenizer)
text_inputs = self.tokenizer(
prompt,
padding="max_length",
max_length=self.tokenizer.model_max_length,
truncation=True,
return_tensors="pt",
)
text_input_ids = text_inputs.input_ids
if hasattr(self.text_encoder.config, "use_attention_mask") and self.text_encoder.config.use_attention_mask:
attention_mask = text_inputs.attention_mask.to(device)
else:
attention_mask = None
prompt_embeds = self.text_encoder(
text_input_ids.to(device),
attention_mask=attention_mask,
)
prompt_embeds = prompt_embeds[0]
if self.text_encoder is not None:
prompt_embeds_dtype = self.text_encoder.dtype
elif self.unet is not None:
prompt_embeds_dtype = self.unet.dtype
else:
prompt_embeds_dtype = prompt_embeds.dtype
prompt_embeds = prompt_embeds.to(dtype=prompt_embeds_dtype, device=device)
bs_embed, seq_len, _ = prompt_embeds.shape
prompt_embeds = prompt_embeds.repeat(1, num_images_per_prompt, 1)
prompt_embeds = prompt_embeds.view(bs_embed * num_images_per_prompt, seq_len, -1)
if do_classifier_free_guidance and negative_prompt_embeds is None:
uncond_tokens: List[str]
if negative_prompt is None:
uncond_tokens = [""] * batch_size
elif isinstance(negative_prompt, str):
uncond_tokens = [negative_prompt]
else:
uncond_tokens = negative_prompt
if isinstance(self, TextualInversionLoaderMixin):
uncond_tokens = self.maybe_convert_prompt(uncond_tokens, self.tokenizer)
max_length = prompt_embeds.shape[1]
uncond_input = self.tokenizer(
uncond_tokens,
padding="max_length",
max_length=max_length,
truncation=True,
return_tensors="pt",
)
if hasattr(self.text_encoder.config, "use_attention_mask") and self.text_encoder.config.use_attention_mask:
attention_mask = uncond_input.attention_mask.to(device)
else:
attention_mask = None
negative_prompt_embeds = self.text_encoder(
uncond_input.input_ids.to(device),
attention_mask=attention_mask,
)
negative_prompt_embeds = negative_prompt_embeds[0]
if do_classifier_free_guidance:
seq_len = negative_prompt_embeds.shape[1]
negative_prompt_embeds = negative_prompt_embeds.to(dtype=prompt_embeds_dtype, device=device)
negative_prompt_embeds = negative_prompt_embeds.repeat(1, num_images_per_prompt, 1)
negative_prompt_embeds = negative_prompt_embeds.view(batch_size * num_images_per_prompt, seq_len, -1)
return prompt_embeds, negative_prompt_embeds
def prepare_extra_step_kwargs(self, generator, eta):
accepts_eta = "eta" in set(inspect.signature(self.scheduler.step).parameters.keys())
extra_step_kwargs = {}
if accepts_eta:
extra_step_kwargs["eta"] = eta
accepts_generator = "generator" in set(inspect.signature(self.scheduler.step).parameters.keys())
if accepts_generator:
extra_step_kwargs["generator"] = generator
return extra_step_kwargs
def prepare_latents(self, batch_size, num_channels_latents, height, width, dtype, device, generator, latents=None):
shape = (batch_size, num_channels_latents, height // self.vae_scale_factor, width // self.vae_scale_factor)
if isinstance(generator, list) and len(generator) != batch_size:
raise ValueError(
f"You have passed a list of generators of length {len(generator)}, but requested an effective batch"
f" size of {batch_size}."
)
if latents is None:
latents = randn_tensor(shape, generator=generator, device=device, dtype=dtype)
else:
latents = latents.to(device)
latents = latents * self.scheduler.init_noise_sigma
return latents
def compute_flows(self, of_model, images, rescale_factor=1):
print('Computing forward flows...')
forward_flows = []
for i in range(1, len(images)):
# RAFT optical flow model requires float32 input
prev_image = images[i - 1].float()
cur_image = images[i].float()
fflow = of.get_flow(of_model, cur_image, prev_image, rescale_factor=rescale_factor)
forward_flows.append(fflow)
return forward_flows
def compute_single_flow(self, of_model, prev_image, cur_image, rescale_factor=1):
"""Compute optical flow between two adjacent frames.
To save VRAM, we compute flow at 1/4 resolution and upscale it back.
This is much more memory efficient for high-resolution inputs.
"""
# RAFT optical flow model requires float32 input
prev_image_f32 = prev_image.float()
cur_image_f32 = cur_image.float()
# Downscale images for RAFT to save VRAM (RAFT is very memory hungry at high res)
# Use 1/2 resolution for flow computation (better quality than 1/4)
flow_scale = 2
_, _, h, w = prev_image_f32.shape
small_h, small_w = h // flow_scale, w // flow_scale
prev_small = F.interpolate(prev_image_f32, size=(small_h, small_w), mode='bilinear', align_corners=False)
cur_small = F.interpolate(cur_image_f32, size=(small_h, small_w), mode='bilinear', align_corners=False)
# Compute flow at lower resolution
fflow_small = of.get_flow(of_model, cur_small, prev_small, rescale_factor=rescale_factor)
# Upscale flow back to original resolution and scale the flow values
# Flow is in [B, H, W, 2] format after get_flow
fflow_small_permuted = fflow_small.permute(0, 3, 1, 2) # [B, 2, H, W]
fflow_upscaled = F.interpolate(fflow_small_permuted, size=(h, w), mode='bilinear', align_corners=False)
fflow = fflow_upscaled.permute(0, 2, 3, 1) # [B, H, W, 2]
# Scale flow values to match the resolution change
fflow = fflow * flow_scale
return fflow
@torch.no_grad()
def __call__(
self,
prompt: Union[str, List[str]] = None,
images: PipelineImageInput = None,
height: Optional[int] = None,
width: Optional[int] = None,
num_inference_steps: int = 50,
guidance_scale: float = 7.5,
negative_prompt: Optional[Union[str, List[str]]] = None,
num_images_per_prompt: Optional[int] = 1,
eta: float = 0.0,
generator: Optional[Union[torch.Generator, List[torch.Generator]]] = None,
latents: Optional[torch.FloatTensor] = None,
prompt_embeds: Optional[torch.FloatTensor] = None,
negative_prompt_embeds: Optional[torch.FloatTensor] = None,
output_type: Optional[str] = "pil",
return_dict: bool = True,
callback: Optional[Callable[[int, int, torch.FloatTensor], None]] = None,
callback_steps: int = 1,
cross_attention_kwargs: Optional[Dict[str, Any]] = None,
controlnet_conditioning_scale: Union[float, List[float]] = 1.0,
guess_mode: bool = False,
control_guidance_start: Union[float, List[float]] = 0.0,
control_guidance_end: Union[float, List[float]] = 1.0,
of_model=None,
of_rescale_factor: int = 1,
timesteps_to_be_used: Optional[List[float]] = None,
# New parameters for chunked processing
prev_frame_rgb: Optional[torch.FloatTensor] = None,
prev_upscaled_for_flow: Optional[torch.FloatTensor] = None,
frame_offset: int = 0,
):
"""
Run the Stream-DiffVSR pipeline.
"""
controlnet = self.controlnet._orig_mod if is_compiled_module(self.controlnet) else self.controlnet
# align format for control guidance
if not isinstance(control_guidance_start, list) and isinstance(control_guidance_end, list):
control_guidance_start = len(control_guidance_end) * [control_guidance_start]
elif not isinstance(control_guidance_end, list) and isinstance(control_guidance_start, list):
control_guidance_end = len(control_guidance_start) * [control_guidance_end]
elif not isinstance(control_guidance_start, list) and not isinstance(control_guidance_end, list):
mult = len(controlnet.nets) if isinstance(controlnet, MultiControlNetModel) else 1
control_guidance_start, control_guidance_end = mult * [control_guidance_start], mult * [control_guidance_end]
# Define call parameters
if prompt is not None and isinstance(prompt, str):
batch_size = 1
elif prompt is not None and isinstance(prompt, list):
batch_size = len(prompt)
else:
batch_size = prompt_embeds.shape[0]
device = self._execution_device
do_classifier_free_guidance = guidance_scale > 1.0
if isinstance(controlnet, MultiControlNetModel) and isinstance(controlnet_conditioning_scale, float):
controlnet_conditioning_scale = [controlnet_conditioning_scale] * len(controlnet.nets)
global_pool_conditions = (
controlnet.config.global_pool_conditions
if isinstance(controlnet, ControlNetModel)
else controlnet.nets[0].config.global_pool_conditions
)
guess_mode = guess_mode or global_pool_conditions
# Encode input prompt
text_encoder_lora_scale = (
cross_attention_kwargs.get("scale", None) if cross_attention_kwargs is not None else None
)
prompt_embeds, negative_prompt_embeds = self.encode_prompt(
prompt,
device,
num_images_per_prompt,
do_classifier_free_guidance,
negative_prompt,
prompt_embeds=prompt_embeds,
negative_prompt_embeds=negative_prompt_embeds,
lora_scale=text_encoder_lora_scale,
)
# Get model dtype early for consistency
model_dtype = self.unet.dtype
if do_classifier_free_guidance:
prompt_embeds = torch.cat([negative_prompt_embeds, prompt_embeds])
# Ensure prompt embeds are in model dtype
prompt_embeds = prompt_embeds.to(dtype=model_dtype)
# Prepare timesteps
if timesteps_to_be_used is None:
self.scheduler.set_timesteps(num_inference_steps, device=device)
else:
self.scheduler.set_timesteps(timesteps=timesteps_to_be_used, device=device)
timesteps = self.scheduler.timesteps
extra_step_kwargs = self.prepare_extra_step_kwargs(generator, eta)
# Create tensor stating which controlnets to keep
controlnet_keep = []
for i in range(len(timesteps)):
keeps = [
1.0 - float(i / len(timesteps) < s or (i + 1) / len(timesteps) > e)
for s, e in zip(control_guidance_start, control_guidance_end)
]
controlnet_keep.append(keeps[0] if isinstance(controlnet, ControlNetModel) else keeps)
interp_mode = 'bilinear' if of_rescale_factor == 1 else 'nearest'
# Initialize state from previous chunk if provided
rgb_for_warpping_to_next_frame = prev_frame_rgb
prev_upscaled = prev_upscaled_for_flow
# Store raw images for per-frame processing
raw_images = images
output_images = []
num_channels_latents = self.vae.config.latent_channels
num_warmup_steps = len(timesteps) - num_inference_steps * self.scheduler.order
total_frames = len(raw_images)
with self.progress_bar(total=len(timesteps)*total_frames) as progress_bar:
for num_image, raw_image in enumerate(raw_images):
# === PROCESS ONE FRAME AT A TIME (VRAM efficient) ===
# Preprocess current frame only - DO NOT pass height/width to avoid resizing
# The preprocessor should just normalize, not resize the input
image = self.control_image_processor.preprocess(raw_image).to(dtype=model_dtype, device=device)
# Upscale current frame only (4x bicubic for flow/conditioning)
upscaled = F.interpolate(image, scale_factor=4, mode='bicubic').to(dtype=model_dtype)
# Get dimensions from upscaled output (for latent preparation)
frame_height, frame_width = upscaled.shape[-2:]
# Prepare latent for current frame only
latent = self.prepare_latents(
batch_size * num_images_per_prompt,
num_channels_latents,
frame_height,
frame_width,
model_dtype,
device,
generator
)
# Compute flow only between adjacent frames
flow = None
if prev_upscaled is not None:
flow = self.compute_single_flow(of_model, prev_upscaled, upscaled, rescale_factor=of_rescale_factor)
dec_temporal_features = None
warped_prev_est = None
# Compute Temporal Texture Guidance if we have previous frame
if rgb_for_warpping_to_next_frame is not None and flow is not None:
warped_prev_est = of.flow_warp(rgb_for_warpping_to_next_frame, flow, interp_mode=interp_mode)
warped_prev_est = warped_prev_est.to(dtype=model_dtype)
enc_layer_features = self.vae.encode(warped_prev_est, return_features_only=True)
dec_temporal_features = enc_layer_features[::-1]
for i, t in enumerate(timesteps):
# Ensure timestep is on the correct device
t = t.to(device)
latent_model_input = torch.cat([latent] * 2) if do_classifier_free_guidance else latent
latent_model_input = self.scheduler.scale_model_input(latent_model_input, t)
# Double image for CFG (unconditional + conditional)
image_input = torch.cat([image] * 2) if do_classifier_free_guidance else image
latent_model_input = torch.cat([latent_model_input, image_input], dim=1)
# controlnet(s) inference
if guess_mode and do_classifier_free_guidance:
control_model_input = latent
control_model_input = self.scheduler.scale_model_input(control_model_input, t)
controlnet_prompt_embeds = prompt_embeds.chunk(2)[1]
else:
control_model_input = latent_model_input
controlnet_prompt_embeds = prompt_embeds
if isinstance(controlnet_keep[i], list):
cond_scale = [c * s for c, s in zip(controlnet_conditioning_scale, controlnet_keep[i])]
else:
controlnet_cond_scale = controlnet_conditioning_scale
if isinstance(controlnet_cond_scale, list):
controlnet_cond_scale = controlnet_cond_scale[0]
cond_scale = controlnet_cond_scale * controlnet_keep[i]
# Use ControlNet only if we have warped previous estimate
if warped_prev_est is None:
down_block_res_samples = None
mid_block_res_sample = None
else:
# Double warped_prev_est for CFG
controlnet_cond_input = torch.cat([warped_prev_est] * 2) if do_classifier_free_guidance else warped_prev_est
down_block_res_samples, mid_block_res_sample = self.controlnet(
control_model_input,
t,
encoder_hidden_states=controlnet_prompt_embeds,
controlnet_cond=controlnet_cond_input,
conditioning_scale=cond_scale,
guess_mode=guess_mode,
return_dict=False,
timestep_cond=None
)
if guess_mode and do_classifier_free_guidance:
down_block_res_samples = [torch.cat([torch.zeros_like(d), d]) for d in down_block_res_samples]
mid_block_res_sample = torch.cat([torch.zeros_like(mid_block_res_sample), mid_block_res_sample])
# predict the noise residual
noise_pred = self.unet(
latent_model_input,
t,
encoder_hidden_states=prompt_embeds,
cross_attention_kwargs=cross_attention_kwargs,
down_block_additional_residuals=down_block_res_samples,
mid_block_additional_residual=mid_block_res_sample,
return_dict=False,
)[0]
# perform guidance
if do_classifier_free_guidance:
noise_pred_uncond, noise_pred_text = noise_pred.chunk(2)
noise_pred = noise_pred_uncond + guidance_scale * (noise_pred_text - noise_pred_uncond)
# compute the previous noisy sample x_t -> x_t-1
step_output = self.scheduler.step(noise_pred, t, latent, **extra_step_kwargs)
latent, x0_est = step_output.prev_sample, step_output.pred_original_sample
if i == len(timesteps) - 1 or ((i + 1) > num_warmup_steps and (i + 1) % self.scheduler.order == 0):
progress_bar.update()
if callback is not None and i % callback_steps == 0:
# Pass frame index (with offset), step index, total frames, total steps
callback(num_image + frame_offset, i, total_frames, len(timesteps), latent)
if not output_type == "latent":
decoded_image = self.vae.decode(latent / self.vae.config.scaling_factor, temporal_features=dec_temporal_features, return_dict=False)[0]
else:
decoded_image = latent
# Update state for next frame
# Clone tensors to avoid reference issues between frames
rgb_for_warpping_to_next_frame = decoded_image.clone()
prev_upscaled = upscaled.clone()
has_nsfw_concept = None
do_denormalize = [True] * decoded_image[0].shape[0]
final_image = self.image_processor.postprocess(decoded_image, output_type=output_type, do_denormalize=do_denormalize)
output_images.append(final_image)
self.vae.reset_temporal_condition()
# Free memory for this frame
del image, latent, upscaled, decoded_image
if flow is not None:
del flow
if warped_prev_est is not None:
del warped_prev_est
if hasattr(self, "final_offload_hook") and self.final_offload_hook is not None:
self.unet.to("cpu")
self.controlnet.to("cpu")
torch.cuda.empty_cache()
if hasattr(self, "final_offload_hook") and self.final_offload_hook is not None:
self.final_offload_hook.offload()
# Return output along with state for next chunk
output = StableDiffusionPipelineOutput(images=output_images, nsfw_content_detected=None)
return output, rgb_for_warpping_to_next_frame, prev_upscaled