JS6969 commited on
Commit
817be8c
·
verified ·
1 Parent(s): 633b1bd

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +170 -94
app.py CHANGED
@@ -1,8 +1,9 @@
1
  # app.py
2
- # A Gradio Space that (1) extracts frames with FFmpeg and (2) upscales each frame with Real-ESRGAN.
3
- # - CPU-friendly (works without GPU). GPU will be faster if available.
4
- # - Choose extraction mode, then choose upscale model/scale, JPG quality, and get a ZIP of upscaled frames.
5
- # - Shows both the FFmpeg command and the ESRGAN settings used.
 
6
 
7
  import os
8
  import io
@@ -18,6 +19,8 @@ from pathlib import Path
18
  from typing import List, Optional
19
 
20
  import gradio as gr
 
 
21
 
22
  # ─────────────────────────────────────────────────────────────
23
  # FFmpeg detection
@@ -141,7 +144,7 @@ def build_ffmpeg_command(
141
  return cmd
142
 
143
  # ─────────────────────────────────────────────────────────────
144
- # Real-ESRGAN setup (CPU-friendly)
145
  # ─────────────────────────────────────────────────────────────
146
  try:
147
  from realesrgan import RealESRGANer
@@ -155,32 +158,23 @@ def get_realesrganer(model_name: str, scale: int, tile: int, half: bool, device:
155
  if not _HAVE_REALESRGAN:
156
  raise RuntimeError("realesrgan is not installed. Check requirements.txt")
157
  # Model selection
158
- if model_name == "x4plus":
159
- model = RRDBNet(num_in_ch=3, num_out_ch=3, num_feat=64, num_block=23, num_grow_ch=32, scale=4)
160
- model_scale = 4
161
- netscale = 4
162
- model_path = None # let the lib auto-download
163
- elif model_name == "x4plus-anime":
164
  model = RRDBNet(num_in_ch=3, num_out_ch=3, num_feat=64, num_block=23, num_grow_ch=32, scale=4)
165
  model_scale = 4
166
- netscale = 4
167
- model_path = None
168
  elif model_name == "x2plus":
169
  model = RRDBNet(num_in_ch=3, num_out_ch=3, num_feat=64, num_block=23, num_grow_ch=32, scale=2)
170
  model_scale = 2
171
- netscale = 2
172
- model_path = None
173
  else:
174
- raise ValueError("Unknown model")
175
 
176
  if scale not in (2, 4):
177
  scale = model_scale
178
 
179
  upsampler = RealESRGANer(
180
  scale=model_scale,
181
- model_path=model_path,
182
  model=model,
183
- tile=tile,
184
  tile_pad=10,
185
  pre_pad=0,
186
  half=half,
@@ -192,33 +186,30 @@ def get_realesrganer(model_name: str, scale: int, tile: int, half: bool, device:
192
  def upscale_images(img_paths: List[Path], out_dir: Path, model_name: str, scale: int, tile: int, precision: str, progress=gr.Progress(track_tqdm=True)) -> List[str]:
193
  device = "cuda" if os.environ.get("CUDA_VISIBLE_DEVICES") else "cpu"
194
  half = (precision == "half") and (device == "cuda")
195
-
196
  upsampler, model_scale = get_realesrganer(model_name, scale, tile, half, device=device)
197
 
198
  out_paths: List[str] = []
199
  for i, p in enumerate(img_paths, 1):
200
  try:
201
- from PIL import Image
202
  img = Image.open(p).convert("RGB")
203
  output, _ = upsampler.enhance(np.array(img), outscale=scale)
204
  out_img = Image.fromarray(output)
205
  out_file = out_dir / p.name
 
 
 
206
  out_img.save(out_file, quality=95)
207
  out_paths.append(str(out_file))
208
  except Exception as e:
209
- # Skip problematic files, continue
210
  print(f"Upscale failed for {p}: {e}")
211
  progress(i / max(1, len(img_paths)))
212
  return out_paths
213
 
214
- # numpy import (used above)
215
- import numpy as np
216
-
217
  # ─────────────────────────────────────────────────────────────
218
- # Pipeline: extract → upscale → zip
219
  # ─────────────────────────────────────────────────────────────
220
 
221
- def run_pipeline(
222
  video: gr.File | None,
223
  mode: str,
224
  every_seconds: float,
@@ -233,7 +224,6 @@ def run_pipeline(
233
  scene_detect: bool,
234
  scene_thresh: float,
235
  prefix: str,
236
- # Upscale
237
  do_upscale: bool,
238
  model_name: str,
239
  scale: int,
@@ -254,6 +244,10 @@ def run_pipeline(
254
  raw_dir.mkdir(parents=True, exist_ok=True)
255
  up_dir.mkdir(parents=True, exist_ok=True)
256
 
 
 
 
 
257
  # Build and run FFmpeg
258
  pattern = str(raw_dir / f"{prefix}_%05d.{out_format}")
259
  cmd = build_ffmpeg_command(
@@ -303,6 +297,48 @@ def run_pipeline(
303
  return gallery, str(zip_path), detail, "", cmd_preview
304
 
305
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
306
  # ─────────────────────────────────────────────────────────────
307
  # UI
308
  # ─────────────────────────────────────────────────────────────
@@ -314,78 +350,118 @@ def build_ui():
314
  """) as demo:
315
  gr.Markdown("""
316
  <div class="cf-title">Video → JPG → Upscale (FFmpeg + Real-ESRGAN)</div>
317
- Upload a video, extract frames, and optionally upscale each image.
318
  """)
319
 
320
- with gr.Row():
321
- video = gr.File(label="Upload video", file_types=[".mp4", ".mov", ".mkv", ".avi", ".webm", ".m4v"], type="filepath")
322
-
323
- with gr.Accordion("Extraction", open=True):
324
- with gr.Row():
325
- mode = gr.Dropdown(["Every N seconds", "Every Nth frame", "Exact FPS", "All frames"], value="Every N seconds", label="Mode")
326
- every_seconds = gr.Number(value=1.0, label="Every N seconds")
327
- nth_frame = gr.Number(value=30, label="Every Nth frame")
328
- exact_fps = gr.Number(value=1.0, label="Exact FPS")
329
- with gr.Row():
330
- start_time = gr.Textbox(value="", label="Start (HH:MM:SS.mmm)")
331
- end_time = gr.Textbox(value="", label="End (HH:MM:SS.mmm)")
332
- long_side = gr.Number(value=0, label="Resize long side px (0 = none)")
333
- with gr.Row():
334
- out_format = gr.Dropdown(["jpg", "png"], value="jpg", label="Output format")
335
- jpg_quality = gr.Slider(2, 31, value=3, step=1, label="JPG quality (2=best)")
336
- png_level = gr.Slider(0, 9, value=2, step=1, label="PNG compression level")
337
- with gr.Row():
338
- scene_detect = gr.Checkbox(False, label="Scene-change detect")
339
- scene_thresh = gr.Slider(0.0, 1.0, value=0.3, step=0.01, label="Scene threshold")
340
- prefix = gr.Textbox(value="frame", label="Filename prefix")
341
-
342
- with gr.Accordion("Upscaling", open=True):
343
- with gr.Row():
344
- do_upscale = gr.Checkbox(True, label="Upscale frames with Real-ESRGAN")
345
- model_name = gr.Dropdown(["x4plus", "x4plus-anime", "x2plus"], value="x4plus", label="Model")
346
- scale = gr.Dropdown([2, 4], value=4, label="Output scale")
347
- with gr.Row():
348
- tile = gr.Number(value=0, label="Tile size (0 = auto)") # tiling reduces RAM; try 256/512 on GPU, 0 for auto
349
- precision = gr.Dropdown(["auto", "half", "full"], value="auto", label="Precision (GPU=half, CPU=full)")
350
-
351
- run_btn = gr.Button("Run: Extract → (Upscale) → ZIP", variant="primary")
352
- gallery = gr.Gallery(label="Preview (first 60)", columns=6, height=300)
353
- zip_out = gr.File(label="Download ZIP")
354
- details = gr.Markdown("Ready.")
355
-
356
- with gr.Accordion("Show FFmpeg command", open=False):
357
- cmd_preview = gr.Textbox(label="ffmpeg command", lines=4, elem_classes=["cmdbox"])
358
- if MISSING_MSG:
359
- gr.Markdown(f"<span style='color:#b45309'>{MISSING_MSG}</span>")
360
-
361
- def _toggle(mode_val, fmt):
362
- return (
363
- gr.update(visible=(mode_val == "Every N seconds")),
364
- gr.update(visible=(mode_val == "Every Nth frame")),
365
- gr.update(visible=(mode_val == "Exact FPS")),
366
- gr.update(visible=(fmt == "jpg")),
367
- gr.update(visible=(fmt == "png")),
368
- )
369
-
370
- mode.change(_toggle, [mode, out_format], [every_seconds, nth_frame, exact_fps, jpg_quality, png_level])
371
- out_format.change(_toggle, [mode, out_format], [every_seconds, nth_frame, exact_fps, jpg_quality, png_level])
372
- demo.load(_toggle, [mode, out_format], [every_seconds, nth_frame, exact_fps, jpg_quality, png_level])
373
-
374
- run_btn.click(
375
- run_pipeline,
376
- inputs=[
377
- video, mode, every_seconds, nth_frame, exact_fps,
378
- start_time, end_time, long_side, out_format, jpg_quality, png_level,
379
- scene_detect, scene_thresh, prefix,
380
- do_upscale, model_name, scale, tile, precision,
381
- ],
382
- outputs=[gallery, zip_out, details, gr.Textbox(), cmd_preview],
383
- api_name="extract_and_upscale",
384
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
385
 
386
  return demo
387
 
388
 
389
  if __name__ == "__main__":
390
  demo = build_ui()
391
- demo.queue().launch()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  # app.py
2
+ # A Gradio Space that (1) extracts frames with FFmpeg and (2) upscales frames with Real-ESRGAN.
3
+ # NEW: You can now also upload existing images directly to the Upscale tab (no video needed).
4
+ # - CPU-friendly (GPU optional)
5
+ # - Exact ffmpeg command preview
6
+ # - ZIP download for results
7
 
8
  import os
9
  import io
 
19
  from typing import List, Optional
20
 
21
  import gradio as gr
22
+ import numpy as np
23
+ from PIL import Image
24
 
25
  # ─────────────────────────────────────────────────────────────
26
  # FFmpeg detection
 
144
  return cmd
145
 
146
  # ─────────────────────────────────────────────────────────────
147
+ # Real-ESRGAN setup (CPU/GPU)
148
  # ─────────────────────────────────────────────────────────────
149
  try:
150
  from realesrgan import RealESRGANer
 
158
  if not _HAVE_REALESRGAN:
159
  raise RuntimeError("realesrgan is not installed. Check requirements.txt")
160
  # Model selection
161
+ if model_name in ("x4plus", "x4plus-anime"):
 
 
 
 
 
162
  model = RRDBNet(num_in_ch=3, num_out_ch=3, num_feat=64, num_block=23, num_grow_ch=32, scale=4)
163
  model_scale = 4
 
 
164
  elif model_name == "x2plus":
165
  model = RRDBNet(num_in_ch=3, num_out_ch=3, num_feat=64, num_block=23, num_grow_ch=32, scale=2)
166
  model_scale = 2
 
 
167
  else:
168
+ raise ValueError("Unknown Real-ESRGAN model")
169
 
170
  if scale not in (2, 4):
171
  scale = model_scale
172
 
173
  upsampler = RealESRGANer(
174
  scale=model_scale,
175
+ model_path=None, # let library fetch weights
176
  model=model,
177
+ tile=tile or 0,
178
  tile_pad=10,
179
  pre_pad=0,
180
  half=half,
 
186
  def upscale_images(img_paths: List[Path], out_dir: Path, model_name: str, scale: int, tile: int, precision: str, progress=gr.Progress(track_tqdm=True)) -> List[str]:
187
  device = "cuda" if os.environ.get("CUDA_VISIBLE_DEVICES") else "cpu"
188
  half = (precision == "half") and (device == "cuda")
 
189
  upsampler, model_scale = get_realesrganer(model_name, scale, tile, half, device=device)
190
 
191
  out_paths: List[str] = []
192
  for i, p in enumerate(img_paths, 1):
193
  try:
 
194
  img = Image.open(p).convert("RGB")
195
  output, _ = upsampler.enhance(np.array(img), outscale=scale)
196
  out_img = Image.fromarray(output)
197
  out_file = out_dir / p.name
198
+ # Save as JPG by extension; keep name but force .jpg to be consistent
199
+ if out_file.suffix.lower() not in [".jpg", ".jpeg"]:
200
+ out_file = out_file.with_suffix(".jpg")
201
  out_img.save(out_file, quality=95)
202
  out_paths.append(str(out_file))
203
  except Exception as e:
 
204
  print(f"Upscale failed for {p}: {e}")
205
  progress(i / max(1, len(img_paths)))
206
  return out_paths
207
 
 
 
 
208
  # ─────────────────────────────────────────────────────────────
209
+ # Pipelines
210
  # ─────────────────────────────────────────────────────────────
211
 
212
+ def run_video_pipeline(
213
  video: gr.File | None,
214
  mode: str,
215
  every_seconds: float,
 
224
  scene_detect: bool,
225
  scene_thresh: float,
226
  prefix: str,
 
227
  do_upscale: bool,
228
  model_name: str,
229
  scale: int,
 
244
  raw_dir.mkdir(parents=True, exist_ok=True)
245
  up_dir.mkdir(parents=True, exist_ok=True)
246
 
247
+ # Default prefix from input filename if blank
248
+ if not prefix or not prefix.strip():
249
+ prefix = Path(video.name).stem
250
+
251
  # Build and run FFmpeg
252
  pattern = str(raw_dir / f"{prefix}_%05d.{out_format}")
253
  cmd = build_ffmpeg_command(
 
297
  return gallery, str(zip_path), detail, "", cmd_preview
298
 
299
 
300
+ def run_image_upscale_pipeline(
301
+ images: List[gr.File] | None,
302
+ model_name: str,
303
+ scale: int,
304
+ tile: int,
305
+ precision: str,
306
+ prefix: str,
307
+ ):
308
+ if not images:
309
+ return None, None, "Upload one or more images.", ""
310
+ if not _HAVE_REALESRGAN:
311
+ return None, None, "realesrgan is not installed (see requirements.txt)", ""
312
+
313
+ work = Path(tempfile.mkdtemp(prefix="imgup_"))
314
+ in_dir = work / "input"
315
+ out_dir = work / "upscaled"
316
+ in_dir.mkdir(parents=True, exist_ok=True)
317
+ out_dir.mkdir(parents=True, exist_ok=True)
318
+
319
+ # Save uploads into a temp folder with cleaned names
320
+ img_paths: List[Path] = []
321
+ for f in images:
322
+ p = Path(f.name)
323
+ name = p.name
324
+ if prefix and prefix.strip():
325
+ # keep original number if present else add incremental index
326
+ name = f"{prefix}_{name}"
327
+ dst = in_dir / name
328
+ shutil.copy2(p, dst)
329
+ img_paths.append(dst)
330
+
331
+ up_paths = upscale_images(img_paths, out_dir, model_name, scale, tile, precision)
332
+ gallery = up_paths[:60]
333
+
334
+ zip_path = work / "upscaled_images.zip"
335
+ with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zf:
336
+ for p in sorted(out_dir.glob("*")):
337
+ zf.write(p, p.name)
338
+
339
+ detail = f"Images upscaled: {len(up_paths)} | Model: {model_name} | Scale: x{scale} | Tile: {tile} | Precision: {precision}"
340
+ return gallery, str(zip_path), detail, ""
341
+
342
  # ─────────────────────────────────────────────────────────────
343
  # UI
344
  # ─────────────────────────────────────────────────────────────
 
350
  """) as demo:
351
  gr.Markdown("""
352
  <div class="cf-title">Video → JPG → Upscale (FFmpeg + Real-ESRGAN)</div>
353
+ Extract frames from video or upload images directly, then upscale with Real-ESRGAN.
354
  """)
355
 
356
+ with gr.Tabs():
357
+ # Tab 1: Video Frames (Upscale)
358
+ with gr.Tab("Video pipeline"):
359
+ with gr.Row():
360
+ video = gr.File(label="Upload video", file_types=[".mp4", ".mov", ".mkv", ".avi", ".webm", ".m4v"], type="filepath")
361
+ with gr.Accordion("Extraction", open=True):
362
+ with gr.Row():
363
+ mode = gr.Dropdown(["Every N seconds", "Every Nth frame", "Exact FPS", "All frames"], value="Every N seconds", label="Mode")
364
+ every_seconds = gr.Number(value=1.0, label="Every N seconds")
365
+ nth_frame = gr.Number(value=30, label="Every Nth frame")
366
+ exact_fps = gr.Number(value=1.0, label="Exact FPS")
367
+ with gr.Row():
368
+ start_time = gr.Textbox(value="", label="Start (HH:MM:SS.mmm)")
369
+ end_time = gr.Textbox(value="", label="End (HH:MM:SS.mmm)")
370
+ long_side = gr.Number(value=0, label="Resize long side px (0 = none)")
371
+ with gr.Row():
372
+ out_format = gr.Dropdown(["jpg", "png"], value="jpg", label="Output format")
373
+ jpg_quality = gr.Slider(2, 31, value=3, step=1, label="JPG quality (2=best)")
374
+ png_level = gr.Slider(0, 9, value=2, step=1, label="PNG compression level")
375
+ with gr.Row():
376
+ scene_detect = gr.Checkbox(False, label="Scene-change detect")
377
+ scene_thresh = gr.Slider(0.0, 1.0, value=0.3, step=0.01, label="Scene threshold")
378
+ prefix_vid = gr.Textbox(value="", label="Filename prefix (defaults to input file name)")
379
+ with gr.Accordion("Upscaling", open=True):
380
+ with gr.Row():
381
+ do_upscale = gr.Checkbox(True, label="Upscale frames with Real-ESRGAN")
382
+ model_name = gr.Dropdown(["x4plus", "x4plus-anime", "x2plus"], value="x4plus", label="Model")
383
+ scale = gr.Dropdown([2, 4], value=4, label="Output scale")
384
+ with gr.Row():
385
+ tile = gr.Number(value=0, label="Tile size (0 = auto)") # tiling reduces RAM; try 256/512 on GPU, 0 for auto
386
+ precision = gr.Dropdown(["auto", "half", "full"], value="auto", label="Precision (GPU=half, CPU=full)")
387
+
388
+ run_btn = gr.Button("Run: Extract → (Upscale) → ZIP", variant="primary")
389
+ gallery = gr.Gallery(label="Preview (first 60)", columns=6, height=300)
390
+ zip_out = gr.File(label="Download ZIP")
391
+ details = gr.Markdown("Ready.")
392
+ with gr.Accordion("Show FFmpeg command", open=False):
393
+ cmd_preview = gr.Textbox(label="ffmpeg command", lines=4, elem_classes=["cmdbox"])
394
+ if MISSING_MSG:
395
+ gr.Markdown(f"<span style='color:#b45309'>{MISSING_MSG}</span>")
396
+
397
+ def _toggle(mode_val, fmt):
398
+ return (
399
+ gr.update(visible=(mode_val == "Every N seconds")),
400
+ gr.update(visible=(mode_val == "Every Nth frame")),
401
+ gr.update(visible=(mode_val == "Exact FPS")),
402
+ gr.update(visible=(fmt == "jpg")),
403
+ gr.update(visible=(fmt == "png")),
404
+ )
405
+
406
+ mode.change(_toggle, [mode, out_format], [every_seconds, nth_frame, exact_fps, jpg_quality, png_level])
407
+ out_format.change(_toggle, [mode, out_format], [every_seconds, nth_frame, exact_fps, jpg_quality, png_level])
408
+ demo.load(_toggle, [mode, out_format], [every_seconds, nth_frame, exact_fps, jpg_quality, png_level])
409
+
410
+ run_btn.click(
411
+ run_video_pipeline,
412
+ inputs=[
413
+ video, mode, every_seconds, nth_frame, exact_fps,
414
+ start_time, end_time, long_side, out_format, jpg_quality, png_level,
415
+ scene_detect, scene_thresh, prefix_vid,
416
+ do_upscale, model_name, scale, tile, precision,
417
+ ],
418
+ outputs=[gallery, zip_out, details, gr.Textbox(), cmd_preview],
419
+ api_name="extract_and_upscale",
420
+ )
421
+
422
+ # Tab 2: Upscale images directly
423
+ with gr.Tab("Upscale images"):
424
+ with gr.Row():
425
+ imgs = gr.Files(label="Upload images (JPG/PNG)", file_types=[".jpg", ".jpeg", ".png"], type="filepath")
426
+ with gr.Row():
427
+ model_name_i = gr.Dropdown(["x4plus", "x4plus-anime", "x2plus"], value="x4plus", label="Model")
428
+ scale_i = gr.Dropdown([2, 4], value=4, label="Output scale")
429
+ tile_i = gr.Number(value=0, label="Tile size (0 = auto)")
430
+ precision_i = gr.Dropdown(["auto", "half", "full"], value="auto", label="Precision (GPU=half, CPU=full)")
431
+ with gr.Row():
432
+ prefix_img = gr.Textbox(value="", label="Optional filename prefix (adds prefix_ to each output)")
433
+ run_btn_i = gr.Button("Run: Upscale → ZIP", variant="primary")
434
+ gallery_i = gr.Gallery(label="Preview (first 60)", columns=6, height=300)
435
+ zip_out_i = gr.File(label="Download ZIP")
436
+ details_i = gr.Markdown("Ready.")
437
+
438
+ run_btn_i.click(
439
+ run_image_upscale_pipeline,
440
+ inputs=[imgs, model_name_i, scale_i, tile_i, precision_i, prefix_img],
441
+ outputs=[gallery_i, zip_out_i, details_i, gr.Textbox()],
442
+ api_name="upscale_images_only",
443
+ )
444
 
445
  return demo
446
 
447
 
448
  if __name__ == "__main__":
449
  demo = build_ui()
450
+ demo.queue().launch()
451
+
452
+ # ─────────────────────────────────────────────────────────────
453
+ # packages.txt (no comments, one per line)
454
+ # ffmpeg
455
+ # libsm6
456
+ # libxext6
457
+ # ─────────────────────────────────────────────────────────────
458
+
459
+ # ─────────────────────────────────────────────────────────────
460
+ # requirements.txt
461
+ # gradio==5.44.1
462
+ # realesrgan==0.3.0
463
+ # basicsr==1.4.2
464
+ # opencv-python-headless==4.10.0.84
465
+ # numpy
466
+ # torch==2.2.2
467
+ # ─────────────────────────────────────────────────────────────