Spaces:
Running on Zero
Running on Zero
Download app.py from hugging-apps/stream-diffvsr-demo: direct link, hf CLI and curl.
- Browser
- Download file 11.5 kB
-
https://huggingface.co/spaces/hugging-apps/stream-diffvsr-demo/resolve/main/app.py
- Command line
-
hf download hf://spaces/hugging-apps/stream-diffvsr-demo/app.py
-
curl -L -o app.py https://huggingface.co/spaces/hugging-apps/stream-diffvsr-demo/resolve/main/app.py
11.5 kB
| import os | |
| os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True") | |
| # `~/.cache/torch` is not writable in the Space image and RAFT weights are fetched | |
| # through torch.hub, so point TORCH_HOME (and matplotlib's config dir) somewhere writable. | |
| os.environ.setdefault("TORCH_HOME", "/tmp/torch_home") | |
| os.environ.setdefault("MPLCONFIGDIR", "/tmp/mplconfig") | |
| os.makedirs(os.path.join(os.environ["TORCH_HOME"], "hub", "checkpoints"), exist_ok=True) | |
| os.makedirs(os.environ["MPLCONFIGDIR"], exist_ok=True) | |
| import spaces # noqa: E402 (must precede any CUDA-touching import) | |
| import tempfile # noqa: E402 | |
| import time # noqa: E402 | |
| from typing import List, Tuple # noqa: E402 | |
| import gradio as gr # noqa: E402 | |
| import imageio.v3 as iio # noqa: E402 | |
| import imageio_ffmpeg # noqa: E402 | |
| import numpy as np # noqa: E402 | |
| import torch # noqa: E402 | |
| from PIL import Image # noqa: E402 | |
| from torchvision.models.optical_flow import Raft_Large_Weights, raft_large # noqa: E402 | |
| from diffusers import DDIMScheduler # noqa: E402 | |
| from pipeline.stream_diffvsr_pipeline import ( # noqa: E402 | |
| ControlNetModel, | |
| StreamDiffVSRPipeline, | |
| UNet2DConditionModel, | |
| ) | |
| from temporal_autoencoder.autoencoder_tiny import TemporalAutoencoderTiny # noqa: E402 | |
| torch.backends.cuda.matmul.allow_tf32 = True | |
| torch.backends.cudnn.allow_tf32 = True | |
| MODEL_ID = "Jamichsu/Stream-DiffVSR" | |
| SCALE = 4 # the model is a x4 super-resolver | |
| MULTIPLE_OF = 8 # UNet needs the low-res side divisible by 8 | |
| # ---------------------------------------------------------------- model load | |
| controlnet = ControlNetModel.from_pretrained(MODEL_ID, subfolder="controlnet") | |
| unet = UNet2DConditionModel.from_pretrained(MODEL_ID, subfolder="unet") | |
| vae = TemporalAutoencoderTiny.from_pretrained(MODEL_ID, subfolder="vae") | |
| scheduler = DDIMScheduler.from_pretrained(MODEL_ID, subfolder="scheduler") | |
| pipe = StreamDiffVSRPipeline.from_pretrained( | |
| MODEL_ID, | |
| controlnet=controlnet, | |
| unet=unet, | |
| vae=vae, | |
| scheduler=scheduler, | |
| safety_checker=None, | |
| feature_extractor=None, | |
| requires_safety_checker=False, | |
| custom_pipeline=None, | |
| ) | |
| pipe.set_progress_bar_config(desc="Stream-DiffVSR") | |
| pipe = pipe.to("cuda") | |
| of_model = raft_large(weights=Raft_Large_Weights.DEFAULT).eval().to("cuda") | |
| of_model.requires_grad_(False) | |
| FFMPEG = imageio_ffmpeg.get_ffmpeg_exe() | |
| # ------------------------------------------------------------------- helpers | |
| def _round_down(value: int, multiple: int = MULTIPLE_OF) -> int: | |
| return max(multiple, (int(value) // multiple) * multiple) | |
| def _read_frames(path: str, max_frames: int) -> Tuple[List[Image.Image], float]: | |
| try: | |
| fps = float(iio.immeta(path, plugin="pyav").get("fps") or 24.0) | |
| except Exception: | |
| fps = 24.0 | |
| if not np.isfinite(fps) or fps <= 0: | |
| fps = 24.0 | |
| frames: List[Image.Image] = [] | |
| for frame in iio.imiter(path, plugin="pyav"): | |
| frames.append(Image.fromarray(np.asarray(frame)).convert("RGB")) | |
| if len(frames) >= max_frames: | |
| break | |
| if not frames: | |
| raise gr.Error("Could not read any frame from this video.") | |
| return frames, fps | |
| def _lowres_size(w: int, h: int, max_input_size: int) -> Tuple[int, int]: | |
| """Downscale to `max_input_size` on the long side and to a 16:9 pixel budget. | |
| Bounding the pixel count (not just the long side) keeps the runtime — and hence | |
| the ZeroGPU reservation — predictable for any aspect ratio. | |
| """ | |
| budget = max_input_size * max_input_size * 9.0 / 16.0 | |
| scale = min(1.0, max_input_size / float(max(w, h)), (budget / float(w * h)) ** 0.5) | |
| return _round_down(w * scale), _round_down(h * scale) | |
| def _prepare_lowres(frames: List[Image.Image], max_input_size: int) -> List[Image.Image]: | |
| w, h = _lowres_size(*frames[0].size, max_input_size=max_input_size) | |
| return [f if f.size == (w, h) else f.resize((w, h), Image.BICUBIC) for f in frames] | |
| def _write_video(frames: List[Image.Image], fps: float, suffix: str) -> str: | |
| w, h = frames[0].size | |
| path = tempfile.NamedTemporaryFile(suffix=f"_{suffix}.mp4", delete=False).name | |
| writer = imageio_ffmpeg.write_frames( | |
| path, | |
| (w, h), | |
| fps=fps, | |
| codec="libx264", | |
| pix_fmt_in="rgb24", | |
| pix_fmt_out="yuv420p", | |
| macro_block_size=1, | |
| quality=None, | |
| output_params=["-crf", "17", "-preset", "veryfast", "-movflags", "+faststart"], | |
| ) | |
| writer.send(None) | |
| for frame in frames: | |
| writer.send(np.asarray(frame, dtype=np.uint8).tobytes()) | |
| writer.close() | |
| return path | |
| def _estimate_duration(*args, **kwargs) -> int: | |
| """ZeroGPU duration estimator, fitted to runtime measured on this Space. | |
| Measured on ZeroGPU at 320x176 LR: 0.67 s/frame at 4 steps, 1.20 s/frame at 8 steps, | |
| i.e. ~0.15 s/frame fixed + ~0.125 s/frame/step, scaling with the LR pixel count. | |
| """ | |
| values = list(args) | |
| def _pick(name, index, default): | |
| if name in kwargs and kwargs[name] is not None: | |
| return kwargs[name] | |
| if len(values) > index and values[index] is not None: | |
| return values[index] | |
| return default | |
| try: | |
| num_frames = int(_pick("num_frames", 1, 30)) | |
| max_input_size = int(_pick("max_input_size", 2, 320)) | |
| steps = int(_pick("num_inference_steps", 3, 4)) | |
| except (TypeError, ValueError): | |
| num_frames, max_input_size, steps = 30, 320, 4 | |
| # assume a 16:9 clip for the pixel-count scaling against the 312x176 reference | |
| # `_lowres_size` bounds the LR frame to a 16:9 pixel budget, so this is an upper bound | |
| px = (max_input_size * max_input_size * 9.0 / 16.0) / (312.0 * 176.0) | |
| seconds = 5.0 + num_frames * (0.15 + 0.125 * steps) * px * 1.25 | |
| return int(min(115, max(20, round(seconds)))) | |
| # ----------------------------------------------------------------- inference | |
| def upscale_video( | |
| video: str, | |
| num_frames: int = 30, | |
| max_input_size: int = 320, | |
| num_inference_steps: int = 4, | |
| seed: int = 42, | |
| progress=gr.Progress(track_tqdm=True), | |
| ) -> Tuple[str, str, str]: | |
| """Upscale a low-resolution video 4x with Stream-DiffVSR. | |
| Args: | |
| video: path to the low-resolution input video. | |
| num_frames: how many frames from the start of the clip to process. | |
| max_input_size: the input is downscaled so its longest side is at most | |
| this many pixels before the 4x upscale. | |
| num_inference_steps: denoising steps per frame (the model is distilled for 4). | |
| seed: RNG seed for the initial latents. | |
| Returns: | |
| The Stream-DiffVSR 4x video, a bicubic 4x video for reference, and a status line. | |
| """ | |
| if not video: | |
| raise gr.Error("Please upload a low-resolution video first.") | |
| num_frames = int(num_frames) | |
| num_inference_steps = int(num_inference_steps) | |
| max_input_size = int(max_input_size) | |
| seed = int(seed) | |
| frames, fps = _read_frames(video, num_frames) | |
| lowres = _prepare_lowres(frames, max_input_size) | |
| in_w, in_h = lowres[0].size | |
| out_w, out_h = in_w * SCALE, in_h * SCALE | |
| generator = torch.Generator(device="cuda").manual_seed(seed) | |
| start = time.perf_counter() | |
| with torch.inference_mode(): | |
| result = pipe( | |
| "", | |
| lowres, | |
| num_inference_steps=num_inference_steps, | |
| guidance_scale=0, | |
| of_model=of_model, | |
| generator=generator, | |
| ) | |
| elapsed = time.perf_counter() - start | |
| sr_frames = [f[0] if isinstance(f, (list, tuple)) else f for f in result.images] | |
| bicubic_frames = [f.resize((out_w, out_h), Image.BICUBIC) for f in lowres] | |
| sr_path = _write_video(sr_frames, fps, "streamdiffvsr") | |
| bicubic_path = _write_video(bicubic_frames, fps, "bicubic") | |
| status = ( | |
| f"{len(sr_frames)} frames · {in_w}x{in_h} → {out_w}x{out_h} · " | |
| f"{num_inference_steps} steps · {elapsed:.1f}s " | |
| f"({elapsed / max(len(sr_frames), 1):.2f}s/frame)" | |
| ) | |
| return sr_path, bicubic_path, status | |
| # ------------------------------------------------------------------------ UI | |
| CSS = """ | |
| #col-container { max-width: 1200px; margin: 0 auto; } | |
| .dark .gradio-container { color: var(--body-text-color); } | |
| """ | |
| with gr.Blocks() as demo: | |
| with gr.Column(elem_id="col-container"): | |
| gr.Markdown( | |
| """ | |
| # Stream-DiffVSR · 4× Video Super-Resolution | |
| Low-latency streamable video super-resolution via auto-regressive diffusion — a | |
| causal, 4-step distilled denoiser with Auto-regressive Temporal Guidance (ARTG) | |
| and a temporal-aware decoder, so every frame is restored from **past frames only**. | |
| [Paper](https://huggingface.co/papers/2512.23709) · | |
| [Model](https://huggingface.co/Jamichsu/Stream-DiffVSR) · | |
| [Code](https://github.com/jamichss/Stream-DiffVSR) · | |
| [Project page](https://jamichss.github.io/stream-diffvsr-project-page/) | |
| """ | |
| ) | |
| with gr.Row(): | |
| with gr.Column(scale=1): | |
| video_in = gr.Video( | |
| label="Low-resolution video", | |
| sources=["upload"], | |
| height=300, | |
| ) | |
| run_btn = gr.Button("Upscale 4×", variant="primary") | |
| status_out = gr.Markdown() | |
| with gr.Column(scale=2): | |
| with gr.Row(): | |
| sr_out = gr.Video(label="Stream-DiffVSR 4×", autoplay=True, loop=True) | |
| bicubic_out = gr.Video(label="Bicubic 4× (reference)", autoplay=True, loop=True) | |
| with gr.Accordion("Advanced settings", open=False): | |
| with gr.Row(): | |
| num_frames = gr.Slider( | |
| label="Frames to process", | |
| minimum=4, | |
| maximum=40, | |
| step=1, | |
| value=30, | |
| ) | |
| max_input_size = gr.Slider( | |
| label="Max input size (long side, px)", | |
| minimum=128, | |
| maximum=384, | |
| step=32, | |
| value=320, | |
| info="The clip is downscaled to this long side (and a matching 16:9 pixel budget) before the 4× upscale.", | |
| ) | |
| with gr.Row(): | |
| num_inference_steps = gr.Slider( | |
| label="Denoising steps", | |
| minimum=1, | |
| maximum=8, | |
| step=1, | |
| value=4, | |
| info="The model is distilled for 4 steps.", | |
| ) | |
| seed = gr.Number(label="Seed", value=42, precision=0) | |
| gr.Examples( | |
| examples=[ | |
| ["examples/portrait_lr.mp4"], | |
| ["examples/city_night_lr.mp4"], | |
| ["examples/waterfall_lr.mp4"], | |
| ], | |
| inputs=[video_in], | |
| outputs=[sr_out, bicubic_out, status_out], | |
| fn=upscale_video, | |
| cache_examples=True, | |
| cache_mode="lazy", | |
| label="Examples (low-resolution 320×176 clips)", | |
| ) | |
| run_btn.click( | |
| fn=upscale_video, | |
| inputs=[video_in, num_frames, max_input_size, num_inference_steps, seed], | |
| outputs=[sr_out, bicubic_out, status_out], | |
| api_name="upscale_video", | |
| ) | |
| if __name__ == "__main__": | |
| # Gradio 6 moved `theme` / `css` from the Blocks constructor to launch() | |
| demo.launch(theme=gr.themes.Citrus(), css=CSS, mcp_server=True) | |