multimodalart's picture
multimodalart HF Staff
Upload folder using huggingface_hub
78f9940 verified
Raw History Blame Contribute Delete
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
@spaces.GPU(duration=_estimate_duration)
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)