John6666 commited on
Commit
611d00b
·
verified ·
1 Parent(s): bdef7c6

Upload 59 files

Browse files
Files changed (2) hide show
  1. diffueraser/diffueraser.py +62 -11
  2. gradio_app.py +629 -118
diffueraser/diffueraser.py CHANGED
@@ -166,7 +166,38 @@ def resize_frames(frames, size=None):
166
 
167
  return frames
168
 
169
- def read_mask(validation_mask, fps, n_total_frames, img_size, mask_dilation_iter, frames):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
170
  cap = cv2.VideoCapture(validation_mask)
171
  if not cap.isOpened():
172
  print("Error: Could not open mask video.")
@@ -189,15 +220,15 @@ def read_mask(validation_mask, fps, n_total_frames, img_size, mask_dilation_iter
189
  if mask.size != img_size:
190
  mask = mask.resize(img_size, Image.NEAREST)
191
  mask = np.asarray(mask)
192
- m = np.array(mask > 0).astype(np.uint8)
193
- m = cv2.erode(m,
194
- cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3)),
195
- iterations=1)
196
- m = cv2.dilate(m,
197
- cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3)),
198
- iterations=mask_dilation_iter)
199
-
200
- mask = Image.fromarray(m * 255)
201
  masks.append(mask)
202
 
203
  masked_image = np.array(frames[idx])*(1-(np.array(mask)[:,:,np.newaxis].astype(np.float32)/255))
@@ -207,6 +238,16 @@ def read_mask(validation_mask, fps, n_total_frames, img_size, mask_dilation_iter
207
  idx += 1
208
  cap.release()
209
 
 
 
 
 
 
 
 
 
 
 
210
  return masks, masked_images
211
 
212
  def read_priori(priori, fps, n_total_frames, img_size):
@@ -394,6 +435,7 @@ class DiffuEraser:
394
 
395
  def forward(self, validation_image, validation_mask, priori, output_path,
396
  max_img_size = 1280, video_length=2, mask_dilation_iter=4,
 
397
  nframes=22, seed=None, revision = None, guidance_scale=None, blended=True,
398
  prompt="", negative_prompt="", request_id=None, output_fps=None, progress_callback=None):
399
  validation_prompt = prompt or ""
@@ -429,6 +471,9 @@ class DiffuEraser:
429
  max_img_size=max_img_size,
430
  nframes=nframes,
431
  mask_dilation_iter=mask_dilation_iter,
 
 
 
432
  guidance_scale=guidance_scale_final,
433
  seed="random" if seed is None else seed,
434
  prompt_chars=len(validation_prompt),
@@ -458,7 +503,13 @@ class DiffuEraser:
458
  _progress(0.04, "DiffuEraser: reading mask")
459
  ################ read mask ################
460
  with timed_stage("diffueraser.read_mask", request_id=request_id, input=validation_mask):
461
- validation_masks_input, validation_images_input = read_mask(validation_mask, fps, video_len, img_size, mask_dilation_iter, frames)
 
 
 
 
 
 
462
  log_event("diffueraser.read_mask", "loaded masks", request_id=request_id, masks=len(validation_masks_input), masked_images=len(validation_images_input))
463
 
464
  _progress(0.07, "DiffuEraser: reading ProPainter priori")
 
166
 
167
  return frames
168
 
169
+ def _odd_kernel_size(value):
170
+ value = max(0, int(value))
171
+ if value <= 0:
172
+ return 0
173
+ return value * 2 + 1
174
+
175
+
176
+ def refine_mask_array(mask, mask_refine_mode="Keep", mask_refine_iterations=0, mask_feather_px=0, mask_dilation_iter=0):
177
+ mode = str(mask_refine_mode)
178
+ refine_iterations = max(0, int(mask_refine_iterations))
179
+ dilation_iterations = max(0, int(mask_dilation_iter))
180
+ feather_px = max(0, int(mask_feather_px))
181
+
182
+ m = (np.asarray(mask) > 0).astype(np.uint8) * 255
183
+ kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))
184
+ if refine_iterations > 0 and mode == "Erode":
185
+ m = cv2.erode(m, kernel, iterations=refine_iterations)
186
+ elif refine_iterations > 0 and mode == "Dilate":
187
+ m = cv2.dilate(m, kernel, iterations=refine_iterations)
188
+
189
+ if dilation_iterations > 0:
190
+ m = cv2.dilate(m, kernel, iterations=dilation_iterations)
191
+
192
+ kernel_size = _odd_kernel_size(feather_px)
193
+ if kernel_size > 0:
194
+ m = cv2.GaussianBlur(m, (kernel_size, kernel_size), 0)
195
+ return m
196
+
197
+
198
+ def read_mask(
199
+ validation_mask, fps, n_total_frames, img_size, mask_dilation_iter, frames,
200
+ mask_refine_mode="Keep", mask_refine_iterations=0, mask_feather_px=0, request_id=None):
201
  cap = cv2.VideoCapture(validation_mask)
202
  if not cap.isOpened():
203
  print("Error: Could not open mask video.")
 
220
  if mask.size != img_size:
221
  mask = mask.resize(img_size, Image.NEAREST)
222
  mask = np.asarray(mask)
223
+ m = refine_mask_array(
224
+ mask,
225
+ mask_refine_mode=mask_refine_mode,
226
+ mask_refine_iterations=mask_refine_iterations,
227
+ mask_feather_px=mask_feather_px,
228
+ mask_dilation_iter=mask_dilation_iter,
229
+ )
230
+
231
+ mask = Image.fromarray(m)
232
  masks.append(mask)
233
 
234
  masked_image = np.array(frames[idx])*(1-(np.array(mask)[:,:,np.newaxis].astype(np.float32)/255))
 
238
  idx += 1
239
  cap.release()
240
 
241
+ log_event(
242
+ "diffueraser.read_mask",
243
+ "mask refinement applied",
244
+ request_id=request_id,
245
+ mask_refine_mode=mask_refine_mode,
246
+ mask_refine_iterations=mask_refine_iterations,
247
+ mask_feather_px=mask_feather_px,
248
+ mask_dilation_iter=mask_dilation_iter,
249
+ masks=len(masks),
250
+ )
251
  return masks, masked_images
252
 
253
  def read_priori(priori, fps, n_total_frames, img_size):
 
435
 
436
  def forward(self, validation_image, validation_mask, priori, output_path,
437
  max_img_size = 1280, video_length=2, mask_dilation_iter=4,
438
+ mask_refine_mode="Keep", mask_refine_iterations=0, mask_feather_px=0,
439
  nframes=22, seed=None, revision = None, guidance_scale=None, blended=True,
440
  prompt="", negative_prompt="", request_id=None, output_fps=None, progress_callback=None):
441
  validation_prompt = prompt or ""
 
471
  max_img_size=max_img_size,
472
  nframes=nframes,
473
  mask_dilation_iter=mask_dilation_iter,
474
+ mask_refine_mode=mask_refine_mode,
475
+ mask_refine_iterations=mask_refine_iterations,
476
+ mask_feather_px=mask_feather_px,
477
  guidance_scale=guidance_scale_final,
478
  seed="random" if seed is None else seed,
479
  prompt_chars=len(validation_prompt),
 
503
  _progress(0.04, "DiffuEraser: reading mask")
504
  ################ read mask ################
505
  with timed_stage("diffueraser.read_mask", request_id=request_id, input=validation_mask):
506
+ validation_masks_input, validation_images_input = read_mask(
507
+ validation_mask, fps, video_len, img_size, mask_dilation_iter, frames,
508
+ mask_refine_mode=mask_refine_mode,
509
+ mask_refine_iterations=mask_refine_iterations,
510
+ mask_feather_px=mask_feather_px,
511
+ request_id=request_id,
512
+ )
513
  log_event("diffueraser.read_mask", "loaded masks", request_id=request_id, masks=len(validation_masks_input), masked_images=len(validation_images_input))
514
 
515
  _progress(0.07, "DiffuEraser: reading ProPainter priori")
gradio_app.py CHANGED
@@ -14,12 +14,15 @@ except ImportError:
14
  import datetime
15
  import os
16
  import platform
 
17
  import time
18
  import traceback
19
  import uuid
20
  from contextlib import contextmanager
21
 
 
22
  import gradio as gr
 
23
  import torch
24
  from moviepy.editor import VideoFileClip
25
 
@@ -319,8 +322,12 @@ diffueraser_path = "weights/diffuEraser"
319
  propainter_model_dir = "weights/propainter"
320
 
321
  # Default Gradio runtime parameters.
 
322
  DEFAULT_MAX_DURATION = 5
323
  DEFAULT_MASK_DILATION_ITER = 8
 
 
 
324
  DEFAULT_MAX_IMG_SIZE = 960
325
  DEFAULT_REF_STRIDE = 10
326
  DEFAULT_NEIGHBOR_LENGTH = 10
@@ -333,6 +340,10 @@ DEFAULT_GUIDANCE_SCALE = -1.0
333
  DEFAULT_OUTPUT_FRAME_RATE = "Same as processed video"
334
  DEFAULT_ZERO_GPU_DURATION_MODE = "Auto"
335
  DEFAULT_ZERO_GPU_DURATION = 180
 
 
 
 
336
 
337
  FRAME_RATE_CHOICES = ["Original", "24 fps", "15 fps", "12 fps"]
