JS6969 commited on
Commit
01d8863
Β·
verified Β·
1 Parent(s): 2ee5af8

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +273 -67
app.py CHANGED
@@ -1,57 +1,48 @@
1
- # app.py β€” MjΓΆlnir Β· Upscale Images (ZeroGPU safe)
2
 
3
- # ─────────────────────────────────────────────
4
- # Force CPU only (ZeroGPU mode, no CUDA allowed)
5
- # ─────────────────────────────────────────────
6
- import os, sys, types, time, zipfile, tempfile, shutil, base64
7
- from pathlib import Path
8
- from typing import List
9
-
10
- os.environ["CUDA_VISIBLE_DEVICES"] = "" # hide GPUs completely
11
-
12
- import torch
13
- def have_gpu() -> bool:
14
- return torch.cuda.is_available()
15
-
16
- if not have_gpu():
17
- print("⚠️ ZeroGPU mode: Running on CPU only (slow, but stable).")
18
- else:
19
- print(f"βœ… GPU detected: {torch.cuda.get_device_name(0)}")
20
-
21
- # ─────────────────────────────────────────────
22
- # TorchVision shim (keeps basicsr happy)
23
- # ─────────────────────────────────────────────
24
  try:
25
  import torchvision.transforms.functional_tensor as _ft # noqa: F401
26
  except Exception:
 
27
  _mod = types.ModuleType("torchvision.transforms.functional_tensor")
28
  def rgb_to_grayscale(img: "torch.Tensor", num_output_channels: int = 1) -> "torch.Tensor":
29
  if not torch.is_tensor(img):
30
  raise TypeError("rgb_to_grayscale expects a torch.Tensor")
31
  if img.ndim < 3 or img.shape[-3] != 3:
32
- raise ValueError(f"expected tensor with C=3 as the third-from-last dim, got {tuple(img.shape)}")
33
  r, g, b = img[..., -3, :, :], img[..., -2, :, :], img[..., -1, :, :]
34
  gray = 0.2989*r + 0.5870*g + 0.1140*b
35
  return torch.stack([gray, gray, gray], dim=-3) if num_output_channels == 3 else gray.unsqueeze(-3)
36
  _mod.rgb_to_grayscale = rgb_to_grayscale
37
  sys.modules["torchvision.transforms.functional_tensor"] = _mod
 
38
 
39
- # ─────────────────────────────────────────────
40
- # Standard libs
41
- # ─────────────────────────────────────────────
 
 
42
  import numpy as np
43
  import cv2
44
  from PIL import Image
45
- import gradio as gr
46
 
 
47
  from basicsr.archs.rrdbnet_arch import RRDBNet as _RRDBNet
48
  from basicsr.utils.download_util import load_file_from_url
49
  from realesrgan import RealESRGANer
50
  from realesrgan.archs.srvgg_arch import SRVGGNetCompact
51
 
52
- # ─────────────────────────────────────────────
53
- # Branding
54
- # ─────────────────────────────────────────────
 
 
 
 
 
 
55
  def try_load_logo_b64() -> str:
56
  try:
57
  with open("bifrost_logo.png", "rb") as f:
@@ -67,16 +58,13 @@ def render_logo_html(px: int = 96) -> str:
67
  {img}
68
  <div>
69
  <div style="font-size:1.6rem;font-weight:800;">MjΓΆlnir Β· Upscale Images</div>
70
- <div style="opacity:0.8;">Real-ESRGAN (batch click with progress, ZeroGPU safe)</div>
71
  </div>
72
  </div>
73
  <hr>
