Spaces:
Running on Zero
Running on Zero
Upload folder using huggingface_hub
Browse files- README.md +2 -2
- app.py +57 -50
- config.json +3 -3
- model.safetensors +1 -1
- model_distilled.pt +3 -0
- requirements.txt +0 -1
README.md
CHANGED
|
@@ -31,7 +31,7 @@ the two weights this project publishes:
|
|
| 31 |
| Model | Cosa è / What it is | Quando sceglierlo / Pick it when |
|
| 32 |
|---|---|---|
|
| 33 |
| 🔍 **Universal Upscale** (default) | LIIF v4 GAN, 4.60 M parametri, `model_scripted.pt` con tiled inference integrata | qualsiasi fattore 1x-9.2x o una dimensione target esatta, immagini grandi |
|
| 34 |
-
| ⚡ **Universal Upscale Distilled** | student knowledge-distilled, 1.30 M parametri,
|
| 35 |
|
| 36 |
Lo Student distillato ha una scala nativa **fissa**: sotto 9x l'output viene
|
| 37 |
ricampionato in area-downsample, sopra viene ricampionato in bicubico. La riga di stato indica
|
|
@@ -42,7 +42,7 @@ quale modello ha girato e se l'output è stato ricampionato. Model card:
|
|
| 42 |
|
| 43 |
1. **Carica un'immagine / Upload an image**: trascina o carica un'immagine dal tuo dispositivo.
|
| 44 |
2. **Scegli il modello / Pick the model**: lascia **Universal Upscale** (default) oppure passa a
|
| 45 |
-
**Universal Upscale Distilled** (
|
| 46 |
3. **Imposta la scala / Set the scale**:
|
| 47 |
- **Scale Factor**: scegli un fattore di scala da **1x fino a 9.2x** (passo 0.2x, es. 2x, 3.6x, 4x, 9.2x).
|
| 48 |
- **Target Long Side (px)**: in alternativa, specifica la dimensione in pixel del lato più lungo desiderato (es. 2048, 3000, 4096).
|
|
|
|
| 31 |
| Model | Cosa è / What it is | Quando sceglierlo / Pick it when |
|
| 32 |
|---|---|---|
|
| 33 |
| 🔍 **Universal Upscale** (default) | LIIF v4 GAN, 4.60 M parametri, `model_scripted.pt` con tiled inference integrata | qualsiasi fattore 1x-9.2x o una dimensione target esatta, immagini grandi |
|
| 34 |
+
| ⚡ **Universal Upscale Distilled** | student knowledge-distilled, 1.30 M parametri, TorchScript `model_distilled.pt` che gira sullo stesso device del modello di default | serve 9x in un solo passaggio: più leggero e veloce |
|
| 35 |
|
| 36 |
Lo Student distillato ha una scala nativa **fissa**: sotto 9x l'output viene
|
| 37 |
ricampionato in area-downsample, sopra viene ricampionato in bicubico. La riga di stato indica
|
|
|
|
| 42 |
|
| 43 |
1. **Carica un'immagine / Upload an image**: trascina o carica un'immagine dal tuo dispositivo.
|
| 44 |
2. **Scegli il modello / Pick the model**: lascia **Universal Upscale** (default) oppure passa a
|
| 45 |
+
**Universal Upscale Distilled** (nativo 9x, usa la stessa GPU del modello di default).
|
| 46 |
3. **Imposta la scala / Set the scale**:
|
| 47 |
- **Scale Factor**: scegli un fattore di scala da **1x fino a 9.2x** (passo 0.2x, es. 2x, 3.6x, 4x, 9.2x).
|
| 48 |
- **Target Long Side (px)**: in alternativa, specifica la dimensione in pixel del lato più lungo desiderato (es. 2048, 3000, 4096).
|
app.py
CHANGED
|
@@ -28,8 +28,11 @@ except ImportError:
|
|
| 28 |
return fn
|
| 29 |
|
| 30 |
MODEL_PATH = Path(__file__).resolve().parent / "model_scripted.pt"
|
| 31 |
-
|
| 32 |
-
#
|
|
|
|
|
|
|
|
|
|
| 33 |
DISTILLED_REPO_ID = "tailot/universal-upscale-distilled"
|
| 34 |
|
| 35 |
MAX_SIDE = 4096
|
|
@@ -44,7 +47,7 @@ DISTILLED_NATIVE = 9
|
|
| 44 |
# Plain strings (not (label, value) tuples) so the dropdown works on any Gradio 5 minor version;
|
| 45 |
# upscale() maps the label back to a model key.
|
| 46 |
MODEL_LIIF = "🔍 Universal Upscale - arbitrary scale 1x-9.2x (default)"
|
| 47 |
-
MODEL_DISTILLED = "⚡ Universal Upscale Distilled -
|
| 48 |
MODEL_CHOICES = [MODEL_LIIF, MODEL_DISTILLED]
|
| 49 |
|
| 50 |
|
|
@@ -52,11 +55,11 @@ def normalize_model_choice(value) -> str:
|
|
| 52 |
"""Map the dropdown selection to a model key.
|
| 53 |
|
| 54 |
Gradio pads short example rows with None, and a Dropdown can hand back either the label or
|
| 55 |
-
the value, so the mapping is deliberately loose: anything mentioning "distilled"
|
| 56 |
-
student, everything else stays on the published default.
|
| 57 |
"""
|
| 58 |
text = str(value or "").strip().lower()
|
| 59 |
-
return "distilled" if "distill" in text or text in ("onnx", "student") else "liif"
|
| 60 |
|
| 61 |
|
| 62 |
def load_model(device: str):
|
|
@@ -76,61 +79,64 @@ def resolve_factor(input_size, scale, target_long_side):
|
|
| 76 |
return factor, ""
|
| 77 |
|
| 78 |
|
| 79 |
-
|
| 80 |
-
_ONNX_NATIVE = 0
|
| 81 |
|
| 82 |
|
| 83 |
-
def
|
| 84 |
-
"""
|
| 85 |
|
| 86 |
-
The
|
| 87 |
-
|
| 88 |
-
|
| 89 |
"""
|
| 90 |
-
global
|
| 91 |
-
if
|
| 92 |
-
return
|
| 93 |
-
|
| 94 |
-
|
| 95 |
-
path = DISTILLED_ONNX_PATH
|
| 96 |
if not path.exists():
|
| 97 |
from huggingface_hub import hf_hub_download
|
| 98 |
|
| 99 |
-
path = Path(hf_hub_download(repo_id=DISTILLED_REPO_ID, filename="
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
|
|
|
|
|
|
|
|
|
| 114 |
"""Tiled inference in LR space for the fixed-factor student.
|
| 115 |
|
| 116 |
Same recipe as scripts/upscale_onnx.py: run the padded tile, resample the WHOLE padded tile
|
| 117 |
to its own target size (so the crop offset stays inside it), then paste the crop on the canvas.
|
| 118 |
"""
|
| 119 |
-
session = onnx_session()
|
| 120 |
-
input_name = session.get_inputs()[0].name
|
| 121 |
height, width = rgb.shape[:2]
|
| 122 |
canvas = np.zeros((out_h, out_w, 3), dtype=np.uint8)
|
| 123 |
resample = Image.Resampling.LANCZOS if native > scale else Image.Resampling.BICUBIC
|
| 124 |
tile = max(8, min(int(tile), height))
|
|
|
|
|
|
|
|
|
|
|
|
|
| 125 |
for top in range(0, height, tile):
|
| 126 |
for left in range(0, width, tile):
|
| 127 |
bottom, right = min(top + tile, height), min(left + tile, width)
|
| 128 |
-
y0,
|
| 129 |
-
|
| 130 |
patch = rgb[y0:y1, x0:x1].astype(np.float32) / 255.0
|
| 131 |
-
tensor =
|
| 132 |
-
|
| 133 |
-
|
|
|
|
| 134 |
full_w, full_h = int(round((y1 - y0) * scale)), int(round((x1 - x0) * scale))
|
| 135 |
sr = np.asarray(Image.fromarray(sr).resize((full_w, full_h), resample))
|
| 136 |
th, tw = int(round((bottom - top) * scale)), int(round((right - left) * scale))
|
|
@@ -162,15 +168,15 @@ def upscale(image, model_choice=None, scale: float = 4.0, target_long_side: floa
|
|
| 162 |
|
| 163 |
if choice == "distilled":
|
| 164 |
try:
|
| 165 |
-
|
| 166 |
except Exception as exc:
|
| 167 |
-
return None, f"Distilled
|
| 168 |
-
native =
|
| 169 |
-
arr = upscale_distilled(np.asarray(img, dtype=np.uint8), applied, native, out_w, out_h
|
| 170 |
-
|
| 171 |
-
model_note = f"Universal Upscale Distilled (
|
| 172 |
model_note += ", output resampled)" if abs(applied - native) > 1e-3 else ")"
|
| 173 |
-
device = f"
|
| 174 |
else:
|
| 175 |
device = "cuda" if torch.cuda.is_available() else "cpu"
|
| 176 |
model = load_model(device)
|
|
@@ -209,8 +215,9 @@ demo = gr.Interface(
|
|
| 209 |
description=(
|
| 210 |
"Pick the engine in the **Model** menu: **Universal Upscale** renders any factor from 1x up to the tested "
|
| 211 |
"maximum 9.2x (or an exact target resolution), **Universal Upscale Distilled** is the 1.3 M "
|
| 212 |
-
"parameter
|
| 213 |
-
"other factors. Tiled inference keeps high-resolution
|
|
|
|
| 214 |
"4096 px on the long side); the status line reports which model ran and what it did."
|
| 215 |
),
|
| 216 |
examples=example_files if example_files else None,
|
|
|
|
| 28 |
return fn
|
| 29 |
|
| 30 |
MODEL_PATH = Path(__file__).resolve().parent / "model_scripted.pt"
|
| 31 |
+
# The distilled student ships beside it as its own TorchScript file: the same torch runtime the
|
| 32 |
+
# default engine already uses, so the student lands on the Space GPU. The ONNX build was dropped
|
| 33 |
+
# because the Space installs the CPU-only `onnxruntime` wheel, which pinned it to CPU.
|
| 34 |
+
DISTILLED_PT_PATH = Path(__file__).resolve().parent / "model_distilled.pt"
|
| 35 |
+
# If the Space folder does not carry model_distilled.pt, it is pulled from the distilled repo on first use.
|
| 36 |
DISTILLED_REPO_ID = "tailot/universal-upscale-distilled"
|
| 37 |
|
| 38 |
MAX_SIDE = 4096
|
|
|
|
| 47 |
# Plain strings (not (label, value) tuples) so the dropdown works on any Gradio 5 minor version;
|
| 48 |
# upscale() maps the label back to a model key.
|
| 49 |
MODEL_LIIF = "🔍 Universal Upscale - arbitrary scale 1x-9.2x (default)"
|
| 50 |
+
MODEL_DISTILLED = "⚡ Universal Upscale Distilled - fixed 9x, runs on GPU"
|
| 51 |
MODEL_CHOICES = [MODEL_LIIF, MODEL_DISTILLED]
|
| 52 |
|
| 53 |
|
|
|
|
| 55 |
"""Map the dropdown selection to a model key.
|
| 56 |
|
| 57 |
Gradio pads short example rows with None, and a Dropdown can hand back either the label or
|
| 58 |
+
the value, so the mapping is deliberately loose: anything mentioning "distilled" (or the old
|
| 59 |
+
ONNX labels) is the fixed-factor student, everything else stays on the published default.
|
| 60 |
"""
|
| 61 |
text = str(value or "").strip().lower()
|
| 62 |
+
return "distilled" if "distill" in text or text in ("onnx", "student", "torchscript") else "liif"
|
| 63 |
|
| 64 |
|
| 65 |
def load_model(device: str):
|
|
|
|
| 79 |
return factor, ""
|
| 80 |
|
| 81 |
|
| 82 |
+
_DISTILLED = None # (model, native factor, device), built once per container
|
|
|
|
| 83 |
|
| 84 |
|
| 85 |
+
def distilled_model():
|
| 86 |
+
"""(model, native factor, device) for the distilled student, built once per container.
|
| 87 |
|
| 88 |
+
The student is loaded with the same torch runtime as the default engine, so it runs on the
|
| 89 |
+
Space GPU whenever there is one. ONNX Runtime was dropped on purpose: the Space installs the
|
| 90 |
+
CPU-only `onnxruntime` wheel, so the ONNX build of these weights could never reach the GPU.
|
| 91 |
"""
|
| 92 |
+
global _DISTILLED
|
| 93 |
+
if _DISTILLED is not None:
|
| 94 |
+
return _DISTILLED
|
| 95 |
+
path = DISTILLED_PT_PATH
|
|
|
|
|
|
|
| 96 |
if not path.exists():
|
| 97 |
from huggingface_hub import hf_hub_download
|
| 98 |
|
| 99 |
+
path = Path(hf_hub_download(repo_id=DISTILLED_REPO_ID, filename="model_scripted.pt"))
|
| 100 |
+
device = "cuda" if torch.cuda.is_available() else "cpu"
|
| 101 |
+
model = torch.jit.load(str(path), map_location=device)
|
| 102 |
+
model.eval()
|
| 103 |
+
with torch.no_grad():
|
| 104 |
+
# Read the fixed factor from the model itself instead of trusting a constant.
|
| 105 |
+
probe = model(torch.zeros(1, 3, 8, 9, device=device))
|
| 106 |
+
native = int(round(max(probe.shape[-2] / 8.0, probe.shape[-1] / 9.0)))
|
| 107 |
+
# Warm the kernels with one full window (tile 64 + 2*6 padding, the defaults of
|
| 108 |
+
# upscale_distilled below) so the first real request does not pay for cuDNN kernel
|
| 109 |
+
# selection, which costs seconds on a cold GPU container.
|
| 110 |
+
model(torch.zeros(1, 3, 76, 76, device=device))
|
| 111 |
+
_DISTILLED = (model, native, device)
|
| 112 |
+
return _DISTILLED
|
| 113 |
+
|
| 114 |
+
|
| 115 |
+
def upscale_distilled(rgb, scale, native, out_w, out_h, model=None, device="cpu",
|
| 116 |
+
tile: int = 64, pad: int = 6):
|
| 117 |
"""Tiled inference in LR space for the fixed-factor student.
|
| 118 |
|
| 119 |
Same recipe as scripts/upscale_onnx.py: run the padded tile, resample the WHOLE padded tile
|
| 120 |
to its own target size (so the crop offset stays inside it), then paste the crop on the canvas.
|
| 121 |
"""
|
|
|
|
|
|
|
| 122 |
height, width = rgb.shape[:2]
|
| 123 |
canvas = np.zeros((out_h, out_w, 3), dtype=np.uint8)
|
| 124 |
resample = Image.Resampling.LANCZOS if native > scale else Image.Resampling.BICUBIC
|
| 125 |
tile = max(8, min(int(tile), height))
|
| 126 |
+
# Every window keeps the SAME size: at the borders it slides inward instead of shrinking.
|
| 127 |
+
# TorchScript re-specialises its fused convolutions for the first window of each new shape
|
| 128 |
+
# (~0.3 s each), so a variable window would make the first request pay for up to nine shapes.
|
| 129 |
+
win_h, win_w = min(tile + 2 * pad, height), min(tile + 2 * pad, width)
|
| 130 |
for top in range(0, height, tile):
|
| 131 |
for left in range(0, width, tile):
|
| 132 |
bottom, right = min(top + tile, height), min(left + tile, width)
|
| 133 |
+
y0, x0 = max(0, min(top - pad, height - win_h)), max(0, min(left - pad, width - win_w))
|
| 134 |
+
y1, x1 = y0 + win_h, x0 + win_w
|
| 135 |
patch = rgb[y0:y1, x0:x1].astype(np.float32) / 255.0
|
| 136 |
+
tensor = torch.from_numpy(patch).permute(2, 0, 1).unsqueeze(0).to(device)
|
| 137 |
+
with torch.no_grad():
|
| 138 |
+
sr = model(tensor)[0].clamp(0.0, 1.0).permute(1, 2, 0).cpu().numpy()
|
| 139 |
+
sr = (sr * 255.0).round().astype(np.uint8)
|
| 140 |
full_w, full_h = int(round((y1 - y0) * scale)), int(round((x1 - x0) * scale))
|
| 141 |
sr = np.asarray(Image.fromarray(sr).resize((full_w, full_h), resample))
|
| 142 |
th, tw = int(round((bottom - top) * scale)), int(round((right - left) * scale))
|
|
|
|
| 168 |
|
| 169 |
if choice == "distilled":
|
| 170 |
try:
|
| 171 |
+
model, native, device = distilled_model()
|
| 172 |
except Exception as exc:
|
| 173 |
+
return None, f"Distilled model unavailable: {exc}"
|
| 174 |
+
native = native or DISTILLED_NATIVE
|
| 175 |
+
arr = upscale_distilled(np.asarray(img, dtype=np.uint8), applied, native, out_w, out_h,
|
| 176 |
+
model=model, device=device)
|
| 177 |
+
model_note = f"Universal Upscale Distilled (TorchScript, native {native}x"
|
| 178 |
model_note += ", output resampled)" if abs(applied - native) > 1e-3 else ")"
|
| 179 |
+
device = f"torch/{device}"
|
| 180 |
else:
|
| 181 |
device = "cuda" if torch.cuda.is_available() else "cpu"
|
| 182 |
model = load_model(device)
|
|
|
|
| 215 |
description=(
|
| 216 |
"Pick the engine in the **Model** menu: **Universal Upscale** renders any factor from 1x up to the tested "
|
| 217 |
"maximum 9.2x (or an exact target resolution), **Universal Upscale Distilled** is the 1.3 M "
|
| 218 |
+
"parameter student that renders 9x natively in one pass on the same device as the "
|
| 219 |
+
"default engine and resamples its output for other factors. Tiled inference keeps high-resolution "
|
| 220 |
+
"upscaling inside memory (outputs are capped at "
|
| 221 |
"4096 px on the long side); the status line reports which model ran and what it did."
|
| 222 |
),
|
| 223 |
examples=example_files if example_files else None,
|
config.json
CHANGED
|
@@ -65,7 +65,7 @@
|
|
| 65 |
},
|
| 66 |
"export": {
|
| 67 |
"torch_version": "2.14.1+cu130",
|
| 68 |
-
"exported_at": "2026-10-
|
| 69 |
"method": "torch.jit.script + torch.jit.freeze",
|
| 70 |
"verification": {
|
| 71 |
"checks": [
|
|
@@ -169,8 +169,8 @@
|
|
| 169 |
"scales": "arbitrary"
|
| 170 |
},
|
| 171 |
"distilled": {
|
| 172 |
-
"file": "
|
| 173 |
-
"type": "
|
| 174 |
"repo_id": "tailot/universal-upscale-distilled",
|
| 175 |
"native_scale": 9,
|
| 176 |
"note": "fixed-factor student; other factors are resampled from the native output"
|
|
|
|
| 65 |
},
|
| 66 |
"export": {
|
| 67 |
"torch_version": "2.14.1+cu130",
|
| 68 |
+
"exported_at": "2026-10-25T23:09:39+00:00",
|
| 69 |
"method": "torch.jit.script + torch.jit.freeze",
|
| 70 |
"verification": {
|
| 71 |
"checks": [
|
|
|
|
| 169 |
"scales": "arbitrary"
|
| 170 |
},
|
| 171 |
"distilled": {
|
| 172 |
+
"file": "model_distilled.pt",
|
| 173 |
+
"type": "torchscript",
|
| 174 |
"repo_id": "tailot/universal-upscale-distilled",
|
| 175 |
"native_scale": 9,
|
| 176 |
"note": "fixed-factor student; other factors are resampled from the native output"
|
model.safetensors
CHANGED
|
@@ -1,3 +1,3 @@
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
-
oid sha256:
|
| 3 |
size 18439020
|
|
|
|
| 1 |
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:3d12afef19d94cf6922b8ede382c85db09cd696fbc28ed8ef964b7278343c2d8
|
| 3 |
size 18439020
|
model_distilled.pt
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:fbfc0d38849bf09d43cf5f56da50bba63de099ea7bfaff4169741cf90328785f
|
| 3 |
+
size 5292159
|
requirements.txt
CHANGED
|
@@ -3,5 +3,4 @@ numpy>=1.26
|
|
| 3 |
Pillow>=10.2
|
| 4 |
huggingface_hub>=0.28.1
|
| 5 |
gradio>=5.0.0
|
| 6 |
-
onnxruntime>=1.16
|
| 7 |
spaces
|
|
|
|
| 3 |
Pillow>=10.2
|
| 4 |
huggingface_hub>=0.28.1
|
| 5 |
gradio>=5.0.0
|
|
|
|
| 6 |
spaces
|