Spaces:
Running on Zero
Running on Zero
Commit ·
6870c03
1
Parent(s): 4445f96
Add Gradio demo app for NanoVSR
Browse files- .gitignore +8 -0
- README.md +49 -3
- app.py +255 -0
- assets/examples/reds4_000_lr.mp4 +3 -0
- assets/teaser.mp4 +3 -0
- inference.py +111 -0
- models/__init__.py +0 -0
- models/nanovsr.py +187 -0
- requirements.txt +8 -0
.gitignore
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
__pycache__/
|
| 2 |
+
*.pyc
|
| 3 |
+
checkpoints/
|
| 4 |
+
.venv/
|
| 5 |
+
*.mp4_nanovsr_x*.mp4
|
| 6 |
+
gradio_cached_examples/
|
| 7 |
+
flagged/
|
| 8 |
+
.DS_Store
|
README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
emoji:
|
| 4 |
colorFrom: yellow
|
| 5 |
colorTo: green
|
| 6 |
sdk: gradio
|
|
@@ -12,4 +12,50 @@ license: mit
|
|
| 12 |
short_description: Real-time 4x video super-resolution on edge devices
|
| 13 |
---
|
| 14 |
|
| 15 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: 'NanoVSR: Towards Real-Time Video Super-Resolution on Edge Devices'
|
| 3 |
+
emoji: 🔍
|
| 4 |
colorFrom: yellow
|
| 5 |
colorTo: green
|
| 6 |
sdk: gradio
|
|
|
|
| 12 |
short_description: Real-time 4x video super-resolution on edge devices
|
| 13 |
---
|
| 14 |
|
| 15 |
+
<div align="center">
|
| 16 |
+
|
| 17 |
+
# NanoVSR — Real-Time 4× Video Super-Resolution on Edge Devices
|
| 18 |
+
|
| 19 |
+
[](https://arxiv.org/abs/2607.10495) [](https://github.com/filippawlicki/nanovsr) [](https://github.com/filippawlicki/nanovsr/blob/main/LICENSE)
|
| 20 |
+
|
| 21 |
+
</div>
|
| 22 |
+
|
| 23 |
+
Interactive demo for **NanoVSR** (ECCV 2026), a fully convolutional, bidirectional-recurrent video
|
| 24 |
+
super-resolution model built for edge devices. It reaches **28.64 dB on REDS4** at **27 FPS on a
|
| 25 |
+
Jetson Orin NX (25 W)**, with no custom CUDA ops — just plain 3×3 convolutions after
|
| 26 |
+
reparameterization.
|
| 27 |
+
|
| 28 |
+
Upload a low-resolution clip (or try the built-in REDS4 example) and pick one of four model sizes
|
| 29 |
+
to see NanoVSR upscale it 4× in real time, with a side-by-side comparison and a before/after slider.
|
| 30 |
+
Inference runs on Hugging Face **ZeroGPU**.
|
| 31 |
+
|
| 32 |
+
## Model zoo
|
| 33 |
+
|
| 34 |
+
| Model | Params | REDS4 PSNR/SSIM | Orin NX (FPS) |
|
| 35 |
+
| :--- | ---: | :---: | ---: |
|
| 36 |
+
| NanoVSR-226k | 226k | 28.23 / 0.8057 | 43.86 |
|
| 37 |
+
| NanoVSR-644k (baseline) | 644k | 28.64 / 0.8215 | 27.20 |
|
| 38 |
+
| NanoVSR-1.7M | 1.7M | 29.15 / 0.8364 | 19.58 |
|
| 39 |
+
| NanoVSR-5.4M | 5.4M | 29.73 / 0.8526 | 8.66 |
|
| 40 |
+
|
| 41 |
+
Checkpoints are downloaded on demand from the [GitHub releases](https://github.com/filippawlicki/nanovsr/releases)
|
| 42 |
+
of the main repo and cached for the lifetime of the Space.
|
| 43 |
+
|
| 44 |
+
## Links
|
| 45 |
+
|
| 46 |
+
- Paper: [arXiv:2607.10495](https://arxiv.org/abs/2607.10495)
|
| 47 |
+
- Code, training and TensorRT deployment: [github.com/filippawlicki/nanovsr](https://github.com/filippawlicki/nanovsr)
|
| 48 |
+
|
| 49 |
+
## Citation
|
| 50 |
+
|
| 51 |
+
```bibtex
|
| 52 |
+
@misc{pawlicki2026nanovsr,
|
| 53 |
+
title={NanoVSR: Towards Real-Time Video Super-Resolution on Edge Devices},
|
| 54 |
+
author={Filip Pawlicki and Marcel Kańduła and Marcin Pucek and Kamil Dobies},
|
| 55 |
+
year={2026},
|
| 56 |
+
eprint={2607.10495},
|
| 57 |
+
archivePrefix={arXiv},
|
| 58 |
+
primaryClass={cs.CV},
|
| 59 |
+
url={https://arxiv.org/abs/2607.10495},
|
| 60 |
+
}
|
| 61 |
+
```
|
app.py
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""NanoVSR — real-time video super-resolution, interactive Gradio demo.
|
| 2 |
+
|
| 3 |
+
Paper: "NanoVSR: Towards Real-Time Video Super-Resolution on Edge Devices" (ECCV 2026)
|
| 4 |
+
Code: https://github.com/filippawlicki/nanovsr
|
| 5 |
+
"""
|
| 6 |
+
import os
|
| 7 |
+
import tempfile
|
| 8 |
+
import time
|
| 9 |
+
|
| 10 |
+
import cv2
|
| 11 |
+
import gradio as gr
|
| 12 |
+
import imageio.v2 as imageio
|
| 13 |
+
import numpy as np
|
| 14 |
+
import torch
|
| 15 |
+
|
| 16 |
+
try:
|
| 17 |
+
import spaces
|
| 18 |
+
except ImportError:
|
| 19 |
+
class _NoOpGPU:
|
| 20 |
+
def __call__(self, fn=None, **kwargs):
|
| 21 |
+
if fn is not None:
|
| 22 |
+
return fn
|
| 23 |
+
return lambda f: f
|
| 24 |
+
|
| 25 |
+
class spaces:
|
| 26 |
+
GPU = _NoOpGPU()
|
| 27 |
+
|
| 28 |
+
from inference import (
|
| 29 |
+
DEFAULT_MODEL,
|
| 30 |
+
MODEL_INFO,
|
| 31 |
+
SCALE,
|
| 32 |
+
get_model,
|
| 33 |
+
run_chunked_inference,
|
| 34 |
+
)
|
| 35 |
+
|
| 36 |
+
ASSETS_DIR = os.path.join(os.path.dirname(__file__), "assets")
|
| 37 |
+
EXAMPLE_VIDEO = os.path.join(ASSETS_DIR, "examples", "reds4_000_lr.mp4")
|
| 38 |
+
TEASER_VIDEO = os.path.join(ASSETS_DIR, "teaser.mp4")
|
| 39 |
+
|
| 40 |
+
gr.set_static_paths([ASSETS_DIR])
|
| 41 |
+
|
| 42 |
+
MAX_SIDE_CHOICES = [180, 240, 320, 480]
|
| 43 |
+
DIVIDER_PX = 8
|
| 44 |
+
|
| 45 |
+
|
| 46 |
+
def _even(n):
|
| 47 |
+
return n - (n % 2)
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
def _read_video(path, max_frames):
|
| 51 |
+
reader = imageio.get_reader(path)
|
| 52 |
+
meta = reader.get_meta_data()
|
| 53 |
+
fps = meta.get("fps", 25.0) or 25.0
|
| 54 |
+
|
| 55 |
+
frames = []
|
| 56 |
+
for i, frame in enumerate(reader):
|
| 57 |
+
if max_frames is not None and i >= max_frames:
|
| 58 |
+
break
|
| 59 |
+
if frame.shape[-1] == 4:
|
| 60 |
+
frame = frame[..., :3]
|
| 61 |
+
frames.append(frame)
|
| 62 |
+
reader.close()
|
| 63 |
+
|
| 64 |
+
if not frames:
|
| 65 |
+
raise gr.Error("Could not read any frames from the input video.")
|
| 66 |
+
return frames, float(fps)
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
def _resize_to_cap(frames, max_side):
|
| 70 |
+
h, w = frames[0].shape[:2]
|
| 71 |
+
longest = max(h, w)
|
| 72 |
+
if longest <= max_side:
|
| 73 |
+
new_h, new_w = _even(h), _even(w)
|
| 74 |
+
else:
|
| 75 |
+
scale = max_side / longest
|
| 76 |
+
new_h, new_w = _even(int(round(h * scale))), _even(int(round(w * scale)))
|
| 77 |
+
new_h, new_w = max(new_h, 8), max(new_w, 8)
|
| 78 |
+
|
| 79 |
+
if (new_h, new_w) == (h, w):
|
| 80 |
+
return frames, (h, w)
|
| 81 |
+
resized = [cv2.resize(f, (new_w, new_h), interpolation=cv2.INTER_AREA) for f in frames]
|
| 82 |
+
return resized, (h, w)
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
def _label(img, text):
|
| 86 |
+
img = np.ascontiguousarray(img)
|
| 87 |
+
scale = max(0.5, img.shape[0] / 720.0)
|
| 88 |
+
thickness = max(1, int(round(2 * scale)))
|
| 89 |
+
origin = (int(16 * scale), int(34 * scale))
|
| 90 |
+
cv2.putText(img, text, origin, cv2.FONT_HERSHEY_SIMPLEX, scale, (0, 0, 0), thickness * 3, cv2.LINE_AA)
|
| 91 |
+
cv2.putText(img, text, origin, cv2.FONT_HERSHEY_SIMPLEX, scale, (255, 255, 255), thickness, cv2.LINE_AA)
|
| 92 |
+
return img
|
| 93 |
+
|
| 94 |
+
|
| 95 |
+
def _make_comparison_frame(lq_rgb, sr_rgb):
|
| 96 |
+
h, w = sr_rgb.shape[:2]
|
| 97 |
+
base = cv2.resize(lq_rgb, (w, h), interpolation=cv2.INTER_NEAREST)
|
| 98 |
+
base = _label(base, "Input (LR, nearest-upscaled)")
|
| 99 |
+
sr = _label(np.ascontiguousarray(sr_rgb), "NanoVSR")
|
| 100 |
+
divider = np.full((h, DIVIDER_PX, 3), 255, dtype=np.uint8)
|
| 101 |
+
return np.hstack([base, divider, sr])
|
| 102 |
+
|
| 103 |
+
|
| 104 |
+
def _estimate_duration(model_name, frames, chunk_size, use_fp16, progress=None):
|
| 105 |
+
del use_fp16, progress
|
| 106 |
+
n = len(frames)
|
| 107 |
+
factor = {"NanoVSR-226k (fastest)": 0.15, "NanoVSR-644k (baseline)": 0.25,
|
| 108 |
+
"NanoVSR-1.7M": 0.4, "NanoVSR-5.4M (best quality)": 0.8}.get(model_name, 0.3)
|
| 109 |
+
return int(min(180, max(25, 20 + n * factor)))
|
| 110 |
+
|
| 111 |
+
|
| 112 |
+
@spaces.GPU(duration=_estimate_duration)
|
| 113 |
+
def _gpu_infer(model_name, frames, chunk_size, use_fp16, progress=gr.Progress()):
|
| 114 |
+
model = get_model(model_name)
|
| 115 |
+
device = "cuda" if torch.cuda.is_available() else "cpu"
|
| 116 |
+
|
| 117 |
+
def cb(done, total):
|
| 118 |
+
progress(done / total, desc=f"Upscaling frames ({done}/{total}) on {device.upper()}")
|
| 119 |
+
|
| 120 |
+
progress(0, desc="Starting inference...")
|
| 121 |
+
return run_chunked_inference(model, frames, chunk_size, device, use_fp16, progress_cb=cb)
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
def upscale(video_path, model_name, chunk_size, max_frames, max_side, use_fp16,
|
| 125 |
+
progress=gr.Progress()):
|
| 126 |
+
if not video_path:
|
| 127 |
+
raise gr.Error("Please upload or select a low-resolution video first.")
|
| 128 |
+
|
| 129 |
+
progress(0, desc="Reading video...")
|
| 130 |
+
frames, fps = _read_video(video_path, max_frames)
|
| 131 |
+
frames, orig_size = _resize_to_cap(frames, max_side)
|
| 132 |
+
in_h, in_w = frames[0].shape[:2]
|
| 133 |
+
|
| 134 |
+
t0 = time.time()
|
| 135 |
+
sr_frames = _gpu_infer(model_name, frames, int(chunk_size), bool(use_fp16), progress)
|
| 136 |
+
elapsed = time.time() - t0
|
| 137 |
+
|
| 138 |
+
progress(0.95, desc="Encoding output video...")
|
| 139 |
+
sr_h, sr_w = sr_frames.shape[1:3]
|
| 140 |
+
|
| 141 |
+
out_frames = [_make_comparison_frame(frames[i], sr_frames[i]) for i in range(len(frames))]
|
| 142 |
+
preview_before = cv2.resize(frames[0], (sr_w, sr_h), interpolation=cv2.INTER_NEAREST)
|
| 143 |
+
|
| 144 |
+
out_fd, out_path = tempfile.mkstemp(suffix=f"_nanovsr_x{SCALE}.mp4")
|
| 145 |
+
os.close(out_fd)
|
| 146 |
+
out_fps = max(1.0, min(fps, 60.0))
|
| 147 |
+
writer = imageio.get_writer(out_path, fps=out_fps, codec="libx264",
|
| 148 |
+
ffmpeg_params=["-crf", "18"], macro_block_size=1)
|
| 149 |
+
for f in out_frames:
|
| 150 |
+
writer.append_data(f)
|
| 151 |
+
writer.close()
|
| 152 |
+
|
| 153 |
+
resize_note = (f" (downscaled from {orig_size[1]}x{orig_size[0]} to fit the "
|
| 154 |
+
f"{max_side}px cap)" if (orig_size[1], orig_size[0]) != (in_w, in_h) else "")
|
| 155 |
+
status = (
|
| 156 |
+
f"**Model:** {model_name} · **{MODEL_INFO[model_name]['psnr']}**\n\n"
|
| 157 |
+
f"**Resolution:** {in_w}x{in_h}{resize_note} → {sr_w}x{sr_h} (x{SCALE})\n\n"
|
| 158 |
+
f"**Frames processed:** {len(frames)} · **Inference time:** {elapsed:.2f}s "
|
| 159 |
+
f"({len(frames) / max(elapsed, 1e-6):.1f} FPS)"
|
| 160 |
+
)
|
| 161 |
+
preview = (preview_before, np.ascontiguousarray(sr_frames[0]))
|
| 162 |
+
return out_path, preview, status
|
| 163 |
+
|
| 164 |
+
|
| 165 |
+
THEME = gr.themes.Soft(primary_hue="blue", secondary_hue="cyan")
|
| 166 |
+
|
| 167 |
+
CSS = """
|
| 168 |
+
.status-box { font-size: 0.95em; }
|
| 169 |
+
.badge-row { display: flex !important; gap: 8px; justify-content: center; flex-wrap: wrap; margin: 0; }
|
| 170 |
+
.badge-row a, .badge-row img { display: inline-block !important; margin: 0 !important; }
|
| 171 |
+
"""
|
| 172 |
+
|
| 173 |
+
with gr.Blocks(title="NanoVSR: Towards Real-Time Video Super-Resolution on Edge Devices") as demo:
|
| 174 |
+
gr.Markdown(
|
| 175 |
+
"""
|
| 176 |
+
<div align="center">
|
| 177 |
+
|
| 178 |
+
# NanoVSR — Real-Time 4× Video Super-Resolution on Edge Devices
|
| 179 |
+
|
| 180 |
+
<p class="badge-row" style="display:flex; gap:8px; justify-content:center; flex-wrap:wrap; margin:0;">
|
| 181 |
+
<a href="https://arxiv.org/abs/2607.10495"><img src="https://img.shields.io/badge/arXiv-2607.10495-b31b1b" alt="arXiv"></a>
|
| 182 |
+
<a href="https://github.com/filippawlicki/nanovsr"><img src="https://img.shields.io/badge/GitHub-nanovsr-181717?logo=github" alt="GitHub"></a>
|
| 183 |
+
<a href="https://github.com/filippawlicki/nanovsr/blob/main/LICENSE"><img src="https://img.shields.io/badge/License-MIT-green.svg" alt="License"></a>
|
| 184 |
+
</p>
|
| 185 |
+
|
| 186 |
+
</div>
|
| 187 |
+
|
| 188 |
+
A fully convolutional, bidirectional-recurrent video super-resolution model that reaches
|
| 189 |
+
**27–44 FPS on a Jetson Orin NX** while upscaling 4×. Upload a low-resolution clip below and
|
| 190 |
+
watch NanoVSR sharpen it in real time — or press one of the examples to try it instantly.
|
| 191 |
+
"""
|
| 192 |
+
)
|
| 193 |
+
gr.HTML(
|
| 194 |
+
f'<video id="header-video" autoplay muted loop playsinline '
|
| 195 |
+
f'style="width:100%; border-radius:12px; display:block;">'
|
| 196 |
+
f'<source src="/gradio_api/file={TEASER_VIDEO}" type="video/mp4"></video>'
|
| 197 |
+
)
|
| 198 |
+
|
| 199 |
+
with gr.Row():
|
| 200 |
+
with gr.Column(scale=1):
|
| 201 |
+
video_in = gr.Video(label="Low-resolution input video", sources=["upload"])
|
| 202 |
+
gr.Examples(
|
| 203 |
+
examples=[[EXAMPLE_VIDEO]],
|
| 204 |
+
inputs=[video_in],
|
| 205 |
+
label="Example (REDS4 clip '000', the paper's own benchmark set)",
|
| 206 |
+
)
|
| 207 |
+
model_dd = gr.Dropdown(
|
| 208 |
+
choices=list(MODEL_INFO.keys()), value=DEFAULT_MODEL, label="Model",
|
| 209 |
+
info="Bigger models = higher quality, more compute.",
|
| 210 |
+
)
|
| 211 |
+
with gr.Accordion("Advanced options", open=False):
|
| 212 |
+
chunk_size = gr.Slider(4, 30, value=15, step=1, label="Temporal chunk size (T)",
|
| 213 |
+
info="Frames processed per forward pass. The paper's edge setting is 15.")
|
| 214 |
+
max_frames = gr.Slider(8, 150, value=60, step=1, label="Max frames to process",
|
| 215 |
+
info="Caps runtime; trim long videos to the first N frames.")
|
| 216 |
+
max_side = gr.Dropdown(MAX_SIDE_CHOICES, value=320, label="Max input side (px)",
|
| 217 |
+
info="Input is downscaled (never upscaled) to this cap before "
|
| 218 |
+
"running NanoVSR, which expects genuinely low-res input.")
|
| 219 |
+
fp16 = gr.Checkbox(value=True, label="FP16 inference (GPU only, faster)")
|
| 220 |
+
run_btn = gr.Button("Upscale ▶", variant="primary")
|
| 221 |
+
|
| 222 |
+
with gr.Column(scale=1):
|
| 223 |
+
video_out = gr.Video(label="NanoVSR result", buttons=["download"])
|
| 224 |
+
slider_out = gr.ImageSlider(label="Before / after (first frame)", buttons=[])
|
| 225 |
+
status_out = gr.Markdown(elem_classes=["status-box"])
|
| 226 |
+
|
| 227 |
+
run_btn.click(
|
| 228 |
+
fn=upscale,
|
| 229 |
+
inputs=[video_in, model_dd, chunk_size, max_frames, max_side, fp16],
|
| 230 |
+
outputs=[video_out, slider_out, status_out],
|
| 231 |
+
)
|
| 232 |
+
|
| 233 |
+
with gr.Accordion("Model zoo & citation", open=False):
|
| 234 |
+
model_list_md = "\n".join(f"- **{name}** — {info['psnr']}" for name, info in MODEL_INFO.items())
|
| 235 |
+
citation_md = model_list_md + "\n\n" + (
|
| 236 |
+
"""All models perform 4× upscaling; larger models trade speed for quality. Full details,
|
| 237 |
+
training code and TensorRT deployment scripts are in the
|
| 238 |
+
[GitHub repo](https://github.com/filippawlicki/nanovsr).
|
| 239 |
+
|
| 240 |
+
```bibtex
|
| 241 |
+
@misc{pawlicki2026nanovsr,
|
| 242 |
+
title={NanoVSR: Towards Real-Time Video Super-Resolution on Edge Devices},
|
| 243 |
+
author={Filip Pawlicki and Marcel Kańduła and Marcin Pucek and Kamil Dobies},
|
| 244 |
+
year={2026},
|
| 245 |
+
eprint={2607.10495},
|
| 246 |
+
archivePrefix={arXiv},
|
| 247 |
+
primaryClass={cs.CV},
|
| 248 |
+
url={https://arxiv.org/abs/2607.10495},
|
| 249 |
+
}
|
| 250 |
+
```"""
|
| 251 |
+
)
|
| 252 |
+
gr.Markdown(citation_md)
|
| 253 |
+
|
| 254 |
+
if __name__ == "__main__":
|
| 255 |
+
demo.queue().launch(theme=THEME, css=CSS)
|
assets/examples/reds4_000_lr.mp4
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:aa60410340d9c8d9f93c8ec74d89b29bb7315644ad4468131df4a736fbd891b0
|
| 3 |
+
size 568786
|
assets/teaser.mp4
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:d4ba8cdb816963a41689599f8299ce31eb1bdc956965acacb907b37836fde8bd
|
| 3 |
+
size 748387
|
inference.py
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Checkpoint management and inference helpers for the NanoVSR Gradio demo.
|
| 2 |
+
|
| 3 |
+
Vendored/adapted from the main nanovsr repo's utils.py + demo.py so this Space
|
| 4 |
+
has no dependency on the training repo at runtime.
|
| 5 |
+
"""
|
| 6 |
+
import os
|
| 7 |
+
|
| 8 |
+
import numpy as np
|
| 9 |
+
import requests
|
| 10 |
+
import torch
|
| 11 |
+
|
| 12 |
+
from models.nanovsr import NanoVSR
|
| 13 |
+
|
| 14 |
+
SCALE = 4
|
| 15 |
+
CKPT_DIR = os.path.join(os.path.dirname(__file__), "checkpoints")
|
| 16 |
+
RELEASE_BASE = "https://github.com/filippawlicki/nanovsr/releases/download/v1.0"
|
| 17 |
+
|
| 18 |
+
MODEL_INFO = {
|
| 19 |
+
"NanoVSR-226k (fastest)": dict(file="nanovsr_226k.pth", num_feat=32, num_blocks=8,
|
| 20 |
+
psnr="28.23 dB REDS4"),
|
| 21 |
+
"NanoVSR-644k (baseline)": dict(file="nanovsr_644k.pth", num_feat=48, num_blocks=12,
|
| 22 |
+
psnr="28.64 dB REDS4"),
|
| 23 |
+
"NanoVSR-1.7M": dict(file="nanovsr_1.7m.pth", num_feat=64, num_blocks=20,
|
| 24 |
+
psnr="29.15 dB REDS4"),
|
| 25 |
+
"NanoVSR-5.4M (best quality)": dict(file="nanovsr_5.4m.pth", num_feat=96, num_blocks=30,
|
| 26 |
+
psnr="29.73 dB REDS4"),
|
| 27 |
+
}
|
| 28 |
+
DEFAULT_MODEL = "NanoVSR-644k (baseline)"
|
| 29 |
+
|
| 30 |
+
_model_cache = {}
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def ensure_checkpoint(model_name):
|
| 34 |
+
info = MODEL_INFO[model_name]
|
| 35 |
+
os.makedirs(CKPT_DIR, exist_ok=True)
|
| 36 |
+
path = os.path.join(CKPT_DIR, info["file"])
|
| 37 |
+
if not os.path.exists(path):
|
| 38 |
+
url = f"{RELEASE_BASE}/{info['file']}"
|
| 39 |
+
tmp_path = path + ".part"
|
| 40 |
+
with requests.get(url, stream=True, timeout=60) as resp:
|
| 41 |
+
resp.raise_for_status()
|
| 42 |
+
with open(tmp_path, "wb") as f:
|
| 43 |
+
for chunk in resp.iter_content(chunk_size=1 << 20):
|
| 44 |
+
f.write(chunk)
|
| 45 |
+
os.replace(tmp_path, path)
|
| 46 |
+
return path
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
def get_model(model_name):
|
| 50 |
+
"""Load (and cache on CPU) the requested NanoVSR variant."""
|
| 51 |
+
if model_name in _model_cache:
|
| 52 |
+
return _model_cache[model_name]
|
| 53 |
+
|
| 54 |
+
info = MODEL_INFO[model_name]
|
| 55 |
+
checkpoint_path = ensure_checkpoint(model_name)
|
| 56 |
+
state_dict = torch.load(checkpoint_path, map_location="cpu")
|
| 57 |
+
if isinstance(state_dict, dict):
|
| 58 |
+
for key in ["params_ema", "params", "model_state_dict"]:
|
| 59 |
+
if key in state_dict:
|
| 60 |
+
state_dict = state_dict[key]
|
| 61 |
+
break
|
| 62 |
+
|
| 63 |
+
model = NanoVSR(num_feat=info["num_feat"], num_blocks=info["num_blocks"])
|
| 64 |
+
model.load_state_dict(state_dict, strict=False)
|
| 65 |
+
model.switch_to_deploy()
|
| 66 |
+
model.eval()
|
| 67 |
+
|
| 68 |
+
_model_cache[model_name] = model
|
| 69 |
+
return model
|
| 70 |
+
|
| 71 |
+
|
| 72 |
+
def frames_to_tensor(frames_rgb):
|
| 73 |
+
"""List/array of [H, W, 3] uint8 RGB frames -> [T, 3, H, W] float tensor in [0, 1]."""
|
| 74 |
+
batch = np.stack(frames_rgb, axis=0).transpose(0, 3, 1, 2)
|
| 75 |
+
batch = np.ascontiguousarray(batch)
|
| 76 |
+
return torch.from_numpy(batch).float() / 255.0
|
| 77 |
+
|
| 78 |
+
|
| 79 |
+
def tensor_to_frames(tensor):
|
| 80 |
+
"""[T, 3, H, W] float tensor in [0, 1] -> [T, H, W, 3] uint8 RGB numpy array."""
|
| 81 |
+
arr = (tensor.clamp(0, 1).mul(255.0).round().to(torch.uint8)
|
| 82 |
+
.permute(0, 2, 3, 1).contiguous().cpu().numpy())
|
| 83 |
+
return arr
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
@torch.inference_mode()
|
| 87 |
+
def run_chunked_inference(model, frames_rgb, chunk_size, device, use_fp16, progress_cb=None):
|
| 88 |
+
"""Run NanoVSR over a list of LR RGB frames in temporal chunks.
|
| 89 |
+
|
| 90 |
+
Returns a [T, H*4, W*4, 3] uint8 RGB numpy array.
|
| 91 |
+
"""
|
| 92 |
+
model = model.to(device)
|
| 93 |
+
autocast_ctx = (torch.autocast(device_type="cuda", dtype=torch.float16)
|
| 94 |
+
if use_fp16 and device == "cuda" else torch.autocast(device_type="cpu", enabled=False))
|
| 95 |
+
|
| 96 |
+
outputs = []
|
| 97 |
+
n = len(frames_rgb)
|
| 98 |
+
for start in range(0, n, chunk_size):
|
| 99 |
+
chunk = frames_rgb[start:start + chunk_size]
|
| 100 |
+
lq = frames_to_tensor(chunk).unsqueeze(0).to(device)
|
| 101 |
+
with autocast_ctx:
|
| 102 |
+
sr = model(lq)
|
| 103 |
+
outputs.append(tensor_to_frames(sr.float().squeeze(0)))
|
| 104 |
+
if progress_cb is not None:
|
| 105 |
+
progress_cb(min(start + chunk_size, n), n)
|
| 106 |
+
|
| 107 |
+
model.cpu()
|
| 108 |
+
if device == "cuda":
|
| 109 |
+
torch.cuda.empty_cache()
|
| 110 |
+
|
| 111 |
+
return np.concatenate(outputs, axis=0)
|
models/__init__.py
ADDED
|
File without changes
|
models/nanovsr.py
ADDED
|
@@ -0,0 +1,187 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
|
| 3 |
+
import torch
|
| 4 |
+
import torch.nn as nn
|
| 5 |
+
import torch.nn.functional as F
|
| 6 |
+
import numpy as np
|
| 7 |
+
|
| 8 |
+
class RepVGGBlock(nn.Module):
|
| 9 |
+
def __init__(self, in_channels, out_channels, stride=1, deploy=False):
|
| 10 |
+
super(RepVGGBlock, self).__init__()
|
| 11 |
+
self.deploy = deploy
|
| 12 |
+
self.stride = stride
|
| 13 |
+
self.in_channels = in_channels
|
| 14 |
+
self.out_channels = out_channels
|
| 15 |
+
self.activation = nn.LeakyReLU(0.1, inplace=True)
|
| 16 |
+
|
| 17 |
+
if deploy:
|
| 18 |
+
self.rbr_reparam = nn.Conv2d(in_channels, out_channels, kernel_size=3, stride=stride, padding=1, bias=True)
|
| 19 |
+
else:
|
| 20 |
+
self.rbr_identity = nn.BatchNorm2d(in_channels) if out_channels == in_channels and stride == 1 else None
|
| 21 |
+
self.rbr_dense = nn.Sequential(
|
| 22 |
+
nn.Conv2d(in_channels, out_channels, kernel_size=3, padding=1, bias=False),
|
| 23 |
+
nn.BatchNorm2d(out_channels)
|
| 24 |
+
)
|
| 25 |
+
self.rbr_1x1 = nn.Sequential(
|
| 26 |
+
nn.Conv2d(in_channels, out_channels, kernel_size=1, padding=0, bias=False),
|
| 27 |
+
nn.BatchNorm2d(out_channels)
|
| 28 |
+
)
|
| 29 |
+
|
| 30 |
+
def forward(self, x):
|
| 31 |
+
if self.deploy:
|
| 32 |
+
return self.activation(self.rbr_reparam(x))
|
| 33 |
+
|
| 34 |
+
if self.rbr_identity is None:
|
| 35 |
+
id_out = 0
|
| 36 |
+
else:
|
| 37 |
+
id_out = self.rbr_identity(x)
|
| 38 |
+
|
| 39 |
+
return self.activation(self.rbr_dense(x) + self.rbr_1x1(x) + id_out)
|
| 40 |
+
|
| 41 |
+
def get_equivalent_kernel_bias(self):
|
| 42 |
+
kernel3x3, bias3x3 = self._fuse_bn_tensor(self.rbr_dense)
|
| 43 |
+
|
| 44 |
+
kernel1x1, bias1x1 = self._fuse_bn_tensor(self.rbr_1x1)
|
| 45 |
+
|
| 46 |
+
kernelid, biasid = self._fuse_bn_tensor(self.rbr_identity)
|
| 47 |
+
|
| 48 |
+
return (
|
| 49 |
+
kernel3x3 + self._pad_1x1_to_3x3_tensor(kernel1x1) + kernelid,
|
| 50 |
+
bias3x3 + bias1x1 + biasid
|
| 51 |
+
)
|
| 52 |
+
|
| 53 |
+
def _pad_1x1_to_3x3_tensor(self, kernel1x1):
|
| 54 |
+
if kernel1x1 is None:
|
| 55 |
+
return 0
|
| 56 |
+
else:
|
| 57 |
+
return F.pad(kernel1x1, [1, 1, 1, 1])
|
| 58 |
+
|
| 59 |
+
def _fuse_bn_tensor(self, branch):
|
| 60 |
+
if branch is None:
|
| 61 |
+
return 0, 0
|
| 62 |
+
if isinstance(branch, nn.Sequential):
|
| 63 |
+
kernel = branch[0].weight
|
| 64 |
+
running_mean = branch[1].running_mean
|
| 65 |
+
running_var = branch[1].running_var
|
| 66 |
+
gamma = branch[1].weight
|
| 67 |
+
beta = branch[1].bias
|
| 68 |
+
eps = branch[1].eps
|
| 69 |
+
else:
|
| 70 |
+
assert isinstance(branch, nn.BatchNorm2d)
|
| 71 |
+
if not hasattr(self, 'id_tensor'):
|
| 72 |
+
input_dim = self.in_channels
|
| 73 |
+
kernel_value = np.zeros((self.in_channels, input_dim, 3, 3), dtype=np.float32)
|
| 74 |
+
for i in range(self.in_channels):
|
| 75 |
+
kernel_value[i, i % input_dim, 1, 1] = 1
|
| 76 |
+
self.id_tensor = torch.from_numpy(kernel_value).to(branch.weight.device)
|
| 77 |
+
kernel = self.id_tensor
|
| 78 |
+
running_mean = branch.running_mean
|
| 79 |
+
running_var = branch.running_var
|
| 80 |
+
gamma = branch.weight
|
| 81 |
+
beta = branch.bias
|
| 82 |
+
eps = branch.eps
|
| 83 |
+
|
| 84 |
+
std = (running_var + eps).sqrt()
|
| 85 |
+
t = (gamma / std).reshape(-1, 1, 1, 1)
|
| 86 |
+
return kernel * t, beta - running_mean * gamma / std
|
| 87 |
+
|
| 88 |
+
def switch_to_deploy(self):
|
| 89 |
+
if self.deploy:
|
| 90 |
+
return
|
| 91 |
+
kernel, bias = self.get_equivalent_kernel_bias()
|
| 92 |
+
self.rbr_reparam = nn.Conv2d(
|
| 93 |
+
in_channels=self.rbr_dense[0].in_channels,
|
| 94 |
+
out_channels=self.rbr_dense[0].out_channels,
|
| 95 |
+
kernel_size=self.rbr_dense[0].kernel_size,
|
| 96 |
+
stride=self.rbr_dense[0].stride,
|
| 97 |
+
padding=self.rbr_dense[0].padding,
|
| 98 |
+
bias=True)
|
| 99 |
+
self.rbr_reparam.weight.data = kernel
|
| 100 |
+
self.rbr_reparam.bias.data = bias
|
| 101 |
+
|
| 102 |
+
self.__delattr__('rbr_dense')
|
| 103 |
+
self.__delattr__('rbr_1x1')
|
| 104 |
+
if hasattr(self, 'rbr_identity'):
|
| 105 |
+
self.__delattr__('rbr_identity')
|
| 106 |
+
self.deploy = True
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
class PixelShuffleBlock(nn.Module):
|
| 110 |
+
def __init__(self, in_channels, out_channels, upscale_factor=2):
|
| 111 |
+
super(PixelShuffleBlock, self).__init__()
|
| 112 |
+
self.conv = nn.Conv2d(in_channels, out_channels * (upscale_factor ** 2), 3, 1, 1)
|
| 113 |
+
self.pixel_shuffle = nn.PixelShuffle(upscale_factor)
|
| 114 |
+
self.prelu = nn.PReLU()
|
| 115 |
+
|
| 116 |
+
def forward(self, x):
|
| 117 |
+
x = self.conv(x)
|
| 118 |
+
x = self.pixel_shuffle(x)
|
| 119 |
+
return self.prelu(x)
|
| 120 |
+
|
| 121 |
+
|
| 122 |
+
class NanoVSR(nn.Module):
|
| 123 |
+
def __init__(self, num_feat=32, num_blocks=7, deploy=False):
|
| 124 |
+
super(NanoVSR, self).__init__()
|
| 125 |
+
self.num_feat = num_feat
|
| 126 |
+
self.deploy = deploy
|
| 127 |
+
|
| 128 |
+
self.feat_extract = RepVGGBlock(3, num_feat, deploy=deploy)
|
| 129 |
+
|
| 130 |
+
self.forward_net = nn.Sequential(*[RepVGGBlock(num_feat, num_feat, deploy=deploy) for _ in range(num_blocks)])
|
| 131 |
+
self.backward_net = nn.Sequential(*[RepVGGBlock(num_feat, num_feat, deploy=deploy) for _ in range(num_blocks)])
|
| 132 |
+
|
| 133 |
+
self.fusion = nn.Conv2d(num_feat * 2, num_feat, 1, 1, 0, bias=True)
|
| 134 |
+
|
| 135 |
+
self.upsample1 = PixelShuffleBlock(num_feat, num_feat, upscale_factor=2)
|
| 136 |
+
self.upsample2 = PixelShuffleBlock(num_feat, 32, upscale_factor=2)
|
| 137 |
+
|
| 138 |
+
self.conv_last = nn.Conv2d(32, 3, 3, 1, 1, bias=True)
|
| 139 |
+
|
| 140 |
+
def forward(self, x):
|
| 141 |
+
b, t, c, h, w = x.size()
|
| 142 |
+
|
| 143 |
+
x_flat = x.view(-1, c, h, w)
|
| 144 |
+
feats = self.feat_extract(x_flat)
|
| 145 |
+
feats = feats.view(b, t, -1, h, w)
|
| 146 |
+
|
| 147 |
+
forward_feats = []
|
| 148 |
+
feat_prop = torch.zeros_like(feats[:, 0, ...])
|
| 149 |
+
for i in range(t):
|
| 150 |
+
feat_prop = self.forward_net(feats[:, i, ...] + feat_prop)
|
| 151 |
+
forward_feats.append(feat_prop)
|
| 152 |
+
|
| 153 |
+
backward_feats = []
|
| 154 |
+
feat_prop = torch.zeros_like(feats[:, 0, ...])
|
| 155 |
+
for i in range(t - 1, -1, -1):
|
| 156 |
+
feat_prop = self.backward_net(feats[:, i, ...] + feat_prop)
|
| 157 |
+
backward_feats.insert(0, feat_prop)
|
| 158 |
+
|
| 159 |
+
outputs = []
|
| 160 |
+
|
| 161 |
+
for i in range(t):
|
| 162 |
+
f_fused = torch.cat([forward_feats[i], backward_feats[i]], dim=1)
|
| 163 |
+
f_fused = self.fusion(f_fused)
|
| 164 |
+
|
| 165 |
+
out = self.upsample1(f_fused)
|
| 166 |
+
out = self.upsample2(out)
|
| 167 |
+
out = self.conv_last(out)
|
| 168 |
+
|
| 169 |
+
base = F.interpolate(x[:, i, ...], scale_factor=4, mode='bilinear', align_corners=False)
|
| 170 |
+
out += base
|
| 171 |
+
outputs.append(out)
|
| 172 |
+
|
| 173 |
+
final_video = torch.stack(outputs, dim=1)
|
| 174 |
+
|
| 175 |
+
return final_video
|
| 176 |
+
|
| 177 |
+
def switch_to_deploy(self):
|
| 178 |
+
if self.deploy:
|
| 179 |
+
return
|
| 180 |
+
|
| 181 |
+
for module in self.modules():
|
| 182 |
+
if module is self:
|
| 183 |
+
continue
|
| 184 |
+
if hasattr(module, 'switch_to_deploy'):
|
| 185 |
+
module.switch_to_deploy()
|
| 186 |
+
|
| 187 |
+
self.deploy = True
|
requirements.txt
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
gradio==6.20.0
|
| 2 |
+
spaces
|
| 3 |
+
torch>=2.1
|
| 4 |
+
numpy>=1.24
|
| 5 |
+
requests>=2.31
|
| 6 |
+
opencv-python-headless>=4.8
|
| 7 |
+
imageio>=2.34
|
| 8 |
+
imageio-ffmpeg>=0.5
|