74
  """
75
 
76
- # ─────────────────────────────────────────────
77
- # Helpers
78
- # ─────────────────────────────────────────────
79
- _num = __import__("re").compile(r'(\d+)')
80
  def _natural_key(p: Path | str):
81
  s = str(p)
82
  return [int(t) if t.isdigit() else t.lower() for t in _num.split(s)]
@@ -89,8 +77,9 @@ def sample_paths(paths: List[Path] | List[str], n: int = 30) -> List[str]:
89
  step = (total - 1) / (n - 1); idxs = [round(i * step) for i in range(n)]
90
  out, seen = [], set()
91
  for i in idxs:
 
92
  if i not in seen:
93
- out.append(str(paths[int(i)])); seen.add(int(i))
94
  return out
95
 
96
  def render_progress(pct: float, label: str = "") -> str:
@@ -99,12 +88,46 @@ def render_progress(pct: float, label: str = "") -> str:
99
  <div style="height:100%;width:{pct:.1f}%;background:#3b82f6;"></div></div>
100
  <div style="font-size:12px;opacity:.8;margin-top:4px;">{label} {pct:.1f}%</div>'''
101
 
 
 
 
 
102
  def build_rrdb(scale: int, num_block: int):
103
- return RRDBNet(num_in_ch=3, num_out_ch=3, num_feat=64,
104
- num_block=num_block, num_grow_ch=32, scale=scale)
 
 
 
 
105
 
106
- def get_realesrganer(model_id: str, scale: int, tile: int,
107
- half: bool, device: str = "cpu") -> RealESRGANer:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
108
  wdir = _weights_dir()
109
 
110
  if model_id == "x4plus":
@@ -112,76 +135,259 @@ def get_realesrganer(model_id: str, scale: int, tile: int,
112
  netscale = 4
113
  url = "https://github.com/xinntao/Real-ESRGAN/releases/download/v0.1.0/RealESRGAN_x4plus.pth"
114
  model_path = os.path.join(wdir, "RealESRGAN_x4plus.pth")
 
 
 
 
 
115
  elif model_id == "x4plus-anime":
116
  model = build_rrdb(scale=4, num_block=6)
117
  netscale = 4
118
  url = "https://github.com/xinntao/Real-ESRGAN/releases/download/v0.2.2.4/RealESRGAN_x4plus_anime_6B.pth"
119
  model_path = os.path.join(wdir, "RealESRGAN_x4plus_anime_6B.pth")
 
 
 
 
 
120
  elif model_id == "x2plus":
121
  model = build_rrdb(scale=2, num_block=23)
122
  netscale = 2
123
  url = "https://github.com/xinntao/Real-ESRGAN/releases/download/v0.2.1/RealESRGAN_x2plus.pth"
124
  model_path = os.path.join(wdir, "RealESRGAN_x2plus.pth")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
125
  else:
126
  raise ValueError(f"Unknown model_id: {model_id}")
127
 
128
- if not os.path.isfile(model_path):
129
- load_file_from_url(url=url, model_dir=wdir, progress=True)
130
-
131
- # auto device
132
- device = "cuda" if torch.cuda.is_available() else "cpu"
133
- half = half and device == "cuda"
134
 
135
  return RealESRGANer(
136
  scale=netscale,
137
- model_path=model_path,
 
138
  model=model,
139
- tile=tile,
140
  tile_pad=10,
141
  pre_pad=0,
142
  half=half,
143
  device=device,
144
  )
145
 
146
- # ─────────────────────────────────────────────
147
- # Step 2: Sources + Processing (batch click)
148
- # ─────────────────────────────────────────────
149
- def _ensure_dir(p: Path) -> Path: p.mkdir(parents=True, exist_ok=True); return p
 
 
 
150
  def _save_zip_of_dir(dir_path: Path, zip_path: Path) -> str:
151
  with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zf:
152
  for p in sorted(dir_path.glob("*.*"), key=_natural_key):
153
  if p.suffix.lower() in [".jpg", ".jpeg", ".png"]:
154
  zf.write(p, p.name)
155
  return str(zip_path)
 
156
  def _list_image_paths_from_upload(files: List[gr.File] | None) -> List[str]:
157
  if not files: return []
158
  return [str(Path(f.name)) for f in files if Path(f.name).suffix.lower() in [".jpg",".jpeg",".png"]]
 
159
  def _build_gallery_from_dir(dir_path: Path, n: int = 30) -> List[str]:
160
  paths = sorted(list(dir_path.glob("*.jpg")) + list(dir_path.glob("*.png")), key=_natural_key)
161
  return sample_paths(paths, n)
162
 
163
- def map_ui_model_to_internal(ui_name: str) -> str:
164
- return {
165
- "RealESRGAN_x4plus": "x4plus",
166
- "RealESRGAN_x4plus_anime_6B": "x4plus-anime",
167
- "RealESRGAN_x2plus": "x2plus",
168
- "RealESRNet_x4plus": "x4plus",
169
- "realesr-general-x4v3": "x4plus",
170
- }.get(ui_name, "x4plus")
171
 
172
- def clamp_scale_for_model(outscale: int, model_id: str) -> int:
173
- return 2 if model_id == "x2plus" else 4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
174
 
175
- # (step2_prepare_sources, step2_process_next_batch stay the same as before)
 
 
 
 
176
 
177
- # ─────────────────────────────────────────────
178
  # UI
179
- # ─────────────────────────────────────────────
 
180
  def build_ui():
181
  with gr.Blocks(theme=gr.themes.Soft()) as demo:
182
  gr.HTML(render_logo_html(88))
183
- gr.Markdown("Upload images and upscale with Real-ESRGAN. Runs in CPU-only ZeroGPU mode (slow).")
184
- # … keep the rest of your batch-click UI wiring unchanged …
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
185
  return demo
186
 
187
  if __name__ == "__main__":
 
1
+ # app.py β€” MjΓΆlnir Β· Upscale Images (Real-ESRGAN) β€” GPU/CPU safe
2
 
3
+ # ---- TorchVision shim (keeps basicsr happy if torchvision isn't installed) ----
4
+ import sys, types
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
  try:
6
  import torchvision.transforms.functional_tensor as _ft # noqa: F401
7
  except Exception:
8
+ import torch
9
  _mod = types.ModuleType("torchvision.transforms.functional_tensor")
10
  def rgb_to_grayscale(img: "torch.Tensor", num_output_channels: int = 1) -> "torch.Tensor":
11
  if not torch.is_tensor(img):
12
  raise TypeError("rgb_to_grayscale expects a torch.Tensor")
13
  if img.ndim < 3 or img.shape[-3] != 3:
14
+ raise ValueError(f"expected tensor with C=3 as the third-from-last dim, got shape {tuple(img.shape)}")
15
  r, g, b = img[..., -3, :, :], img[..., -2, :, :], img[..., -1, :, :]
16
  gray = 0.2989*r + 0.5870*g + 0.1140*b
17
  return torch.stack([gray, gray, gray], dim=-3) if num_output_channels == 3 else gray.unsqueeze(-3)
18
  _mod.rgb_to_grayscale = rgb_to_grayscale
19
  sys.modules["torchvision.transforms.functional_tensor"] = _mod
20
+ # ------------------------------------------------------------------------------
21
 
22
+ import os, time, zipfile, tempfile, shutil, base64
23
+ from pathlib import Path
24
+ from typing import List, Optional
25
+ import re
26
+ import gradio as gr
27
  import numpy as np
28
  import cv2
29
  from PIL import Image
 
30
 
31
+ import torch
32
  from basicsr.archs.rrdbnet_arch import RRDBNet as _RRDBNet
33
  from basicsr.utils.download_util import load_file_from_url
34
  from realesrgan import RealESRGANer
35
  from realesrgan.archs.srvgg_arch import SRVGGNetCompact
36
 
37
+ # ────────────────────────────────────────────────────────
38
+ # Small utils
39
+ # ────────────────────────────────────────────────────────
40
+
41
+ def have_gpu() -> bool:
42
+ return torch.cuda.is_available()
43
+
44
+ print("βœ… GPU available" if have_gpu() else "⚠️ No GPU detected. Will use CPU (slow).")
45
+
46
  def try_load_logo_b64() -> str:
47
  try:
48
  with open("bifrost_logo.png", "rb") as f:
 
58
  {img}
59
  <div>
60
  <div style="font-size:1.6rem;font-weight:800;">MjΓΆlnir Β· Upscale Images</div>
61
+ <div style="opacity:0.8;">Real-ESRGAN (batch click with progress)</div>
62
  </div>
63
  </div>
64
  <hr>
65
  """
