CoolFace
Apppublic

drizzymedia/StreamDiffusionV2-Realtime

sourceHugging Faceupdated 2mo agoView on Hugging Face
1likes
pipeline.py438 linesDownload Raw Back to streamdiffusionv2
1"""Readable staged video-to-video API for StreamDiffusionV2."""2 3from __future__ import annotations4 5from contextlib import ExitStack6from dataclasses import dataclass7from importlib.resources import as_file, files8from pathlib import Path9from typing import Literal10 11from diffusers.utils import export_to_video as diffusers_export_to_video12import numpy as np13import torch14 15from models.util import set_seed16from streamv2v.inference import (17    SingleGPUInferencePipeline as StreamBatchInferencePipeline,18    compute_noise_scale_and_step,19)20from streamv2v.inference_common import load_mp4_as_tensor, merge_cli_config, normalize_acceleration_flags21from streamv2v.inference_wo_batch import SingleGPUInferencePipeline as StreamNoBatchInferencePipeline22 23 24SingleMode = Literal["single", "single-wo"]25 26 27@dataclass28class VideoChunk:29    """One video chunk prepared for the encode -> denoise -> decode loop."""30 31    frames: torch.Tensor32    start_idx: int33    end_idx: int34    current_start: int35    current_end: int36 37 38@dataclass39class EncodedChunk:40    """Encoded latent chunk plus the schedule metadata needed for denoising."""41 42    noisy_latents: torch.Tensor43    current_start: int44    current_end: int45    noise_scale: float46    current_step: int | None = None47 48 49@dataclass50class DenoisedChunk:51    """Denoised latent chunk ready for VAE decoding."""52 53    denoised_pred: torch.Tensor54    last_frame_only: bool55 56 57def _resolve_default_config_path(resource_stack: ExitStack) -> str:58    resource = files("streamv2v.configs").joinpath("wan_causal_dmd_v2v.yaml")59    return str(resource_stack.enter_context(as_file(resource)))60 61 62def _resolve_device(device: str | torch.device | None) -> torch.device:63    cuda_available = torch.cuda.is_available()64    if device is None:65        return torch.device("cuda" if cuda_available else "cpu")66    resolved = torch.device(device)67    if resolved.type == "cuda" and not cuda_available:68        raise RuntimeError("CUDA is not available in the current Python environment")69    if resolved.type == "cuda" and resolved.index is not None:70        torch.cuda.set_device(resolved.index)71    return resolved72 73 74def _normalize_video_tensor(75    video: str | Path | torch.Tensor,76    *,77    height: int,78    width: int,79    device: torch.device,80) -> torch.Tensor:81    if isinstance(video, (str, Path)):82        tensor = load_mp4_as_tensor(str(video), resize_hw=(height, width)).unsqueeze(0)83    else:84        tensor = video85        if tensor.ndim == 4:86            tensor = tensor.unsqueeze(0)87        if tensor.ndim != 5:88            raise ValueError("video tensor must have shape [B, C, T, H, W] or [C, T, H, W]")89    if tensor.dtype != torch.bfloat16:90        tensor = tensor.to(dtype=torch.bfloat16)91    return tensor.to(device)92 93 94def load_video(video_path: str, *, height: int = 480, width: int = 832) -> torch.Tensor:95    """Load a video file as a normalized tensor with shape [C, T, H, W]."""96    return load_mp4_as_tensor(video_path, resize_hw=(height, width))97 98 99def export_video(video: np.ndarray, output_path: str, *, fps: int = 16) -> str:100    """Write a `[T, H, W, C]` float video array to an mp4 file."""101    output_file = Path(output_path)102    output_file.parent.mkdir(parents=True, exist_ok=True)103    diffusers_export_to_video(video, str(output_file), fps=fps)104    return str(output_file)105 106 107class StreamDiffusionV2Pipeline:108    """Readable staged single-GPU API that mirrors the offline inference flow."""109 110    def __init__(111        self,112        checkpoint_folder: str,113        *,114        mode: SingleMode = "single",115        config_path: str | None = None,116        device: str | torch.device | None = None,117        noise_scale: float = 0.8,118        height: int = 480,119        width: int = 832,120        fps: int = 16,121        step: int = 2,122        seed: int = 0,123        model_type: str = "T2V-1.3B",124        use_taehv: bool = False,125        use_tensorrt: bool = False,126        fast: bool = False,127        profile: bool = False,128    ) -> None:129        if mode not in {"single", "single-wo"}:130            raise ValueError("StreamDiffusionV2Pipeline only supports 'single' and 'single-wo'")131 132        self._resource_stack = ExitStack()133        self.mode = mode134        self.device = _resolve_device(device)135        self.checkpoint_folder = checkpoint_folder136        self.noise_scale = float(noise_scale)137        self.height = int(height)138        self.width = int(width)139        self.fps = int(fps)140        self.seed = int(seed)141        self.step = int(step)142        self.profile = bool(profile)143        self.model_type = model_type144        self.prompt: str | None = None145 146        resolved_config_path = config_path or _resolve_default_config_path(self._resource_stack)147        self.config_path = resolved_config_path148        flags = normalize_acceleration_flags(149            {150                "use_taehv": use_taehv,151                "use_tensorrt": use_tensorrt,152                "fast": fast,153            }154        )155        self.use_taehv = bool(flags["use_taehv"])156        self.use_tensorrt = bool(flags["use_tensorrt"])157        self.fast = bool(flags["fast"])158        config_args = {159            "config_path": resolved_config_path,160            "checkpoint_folder": checkpoint_folder,161            "noise_scale": noise_scale,162            "height": height,163            "width": width,164            "fps": fps,165            "step": step,166            "seed": seed,167            "model_type": model_type,168            "profile": profile,169            "use_taehv": self.use_taehv,170            "use_tensorrt": self.use_tensorrt,171            "fast": self.fast,172            "t2v": False,173            "target_fps": None,174            "fixed_noise_scale": False,175            "num_frames": 81,176        }177        self.config = merge_cli_config(resolved_config_path, config_args)178 179        manager_cls = (180            StreamBatchInferencePipeline if mode == "single" else StreamNoBatchInferencePipeline181        )182        torch.set_grad_enabled(False)183        set_seed(self.seed)184        self.pipeline_manager = manager_cls(self.config, self.device)185        self.pipeline_manager.load_model(checkpoint_folder)186        self.chunk_size = 4 * self.config.num_frame_per_block187        self.num_steps = len(self.pipeline_manager.pipeline.denoising_step_list)188        self._next_chunk_index = 0189 190    def close(self) -> None:191        self._resource_stack.close()192 193    def __enter__(self) -> "StreamDiffusionV2Pipeline":194        return self195 196    def __exit__(self, exc_type, exc_val, exc_tb) -> None:197        self.close()198 199    def enable_acceleration(200        self,201        *,202        use_taehv: bool = False,203        use_tensorrt: bool = False,204        fast: bool = False,205    ) -> "StreamDiffusionV2Pipeline":206        """Rebuild the pipeline with the requested acceleration flags."""207        replacement = StreamDiffusionV2Pipeline(208            checkpoint_folder=self.checkpoint_folder,209            mode=self.mode,210            config_path=self.config_path,211            device=self.device,212            noise_scale=self.noise_scale,213            height=self.height,214            width=self.width,215            fps=self.fps,216            step=self.step,217            seed=self.seed,218            model_type=self.model_type,219            use_taehv=use_taehv,220            use_tensorrt=use_tensorrt,221            fast=fast,222            profile=self.profile,223        )224        self.close()225        self.__dict__.update(replacement.__dict__)226        return self227 228    def prepare(self, prompt: str) -> None:229        """Reset the stream state and store the prompt for the next denoising pass."""230        self.prompt = prompt231        self.pipeline_manager.reset_stream_state(reset_vae_flags=True)232        self.pipeline_manager.processed = 0233        self._next_chunk_index = 0234 235    def chunk_video(self, video: str | Path | torch.Tensor) -> list[VideoChunk]:236        """Split a full input video into the same chunks used by the offline inference loop."""237        input_video = _normalize_video_tensor(238            video,239            height=self.height,240            width=self.width,241            device=self.device,242        )243        _, _, total_frames, _, _ = input_video.shape244        if total_frames < 1 + self.chunk_size:245            raise ValueError(f"video must contain at least {1 + self.chunk_size} frames")246 247        chunks: list[VideoChunk] = []248        start_idx = 0249        end_idx = 1 + self.chunk_size250        current_start = 0251        current_end = self.pipeline_manager.pipeline.frame_seq_length * (1 + self.chunk_size // 4)252 253        chunks.append(254            VideoChunk(255                frames=input_video[:, :, start_idx:end_idx],256                start_idx=start_idx,257                end_idx=end_idx,258                current_start=current_start,259                current_end=current_end,260            )261        )262 263        while True:264            start_idx = end_idx265            end_idx = end_idx + self.chunk_size266            if end_idx > total_frames:267                break268            current_start = current_end269            current_end = current_end + (self.chunk_size // 4) * self.pipeline_manager.pipeline.frame_seq_length270            chunks.append(271                VideoChunk(272                    frames=input_video[:, :, start_idx:end_idx],273                    start_idx=start_idx,274                    end_idx=end_idx,275                    current_start=current_start,276                    current_end=current_end,277                )278            )279        return chunks280 281    @torch.inference_mode()282    def encode_chunk(283        self,284        input_video: str | Path | torch.Tensor,285        chunk: VideoChunk,286        *,287        previous_noise_scale: float | None = None,288        initial_noise_scale: float | None = None,289    ) -> EncodedChunk:290        """Encode one chunk in the same style as the offline inference loop."""291        full_video = _normalize_video_tensor(292            input_video,293            height=self.height,294            width=self.width,295            device=self.device,296        )297        noise_scale = self.noise_scale if previous_noise_scale is None else float(previous_noise_scale)298        init_noise_scale = self.noise_scale if initial_noise_scale is None else float(initial_noise_scale)299        current_step = None300 301        if chunk.start_idx != 0:302            noise_scale, current_step = compute_noise_scale_and_step(303                full_video,304                chunk.end_idx,305                self.chunk_size,306                noise_scale,307                init_noise_scale,308            )309 310        latents = self.pipeline_manager._timed_stream_encode(chunk.frames)311        latents = latents.transpose(2, 1).contiguous().to(dtype=torch.bfloat16)312        noise = torch.randn_like(latents)313        return EncodedChunk(314            noisy_latents=noise * noise_scale + latents * (1 - noise_scale),315            current_start=chunk.current_start,316            current_end=chunk.current_end,317            noise_scale=float(noise_scale),318            current_step=current_step,319        )320 321    @torch.inference_mode()322    def encode_video(self, video: str | Path | torch.Tensor) -> list[EncodedChunk]:323        """Encode a full input video into noisy latent chunks."""324        chunks: list[EncodedChunk] = []325        noise_scale = float(self.noise_scale)326        init_noise_scale = noise_scale327        video_chunks = self.chunk_video(video)328        full_video = _normalize_video_tensor(329            video,330            height=self.height,331            width=self.width,332            device=self.device,333        )334        for chunk in video_chunks:335            encoded_chunk = self.encode_chunk(336                full_video,337                chunk,338                previous_noise_scale=noise_scale,339                initial_noise_scale=init_noise_scale,340            )341            noise_scale = encoded_chunk.noise_scale342            chunks.append(encoded_chunk)343        return chunks344 345    @torch.inference_mode()346    def denoise_chunks(self, chunks: list[EncodedChunk]) -> list[DenoisedChunk]:347        """Run DiT denoising over the encoded chunks."""348        if not chunks:349            raise ValueError("chunks must not be empty")350        if self.prompt is None:351            raise RuntimeError("Call prepare(prompt) before denoise_chunks(...)")352 353        self.prepare(self.prompt)354        outputs: list[DenoisedChunk] = []355        for chunk in chunks:356            denoised_chunk = self.denoise_chunk(chunk)357            if denoised_chunk is not None:358                outputs.append(denoised_chunk)359        return outputs360 361    @torch.inference_mode()362    def denoise_chunk(self, chunk: EncodedChunk) -> DenoisedChunk | None:363        """Run DiT on one encoded chunk and return a decodable latent when available."""364        if self.prompt is None:365            raise RuntimeError("Call prepare(prompt) before denoise_chunk(...)")366 367        if self._next_chunk_index == 0:368            if self.mode == "single":369                denoised_pred = self.pipeline_manager.prepare_pipeline(370                    text_prompts=[self.prompt],371                    noise=chunk.noisy_latents,372                    current_start=chunk.current_start,373                    current_end=chunk.current_end,374                )375            else:376                denoised_pred = self.pipeline_manager.prepare_pipeline(377                    text_prompts=[self.prompt],378                    noise=chunk.noisy_latents,379                    current_start=chunk.current_start,380                    current_end=chunk.current_end,381                    batch_denoise=False,382                )383            self._next_chunk_index += 1384            return DenoisedChunk(denoised_pred=denoised_pred, last_frame_only=False)385 386        current_start = chunk.current_start387        current_end = chunk.current_end388 389        if current_start // self.pipeline_manager.pipeline.frame_seq_length >= self.pipeline_manager.t_refresh:390            current_start = self.pipeline_manager.pipeline.kv_cache_length - self.pipeline_manager.pipeline.frame_seq_length391            current_end = current_start + (self.chunk_size // 4) * self.pipeline_manager.pipeline.frame_seq_length392 393        if self.mode == "single":394            denoised_pred = self.pipeline_manager.pipeline.inference_stream(395                noise=chunk.noisy_latents,396                current_start=current_start,397                current_end=current_end,398                current_step=chunk.current_step,399            )400            self.pipeline_manager.processed += 1401            self._next_chunk_index += 1402            if self.pipeline_manager.processed < self.num_steps:403                return None404            return DenoisedChunk(denoised_pred=denoised_pred, last_frame_only=True)405 406        denoised_pred = self.pipeline_manager.pipeline.inference_wo_batch(407            noise=chunk.noisy_latents,408            current_start=current_start,409            current_end=current_end,410            current_step=chunk.current_step,411        )412        self.pipeline_manager.processed += 1413        self._next_chunk_index += 1414        return DenoisedChunk(denoised_pred=denoised_pred, last_frame_only=True)415 416    @torch.inference_mode()417    def decode_chunks(self, chunks: list[DenoisedChunk]) -> np.ndarray:418        """Decode denoised latent chunks into a `[T, H, W, C]` video array."""419        if not chunks:420            raise ValueError("chunks must not be empty")421        decoded = [self.decode_chunk(chunk) for chunk in chunks]422        return np.concatenate(decoded, axis=0)423 424    @torch.inference_mode()425    def decode_chunk(self, chunk: DenoisedChunk) -> np.ndarray:426        """Decode one denoised latent chunk into `[T, H, W, C]` frames."""427        return self.pipeline_manager._decode_video_array(428            chunk.denoised_pred,429            last_frame_only=chunk.last_frame_only,430        )431 432    @torch.inference_mode()433    def __call__(self, video: str | Path | torch.Tensor) -> np.ndarray:434        """Run the full staged pipeline after `prepare(prompt)` has been called."""435        encoded = self.encode_video(video)436        denoised = self.denoise_chunks(encoded)437        return self.decode_chunks(denoised)438