JS6969 commited on
Commit
69c0c98
·
verified ·
1 Parent(s): 0851c0d

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +174 -43
app.py CHANGED
@@ -518,9 +518,11 @@ def step1_extract(
518
  prog_html: str,
519
  ):
520
  if not video or not video.name:
521
- return None, None, "Upload a video.", "", prog_html, None, None, None
 
522
  if not FFMPEG or not FFPROBE:
523
- return None, None, "FFmpeg missing. See note below.", MISSING_MSG, prog_html, None, None, None
 
524
 
525
  work = Path(tempfile.mkdtemp(prefix="vid2img_"))
526
  raw_dir = work / "frames_raw"
@@ -528,9 +530,24 @@ def step1_extract(
528
 
529
  prefix = sanitize_prefix(prefix_in) or Path(video.name).stem
530
 
531
- info = parse_video_info(ffprobe_json(video.name))
532
- est = estimate_output_count(mode, info.get("duration"), info.get("fps"), every_seconds, nth_frame, exact_fps)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
533
 
 
534
  pattern = str(raw_dir / f"{prefix}_%05d.{out_format}")
535
  cmd = build_ffmpeg_extract(
536
  input_path=video.name,
@@ -548,41 +565,67 @@ def step1_extract(
548
  scene_thresh=scene_thresh,
549
  out_pattern=pattern,
550
  )
 
 
 
 
551
  cmd_preview = " ".join([s if " " not in s else f'"{s}"' for s in cmd])
552
 
553
- proc = subprocess.Popen(cmd, stderr=subprocess.PIPE, stdout=subprocess.DEVNULL, text=True, bufsize=1)
 
 
554
 
555
- created = 0
556
- total = est or None
557
- last_html = prog_html
558
  while True:
559
  line = proc.stderr.readline()
560
  if not line and proc.poll() is not None:
561
  break
562
- if int(time.time()*10) % 3 == 0:
563
- created = len(list(raw_dir.glob(f"{prefix}_*.{out_format}")))
564
- if total and total > 0:
565
- pct = min(100.0, (created / total) * 100.0)
566
- last_html = render_progress(pct, f"Extracting {created}/{total}")
567
- else:
568
- last_html = render_progress(0.0, f"Extracting… {created} created")
 
 
 
 
 
 
 
 
 
 
 
569
  ret = proc.wait()
570
 
571
  frames = sorted(raw_dir.glob(f"{prefix}_*.{out_format}"), key=_natural_key)
572
 
573
- # Adaptive preview
574
  if len(frames) <= 100:
575
- gallery = [str(p) for p in frames] # show all if small set
576
  else:
577
- gallery = sample_paths(frames, 100) # evenly sample 100
578
 
579
  zip_path = work / "frames.zip"
580
  with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zf:
581
  for p in frames:
582
  zf.write(p, p.name)
583
 
 
 
 
 
 
 
 
 
 
584
  details = f"Frames extracted: {len(frames)} | Saved to: {raw_dir}"
585
- return gallery, str(zip_path), details, cmd_preview, render_progress(100.0, f"Extracted {len(frames)} frames"), [str(p) for p in frames], str(raw_dir), prefix
586
 
587
  # ───────────────── Upscale (Step 2) — supports uploaded images OR frames from Step 1
588
 
@@ -628,8 +671,10 @@ def step2_process_next_batch(
628
  up_src_paths, up_out_dir, up_done_idx, up_total,
629
  ui_model_name, outscale, tile, precision, denoise_strength, face_enhance, batch_size,
630
  ):
 
631
  if not up_src_paths or not up_out_dir:
632
- return None, None, "Load sources first.", render_progress(0.0, "Idle"), up_done_idx, up_out_dir
 
633
 
634
  model_id = map_ui_model_to_internal(ui_model_name)
635
  scale = clamp_scale_for_model(int(outscale or 4), model_id)
@@ -667,10 +712,16 @@ def step2_process_next_batch(
667
  zip_file = _save_zip_of_dir(out_dir, zip_path)
668
  prog = render_progress(100.0, "All images processed")
669
  details = f"Done. Total upscaled: {len(list(out_dir.glob('*.jpg')))+len(list(out_dir.glob('*.png')))}"
670
- return gallery, zip_file, details, prog, start, up_out_dir
 
671
 
 
 
672
  processed_now = 0
673
- for fp in up_src_paths[start:end]:
 
 
 
674
  try:
675
  with Image.open(fp) as im:
676
  img = im.convert("RGB")
@@ -681,27 +732,41 @@ def step2_process_next_batch(
681
  cv_img, has_aligned=False, only_center_face=False, paste_back=True
682
  )
683
  else:
 
684
  output, _ = upsampler.enhance(cv_img, outscale=scale, denoise_strength=float(denoise_strength or 0.5))
685
 
686
  Image.fromarray(output).save(out_dir / (Path(fp).stem + ".jpg"), quality=95)
687
 
688
  except Exception as e:
689
  print("Upscale error:", e)
690
- processed_now += 1
691
 
692
- next_idx = end
693
- pct = int(round((next_idx / up_total) * 100)) if up_total else 0
694
- label = (f"Processed {processed_now} image(s) this batch. "
695
- f"{next_idx}/{up_total} done (x{scale}, model={ui_model_name}, "
696
- f"denoise={denoise_strength}, face={face_enhance}).")
697
- prog = render_progress(pct, f"Upscaling… {pct}%")
 
 
 
 
 
 
 
 
 
698
 
 
 
699
  gallery = _build_gallery_from_dir(out_dir, 30)
700
  zip_path = Path(out_dir.parent) / "upscaled.zip"
701
  zip_file = _save_zip_of_dir(out_dir, zip_path)
702
- return gallery, zip_file, label, prog, next_idx, up_out_dir
703
-
704
 
 
 
 
 
 
705
 
706
 
707
  def save_uploaded_images(files: List[gr.File] | None, prefix: str = "upload") -> Tuple[List[Path], Path]:
@@ -876,9 +941,17 @@ def build_ffmpeg_encode(frames_dir: str, prefix: str, fps: float, fmt: str, incl
876
  return args
877
 
878
 
879
- def step3_encode(frames_dir_state: str | None, prefix_state: str | None, orig_video: gr.File | None,
880
- fps: float | None, fmt: str, include_audio: bool, prog_html: str,
881
- uploaded_frames: List[gr.File] | None, uploaded_audio_video: gr.File | None):
 
 
 
 
 
 
 
 
882
  # Choose frames source: uploaded takes priority
883
  frames_dir = frames_dir_state
884
  prefix = prefix_state
@@ -887,32 +960,68 @@ def step3_encode(frames_dir_state: str | None, prefix_state: str | None, orig_vi
887
  if detected:
888
  prefix = detected
889
  if not frames_dir or not prefix:
890
- return None, None, "No frames available. Upload frames (ZIP/images) or run Step 1.", prog_html
 
891
 
892
  fps = float(fps or 30.0)
893
  orig_path = uploaded_audio_video.name if uploaded_audio_video else (orig_video.name if orig_video else None)
894
 
 
895
  cmd = build_ffmpeg_encode(frames_dir, prefix, fps, fmt, include_audio, orig_path)
896
- proc = subprocess.Popen(cmd, stderr=subprocess.PIPE, stdout=subprocess.DEVNULL, text=True, bufsize=1, cwd=frames_dir)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
897
 
898
  last_html = prog_html
 
 
899
  while True:
900
  line = proc.stderr.readline()
901
  if not line and proc.poll() is not None:
902
  break
903
- if int(time.time()*10) % 5 == 0:
904
- last_html = render_progress(50.0, "Encoding…")
905
- ret = proc.wait()
906
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
907
  out_file = Path(frames_dir) / ("output.mp4" if fmt in ("h264", "h265") else "output.webm")
 
908
  if ret != 0 or not out_file.exists():
909
  try:
910
  err = proc.stderr.read() if proc.stderr else ""
911
  except Exception:
912
  err = ""
913
- return None, None, f"Encoding failed.\\n\\n{err}", last_html
914
-
915
- return str(out_file), f"Video created: {out_file.name}", render_progress(100.0, "Encoding complete")
 
 
916
 
917
  # ───────────────── Quick Mode — one click: All frames → Upscale ×4 → MP4 (audio)
918
 
@@ -1061,7 +1170,29 @@ def build_ui():
1061
  cmd_preview = gr.Textbox(label="ffmpeg command", lines=4, elem_classes=["cmdbox"])
1062
  if MISSING_MSG:
1063
  gr.Markdown(f"<span style='color:#b45309'>{MISSING_MSG}</span>")
1064
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1065
  def update_estimate(vfile, mode_val, evs, nth, exfps, st, et):
1066
  if not vfile or not getattr(vfile, 'name', None):
1067
  return "Estimated output: —"
 
518
  prog_html: str,
519
  ):
520
  if not video or not video.name:
521
+ yield None, None, "Upload a video.", "", prog_html, None, None, None
522
+ return
523
  if not FFMPEG or not FFPROBE:
524
+ yield None, None, "FFmpeg missing. See note below.", MISSING_MSG, prog_html, None, None, None
525
+ return
526
 
527
  work = Path(tempfile.mkdtemp(prefix="vid2img_"))
528
  raw_dir = work / "frames_raw"
 
530
 
531
  prefix = sanitize_prefix(prefix_in) or Path(video.name).stem
532
 
533
+ # Duration for progress %
534
+ vinfo = parse_video_info(ffprobe_json(video.name))
535
+ full_duration = float(vinfo.get("duration") or 0.0)
536
+
537
+ # If trimming, adjust expected duration window
538
+ def _parse_ts(ts: str) -> float:
539
+ if not ts: return 0.0
540
+ h, m, s = ts.split(":") if ":" in ts else ("0","0",ts)
541
+ return float(h)*3600 + float(m)*60 + float(s)
542
+
543
+ st_s = _parse_ts((start_time or "").strip())
544
+ et_s = _parse_ts((end_time or "").strip())
545
+ if full_duration and st_s > 0:
546
+ full_duration = max(0.0, full_duration - st_s)
547
+ if full_duration and et_s > 0 and et_s < (vinfo.get("duration") or 0):
548
+ full_duration = max(0.0, min(full_duration, et_s))
549
 
550
+ # Build command
551
  pattern = str(raw_dir / f"{prefix}_%05d.{out_format}")
552
  cmd = build_ffmpeg_extract(
553
  input_path=video.name,
 
565
  scene_thresh=scene_thresh,
566
  out_pattern=pattern,
567
  )
568
+
569
+ # Inject progress reporting
570
+ # ffmpeg will write key=value lines including out_time to stderr
571
+ cmd = [cmd[0], "-progress", "pipe:2"] + cmd[1:]
572
  cmd_preview = " ".join([s if " " not in s else f'"{s}"' for s in cmd])
573
 
574
+ proc = subprocess.Popen(
575
+ cmd, stderr=subprocess.PIPE, stdout=subprocess.DEVNULL, text=True, bufsize=1
576
+ )
577
 
578
+ # Stream updates
579
+ last_pct = 0.0
580
+ gallery_preview = []
581
  while True:
582
  line = proc.stderr.readline()
583
  if not line and proc.poll() is not None:
584
  break
585
+
586
+ line = (line or "").strip()
587
+ # out_time is in HH:MM:SS.microsec
588
+ if line.startswith("out_time=") and full_duration > 0:
589
+ t = line.split("=", 1)[1]
590
+ # Convert HH:MM:SS.xx to seconds
591
+ try:
592
+ h, m, s = t.split(":")
593
+ secs = float(h) * 3600 + float(m) * 60 + float(s)
594
+ except Exception:
595
+ secs = 0.0
596
+ pct = max(0.0, min(100.0, (secs / full_duration) * 100.0))
597
+ if pct - last_pct >= 1.0 or pct in (0.0, 100.0):
598
+ last_pct = pct
599
+ # Lightweight live gallery sample for feel; not required
600
+ gallery_preview = sample_paths(sorted(raw_dir.glob(f"{prefix}_*.{out_format}"), key=_natural_key), 36)
601
+ yield gallery_preview, None, "Extracting…", cmd_preview, render_progress(pct, f"Extracting {pct:.0f}%"), None, str(raw_dir), prefix
602
+
603
  ret = proc.wait()
604
 
605
  frames = sorted(raw_dir.glob(f"{prefix}_*.{out_format}"), key=_natural_key)
606
 
607
+ # Show all if ≤100, else sample 100
608
  if len(frames) <= 100:
609
+ gallery = [str(p) for p in frames]
610
  else:
611
+ gallery = sample_paths(frames, 100)
612
 
613
  zip_path = work / "frames.zip"
614
  with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED) as zf:
615
  for p in frames:
616
  zf.write(p, p.name)
617
 
618
+ if ret != 0 or not frames:
619
+ err = ""
620
+ try:
621
+ err = proc.stderr.read() if proc.stderr else ""
622
+ except Exception:
623
+ pass
624
+ yield gallery, None, f"Extraction failed.\n\n{err}", cmd_preview, render_progress(0.0, "Failed"), None, str(raw_dir), prefix
625
+ return
626
+
627
  details = f"Frames extracted: {len(frames)} | Saved to: {raw_dir}"
628
+ yield gallery, str(zip_path), details, cmd_preview, render_progress(100.0, f"Extracted {len(frames)} frames"), [str(p) for p in frames], str(raw_dir), prefix
629
 
630
  # ───────────────── Upscale (Step 2) — supports uploaded images OR frames from Step 1
631
 
 
671
  up_src_paths, up_out_dir, up_done_idx, up_total,
672
  ui_model_name, outscale, tile, precision, denoise_strength, face_enhance, batch_size,
673
  ):
674
+ # Turn this into a generator that streams progress
675
  if not up_src_paths or not up_out_dir:
676
+ yield None, None, "Load sources first.", render_progress(0.0, "Idle"), up_done_idx, up_out_dir
677
+ return
678
 
679
  model_id = map_ui_model_to_internal(ui_model_name)
680
  scale = clamp_scale_for_model(int(outscale or 4), model_id)
 
712
  zip_file = _save_zip_of_dir(out_dir, zip_path)
713
  prog = render_progress(100.0, "All images processed")
714
  details = f"Done. Total upscaled: {len(list(out_dir.glob('*.jpg')))+len(list(out_dir.glob('*.png')))}"
715
+ yield gallery, zip_file, details, prog, start, up_out_dir
716
+ return
717
 
718
+ batch_paths = up_src_paths[start:end]
719
+ total_in_batch = len(batch_paths)
720
  processed_now = 0
721
+
722
+ # For ETA
723
+ t0 = time.time()
724
+ for idx, fp in enumerate(batch_paths, start=1):
725
  try:
726
  with Image.open(fp) as im:
727
  img = im.convert("RGB")
 
732
  cv_img, has_aligned=False, only_center_face=False, paste_back=True
733
  )
734
  else:
735
+ # denoise_strength only applies to general-x4v3, but harmless otherwise
736
  output, _ = upsampler.enhance(cv_img, outscale=scale, denoise_strength=float(denoise_strength or 0.5))
737
 
738
  Image.fromarray(output).save(out_dir / (Path(fp).stem + ".jpg"), quality=95)
739
 
740
  except Exception as e:
741
  print("Upscale error:", e)
 
742
 
743
+ processed_now = idx
744
+ # Progress & ETA for THIS batch
745
+ pct_batch = (processed_now / total_in_batch) * 100.0
746
+ elapsed = time.time() - t0
747
+ secs_per_img = elapsed / max(1, processed_now)
748
+ remaining_imgs = total_in_batch - processed_now
749
+ eta = remaining_imgs * secs_per_img
750
+ label = (f"Batch: {processed_now}/{total_in_batch} · "
751
+ f"~{eta:.1f}s ETA · global {start+processed_now}/{up_total} "
752
+ f"(x{scale}, model={ui_model_name}, denoise={denoise_strength}, face={face_enhance})")
753
+
754
+ gallery = _build_gallery_from_dir(out_dir, 30)
755
+ zip_path = Path(out_dir.parent) / "upscaled.zip"
756
+ zip_file = _save_zip_of_dir(out_dir, zip_path)
757
+ yield gallery, zip_file, label, render_progress(pct_batch, f"Upscaling… {pct_batch:.0f}% (this batch)"), start+processed_now, up_out_dir
758
 
759
+ # Batch complete — final emit for this click
760
+ next_idx = end
761
  gallery = _build_gallery_from_dir(out_dir, 30)
762
  zip_path = Path(out_dir.parent) / "upscaled.zip"
763
  zip_file = _save_zip_of_dir(out_dir, zip_path)
 
 
764
 
765
+ # Total (global) percentage across all sources
766
+ pct_global = (next_idx / up_total) * 100.0 if up_total else 100.0
767
+ final_label = (f"Processed batch {total_in_batch} image(s). "
768
+ f"{next_idx}/{up_total} done (global {pct_global:.0f}%).")
769
+ yield gallery, zip_file, final_label, render_progress(pct_global, "Upscaling… (global)"), next_idx, up_out_dir
770
 
771
 
772
  def save_uploaded_images(files: List[gr.File] | None, prefix: str = "upload") -> Tuple[List[Path], Path]:
 
941
  return args
942
 
943
 
944
+ def step3_encode(
945
+ frames_dir_state: str | None,
946
+ prefix_state: str | None,
947
+ orig_video: gr.File | None,
948
+ fps: float | None,
949
+ fmt: str,
950
+ include_audio: bool,
951
+ prog_html: str,
952
+ uploaded_frames: List[gr.File] | None,
953
+ uploaded_audio_video: gr.File | None
954
+ ):
955
  # Choose frames source: uploaded takes priority
956
  frames_dir = frames_dir_state
957
  prefix = prefix_state
 
960
  if detected:
961
  prefix = detected
962
  if not frames_dir or not prefix:
963
+ yield None, "No frames available. Upload frames (ZIP/images) or run Step 1.", prog_html
964
+ return
965
 
966
  fps = float(fps or 30.0)
967
  orig_path = uploaded_audio_video.name if uploaded_audio_video else (orig_video.name if orig_video else None)
968
 
969
+ # Build ffmpeg command
970
  cmd = build_ffmpeg_encode(frames_dir, prefix, fps, fmt, include_audio, orig_path)
971
+
972
+ # Inject progress reporting
973
+ cmd.insert(1, "-progress")
974
+ cmd.insert(2, "pipe:2")
975
+
976
+ # Try to estimate total frames for progress %
977
+ total_frames = len(list(Path(frames_dir).glob(f"{prefix}_*.jpg"))) \
978
+ + len(list(Path(frames_dir).glob(f"{prefix}_*.png")))
979
+
980
+ proc = subprocess.Popen(
981
+ cmd,
982
+ stderr=subprocess.PIPE,
983
+ stdout=subprocess.DEVNULL,
984
+ text=True,
985
+ bufsize=1,
986
+ cwd=frames_dir
987
+ )
988
 
989
  last_html = prog_html
990
+ current_frame = 0
991
+
992
  while True:
993
  line = proc.stderr.readline()
994
  if not line and proc.poll() is not None:
995
  break
 
 
 
996
 
997
+ if "frame=" in line:
998
+ try:
999
+ # parse `frame=123`
1000
+ current_frame = int(line.strip().split("=")[-1])
1001
+ except Exception:
1002
+ pass
1003
+
1004
+ if total_frames > 0:
1005
+ pct = min(100.0, (current_frame / total_frames) * 100.0)
1006
+ last_html = render_progress(pct, f"Encoding… {current_frame}/{total_frames} frames")
1007
+ yield None, f"Encoding in progress… {current_frame}/{total_frames}", last_html
1008
+ else:
1009
+ last_html = render_progress(50.0, "Encoding…")
1010
+ yield None, "Encoding in progress…", last_html
1011
+
1012
+ ret = proc.wait()
1013
  out_file = Path(frames_dir) / ("output.mp4" if fmt in ("h264", "h265") else "output.webm")
1014
+
1015
  if ret != 0 or not out_file.exists():
1016
  try:
1017
  err = proc.stderr.read() if proc.stderr else ""
1018
  except Exception:
1019
  err = ""
1020
+ yield None, f"Encoding failed.\n\n{err}", last_html
1021
+ return
1022
+
1023
+ yield str(out_file), f"Video created: {out_file.name}", render_progress(100.0, "Encoding complete")
1024
+
1025
 
1026
  # ───────────────── Quick Mode — one click: All frames → Upscale ×4 → MP4 (audio)
1027
 
 
1170
  cmd_preview = gr.Textbox(label="ffmpeg command", lines=4, elem_classes=["cmdbox"])
1171
  if MISSING_MSG:
1172
  gr.Markdown(f"<span style='color:#b45309'>{MISSING_MSG}</span>")
1173
+ # Wire behavior: enable/disable param groups depending on mode / format
1174
+ def _toggle_params(mode_val, fmt):
1175
+ return (
1176
+ gr.update(visible=(mode_val == "Every N seconds")),
1177
+ gr.update(visible=(mode_val == "Every Nth frame")),
1178
+ gr.update(visible=(mode_val == "Exact FPS")),
1179
+ gr.update(visible=(fmt == "jpg")),
1180
+ gr.update(visible=(fmt == "png")),
1181
+ )
1182
+
1183
+ mode.change(
1184
+ _toggle_params,
1185
+ inputs=[mode, out_format],
1186
+ outputs=[every_seconds, nth_frame, exact_fps, jpg_quality, png_level],
1187
+ )
1188
+ out_format.change(
1189
+ _toggle_params,
1190
+ inputs=[mode, out_format],
1191
+ outputs=[every_seconds, nth_frame, exact_fps, jpg_quality, png_level],
1192
+ )
1193
+ # Initialize visibility
1194
+ demo.load(_toggle_params, inputs=[mode, out_format], outputs=[every_seconds, nth_frame, exact_fps, jpg_quality, png_level])
1195
+
1196
  def update_estimate(vfile, mode_val, evs, nth, exfps, st, et):
1197
  if not vfile or not getattr(vfile, 'name', None):
1198
  return "Estimated output: —"