66
 
67
+ _num = re.compile(r'(\d+)')
 
 
 
68
  def _natural_key(p: Path | str):
69
  s = str(p)
70
  return [int(t) if t.isdigit() else t.lower() for t in _num.split(s)]
 
77
  step = (total - 1) / (n - 1); idxs = [round(i * step) for i in range(n)]
78
  out, seen = [], set()
79
  for i in idxs:
80
+ i = int(i)
81
  if i not in seen:
82
+ out.append(str(paths[i])); seen.add(i)
83
  return out
84
 
85
  def render_progress(pct: float, label: str = "") -> str:
 
88
  <div style="height:100%;width:{pct:.1f}%;background:#3b82f6;"></div></div>
89
  <div style="font-size:12px;opacity:.8;margin-top:4px;">{label} {pct:.1f}%</div>'''
90
 
91
+ # ────────────────────────────────────────────────────────
92
+ # Real-ESRGAN wiring (GPU/CPU safe)
93
+ # ────────────────────────────────────────────────────────
94
+
95
  def build_rrdb(scale: int, num_block: int):
96
+ return _RRDBNet(num_in_ch=3, num_out_ch=3, num_feat=64, num_block=num_block, num_grow_ch=32, scale=scale)
97
+
98
+ def _weights_dir() -> str:
99
+ wdir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "weights")
100
+ os.makedirs(wdir, exist_ok=True)
101
+ return wdir
102
 
103
+ def map_ui_model_to_internal(ui_name: str) -> str:
104
+ return {
105
+ "RealESRGAN_x4plus": "x4plus",
106
+ "RealESRGAN_x4plus_anime_6B": "x4plus-anime",
107
+ "RealESRGAN_x2plus": "x2plus",
108
+ "RealESRNet_x4plus": "x4plus", # fallback to RRDB x4
109
+ "realesr-general-x4v3": "general-x4v3", # SRVGG
110
+ }.get(ui_name, "x4plus")
111
+
112
+ def clamp_scale_for_model(outscale: int, model_id: str) -> int:
113
+ if model_id == "x2plus": # true 2x model
114
+ return 2
115
+ return 4 # rest are 4x
116
+
117
+ def get_realesrganer(
118
+ model_id: str,
119
+ tile: int,
120
+ half: bool,
121
+ device: str,
122
+ denoise_strength: float = 0.5
123
+ ) -> RealESRGANer:
124
+ """
125
+ Returns a RealESRGANer for:
126
+ - x4plus (RRDB)
127
+ - x4plus-anime (RRDB)
128
+ - x2plus (RRDB)
129
+ - general-x4v3 (SRVGG, supports denoise_strength via DNI weights)
130
+ """
131
  wdir = _weights_dir()
132
 
133
  if model_id == "x4plus":
 
135
  netscale = 4
136
  url = "https://github.com/xinntao/Real-ESRGAN/releases/download/v0.1.0/RealESRGAN_x4plus.pth"
137
  model_path = os.path.join(wdir, "RealESRGAN_x4plus.pth")
138
+ if not os.path.isfile(model_path):
139
+ load_file_from_url(url=url, model_dir=wdir, progress=True)
140
+ model_path_arg = model_path
141
+ dni_weight = None
142
+
143
  elif model_id == "x4plus-anime":
144
  model = build_rrdb(scale=4, num_block=6)
145
  netscale = 4
146
  url = "https://github.com/xinntao/Real-ESRGAN/releases/download/v0.2.2.4/RealESRGAN_x4plus_anime_6B.pth"
147
  model_path = os.path.join(wdir, "RealESRGAN_x4plus_anime_6B.pth")
148
+ if not os.path.isfile(model_path):
149
+ load_file_from_url(url=url, model_dir=wdir, progress=True)
150
+ model_path_arg = model_path
151
+ dni_weight = None
152
+
153
  elif model_id == "x2plus":
154
  model = build_rrdb(scale=2, num_block=23)
155
  netscale = 2
156
  url = "https://github.com/xinntao/Real-ESRGAN/releases/download/v0.2.1/RealESRGAN_x2plus.pth"
157
  model_path = os.path.join(wdir, "RealESRGAN_x2plus.pth")
158
+ if not os.path.isfile(model_path):
159
+ load_file_from_url(url=url, model_dir=wdir, progress=True)
160
+ model_path_arg = model_path
161
+ dni_weight = None
162
+
163
+ elif model_id == "general-x4v3":
164
+ # SRVGG + two weights for DNI blend (denoise control)
165
+ model = SRVGGNetCompact(num_in_ch=3, num_out_ch=3, num_feat=64, num_conv=32, upscale=4, act_type='prelu')
166
+ netscale = 4
167
+ base_url = "https://github.com/xinntao/Real-ESRGAN/releases/download/v0.2.5.0/"
168
+ base_path = os.path.join(wdir, "realesr-general-x4v3.pth")
169
+ wdn_path = os.path.join(wdir, "realesr-general-wdn-x4v3.pth")
170
+ if not os.path.isfile(base_path):
171
+ load_file_from_url(url=base_url + "realesr-general-x4v3.pth", model_dir=wdir, progress=True)
172
+ if not os.path.isfile(wdn_path):
173
+ load_file_from_url(url=base_url + "realesr-general-wdn-x4v3.pth", model_dir=wdir, progress=True)
174
+ model_path_arg = [base_path, wdn_path]
175
+ # blend base vs denoised
176
+ d = float(denoise_strength)
177
+ d = max(0.0, min(1.0, d))
178
+ dni_weight = [1.0 - d, d]
179
+
180
  else:
181
  raise ValueError(f"Unknown model_id: {model_id}")
182
 
183
+ # Final device policy
184
+ device = device if device in ("cuda", "cpu") else ("cuda" if torch.cuda.is_available() else "cpu")
185
+ half = bool(half and device == "cuda")
 
 
 
186
 
187
  return RealESRGANer(
188
  scale=netscale,
189
+ model_path=model_path_arg,
190
+ dni_weight=dni_weight,
191
  model=model,
192
+ tile=int(tile or 256),
193
  tile_pad=10,
194
  pre_pad=0,
195
  half=half,
196
  device=device,
197
  )
198
 
199
+ # ────────────────────────────────────────────────────────
200
+ # Batch upscaling helpers
201
+ # ────────────────────────────────────────────────────────
202
+
203
+ def _ensure_dir(p: Path) -> Path:
204
+ p.mkdir(parents=True, exist_ok=True); return p
205
+
206
  def _save_zip_of_dir(dir_path: Path, zip_path: Path) -> str:
207
  with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zf:
208
  for p in sorted(dir_path.glob("*.*"), key=_natural_key):
209
  if p.suffix.lower() in [".jpg", ".jpeg", ".png"]:
210
  zf.write(p, p.name)
211
  return str(zip_path)
212
+
213
  def _list_image_paths_from_upload(files: List[gr.File] | None) -> List[str]:
214
  if not files: return []
215
  return [str(Path(f.name)) for f in files if Path(f.name).suffix.lower() in [".jpg",".jpeg",".png"]]
216
+
217
  def _build_gallery_from_dir(dir_path: Path, n: int = 30) -> List[str]:
218
  paths = sorted(list(dir_path.glob("*.jpg")) + list(dir_path.glob("*.png")), key=_natural_key)
219
  return sample_paths(paths, n)
220
 
221
+ # ────────────────────────────────────────────────────────
222
+ # Step 2 Β· Prepare + Process (generator for streaming)
223
+ # ────────────────────────────────────────────────────────
 
 
 
 
 
224
 
225
+ def step2_prepare_sources(frames_list, uploaded_imgs, max_images):
226
+ src = _list_image_paths_from_upload(uploaded_imgs) or (frames_list or [])
227
+ if not src:
228
+ return [], "", 0, 0, "No images found. Upload files first.", render_progress(0.0, "Idle")
229
+ try:
230
+ max_images = int(max_images or 0)
231
+ except Exception:
232
+ max_images = 0
233
+ if max_images > 0:
234
+ src = src[:max_images]
235
+ work = Path(tempfile.mkdtemp(prefix="up_manual_"))
236
+ out_dir = _ensure_dir(work / "upscaled")
237
+ total = len(src); done_idx = 0
238
+ return src, str(out_dir), done_idx, total, f"Sources loaded: {total} image(s). Click 'Process Next Batch'.", render_progress(0.0, "Ready")
239
+
240
+ def step2_process_next_batch(
241
+ up_src_paths, up_out_dir, up_done_idx, up_total,
242
+ ui_model_name, outscale, tile, precision, denoise_strength, face_enhance, batch_size,
243
+ force_cpu
244
+ ):
245
+ if not up_src_paths or not up_out_dir:
246
+ yield None, None, "Load sources first.", render_progress(0.0, "Idle"), up_done_idx, up_out_dir
247
+ return
248
+
249
+ model_id = map_ui_model_to_internal(ui_model_name)
250
+ scale = clamp_scale_for_model(int(outscale or 4), model_id)
251
+
252
+ # Device policy
253
+ device = "cpu" if force_cpu else ("cuda" if torch.cuda.is_available() else "cpu")
254
+ # Precision policy
255
+ if precision == "half":
256
+ use_half = (device == "cuda")
257
+ elif precision == "full":
258
+ use_half = False
259
+ else: # auto
260
+ use_half = (device == "cuda")
261
+
262
+ tile = int(tile or 256)
263
+ batch_size = max(1, int(batch_size or 8))
264
+
265
+ # Build upsampler (handles general-x4v3 as well)
266
+ upsampler = get_realesrganer(
267
+ model_id=model_id,
268
+ tile=tile,
269
+ half=use_half,
270
+ device=device,
271
+ denoise_strength=float(denoise_strength or 0.5)
272
+ )
273
+
274
+ # Optional: GFPGAN face enhancer
275
+ face_enhancer = None
276
+ if face_enhance:
277
+ try:
278
+ from gfpgan import GFPGANer
279
+ face_enhancer = GFPGANer(
280
+ model_path="https://github.com/TencentARC/GFPGAN/releases/download/v1.3.0/GFPGANv1.3.pth",
281
+ upscale=scale,
282
+ arch="clean",
283
+ channel_multiplier=2,
284
+ bg_upsampler=upsampler
285
+ )
286
+ except Exception as e:
287
+ print("GFPGAN load failed:", e)
288
+ face_enhancer = None
289
+
290
+ start = int(up_done_idx or 0)
291
+ end = min(start + batch_size, int(up_total or 0))
292
+ out_dir = Path(up_out_dir)
293
+
294
+ if start >= up_total:
295
+ gallery = _build_gallery_from_dir(out_dir, 30)
296
+ zip_file = _save_zip_of_dir(out_dir, Path(out_dir.parent) / "upscaled.zip")
297
+ yield gallery, zip_file, "All images processed.", render_progress(100.0, "Done"), start, up_out_dir
298
+ return
299
+
300
+ batch_paths = up_src_paths[start:end]
301
+ total_in_batch = len(batch_paths)
302
+ t0 = time.time()
303
+
304
+ for idx, fp in enumerate(batch_paths, start=1):
305
+ try:
306
+ with Image.open(fp) as im:
307
+ img = im.convert("RGB")
308
+ cv_img = np.array(img)
309
+ if face_enhancer:
310
+ _, _, output = face_enhancer.enhance(cv_img, has_aligned=False, only_center_face=False, paste_back=True)
311
+ else:
312
+ # For general-x4v3, denoise_strength is already in DNI weights
313
+ output, _ = upsampler.enhance(cv_img, outscale=scale)
314
+ Image.fromarray(output).save(out_dir / (Path(fp).stem + ".jpg"), quality=95)
315
+ except Exception as e:
316
+ print("Upscale error:", e)
317
+
318
+ elapsed = time.time() - t0
319
+ pct_batch = (idx / total_in_batch) * 100.0
320
+ eta = (total_in_batch - idx) * (elapsed / max(1, idx))
321
+ label = (f"Batch: {idx}/{total_in_batch} Β· ~{eta:.1f}s ETA Β· "
322
+ f"global {start+idx}/{up_total} (x{scale}, model={ui_model_name}, device={device}, half={use_half})")
323
+ gallery = _build_gallery_from_dir(out_dir, 30)
324
+ zip_file = _save_zip_of_dir(out_dir, Path(out_dir.parent) / "upscaled.zip")
325
+ yield gallery, zip_file, label, render_progress(pct_batch, f"Upscaling {pct_batch:.0f}% (batch)"), start+idx, up_out_dir
326
 
327
+ next_idx = end
328
+ pct_global = (next_idx / up_total) * 100.0 if up_total else 100.0
329
+ gallery = _build_gallery_from_dir(out_dir, 30)
330
+ zip_file = _save_zip_of_dir(out_dir, Path(out_dir.parent) / "upscaled.zip")
331
+ yield gallery, zip_file, f"Processed batch of {total_in_batch}. {next_idx}/{up_total} done.", render_progress(pct_global, "Upscaling… (global)"), next_idx, up_out_dir
332
 
333
+ # ────────────────────────────────────────────────────────
334
  # UI
335
+ # ────────────────────────────────────────────────────────
336
+
337
  def build_ui():
338
  with gr.Blocks(theme=gr.themes.Soft()) as demo:
339
  gr.HTML(render_logo_html(88))
340
+ gr.Markdown("Upload images and upscale with Real-ESRGAN. Process in batches with live progress. GPU if available, CPU fallback.")
341
+
342
+ # States
343
+ frames_state = gr.State([]) # kept for parity
344
+ up_src_paths_state = gr.State([])
345
+ up_out_dir_state = gr.State("")
346
+ up_done_idx_state = gr.State(0)
347
+ up_total_state = gr.State(0)
348
+
349
+ imgs_override = gr.Files(label="Upload images (JPG/PNG)", file_types=[".jpg",".jpeg",".png"], type="filepath")
350
+
351
+ with gr.Accordion("Upscaling options", open=True):
352
+ with gr.Row():
353
+ ui_model_name = gr.Dropdown(
354
+ label="Upscaler model",
355
+ choices=["RealESRGAN_x4plus", "RealESRNet_x4plus", "RealESRGAN_x4plus_anime_6B", "RealESRGAN_x2plus", "realesr-general-x4v3"],
356
+ value="RealESRGAN_x4plus"
357
+ )
358
+ denoise_strength = gr.Slider(0, 1, value=0.5, step=0.1, label="Denoise (only general-x4v3)")
359
+ outscale = gr.Slider(1, 6, value=4, step=1, label="Resolution upscale (model-limited)")
360
+ face_enhance = gr.Checkbox(value=False, label="Face Enhancement (GFPGAN)")
361
+ with gr.Row():
362
+ tile = gr.Number(value=256, label="Tile size (try 128 if OOM; 0=auto)")
363
+ precision = gr.Dropdown(["auto", "half", "full"], value="auto", label="Precision (GPU=half, CPU=full)")
364
+ force_cpu = gr.Checkbox(value=False, label="Zero-GPU Mode (force CPU)")
365
+ with gr.Row():
366
+ batch_size = gr.Number(value=12, precision=0, label="Batch size per click")
367
+ max_images = gr.Number(value=0, precision=0, label="Max images to process (0 = all)")
368
+
369
+ with gr.Row():
370
+ btn_prepare = gr.Button("Load / Reset Sources", variant="secondary")
371
+ btn_next = gr.Button("Process Next Batch", variant="primary")
372
+
373
+ prog = gr.HTML(render_progress(0.0, "Idle"))
374
+ gallery_up = gr.Gallery(label="Upscaled preview (30 sampled)", columns=6, height=480)
375
+ zip_up = gr.File(label="Download upscaled ZIP")
376
+ details = gr.Markdown("")
377
+
378
+ btn_prepare.click(
379
+ step2_prepare_sources,
380
+ inputs=[frames_state, imgs_override, max_images],
381
+ outputs=[up_src_paths_state, up_out_dir_state, up_done_idx_state, up_total_state, details, prog]
382
+ )
383
+
384
+ btn_next.click(
385
+ step2_process_next_batch,
386
+ inputs=[up_src_paths_state, up_out_dir_state, up_done_idx_state, up_total_state,
387
+ ui_model_name, outscale, tile, precision, denoise_strength, face_enhance, batch_size, force_cpu],
388
+ outputs=[gallery_up, zip_up, details, prog, up_done_idx_state, up_out_dir_state]
389
+ )
390
+
391
  return demo
392
 
393
  if __name__ == "__main__":