338
  FRAME_RATE_MAP = {
@@ -359,6 +370,8 @@ PCM_STEP_CHOICES = [
359
  "LCM-Like LoRA",
360
  ]
361
  SCHEDULER_CHOICES = ["Auto", "TCD", "LCM", "UniPC", "DDIM", "Euler", "DPM++ 2M"]
 
 
362
  ZERO_GPU_DURATION_MODE_CHOICES = ["Auto", "Manual"]
363
  PCM_STEP_COUNT = {
364
  "2-Step": 2,
@@ -372,30 +385,40 @@ PCM_STEP_COUNT = {
372
  }
373
  IS_ZEROGPU_SPACE = is_zerogpu_env()
374
  PRESETS = {
375
- "Fast preview": {
376
- "max_duration": 4,
377
- "max_img_size": 768,
378
- "mask_dilation_iter": 6,
379
- "subvideo_length": 40,
380
- "ref_stride": 12,
381
- "neighbor_length": 8,
382
- "ckpt": "2-Step",
383
- "frame_rate_limit": "12 fps",
 
 
 
384
  "output_frame_rate": DEFAULT_OUTPUT_FRAME_RATE,
385
  "scheduler_name": DEFAULT_SCHEDULER,
386
  "seed": DEFAULT_SEED,
387
  "guidance_scale": DEFAULT_GUIDANCE_SCALE,
388
  "zero_gpu_duration_mode": DEFAULT_ZERO_GPU_DURATION_MODE,
389
- "zero_gpu_duration": 90,
 
 
 
 
390
  },
391
- "Balanced": {
392
- "max_duration": DEFAULT_MAX_DURATION,
393
- "max_img_size": DEFAULT_MAX_IMG_SIZE,
394
- "mask_dilation_iter": DEFAULT_MASK_DILATION_ITER,
 
 
 
395
  "subvideo_length": DEFAULT_SUBVIDEO_LENGTH,
396
  "ref_stride": DEFAULT_REF_STRIDE,
397
  "neighbor_length": DEFAULT_NEIGHBOR_LENGTH,
398
- "ckpt": DEFAULT_CKPT,
399
  "frame_rate_limit": DEFAULT_FRAME_RATE_LIMIT,
400
  "output_frame_rate": DEFAULT_OUTPUT_FRAME_RATE,
401
  "scheduler_name": DEFAULT_SCHEDULER,
@@ -403,11 +426,64 @@ PRESETS = {
403
  "guidance_scale": DEFAULT_GUIDANCE_SCALE,
404
  "zero_gpu_duration_mode": DEFAULT_ZERO_GPU_DURATION_MODE,
405
  "zero_gpu_duration": DEFAULT_ZERO_GPU_DURATION,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
406
  },
407
- "Higher quality": {
408
  "max_duration": 8,
409
  "max_img_size": 1152,
410
  "mask_dilation_iter": 8,
 
 
 
411
  "subvideo_length": 60,
412
  "ref_stride": 10,
413
  "neighbor_length": 12,
@@ -419,9 +495,36 @@ PRESETS = {
419
  "guidance_scale": DEFAULT_GUIDANCE_SCALE,
420
  "zero_gpu_duration_mode": DEFAULT_ZERO_GPU_DURATION_MODE,
421
  "zero_gpu_duration": DEFAULT_ZERO_GPU_DURATION,
 
 
 
 
422
  },
423
  }
424
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
425
  # Model setup
426
  with timed_stage("startup.device"):
427
  device = get_device()
@@ -476,11 +579,98 @@ def trim_video(
476
  log_event("video.trimmed", label, request_id=request_id, **meta)
477
 
478
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
479
  def apply_preset(
480
  preset_name,
481
  max_duration,
482
  max_img_size,
483
  mask_dilation_iter,
 
 
 
484
  subvideo_length,
485
  ref_stride,
486
  neighbor_length,
@@ -492,14 +682,20 @@ def apply_preset(
492
  guidance_scale,
493
  zero_gpu_duration_mode,
494
  zero_gpu_duration,
 
 
495
  prompt,
496
  negative_prompt,
497
  ):
498
  if preset_name == "Custom":
499
  return (
 
500
  max_duration,
501
  max_img_size,
502
  mask_dilation_iter,
 
 
 
503
  subvideo_length,
504
  ref_stride,
505
  neighbor_length,
@@ -511,15 +707,21 @@ def apply_preset(
511
  guidance_scale,
512
  zero_gpu_duration_mode,
513
  zero_gpu_duration,
 
 
514
  prompt,
515
  negative_prompt,
516
  )
517
 
518
  preset = PRESETS[preset_name]
519
  return (
 
520
  preset["max_duration"],
521
  preset["max_img_size"],
522
  preset["mask_dilation_iter"],
 
 
 
523
  preset["subvideo_length"],
524
  preset["ref_stride"],
525
  preset["neighbor_length"],
@@ -531,13 +733,72 @@ def apply_preset(
531
  preset["guidance_scale"],
532
  preset["zero_gpu_duration_mode"],
533
  preset["zero_gpu_duration"],
534
- prompt,
535
- negative_prompt,
 
 
536
  )
537
 
538
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
539
  def clear_inputs():
540
- return None, None, None, None, None, None
541
 
542
 
543
  def make_ui_progress(progress, request_id=None):
@@ -577,6 +838,110 @@ def make_stage_progress(progress_update, start, end):
577
 
578
 
579
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
580
  def _clip_int(value, minimum=30, maximum=300):
581
  return max(minimum, min(maximum, int(round(value))))
582
 
@@ -609,6 +974,7 @@ def estimate_zero_gpu_duration(
609
  output_frame_rate=DEFAULT_OUTPUT_FRAME_RATE,
610
  scheduler_name=DEFAULT_SCHEDULER,
611
  guidance_scale=DEFAULT_GUIDANCE_SCALE,
 
612
  ):
613
  max_duration = max(1, int(max_duration))
614
  max_img_size = max(256, int(max_img_size))
@@ -634,9 +1000,10 @@ def estimate_zero_gpu_duration(
634
  lora_cost = 4.0
635
  scheduler_cost = 2.0 if scheduler_name != "Auto" else 0.0
636
  guidance_cost = 4.0 if guidance_scale > 1.0 else 0.0
 
637
  margin = 5.0
638
 
639
- estimate = base_cost + trim_cost + propainter_cost + lora_cost + scheduler_cost + guidance_cost + frame_cost + margin
640
  duration = _clip_int(estimate)
641
  return duration, {
642
  "fps": f"{fps:.2f}",
@@ -646,6 +1013,7 @@ def estimate_zero_gpu_duration(
646
  "step_factor": f"{step_factor:.2f}",
647
  "frame_cost_sec": f"{frame_cost:.1f}",
648
  "propainter_cost_sec": f"{propainter_cost:.1f}",
 
649
  "raw_estimate_sec": f"{estimate:.1f}",
650
  "margin_sec": f"{margin:.1f}",
651
  "duration_floor_sec": 30,
@@ -663,6 +1031,7 @@ def resolve_zero_gpu_duration(
663
  output_frame_rate=DEFAULT_OUTPUT_FRAME_RATE,
664
  scheduler_name=DEFAULT_SCHEDULER,
665
  guidance_scale=DEFAULT_GUIDANCE_SCALE,
 
666
  zero_gpu_duration_mode=DEFAULT_ZERO_GPU_DURATION_MODE,
667
  zero_gpu_duration=DEFAULT_ZERO_GPU_DURATION,
668
  ):
@@ -676,6 +1045,7 @@ def resolve_zero_gpu_duration(
676
  frame_rate_limit=frame_rate_limit,
677
  scheduler_name=scheduler_name,
678
  guidance_scale=guidance_scale,
 
679
  )
680
  mode = str(zero_gpu_duration_mode)
681
  if mode == "Manual":
@@ -701,6 +1071,9 @@ def get_duration(
701
  input_mask,
702
  max_duration=DEFAULT_MAX_DURATION,
703
  mask_dilation_iter=DEFAULT_MASK_DILATION_ITER,
 
 
 
704
  max_img_size=DEFAULT_MAX_IMG_SIZE,
705
  ref_stride=DEFAULT_REF_STRIDE,
706
  neighbor_length=DEFAULT_NEIGHBOR_LENGTH,
@@ -713,6 +1086,8 @@ def get_duration(
713
  guidance_scale=DEFAULT_GUIDANCE_SCALE,
714
  zero_gpu_duration_mode=DEFAULT_ZERO_GPU_DURATION_MODE,
715
  zero_gpu_duration=DEFAULT_ZERO_GPU_DURATION,
 
 
716
  prompt="",
717
  negative_prompt="",
718
  *extra_args,
@@ -727,6 +1102,7 @@ def get_duration(
727
  frame_rate_limit=frame_rate_limit,
728
  scheduler_name=scheduler_name,
729
  guidance_scale=guidance_scale,
 
730
  zero_gpu_duration_mode=zero_gpu_duration_mode,
731
  zero_gpu_duration=zero_gpu_duration,
732
  )
@@ -742,6 +1118,9 @@ def infer(
742
  input_mask,
743
  max_duration=DEFAULT_MAX_DURATION,
744
  mask_dilation_iter=DEFAULT_MASK_DILATION_ITER,
 
 
 
745
  max_img_size=DEFAULT_MAX_IMG_SIZE,
746
  ref_stride=DEFAULT_REF_STRIDE,
747
  neighbor_length=DEFAULT_NEIGHBOR_LENGTH,
@@ -754,6 +1133,8 @@ def infer(
754
  guidance_scale=DEFAULT_GUIDANCE_SCALE,
755
  zero_gpu_duration_mode=DEFAULT_ZERO_GPU_DURATION_MODE,
756
  zero_gpu_duration=DEFAULT_ZERO_GPU_DURATION,
 
 
757
  prompt="",
758
  negative_prompt="",
759
  progress=gr.Progress(track_tqdm=True),
@@ -778,6 +1159,9 @@ def infer(
778
  # Setup paths and parameters
779
  max_duration = int(max_duration)
780
  mask_dilation_iter = int(mask_dilation_iter)
 
 
 
781
  max_img_size = int(max_img_size)
782
  ref_stride = int(ref_stride)
783
  neighbor_length = int(neighbor_length)
@@ -792,6 +1176,8 @@ def infer(
792
  guidance_scale = None if guidance_scale_value < 0 else guidance_scale_value
793
  zero_gpu_duration_mode = str(zero_gpu_duration_mode)
794
  zero_gpu_duration = int(zero_gpu_duration)
 
 
795
  prompt = str(prompt or "")
796
  negative_prompt = str(negative_prompt or "")
797
 
@@ -804,6 +1190,7 @@ def infer(
804
  frame_rate_limit=frame_rate_limit,
805
  scheduler_name=scheduler_name,
806
  guidance_scale=guidance_scale_value,
 
807
  zero_gpu_duration_mode=zero_gpu_duration_mode,
808
  zero_gpu_duration=zero_gpu_duration,
809
  )
@@ -814,6 +1201,9 @@ def infer(
814
  request_id=request_id,
815
  max_duration=max_duration,
816
  mask_dilation_iter=mask_dilation_iter,
 
 
 
817
  max_img_size=max_img_size,
818
  ref_stride=ref_stride,
819
  neighbor_length=neighbor_length,
@@ -829,6 +1219,8 @@ def infer(
829
  zero_gpu_duration_manual_slider=zero_gpu_duration_details["manual_slider_sec"],
830
  zero_gpu_duration_auto_estimate=zero_gpu_duration_details["auto_estimate_sec"],
831
  zero_gpu_duration_effective=zero_gpu_effective_duration,
 
 
832
  prompt_chars=len(prompt),
833
  negative_prompt_chars=len(negative_prompt),
834
  )
@@ -836,7 +1228,9 @@ def infer(
836
  # Timestamp for unique filenames
837
  trimmed_video_path = os.path.join(save_path, f"trimmed_video_{timestamp}.mp4")
838
  trimmed_mask_path = os.path.join(save_path, f"trimmed_mask_{timestamp}.mp4")
 
839
  priori_path = os.path.join(save_path, f"priori_{timestamp}.mp4")
 
840
  output_path = os.path.join(save_path, f"diffueraser_result_{timestamp}.mp4")
841
  log_event(
842
  "request.paths",
@@ -844,7 +1238,9 @@ def infer(
844
  request_id=request_id,
845
  trimmed_video=trimmed_video_path,
846
  trimmed_mask=trimmed_mask_path,
 
847
  priori=priori_path,
 
848
  output=output_path,
849
  )
850
 
@@ -866,6 +1262,21 @@ def infer(
866
  request_id=request_id,
867
  label="input_mask",
868
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
869
 
870
  # Dynamically compute video_length in frames from the trimmed video.
871
  with timed_stage("video.inspect_trimmed", request_id=request_id):
@@ -896,6 +1307,9 @@ def infer(
896
  neighbor_length=neighbor_length,
897
  subvideo_length=subvideo_length,
898
  mask_dilation=mask_dilation_iter,
 
 
 
899
  ):
900
  propainter.forward(trimmed_video_path, trimmed_mask_path, priori_path,
901
  video_length=video_length, ref_stride=ref_stride,
@@ -920,15 +1334,30 @@ def infer(
920
  guidance_scale=guidance_scale,
921
  seed="random" if seed is None else seed,
922
  ):
923
- video_inpainting_sd.forward(trimmed_video_path, trimmed_mask_path, priori_path, output_path,
924
  max_img_size=max_img_size, video_length=video_length,
925
  mask_dilation_iter=mask_dilation_iter,
 
 
 
926
  seed=seed, guidance_scale=guidance_scale,
927
  prompt=prompt, negative_prompt=negative_prompt,
928
  request_id=request_id, output_fps=output_fps,
929
  progress_callback=make_stage_progress(progress_update, 0.40, 0.90))
930
  log_cuda_memory("after_diffueraser", request_id=request_id)
931
 
 
 
 
 
 
 
 
 
 
 
 
 
932
  progress_update(0.92, desc="Finalizing outputs")
933
  with timed_stage("video.inspect_output", request_id=request_id):
934
  output_meta = video_metadata(output_path)
@@ -944,12 +1373,14 @@ def infer(
944
  request_id=request_id,
945
  total_elapsed_sec=f"{total_elapsed:.2f}",
946
  output=output_path,
 
947
  trimmed_video=trimmed_video_path,
 
948
  priori=priori_path,
949
  debug_log=debug_log_path,
950
  )
951
  progress_update(1.0, desc="Done")
952
- return output_path, trimmed_video_path, priori_path, debug_log_path
953
 
954
  except Exception as exc:
955
  total_elapsed = time.perf_counter() - total_start
@@ -985,29 +1416,23 @@ with gr.Blocks() as demo:
985
  input_video = gr.Video(label="Input Video (MP4 ONLY)")
986
  input_mask = gr.Video(label="Input Mask Video (MP4 ONLY)")
987
  with gr.Accordion("Basic settings", open=True):
988
- gr.Markdown("For a quick ZeroGPU check, keep the default values or use Fast preview.")
989
  preset = gr.Dropdown(
990
- label="Preset",
991
- choices=["Fast preview", "Balanced", "Higher quality", "Custom"],
992
- value="Balanced",
993
- )
994
- with gr.Row():
995
- ckpt = gr.Dropdown(
996
- label="PCM checkpoint",
997
- choices=PCM_STEP_CHOICES,
998
- value=DEFAULT_CKPT,
999
- )
1000
- frame_rate_limit = gr.Dropdown(
1001
- label="Frame rate limit",
1002
- choices=FRAME_RATE_CHOICES,
1003
- value=DEFAULT_FRAME_RATE_LIMIT,
1004
- )
1005
- output_frame_rate = gr.Dropdown(
1006
- label="Output frame rate",
1007
- choices=OUTPUT_FRAME_RATE_CHOICES,
1008
- value=DEFAULT_OUTPUT_FRAME_RATE,
1009
- info="Can reduce the output video FPS without changing the inference frame rate.",
1010
  )
 
 
 
1011
  with gr.Row():
1012
  max_duration = gr.Slider(
1013
  label="Max clip duration (seconds)",
@@ -1016,91 +1441,159 @@ with gr.Blocks() as demo:
1016
  value=DEFAULT_MAX_DURATION,
1017
  step=1,
1018
  )
1019
- max_img_size = gr.Slider(
1020
- label="Maximum image size",
1021
- minimum=512,
1022
- maximum=1280,
1023
- value=DEFAULT_MAX_IMG_SIZE,
1024
- step=64,
1025
  )
1026
- mask_dilation_iter = gr.Slider(
1027
- label="Mask dilation iterations",
1028
- minimum=0,
1029
- maximum=20,
1030
- value=DEFAULT_MASK_DILATION_ITER,
1031
- step=1,
1032
- )
1033
 
1034
  with gr.Accordion("Advanced settings", open=False):
1035
- gr.Markdown("Optional controls. Auto scheduler keeps the default PCM behavior. Negative prompt is most useful when guidance scale is above 1.")
1036
- prompt = gr.Textbox(
1037
- label="Prompt",
1038
- value="",
1039
- lines=2,
1040
- placeholder="Optional. Leave blank for the original DiffuEraser behavior.",
1041
- )
1042
- negative_prompt = gr.Textbox(
1043
- label="Negative prompt",
1044
- value="",
1045
- lines=2,
1046
- placeholder="Optional. Mainly affects results when guidance scale is above 1.",
1047
- )
1048
- with gr.Row():
1049
- scheduler_name = gr.Dropdown(
1050
- label="Scheduler",
1051
- choices=SCHEDULER_CHOICES,
1052
- value=DEFAULT_SCHEDULER,
1053
- )
 
 
 
 
 
 
 
 
 
 
 
 
1054
  seed = gr.Number(
1055
  label="Seed (-1 for random)",
1056
  value=DEFAULT_SEED,
1057
  precision=0,
1058
  )
1059
- guidance_scale = gr.Slider(
1060
- label="Guidance scale (-1 uses checkpoint default)",
1061
- minimum=-1,
1062
- maximum=12,
1063
- value=DEFAULT_GUIDANCE_SCALE,
1064
- step=0.5,
1065
- info="Use -1 to keep each PCM checkpoint's default guidance scale.",
1066
- )
1067
- with gr.Row(visible=IS_ZEROGPU_SPACE):
1068
- zero_gpu_duration_mode = gr.Dropdown(
1069
- label="ZeroGPU duration mode",
1070
- choices=ZERO_GPU_DURATION_MODE_CHOICES,
1071
- value=DEFAULT_ZERO_GPU_DURATION_MODE,
1072
- info="Auto estimates the reservation from the current settings. Normal GPU Spaces ignore this setting.",
1073
  )
1074
- zero_gpu_duration = gr.Slider(
1075
- label="Manual ZeroGPU reserved duration (seconds)",
1076
- minimum=30,
1077
- maximum=300,
1078
- value=DEFAULT_ZERO_GPU_DURATION,
1079
- step=30,
1080
- info="Used only when ZeroGPU duration mode is Manual.",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1081
  )
1082
- with gr.Row():
1083
- subvideo_length = gr.Slider(
1084
- label="ProPainter subvideo length",
1085
- minimum=10,
1086
- maximum=100,
1087
- value=DEFAULT_SUBVIDEO_LENGTH,
1088
- step=5,
 
1089
  )
1090
- ref_stride = gr.Slider(
1091
- label="ProPainter reference stride",
1092
- minimum=1,
1093
- maximum=20,
1094
- value=DEFAULT_REF_STRIDE,
1095
- step=1,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1096
  )
1097
- neighbor_length = gr.Slider(
1098
- label="ProPainter neighbor length",
1099
- minimum=2,
1100
- maximum=30,
1101
- value=DEFAULT_NEIGHBOR_LENGTH,
1102
- step=2,
1103
- )
1104
 
1105
  with gr.Row():
1106
  submit_btn = gr.Button("Run", variant="primary")
@@ -1110,7 +1603,9 @@ with gr.Blocks() as demo:
1110
  video_result = gr.Video(label="Result", buttons=["download"])
1111
  with gr.Accordion("Debug outputs", open=False):
1112
  trimmed_video_result = gr.Video(label="Trimmed input video", interactive=False)
 
1113
  priori_result = gr.Video(label="ProPainter priori video", interactive=False)
 
1114
  debug_log_file = gr.File(label="Debug log file", interactive=False)
1115
  gr.Examples(
1116
  examples=[
@@ -1128,6 +1623,9 @@ with gr.Blocks() as demo:
1128
  max_duration,
1129
  max_img_size,
1130
  mask_dilation_iter,
 
 
 
1131
  subvideo_length,
1132
  ref_stride,
1133
  neighbor_length,
@@ -1139,13 +1637,19 @@ with gr.Blocks() as demo:
1139
  guidance_scale,
1140
  zero_gpu_duration_mode,
1141
  zero_gpu_duration,
 
 
1142
  prompt,
1143
  negative_prompt,
1144
  ],
1145
  outputs=[
 
1146
  max_duration,
1147
  max_img_size,
1148
  mask_dilation_iter,
 
 
 
1149
  subvideo_length,
1150
  ref_stride,
1151
  neighbor_length,
@@ -1157,6 +1661,8 @@ with gr.Blocks() as demo:
1157
  guidance_scale,
1158
  zero_gpu_duration_mode,
1159
  zero_gpu_duration,
 
 
1160
  prompt,
1161
  negative_prompt,
1162
  ],
@@ -1165,7 +1671,7 @@ with gr.Blocks() as demo:
1165
  clear_btn.click(
1166
  fn=clear_inputs,
1167
  inputs=None,
1168
- outputs=[input_video, input_mask, video_result, trimmed_video_result, priori_result, debug_log_file],
1169
  )
1170
 
1171
  submit_btn.click(
@@ -1175,6 +1681,9 @@ with gr.Blocks() as demo:
1175
  input_mask,
1176
  max_duration,
1177
  mask_dilation_iter,
 
 
 
1178
  max_img_size,
1179
  ref_stride,
1180
  neighbor_length,
@@ -1187,10 +1696,12 @@ with gr.Blocks() as demo:
1187
  guidance_scale,
1188
  zero_gpu_duration_mode,
1189
  zero_gpu_duration,
 
 
1190
  prompt,
1191
  negative_prompt,
1192
  ],
1193
- outputs=[video_result, trimmed_video_result, priori_result, debug_log_file],
1194
  )
1195
 
1196
  demo.queue().launch(show_error=True, ssr_mode=False)
 
14
  import datetime
15
  import os
16
  import platform
17
+ import shutil
18
  import time
19
  import traceback
20
  import uuid
21
  from contextlib import contextmanager
22
 
23
+ import cv2
24
  import gradio as gr
25
+ import numpy as np
26
  import torch
27
  from moviepy.editor import VideoFileClip
28
 
 
322
  propainter_model_dir = "weights/propainter"
323
 
324
  # Default Gradio runtime parameters.
325
+ DEFAULT_PRESET = "Remove object / person"
326
  DEFAULT_MAX_DURATION = 5
327
  DEFAULT_MASK_DILATION_ITER = 8
328
+ DEFAULT_MASK_REFINE_MODE = "Keep"
329
+ DEFAULT_MASK_REFINE_ITERATIONS = 0
330
+ DEFAULT_MASK_FEATHER_PX = 0
331
  DEFAULT_MAX_IMG_SIZE = 960
332
  DEFAULT_REF_STRIDE = 10
333
  DEFAULT_NEIGHBOR_LENGTH = 10
 
340
  DEFAULT_OUTPUT_FRAME_RATE = "Same as processed video"
341
  DEFAULT_ZERO_GPU_DURATION_MODE = "Auto"
342
  DEFAULT_ZERO_GPU_DURATION = 180
343
+ DEFAULT_PROMPT = ""
344
+ DEFAULT_NEGATIVE_PROMPT = ""
345
+ DEFAULT_FACE_WATERMARK_MODE = "Off"
346
+ DEFAULT_FACE_CORRECTION_STRENGTH = 0.65
347
 
348
  FRAME_RATE_CHOICES = ["Original", "24 fps", "15 fps", "12 fps"]
349
  FRAME_RATE_MAP = {
 
370
  "LCM-Like LoRA",
371
  ]
372
  SCHEDULER_CHOICES = ["Auto", "TCD", "LCM", "UniPC", "DDIM", "Euler", "DPM++ 2M"]
373
+ MASK_REFINE_MODE_CHOICES = ["Erode", "Keep", "Dilate"]
374
+ FACE_WATERMARK_MODE_CHOICES = ["Off", "Preserve original face detail", "Use ProPainter-prior blend"]
375
  ZERO_GPU_DURATION_MODE_CHOICES = ["Auto", "Manual"]
376
  PCM_STEP_COUNT = {
377
  "2-Step": 2,
 
385
  }
386
  IS_ZEROGPU_SPACE = is_zerogpu_env()
387
  PRESETS = {
388
+ "Remove object / person": {
389
+ "max_duration": 5,
390
+ "max_img_size": 960,
391
+ "mask_dilation_iter": 8,
392
+ "mask_refine_mode": "Keep",
393
+ "mask_refine_iterations": 0,
394
+ "mask_feather_px": 0,
395
+ "subvideo_length": DEFAULT_SUBVIDEO_LENGTH,
396
+ "ref_stride": DEFAULT_REF_STRIDE,
397
+ "neighbor_length": DEFAULT_NEIGHBOR_LENGTH,
398
+ "ckpt": "4-Step",
399
+ "frame_rate_limit": DEFAULT_FRAME_RATE_LIMIT,
400
  "output_frame_rate": DEFAULT_OUTPUT_FRAME_RATE,
401
  "scheduler_name": DEFAULT_SCHEDULER,
402
  "seed": DEFAULT_SEED,
403
  "guidance_scale": DEFAULT_GUIDANCE_SCALE,
404
  "zero_gpu_duration_mode": DEFAULT_ZERO_GPU_DURATION_MODE,
405
+ "zero_gpu_duration": DEFAULT_ZERO_GPU_DURATION,
406
+ "prompt": "",
407
+ "negative_prompt": "",
408
+ "face_watermark_mode": DEFAULT_FACE_WATERMARK_MODE,
409
+ "face_correction_strength": DEFAULT_FACE_CORRECTION_STRENGTH,
410
  },
411
+ "Remove small watermark or logo": {
412
+ "max_duration": 5,
413
+ "max_img_size": 960,
414
+ "mask_dilation_iter": 4,
415
+ "mask_refine_mode": "Keep",
416
+ "mask_refine_iterations": 0,
417
+ "mask_feather_px": 4,
418
  "subvideo_length": DEFAULT_SUBVIDEO_LENGTH,
419
  "ref_stride": DEFAULT_REF_STRIDE,
420
  "neighbor_length": DEFAULT_NEIGHBOR_LENGTH,
421
+ "ckpt": "4-Step",
422
  "frame_rate_limit": DEFAULT_FRAME_RATE_LIMIT,
423
  "output_frame_rate": DEFAULT_OUTPUT_FRAME_RATE,
424
  "scheduler_name": DEFAULT_SCHEDULER,
 
426
  "guidance_scale": DEFAULT_GUIDANCE_SCALE,
427
  "zero_gpu_duration_mode": DEFAULT_ZERO_GPU_DURATION_MODE,
428
  "zero_gpu_duration": DEFAULT_ZERO_GPU_DURATION,
429
+ "prompt": "",
430
+ "negative_prompt": "",
431
+ "face_watermark_mode": DEFAULT_FACE_WATERMARK_MODE,
432
+ "face_correction_strength": DEFAULT_FACE_CORRECTION_STRENGTH,
433
+ },
434
+ "Face-area watermark": {
435
+ "max_duration": 5,
436
+ "max_img_size": 1152,
437
+ "mask_dilation_iter": 2,
438
+ "mask_refine_mode": "Erode",
439
+ "mask_refine_iterations": 1,
440
+ "mask_feather_px": 4,
441
+ "subvideo_length": DEFAULT_SUBVIDEO_LENGTH,
442
+ "ref_stride": DEFAULT_REF_STRIDE,
443
+ "neighbor_length": DEFAULT_NEIGHBOR_LENGTH,
444
+ "ckpt": "8-Step",
445
+ "frame_rate_limit": "Original",
446
+ "output_frame_rate": DEFAULT_OUTPUT_FRAME_RATE,
447
+ "scheduler_name": DEFAULT_SCHEDULER,
448
+ "seed": DEFAULT_SEED,
449
+ "guidance_scale": DEFAULT_GUIDANCE_SCALE,
450
+ "zero_gpu_duration_mode": DEFAULT_ZERO_GPU_DURATION_MODE,
451
+ "zero_gpu_duration": DEFAULT_ZERO_GPU_DURATION,
452
+ "prompt": "",
453
+ "negative_prompt": "distorted face, deformed face, blurry face, warped mouth",
454
+ "face_watermark_mode": "Preserve original face detail",
455
+ "face_correction_strength": 0.65,
456
+ },
457
+ "Fast preview": {
458
+ "max_duration": 4,
459
+ "max_img_size": 768,
460
+ "mask_dilation_iter": 8,
461
+ "mask_refine_mode": "Keep",
462
+ "mask_refine_iterations": 0,
463
+ "mask_feather_px": 0,
464
+ "subvideo_length": 40,
465
+ "ref_stride": 12,
466
+ "neighbor_length": 8,
467
+ "ckpt": "2-Step",
468
+ "frame_rate_limit": "12 fps",
469
+ "output_frame_rate": DEFAULT_OUTPUT_FRAME_RATE,
470
+ "scheduler_name": DEFAULT_SCHEDULER,
471
+ "seed": DEFAULT_SEED,
472
+ "guidance_scale": DEFAULT_GUIDANCE_SCALE,
473
+ "zero_gpu_duration_mode": DEFAULT_ZERO_GPU_DURATION_MODE,
474
+ "zero_gpu_duration": 90,
475
+ "prompt": "",
476
+ "negative_prompt": "",
477
+ "face_watermark_mode": DEFAULT_FACE_WATERMARK_MODE,
478
+ "face_correction_strength": DEFAULT_FACE_CORRECTION_STRENGTH,
479
  },
480
+ "High quality object removal": {
481
  "max_duration": 8,
482
  "max_img_size": 1152,
483
  "mask_dilation_iter": 8,
484
+ "mask_refine_mode": "Keep",
485
+ "mask_refine_iterations": 0,
486
+ "mask_feather_px": 0,
487
  "subvideo_length": 60,
488
  "ref_stride": 10,
489
  "neighbor_length": 12,
 
495
  "guidance_scale": DEFAULT_GUIDANCE_SCALE,
496
  "zero_gpu_duration_mode": DEFAULT_ZERO_GPU_DURATION_MODE,
497
  "zero_gpu_duration": DEFAULT_ZERO_GPU_DURATION,
498
+ "prompt": "",
499
+ "negative_prompt": "",
500
+ "face_watermark_mode": DEFAULT_FACE_WATERMARK_MODE,
501
+ "face_correction_strength": DEFAULT_FACE_CORRECTION_STRENGTH,
502
  },
503
  }
504
 
505
+ TASK_MODE_SUMMARIES = {
506
+ "Remove object / person": "**Current mode:** object/person removal · wider mask · balanced quality.",
507
+ "Remove small watermark or logo": "**Current mode:** small logo/watermark · moderate mask · keeps normal inpainting behavior.",
508
+ "Face-area watermark": "**Current mode:** face-area watermark · small mask · original-face blend · slower.",
509
+ "Fast preview": "**Current mode:** fast preview · short clip · low FPS · lower quality.",
510
+ "High quality object removal": "**Current mode:** high quality object removal · larger image size · slower.",
511
+ "Custom": "**Current mode:** custom settings · Advanced values are used as-is.",
512
+ }
513
+
514
+ SITUATION_GUIDE = """
515
+ ### Choose a task mode
516
+
517
+ | Situation | Recommended mode | Notes |
518
+ |---|---|---|
519
+ | Remove a whole person, object, or large unwanted region | **Remove object / person** | Best default. Uses a wider mask so object edges are less likely to remain. |
520
+ | Remove a small logo, subtitle, or watermark that is not on a face | **Remove small watermark or logo** | Uses moderate feathering and less dilation than full object removal. |
521
+ | Remove a small watermark over eyes, mouth, or other facial details | **Face-area watermark** | Preserves more original face detail. It may leave some watermark and is not for full-body removal. |
522
+ | Quickly check that the Space works | **Fast preview** | Lower quality, shorter duration, lower FPS. |
523
+ | Object/person removal with more quality and more runtime | **High quality object removal** | Slower. Useful after the normal object mode works. |
524
+
525
+ If the mask covers a large area, start with **Remove object / person**. Use **Face-area watermark** only for small masks over facial details.
526
+ """
527
+
528
  # Model setup
529
  with timed_stage("startup.device"):
530
  device = get_device()
 
579
  log_event("video.trimmed", label, request_id=request_id, **meta)
580
 
581
 
582
+ def _odd_kernel_size(value):
583
+ value = max(0, int(value))
584
+ if value <= 0:
585
+ return 0
586
+ # Use a radius-like UI value and convert it to an odd Gaussian kernel size.
587
+ return value * 2 + 1
588
+
589
+
590
+ def refine_mask_array(mask, mask_refine_mode="Keep", mask_refine_iterations=0, mask_feather_px=0, mask_dilation_iter=0):
591
+ mode = str(mask_refine_mode)
592
+ refine_iterations = max(0, int(mask_refine_iterations))
593
+ dilation_iterations = max(0, int(mask_dilation_iter))
594
+ feather_px = max(0, int(mask_feather_px))
595
+
596
+ m = (np.asarray(mask) > 0).astype(np.uint8) * 255
597
+ kernel = cv2.getStructuringElement(cv2.MORPH_RECT, (3, 3))
598
+ if refine_iterations > 0 and mode == "Erode":
599
+ m = cv2.erode(m, kernel, iterations=refine_iterations)
600
+ elif refine_iterations > 0 and mode == "Dilate":
601
+ m = cv2.dilate(m, kernel, iterations=refine_iterations)
602
+
603
+ if dilation_iterations > 0:
604
+ m = cv2.dilate(m, kernel, iterations=dilation_iterations)
605
+
606
+ kernel_size = _odd_kernel_size(feather_px)
607
+ if kernel_size > 0:
608
+ m = cv2.GaussianBlur(m, (kernel_size, kernel_size), 0)
609
+ return m
610
+
611
+
612
+ def create_refined_mask_preview(
613
+ input_path,
614
+ output_path,
615
+ mask_refine_mode=DEFAULT_MASK_REFINE_MODE,
616
+ mask_refine_iterations=DEFAULT_MASK_REFINE_ITERATIONS,
617
+ mask_feather_px=DEFAULT_MASK_FEATHER_PX,
618
+ mask_dilation_iter=DEFAULT_MASK_DILATION_ITER,
619
+ request_id=None,
620
+ ):
621
+ with timed_stage(
622
+ "mask.preview_refined",
623
+ request_id=request_id,
624
+ input=input_path,
625
+ output=output_path,
626
+ mask_refine_mode=mask_refine_mode,
627
+ mask_refine_iterations=mask_refine_iterations,
628
+ mask_feather_px=mask_feather_px,
629
+ mask_dilation_iter=mask_dilation_iter,
630
+ ):
631
+ cap = cv2.VideoCapture(input_path)
632
+ if not cap.isOpened():
633
+ raise ValueError("Could not open mask video for refinement preview.")
634
+ fps = cap.get(cv2.CAP_PROP_FPS) or 15
635
+ width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))
636
+ height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
637
+ fourcc = cv2.VideoWriter_fourcc(*"mp4v")
638
+ writer = cv2.VideoWriter(output_path, fourcc, fps, (width, height))
639
+ frames = 0
640
+ try:
641
+ while True:
642
+ ret, frame = cap.read()
643
+ if not ret:
644
+ break
645
+ gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
646
+ refined = refine_mask_array(
647
+ gray,
648
+ mask_refine_mode=mask_refine_mode,
649
+ mask_refine_iterations=mask_refine_iterations,
650
+ mask_feather_px=mask_feather_px,
651
+ mask_dilation_iter=mask_dilation_iter,
652
+ )
653
+ writer.write(cv2.cvtColor(refined, cv2.COLOR_GRAY2BGR))
654
+ frames += 1
655
+ finally:
656
+ cap.release()
657
+ writer.release()
658
+ meta = video_metadata(output_path)
659
+ log_event("mask.preview_refined", "created refined mask preview", request_id=request_id, frames=frames, **meta)
660
+
661
+
662
+ def task_mode_summary(task_mode):
663
+ return TASK_MODE_SUMMARIES.get(str(task_mode), TASK_MODE_SUMMARIES["Custom"])
664
+
665
+
666
  def apply_preset(
667
  preset_name,
668
  max_duration,
669
  max_img_size,
670
  mask_dilation_iter,
671
+ mask_refine_mode,
672
+ mask_refine_iterations,
673
+ mask_feather_px,
674
  subvideo_length,
675
  ref_stride,
676
  neighbor_length,
 
682
  guidance_scale,
683
  zero_gpu_duration_mode,
684
  zero_gpu_duration,
685
+ face_watermark_mode,
686
+ face_correction_strength,
687
  prompt,
688
  negative_prompt,
689
  ):
690
  if preset_name == "Custom":
691
  return (
692
+ task_mode_summary(preset_name),
693
  max_duration,
694
  max_img_size,
695
  mask_dilation_iter,
696
+ mask_refine_mode,
697
+ mask_refine_iterations,
698
+ mask_feather_px,
699
  subvideo_length,
700
  ref_stride,
701
  neighbor_length,
 
707
  guidance_scale,
708
  zero_gpu_duration_mode,
709
  zero_gpu_duration,
710
+ face_watermark_mode,
711
+ face_correction_strength,
712
  prompt,
713
  negative_prompt,
714
  )
715
 
716
  preset = PRESETS[preset_name]
717
  return (
718
+ task_mode_summary(preset_name),
719
  preset["max_duration"],
720
  preset["max_img_size"],
721
  preset["mask_dilation_iter"],
722
+ preset["mask_refine_mode"],
723
+ preset["mask_refine_iterations"],
724
+ preset["mask_feather_px"],
725
  preset["subvideo_length"],
726
  preset["ref_stride"],
727
  preset["neighbor_length"],
 
733
  preset["guidance_scale"],
734
  preset["zero_gpu_duration_mode"],
735
  preset["zero_gpu_duration"],
736
+ preset.get("face_watermark_mode", DEFAULT_FACE_WATERMARK_MODE),
737
+ preset.get("face_correction_strength", DEFAULT_FACE_CORRECTION_STRENGTH),
738
+ preset.get("prompt", prompt),
739
+ preset.get("negative_prompt", negative_prompt),
740
  )
741
 
742
 
743
+ def inspect_mask_for_warning(mask_path, face_watermark_mode, mask_refine_mode, request_id=None):
744
+ face_like_mode = str(face_watermark_mode) != "Off" or str(mask_refine_mode) == "Erode"
745
+ if not face_like_mode:
746
+ return
747
+ try:
748
+ cap = cv2.VideoCapture(mask_path)
749
+ if not cap.isOpened():
750
+ return
751
+ frame_count = int(cap.get(cv2.CAP_PROP_FRAME_COUNT) or 0)
752
+ sample_stride = max(1, frame_count // 10) if frame_count else 1
753
+ ratios = []
754
+ bbox_ratios = []
755
+ index = 0
756
+ while True:
757
+ ret, frame = cap.read()
758
+ if not ret:
759
+ break
760
+ if index % sample_stride == 0:
761
+ gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
762
+ mask = gray > 24
763
+ ratio = float(mask.mean())
764
+ ratios.append(ratio)
765
+ coords = cv2.findNonZero(mask.astype(np.uint8))
766
+ if coords is not None:
767
+ x, y, w, h = cv2.boundingRect(coords)
768
+ bbox_ratios.append((w * h) / float(mask.shape[0] * mask.shape[1]))
769
+ index += 1
770
+ cap.release()
771
+ if not ratios:
772
+ return
773
+ avg_ratio = sum(ratios) / len(ratios)
774
+ max_bbox_ratio = max(bbox_ratios) if bbox_ratios else 0.0
775
+ log_event(
776
+ "mask.warning_check",
777
+ "computed mask area",
778
+ request_id=request_id,
779
+ avg_mask_ratio=f"{avg_ratio:.3f}",
780
+ max_bbox_ratio=f"{max_bbox_ratio:.3f}",
781
+ samples=len(ratios),
782
+ face_watermark_mode=face_watermark_mode,
783
+ mask_refine_mode=mask_refine_mode,
784
+ )
785
+ if avg_ratio > 0.10 or max_bbox_ratio > 0.22:
786
+ gr.Warning(
787
+ "Face-area watermark settings are intended for small face-detail masks. "
788
+ "This mask looks fairly large, so Remove object / person may work better."
789
+ )
790
+ log_event(
791
+ "mask.warning_check",
792
+ "warning emitted",
793
+ request_id=request_id,
794
+ avg_mask_ratio=f"{avg_ratio:.3f}",
795
+ max_bbox_ratio=f"{max_bbox_ratio:.3f}",
796
+ )
797
+ except Exception as exc:
798
+ log_event("mask.warning_check", "failed", request_id=request_id, error_type=type(exc).__name__, error=str(exc))
799
+
800
  def clear_inputs():
801
+ return None, None, None, None, None, None, None, None
802
 
803
 
804
  def make_ui_progress(progress, request_id=None):
 
838
 
839
 
840
 
841
+ def blend_video_with_mask(
842
+ generated_path,
843
+ base_path,
844
+ mask_path,
845
+ output_path,
846
+ correction_strength=0.65,
847
+ request_id=None,
848
+ label="face_watermark_blend",
849
+ ):
850
+ correction_strength = max(0.0, min(1.0, float(correction_strength)))
851
+ with timed_stage(
852
+ "postprocess.face_watermark_blend",
853
+ request_id=request_id,
854
+ generated=generated_path,
855
+ base=base_path,
856
+ mask=mask_path,
857
+ output=output_path,
858
+ correction_strength=f"{correction_strength:.2f}",
859
+ label=label,
860
+ ):
861
+ gen_cap = cv2.VideoCapture(generated_path)
862
+ base_cap = cv2.VideoCapture(base_path)
863
+ mask_cap = cv2.VideoCapture(mask_path)
864
+ if not gen_cap.isOpened() or not base_cap.isOpened() or not mask_cap.isOpened():
865
+ raise ValueError("Could not open one or more videos for face watermark blending.")
866
+
867
+ fps = gen_cap.get(cv2.CAP_PROP_FPS) or 15
868
+ width = int(gen_cap.get(cv2.CAP_PROP_FRAME_WIDTH))
869
+ height = int(gen_cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
870
+ fourcc = cv2.VideoWriter_fourcc(*"mp4v")
871
+ writer = cv2.VideoWriter(output_path, fourcc, fps, (width, height))
872
+ frames = 0
873
+ try:
874
+ while True:
875
+ ret_gen, gen = gen_cap.read()
876
+ ret_base, base = base_cap.read()
877
+ ret_mask, mask_frame = mask_cap.read()
878
+ if not ret_gen:
879
+ break
880
+ if not ret_base:
881
+ base = gen
882
+ if not ret_mask:
883
+ mask_frame = np.zeros_like(gen)
884
+
885
+ if base.shape[:2] != gen.shape[:2]:
886
+ base = cv2.resize(base, (width, height), interpolation=cv2.INTER_LINEAR)
887
+ if mask_frame.shape[:2] != gen.shape[:2]:
888
+ mask_frame = cv2.resize(mask_frame, (width, height), interpolation=cv2.INTER_LINEAR)
889
+
890
+ gray = cv2.cvtColor(mask_frame, cv2.COLOR_BGR2GRAY).astype(np.float32) / 255.0
891
+ # The mask controls where to preserve base details. Outside the mask, keep DiffuEraser's output.
892
+ preserve_alpha = gray * (1.0 - correction_strength)
893
+ preserve_alpha = preserve_alpha[:, :, None]
894
+ blended = gen.astype(np.float32) * (1.0 - preserve_alpha) + base.astype(np.float32) * preserve_alpha
895
+ writer.write(np.clip(blended, 0, 255).astype(np.uint8))
896
+ frames += 1
897
+ finally:
898
+ gen_cap.release()
899
+ base_cap.release()
900
+ mask_cap.release()
901
+ writer.release()
902
+
903
+ meta = video_metadata(output_path)
904
+ log_event("postprocess.face_watermark_blend", "created blended video", request_id=request_id, frames=frames, **meta)
905
+
906
+
907
+ def finalize_output_video(
908
+ raw_output_path,
909
+ trimmed_video_path,
910
+ priori_path,
911
+ refined_mask_path,
912
+ output_path,
913
+ face_watermark_mode=DEFAULT_FACE_WATERMARK_MODE,
914
+ face_correction_strength=DEFAULT_FACE_CORRECTION_STRENGTH,
915
+ request_id=None,
916
+ ):
917
+ mode = str(face_watermark_mode)
918
+ if mode == "Preserve original face detail":
919
+ blend_video_with_mask(
920
+ raw_output_path,
921
+ trimmed_video_path,
922
+ refined_mask_path,
923
+ output_path,
924
+ correction_strength=face_correction_strength,
925
+ request_id=request_id,
926
+ label=mode,
927
+ )
928
+ elif mode == "Use ProPainter-prior blend":
929
+ blend_video_with_mask(
930
+ raw_output_path,
931
+ priori_path,
932
+ refined_mask_path,
933
+ output_path,
934
+ correction_strength=face_correction_strength,
935
+ request_id=request_id,
936
+ label=mode,
937
+ )
938
+ else:
939
+ with timed_stage("postprocess.finalize_output", request_id=request_id, mode=mode, raw_output=raw_output_path, output=output_path):
940
+ shutil.copyfile(raw_output_path, output_path)
941
+ meta = video_metadata(output_path)
942
+ log_event("postprocess.finalize_output", "copied raw DiffuEraser output", request_id=request_id, **meta)
943
+
944
+
945
  def _clip_int(value, minimum=30, maximum=300):
946
  return max(minimum, min(maximum, int(round(value))))
947
 
 
974
  output_frame_rate=DEFAULT_OUTPUT_FRAME_RATE,
975
  scheduler_name=DEFAULT_SCHEDULER,
976
  guidance_scale=DEFAULT_GUIDANCE_SCALE,
977
+ face_watermark_mode=DEFAULT_FACE_WATERMARK_MODE,
978
  ):
979
  max_duration = max(1, int(max_duration))
980
  max_img_size = max(256, int(max_img_size))
 
1000
  lora_cost = 4.0
1001
  scheduler_cost = 2.0 if scheduler_name != "Auto" else 0.0
1002
  guidance_cost = 4.0 if guidance_scale > 1.0 else 0.0
1003
+ postprocess_cost = 0.0 if str(face_watermark_mode) == "Off" else max(1.0, 0.015 * estimated_frames)
1004
  margin = 5.0
1005
 
1006
+ estimate = base_cost + trim_cost + propainter_cost + lora_cost + scheduler_cost + guidance_cost + frame_cost + postprocess_cost + margin
1007
  duration = _clip_int(estimate)
1008
  return duration, {
1009
  "fps": f"{fps:.2f}",
 
1013
  "step_factor": f"{step_factor:.2f}",
1014
  "frame_cost_sec": f"{frame_cost:.1f}",
1015
  "propainter_cost_sec": f"{propainter_cost:.1f}",
1016
+ "postprocess_cost_sec": f"{postprocess_cost:.1f}",
1017
  "raw_estimate_sec": f"{estimate:.1f}",
1018
  "margin_sec": f"{margin:.1f}",
1019
  "duration_floor_sec": 30,
 
1031
  output_frame_rate=DEFAULT_OUTPUT_FRAME_RATE,
1032
  scheduler_name=DEFAULT_SCHEDULER,
1033
  guidance_scale=DEFAULT_GUIDANCE_SCALE,
1034
+ face_watermark_mode=DEFAULT_FACE_WATERMARK_MODE,
1035
  zero_gpu_duration_mode=DEFAULT_ZERO_GPU_DURATION_MODE,
1036
  zero_gpu_duration=DEFAULT_ZERO_GPU_DURATION,
1037
  ):
 
1045
  frame_rate_limit=frame_rate_limit,
1046
  scheduler_name=scheduler_name,
1047
  guidance_scale=guidance_scale,
1048
+ face_watermark_mode=face_watermark_mode,
1049
  )
1050
  mode = str(zero_gpu_duration_mode)
1051
  if mode == "Manual":
 
1071
  input_mask,
1072
  max_duration=DEFAULT_MAX_DURATION,
1073
  mask_dilation_iter=DEFAULT_MASK_DILATION_ITER,
1074
+ mask_refine_mode=DEFAULT_MASK_REFINE_MODE,
1075
+ mask_refine_iterations=DEFAULT_MASK_REFINE_ITERATIONS,
1076
+ mask_feather_px=DEFAULT_MASK_FEATHER_PX,
1077
  max_img_size=DEFAULT_MAX_IMG_SIZE,
1078
  ref_stride=DEFAULT_REF_STRIDE,
1079
  neighbor_length=DEFAULT_NEIGHBOR_LENGTH,
 
1086
  guidance_scale=DEFAULT_GUIDANCE_SCALE,
1087
  zero_gpu_duration_mode=DEFAULT_ZERO_GPU_DURATION_MODE,
1088
  zero_gpu_duration=DEFAULT_ZERO_GPU_DURATION,
1089
+ face_watermark_mode=DEFAULT_FACE_WATERMARK_MODE,
1090
+ face_correction_strength=DEFAULT_FACE_CORRECTION_STRENGTH,
1091
  prompt="",
1092
  negative_prompt="",
1093
  *extra_args,
 
1102
  frame_rate_limit=frame_rate_limit,
1103
  scheduler_name=scheduler_name,
1104
  guidance_scale=guidance_scale,
1105
+ face_watermark_mode=face_watermark_mode,
1106
  zero_gpu_duration_mode=zero_gpu_duration_mode,
1107
  zero_gpu_duration=zero_gpu_duration,
1108
  )
 
1118
  input_mask,
1119
  max_duration=DEFAULT_MAX_DURATION,
1120
  mask_dilation_iter=DEFAULT_MASK_DILATION_ITER,
1121
+ mask_refine_mode=DEFAULT_MASK_REFINE_MODE,
1122
+ mask_refine_iterations=DEFAULT_MASK_REFINE_ITERATIONS,
1123
+ mask_feather_px=DEFAULT_MASK_FEATHER_PX,
1124
  max_img_size=DEFAULT_MAX_IMG_SIZE,
1125
  ref_stride=DEFAULT_REF_STRIDE,
1126
  neighbor_length=DEFAULT_NEIGHBOR_LENGTH,
 
1133
  guidance_scale=DEFAULT_GUIDANCE_SCALE,
1134
  zero_gpu_duration_mode=DEFAULT_ZERO_GPU_DURATION_MODE,
1135
  zero_gpu_duration=DEFAULT_ZERO_GPU_DURATION,
1136
+ face_watermark_mode=DEFAULT_FACE_WATERMARK_MODE,
1137
+ face_correction_strength=DEFAULT_FACE_CORRECTION_STRENGTH,
1138
  prompt="",
1139
  negative_prompt="",
1140
  progress=gr.Progress(track_tqdm=True),
 
1159
  # Setup paths and parameters
1160
  max_duration = int(max_duration)
1161
  mask_dilation_iter = int(mask_dilation_iter)
1162
+ mask_refine_mode = str(mask_refine_mode)
1163
+ mask_refine_iterations = int(mask_refine_iterations)
1164
+ mask_feather_px = int(mask_feather_px)
1165
  max_img_size = int(max_img_size)
1166
  ref_stride = int(ref_stride)
1167
  neighbor_length = int(neighbor_length)
 
1176
  guidance_scale = None if guidance_scale_value < 0 else guidance_scale_value
1177
  zero_gpu_duration_mode = str(zero_gpu_duration_mode)
1178
  zero_gpu_duration = int(zero_gpu_duration)
1179
+ face_watermark_mode = str(face_watermark_mode)
1180
+ face_correction_strength = float(face_correction_strength)
1181
  prompt = str(prompt or "")
1182
  negative_prompt = str(negative_prompt or "")
1183
 
 
1190
  frame_rate_limit=frame_rate_limit,
1191
  scheduler_name=scheduler_name,
1192
  guidance_scale=guidance_scale_value,
1193
+ face_watermark_mode=face_watermark_mode,
1194
  zero_gpu_duration_mode=zero_gpu_duration_mode,
1195
  zero_gpu_duration=zero_gpu_duration,
1196
  )
 
1201
  request_id=request_id,
1202
  max_duration=max_duration,
1203
  mask_dilation_iter=mask_dilation_iter,
1204
+ mask_refine_mode=mask_refine_mode,
1205
+ mask_refine_iterations=mask_refine_iterations,
1206
+ mask_feather_px=mask_feather_px,
1207
  max_img_size=max_img_size,
1208
  ref_stride=ref_stride,
1209
  neighbor_length=neighbor_length,
 
1219
  zero_gpu_duration_manual_slider=zero_gpu_duration_details["manual_slider_sec"],
1220
  zero_gpu_duration_auto_estimate=zero_gpu_duration_details["auto_estimate_sec"],
1221
  zero_gpu_duration_effective=zero_gpu_effective_duration,
1222
+ face_watermark_mode=face_watermark_mode,
1223
+ face_correction_strength=f"{face_correction_strength:.2f}",
1224
  prompt_chars=len(prompt),
1225
  negative_prompt_chars=len(negative_prompt),
1226
  )
 
1228
  # Timestamp for unique filenames
1229
  trimmed_video_path = os.path.join(save_path, f"trimmed_video_{timestamp}.mp4")
1230
  trimmed_mask_path = os.path.join(save_path, f"trimmed_mask_{timestamp}.mp4")
1231
+ refined_mask_path = os.path.join(save_path, f"refined_mask_{timestamp}.mp4")
1232
  priori_path = os.path.join(save_path, f"priori_{timestamp}.mp4")
1233
+ raw_output_path = os.path.join(save_path, f"diffueraser_raw_{timestamp}.mp4")
1234
  output_path = os.path.join(save_path, f"diffueraser_result_{timestamp}.mp4")
1235
  log_event(
1236
  "request.paths",
 
1238
  request_id=request_id,
1239
  trimmed_video=trimmed_video_path,
1240
  trimmed_mask=trimmed_mask_path,
1241
+ refined_mask=refined_mask_path,
1242
  priori=priori_path,
1243
+ raw_output=raw_output_path,
1244
  output=output_path,
1245
  )
1246
 
 
1262
  request_id=request_id,
1263
  label="input_mask",
1264
  )
1265
+ create_refined_mask_preview(
1266
+ trimmed_mask_path,
1267
+ refined_mask_path,
1268
+ mask_refine_mode=mask_refine_mode,
1269
+ mask_refine_iterations=mask_refine_iterations,
1270
+ mask_feather_px=mask_feather_px,
1271
+ mask_dilation_iter=mask_dilation_iter,
1272
+ request_id=request_id,
1273
+ )
1274
+ inspect_mask_for_warning(
1275
+ refined_mask_path,
1276
+ face_watermark_mode=face_watermark_mode,
1277
+ mask_refine_mode=mask_refine_mode,
1278
+ request_id=request_id,
1279
+ )
1280
 
1281
  # Dynamically compute video_length in frames from the trimmed video.
1282
  with timed_stage("video.inspect_trimmed", request_id=request_id):
 
1307
  neighbor_length=neighbor_length,
1308
  subvideo_length=subvideo_length,
1309
  mask_dilation=mask_dilation_iter,
1310
+ mask_refine_mode=mask_refine_mode,
1311
+ mask_refine_iterations=mask_refine_iterations,
1312
+ mask_feather_px=mask_feather_px,
1313
  ):
1314
  propainter.forward(trimmed_video_path, trimmed_mask_path, priori_path,
1315
  video_length=video_length, ref_stride=ref_stride,
 
1334
  guidance_scale=guidance_scale,
1335
  seed="random" if seed is None else seed,
1336
  ):
1337
+ video_inpainting_sd.forward(trimmed_video_path, trimmed_mask_path, priori_path, raw_output_path,
1338
  max_img_size=max_img_size, video_length=video_length,
1339
  mask_dilation_iter=mask_dilation_iter,
1340
+ mask_refine_mode=mask_refine_mode,
1341
+ mask_refine_iterations=mask_refine_iterations,
1342
+ mask_feather_px=mask_feather_px,
1343
  seed=seed, guidance_scale=guidance_scale,
1344
  prompt=prompt, negative_prompt=negative_prompt,
1345
  request_id=request_id, output_fps=output_fps,
1346
  progress_callback=make_stage_progress(progress_update, 0.40, 0.90))
1347
  log_cuda_memory("after_diffueraser", request_id=request_id)
1348
 
1349
+ progress_update(0.90, desc="Applying final blend")
1350
+ finalize_output_video(
1351
+ raw_output_path,
1352
+ trimmed_video_path,
1353
+ priori_path,
1354
+ refined_mask_path,
1355
+ output_path,
1356
+ face_watermark_mode=face_watermark_mode,
1357
+ face_correction_strength=face_correction_strength,
1358
+ request_id=request_id,
1359
+ )
1360
+
1361
  progress_update(0.92, desc="Finalizing outputs")
1362
  with timed_stage("video.inspect_output", request_id=request_id):
1363
  output_meta = video_metadata(output_path)
 
1373
  request_id=request_id,
1374
  total_elapsed_sec=f"{total_elapsed:.2f}",
1375
  output=output_path,
1376
+ raw_output=raw_output_path,
1377
  trimmed_video=trimmed_video_path,
1378
+ refined_mask=refined_mask_path,
1379
  priori=priori_path,
1380
  debug_log=debug_log_path,
1381
  )
1382
  progress_update(1.0, desc="Done")
1383
+ return output_path, trimmed_video_path, refined_mask_path, priori_path, raw_output_path, debug_log_path
1384
 
1385
  except Exception as exc:
1386
  total_elapsed = time.perf_counter() - total_start
 
1416
  input_video = gr.Video(label="Input Video (MP4 ONLY)")
1417
  input_mask = gr.Video(label="Input Mask Video (MP4 ONLY)")
1418
  with gr.Accordion("Basic settings", open=True):
1419
+ gr.Markdown("Choose a **Task mode** first. Most users only need this and **Max clip duration**.")
1420
  preset = gr.Dropdown(
1421
+ label="Task mode",
1422
+ choices=[
1423
+ "Remove object / person",
1424
+ "Remove small watermark or logo",
1425
+ "Face-area watermark",
1426
+ "Fast preview",
1427
+ "High quality object removal",
1428
+ "Custom",
1429
+ ],
1430
+ value=DEFAULT_PRESET,
1431
+ info="Start with the mode that matches what you want to remove.",
 
 
 
 
 
 
 
 
 
1432
  )
1433
+ settings_summary = gr.Markdown(task_mode_summary(DEFAULT_PRESET))
1434
+ with gr.Accordion("Which mode should I choose?", open=False):
1435
+ gr.Markdown(SITUATION_GUIDE)
1436
  with gr.Row():
1437
  max_duration = gr.Slider(
1438
  label="Max clip duration (seconds)",
 
1441
  value=DEFAULT_MAX_DURATION,
1442
  step=1,
1443
  )
1444
+ frame_rate_limit = gr.Dropdown(
1445
+ label="Frame rate limit",
1446
+ choices=FRAME_RATE_CHOICES,
1447
+ value=DEFAULT_FRAME_RATE_LIMIT,
1448
+ info="Use Original for quality; lower FPS can speed up previews.",
 
1449
  )
 
 
 
 
 
 
 
1450
 
1451
  with gr.Accordion("Advanced settings", open=False):
1452
+ gr.Markdown("Optional controls. Keep the task mode defaults unless you are tuning a difficult case.")
1453
+
1454
+ with gr.Accordion("Quality and speed", open=False):
1455
+ with gr.Row():
1456
+ ckpt = gr.Dropdown(
1457
+ label="PCM checkpoint",
1458
+ choices=PCM_STEP_CHOICES,
1459
+ value=DEFAULT_CKPT,
1460
+ info="More steps can improve quality but take longer.",
1461
+ )
1462
+ scheduler_name = gr.Dropdown(
1463
+ label="Scheduler",
1464
+ choices=SCHEDULER_CHOICES,
1465
+ value=DEFAULT_SCHEDULER,
1466
+ info="Auto keeps the default PCM scheduler behavior.",
1467
+ )
1468
+ with gr.Row():
1469
+ max_img_size = gr.Slider(
1470
+ label="Maximum image size",
1471
+ minimum=512,
1472
+ maximum=1280,
1473
+ value=DEFAULT_MAX_IMG_SIZE,
1474
+ step=64,
1475
+ info="Higher values preserve detail but are slower.",
1476
+ )
1477
+ output_frame_rate = gr.Dropdown(
1478
+ label="Output frame rate",
1479
+ choices=OUTPUT_FRAME_RATE_CHOICES,
1480
+ value=DEFAULT_OUTPUT_FRAME_RATE,
1481
+ info="Can reduce the output video FPS without changing the inference frame rate.",
1482
+ )
1483
  seed = gr.Number(
1484
  label="Seed (-1 for random)",
1485
  value=DEFAULT_SEED,
1486
  precision=0,
1487
  )
1488
+
1489
+ with gr.Accordion("Mask behavior", open=False):
1490
+ gr.Markdown("Use **Keep/Dilate** for full objects. Use **Erode** only for small face-area masks where preserving facial detail matters.")
1491
+ mask_dilation_iter = gr.Slider(
1492
+ label="Mask dilation iterations",
1493
+ minimum=0,
1494
+ maximum=20,
1495
+ value=DEFAULT_MASK_DILATION_ITER,
1496
+ step=1,
1497
+ info="Higher values cover object edges better. Lower values preserve more face detail.",
 
 
 
 
1498
  )
1499
+ with gr.Row():
1500
+ mask_refine_mode = gr.Dropdown(
1501
+ label="Mask refine mode",
1502
+ choices=MASK_REFINE_MODE_CHOICES,
1503
+ value=DEFAULT_MASK_REFINE_MODE,
1504
+ info="Keep is safest. Erode shrinks masks. Dilate expands masks.",
1505
+ )
1506
+ mask_refine_iterations = gr.Slider(
1507
+ label="Mask refine iterations",
1508
+ minimum=0,
1509
+ maximum=4,
1510
+ value=DEFAULT_MASK_REFINE_ITERATIONS,
1511
+ step=1,
1512
+ )
1513
+ mask_feather_px = gr.Slider(
1514
+ label="Mask feather (px)",
1515
+ minimum=0,
1516
+ maximum=16,
1517
+ value=DEFAULT_MASK_FEATHER_PX,
1518
+ step=1,
1519
+ info="Softens mask edges for DiffuEraser. Use 0 for the original hard mask behavior.",
1520
  )
1521
+
1522
+ with gr.Accordion("Face watermark tuning", open=False):
1523
+ gr.Markdown("Only for small masks over facial details. This mode trades complete removal for preserving more face detail.")
1524
+ face_watermark_mode = gr.Dropdown(
1525
+ label="Face watermark mode",
1526
+ choices=FACE_WATERMARK_MODE_CHOICES,
1527
+ value=DEFAULT_FACE_WATERMARK_MODE,
1528
+ info="Off keeps the normal result. Original blend preserves more original face detail. ProPainter blend can reduce DiffuEraser hallucination.",
1529
  )
1530
+ face_correction_strength = gr.Slider(
1531
+ label="Face watermark correction strength",
1532
+ minimum=0.0,
1533
+ maximum=1.0,
1534
+ value=DEFAULT_FACE_CORRECTION_STRENGTH,
1535
+ step=0.05,
1536
+ info="Higher removes more of the masked mark. Lower preserves more original/prior face detail.",
1537
+ )
1538
+ prompt = gr.Textbox(
1539
+ label="Prompt",
1540
+ value=DEFAULT_PROMPT,
1541
+ lines=2,
1542
+ placeholder="Optional. Leave blank for the original DiffuEraser behavior.",
1543
+ )
1544
+ negative_prompt = gr.Textbox(
1545
+ label="Negative prompt",
1546
+ value=DEFAULT_NEGATIVE_PROMPT,
1547
+ lines=2,
1548
+ placeholder="Optional. Mainly affects results when guidance scale is above 1.",
1549
+ )
1550
+ guidance_scale = gr.Slider(
1551
+ label="Guidance scale (-1 uses checkpoint default)",
1552
+ minimum=-1,
1553
+ maximum=12,
1554
+ value=DEFAULT_GUIDANCE_SCALE,
1555
+ step=0.5,
1556
+ info="Use -1 to keep each PCM checkpoint's default guidance scale.",
1557
+ )
1558
+
1559
+ with gr.Accordion("ProPainter and runtime", open=False):
1560
+ with gr.Row(visible=IS_ZEROGPU_SPACE):
1561
+ zero_gpu_duration_mode = gr.Dropdown(
1562
+ label="ZeroGPU duration mode",
1563
+ choices=ZERO_GPU_DURATION_MODE_CHOICES,
1564
+ value=DEFAULT_ZERO_GPU_DURATION_MODE,
1565
+ info="Auto estimates the reservation from the current settings. Normal GPU Spaces ignore this setting.",
1566
+ )
1567
+ zero_gpu_duration = gr.Slider(
1568
+ label="Manual ZeroGPU reserved duration (seconds)",
1569
+ minimum=30,
1570
+ maximum=300,
1571
+ value=DEFAULT_ZERO_GPU_DURATION,
1572
+ step=30,
1573
+ info="Used only when ZeroGPU duration mode is Manual.",
1574
+ )
1575
+ with gr.Row():
1576
+ subvideo_length = gr.Slider(
1577
+ label="ProPainter subvideo length",
1578
+ minimum=10,
1579
+ maximum=100,
1580
+ value=DEFAULT_SUBVIDEO_LENGTH,
1581
+ step=5,
1582
+ )
1583
+ ref_stride = gr.Slider(
1584
+ label="ProPainter reference stride",
1585
+ minimum=1,
1586
+ maximum=20,
1587
+ value=DEFAULT_REF_STRIDE,
1588
+ step=1,
1589
+ )
1590
+ neighbor_length = gr.Slider(
1591
+ label="ProPainter neighbor length",
1592
+ minimum=2,
1593
+ maximum=30,
1594
+ value=DEFAULT_NEIGHBOR_LENGTH,
1595
+ step=2,
1596
  )
 
 
 
 
 
 
 
1597
 
1598
  with gr.Row():
1599
  submit_btn = gr.Button("Run", variant="primary")
 
1603
  video_result = gr.Video(label="Result", buttons=["download"])
1604
  with gr.Accordion("Debug outputs", open=False):
1605
  trimmed_video_result = gr.Video(label="Trimmed input video", interactive=False)
1606
+ refined_mask_result = gr.Video(label="Refined mask preview", interactive=False)
1607
  priori_result = gr.Video(label="ProPainter priori video", interactive=False)
1608
+ raw_diffueraser_result = gr.Video(label="Raw DiffuEraser result", interactive=False)
1609
  debug_log_file = gr.File(label="Debug log file", interactive=False)
1610
  gr.Examples(
1611
  examples=[
 
1623
  max_duration,
1624
  max_img_size,
1625
  mask_dilation_iter,
1626
+ mask_refine_mode,
1627
+ mask_refine_iterations,
1628
+ mask_feather_px,
1629
  subvideo_length,
1630
  ref_stride,
1631
  neighbor_length,
 
1637
  guidance_scale,
1638
  zero_gpu_duration_mode,
1639
  zero_gpu_duration,
1640
+ face_watermark_mode,
1641
+ face_correction_strength,
1642
  prompt,
1643
  negative_prompt,
1644
  ],
1645
  outputs=[
1646
+ settings_summary,
1647
  max_duration,
1648
  max_img_size,
1649
  mask_dilation_iter,
1650
+ mask_refine_mode,
1651
+ mask_refine_iterations,
1652
+ mask_feather_px,
1653
  subvideo_length,
1654
  ref_stride,
1655
  neighbor_length,
 
1661
  guidance_scale,
1662
  zero_gpu_duration_mode,
1663
  zero_gpu_duration,
1664
+ face_watermark_mode,
1665
+ face_correction_strength,
1666
  prompt,
1667
  negative_prompt,
1668
  ],
 
1671
  clear_btn.click(
1672
  fn=clear_inputs,
1673
  inputs=None,
1674
+ outputs=[input_video, input_mask, video_result, trimmed_video_result, refined_mask_result, priori_result, raw_diffueraser_result, debug_log_file],
1675
  )
1676
 
1677
  submit_btn.click(
 
1681
  input_mask,
1682
  max_duration,
1683
  mask_dilation_iter,
1684
+ mask_refine_mode,
1685
+ mask_refine_iterations,
1686
+ mask_feather_px,
1687
  max_img_size,
1688
  ref_stride,
1689
  neighbor_length,
 
1696
  guidance_scale,
1697
  zero_gpu_duration_mode,
1698
  zero_gpu_duration,
1699
+ face_watermark_mode,
1700
+ face_correction_strength,
1701
  prompt,
1702
  negative_prompt,
1703
  ],
1704
+ outputs=[video_result, trimmed_video_result, refined_mask_result, priori_result, raw_diffueraser_result, debug_log_file],
1705
  )
1706
 
1707
  demo.queue().launch(show_error=True, ssr_mode=False)