John2J commited on
Commit
850e6bb
·
verified ·
1 Parent(s): 080431a

Create app.py

Browse files
Files changed (1) hide show
  1. app.py +538 -0
app.py ADDED
@@ -0,0 +1,538 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import sys
3
+ import subprocess
4
+ import tempfile
5
+ import shutil
6
+ from pathlib import Path
7
+
8
+ # ---------------------------------------------------------
9
+ # Configuration
10
+ # ---------------------------------------------------------
11
+
12
+ REPO_DIR = Path("/tmp/EraserDiT")
13
+
14
+ # Enable faster Hugging Face downloads when available
15
+ os.environ.setdefault("HF_HUB_ENABLE_HF_TRANSFER", "1")
16
+
17
+ # ---------------------------------------------------------
18
+ # Clone EraserDiT source
19
+ # ---------------------------------------------------------
20
+
21
+ if not REPO_DIR.exists():
22
+ print("Cloning EraserDiT...")
23
+ subprocess.run(
24
+ [
25
+ "git",
26
+ "clone",
27
+ "--depth",
28
+ "1",
29
+ "https://github.com/JieLiu95/EraserDiT.git",
30
+ str(REPO_DIR),
31
+ ],
32
+ check=True,
33
+ )
34
+
35
+ sys.path.insert(0, str(REPO_DIR))
36
+
37
+ # ---------------------------------------------------------
38
+ # Imports from EraserDiT
39
+ # ---------------------------------------------------------
40
+
41
+ import torch
42
+ import gradio as gr
43
+ import spaces
44
+
45
+ from utils.common import GlobalValues
46
+ from utils.pre import VideoInpaintPre
47
+ from utils.inference_utils import init, inference_batch
48
+ from utils.post import post_stream_normalized
49
+ from utils.post_pkg import FFmpegWriter
50
+
51
+ import decord
52
+ import ffmpeg
53
+ import datetime
54
+ import math
55
+
56
+
57
+ # ---------------------------------------------------------
58
+ # EraserDiT configuration
59
+ # ---------------------------------------------------------
60
+
61
+ GlobalValues.DEBUG = False
62
+
63
+ MODEL_ID = "jieeliu/EraserDiT"
64
+
65
+ NEGATIVE_PROMPT = (
66
+ "Colorful color tone, overexposure, static, blurry details, "
67
+ "subtitles, style, artwork, picture, static, overall graying, "
68
+ "worst quality, low-quality, JPEG compression residue, ugly, "
69
+ "incomplete, extra fingers, poorly painted hands, poorly painted "
70
+ "faces, deformed, disfigured, deformed limbs, finger fusion, "
71
+ "still image, cluttered background, three legs, many people in "
72
+ "the background, walking backwards, no noise"
73
+ )
74
+
75
+ DEVICE = torch.device("cuda")
76
+
77
+ # Original model uses bfloat16
78
+ WEIGHT_DTYPE = torch.bfloat16
79
+
80
+ pipeline = None
81
+
82
+
83
+ # ---------------------------------------------------------
84
+ # Load model once
85
+ # ---------------------------------------------------------
86
+
87
+ def load_model():
88
+ global pipeline
89
+
90
+ if pipeline is not None:
91
+ return pipeline
92
+
93
+ if not torch.cuda.is_available():
94
+ raise RuntimeError("CUDA GPU is required.")
95
+
96
+ print("========================================")
97
+ print("Loading EraserDiT")
98
+ print("GPU:", torch.cuda.get_device_name(0))
99
+ print("VRAM:",
100
+ round(torch.cuda.get_device_properties(0).total_memory / 1024**3, 2),
101
+ "GB")
102
+ print("========================================")
103
+
104
+ pipeline = init(
105
+ DEVICE,
106
+ WEIGHT_DTYPE,
107
+ pre_dir=MODEL_ID,
108
+ )
109
+
110
+ return pipeline
111
+
112
+
113
+ # ---------------------------------------------------------
114
+ # Main inference
115
+ # ---------------------------------------------------------
116
+
117
+ @spaces.GPU(duration=1200)
118
+ def process_video(video_path, mask_path, prompt, progress=gr.Progress()):
119
+ if video_path is None:
120
+ raise gr.Error("Please upload an input video.")
121
+
122
+ if mask_path is None:
123
+ raise gr.Error("Please upload a mask video.")
124
+
125
+ if not prompt or not prompt.strip():
126
+ raise gr.Error("Please provide a description/prompt for the video.")
127
+
128
+ video_path = str(video_path)
129
+ mask_path = str(mask_path)
130
+
131
+ # Make sure files exist
132
+ if not os.path.isfile(video_path):
133
+ raise gr.Error(f"Input video does not exist: {video_path}")
134
+
135
+ if not os.path.isfile(mask_path):
136
+ raise gr.Error(f"Mask video does not exist: {mask_path}")
137
+
138
+ print()
139
+ print("========================================")
140
+ print("EraserDiT inference")
141
+ print("Video:", video_path)
142
+ print("Mask :", mask_path)
143
+ print("Prompt:", prompt)
144
+ print("========================================")
145
+
146
+ # -----------------------------------------------------
147
+ # Validate video
148
+ # -----------------------------------------------------
149
+
150
+ try:
151
+ video_info = ffmpeg.probe(video_path)
152
+ mask_info = ffmpeg.probe(mask_path)
153
+
154
+ video_stream = next(
155
+ s for s in video_info["streams"]
156
+ if s.get("codec_type") == "video"
157
+ )
158
+
159
+ mask_stream = next(
160
+ s for s in mask_info["streams"]
161
+ if s.get("codec_type") == "video"
162
+ )
163
+
164
+ width = int(video_stream["width"])
165
+ height = int(video_stream["height"])
166
+
167
+ mask_width = int(mask_stream["width"])
168
+ mask_height = int(mask_stream["height"])
169
+
170
+ if width != mask_width or height != mask_height:
171
+ raise gr.Error(
172
+ f"Video and mask resolution must match.\n"
173
+ f"Video: {width}x{height}\n"
174
+ f"Mask: {mask_width}x{mask_height}"
175
+ )
176
+
177
+ # EraserDiT's normal path handles resolutions up to
178
+ # approximately 1920x1088 without bbox cropping.
179
+ if width * height > 1920 * 1088:
180
+ raise gr.Error(
181
+ "This Space currently expects video resolution up to "
182
+ "1920x1088. Larger videos require EraserDiT's bbox "
183
+ "cropping workflow."
184
+ )
185
+
186
+ print(f"Resolution: {width}x{height}")
187
+
188
+ except gr.Error:
189
+ raise
190
+
191
+ except Exception as e:
192
+ raise gr.Error(f"Could not inspect video: {e}")
193
+
194
+ # -----------------------------------------------------
195
+ # Load model
196
+ # -----------------------------------------------------
197
+
198
+ pipe = load_model()
199
+
200
+ # -----------------------------------------------------
201
+ # Temporary output directory
202
+ # -----------------------------------------------------
203
+
204
+ output_dir = Path(tempfile.mkdtemp(prefix="eraserdit_"))
205
+
206
+ video_name = Path(video_path).stem
207
+
208
+ output_path = output_dir / f"{video_name}_eraserdit.mp4"
209
+
210
+ # -----------------------------------------------------
211
+ # Preprocessor
212
+ # -----------------------------------------------------
213
+
214
+ preprocessor = VideoInpaintPre(
215
+ device=DEVICE,
216
+ align_h=32,
217
+ align_w=32,
218
+ ksize=(9, 9),
219
+ dilate_iter=9,
220
+ shift_alpha=1 * 8 + 1,
221
+ TEMP_INFER_LEN=121,
222
+ crop_flag=False,
223
+ )
224
+
225
+ # -----------------------------------------------------
226
+ # Video information
227
+ # -----------------------------------------------------
228
+
229
+ org_video_info = ffmpeg.probe(video_path)
230
+
231
+ video_stream = next(
232
+ s for s in org_video_info["streams"]
233
+ if s.get("codec_type") == "video"
234
+ )
235
+
236
+ fps_string = video_stream.get("r_frame_rate", "30/1")
237
+
238
+ try:
239
+ fps_num, fps_den = map(int, fps_string.split("/"))
240
+ fps = fps_num / fps_den
241
+ except Exception:
242
+ fps = 30.0
243
+
244
+ bitrate = video_stream.get("bit_rate")
245
+
246
+ if bitrate:
247
+ try:
248
+ bitrate_m = max(1, int(bitrate) // 1_000_000)
249
+ except Exception:
250
+ bitrate_m = 10
251
+ else:
252
+ bitrate_m = 10
253
+
254
+ # -----------------------------------------------------
255
+ # Processing
256
+ # -----------------------------------------------------
257
+
258
+ video_save_writer = None
259
+ pre_video_shift = None
260
+
261
+ current_batch = 0
262
+
263
+ # The model processes 121-frame temporal chunks
264
+ # with 9-frame overlap.
265
+ total_frames = None
266
+
267
+ try:
268
+ probe_reader = decord.VideoReader(
269
+ video_path,
270
+ ctx=decord.cpu(0),
271
+ )
272
+ total_frames = len(probe_reader)
273
+ del probe_reader
274
+ except Exception:
275
+ pass
276
+
277
+ while True:
278
+
279
+ video_ori, mask_ori, fps_loaded, _, _ = (
280
+ preprocessor.load_videos(
281
+ video_path=video_path,
282
+ mask_path=mask_path,
283
+ bbox_path=None,
284
+ decord_device=decord.cpu(0),
285
+ sample_rate=1,
286
+ batch_idx=current_batch,
287
+ )
288
+ )
289
+
290
+ if video_ori is None:
291
+ break
292
+
293
+ video_input, mask_input, _ = preprocessor(
294
+ video_ori,
295
+ mask_ori,
296
+ batch_idx=current_batch,
297
+ format="nhwc",
298
+ )
299
+
300
+ input_shape = preprocessor.TranslateShape(
301
+ video_input.shape,
302
+ src="nchw",
303
+ dst="nhwc",
304
+ )
305
+
306
+ # -------------------------------------------------
307
+ # Initialize writer
308
+ # -------------------------------------------------
309
+
310
+ if video_save_writer is None:
311
+
312
+ video_save_writer = FFmpegWriter(
313
+ path=str(output_path),
314
+ width=video_ori.shape[2],
315
+ height=video_ori.shape[1],
316
+ fps=fps_loaded,
317
+ bitrate=f"{bitrate_m}M",
318
+ )
319
+
320
+ # -------------------------------------------------
321
+ # Temporal overlap
322
+ # -------------------------------------------------
323
+
324
+ if current_batch == 0:
325
+
326
+ masks_zero_shift = torch.zeros(
327
+ (
328
+ math.ceil(preprocessor.shift_alpha / 8),
329
+ mask_input.shape[1],
330
+ mask_input.shape[2],
331
+ mask_input.shape[3],
332
+ ),
333
+ dtype=mask_input.dtype,
334
+ )
335
+
336
+ else:
337
+
338
+ video_input = torch.cat(
339
+ [
340
+ pre_video_shift,
341
+ video_input,
342
+ ]
343
+ )
344
+
345
+ mask_input = torch.cat(
346
+ [
347
+ masks_zero_shift,
348
+ mask_input,
349
+ ]
350
+ )
351
+
352
+ print(
353
+ f"Processing batch {current_batch} "
354
+ f"({video_ori.shape[0]} source frames)"
355
+ )
356
+
357
+ # -------------------------------------------------
358
+ # Model inference
359
+ # -------------------------------------------------
360
+
361
+ output_frames = inference_batch(
362
+ videos=video_input,
363
+ masks_input=mask_input,
364
+ prompt=prompt,
365
+ negative_prompt=NEGATIVE_PROMPT,
366
+ pipeline=pipe,
367
+ generator=None,
368
+ device=DEVICE,
369
+ weight_dtype=WEIGHT_DTYPE,
370
+ )
371
+
372
+ # Save overlap frames for next batch
373
+ pre_video_shift = (
374
+ output_frames[
375
+ -preprocessor.shift_alpha:
376
+ ].cpu()
377
+ )
378
+
379
+ # -------------------------------------------------
380
+ # Write output
381
+ # -------------------------------------------------
382
+
383
+ if current_batch == 0:
384
+
385
+ post_stream_normalized(
386
+ output_frames=output_frames,
387
+ ori_shape=video_ori.shape,
388
+ model_video_shape=input_shape,
389
+ writer=video_save_writer,
390
+ crop_flag=False,
391
+ videos_input_ori=None,
392
+ video_ori=video_ori,
393
+ mask_ori=mask_ori,
394
+ output_bbox=None,
395
+ write_to=True,
396
+ )
397
+
398
+ else:
399
+
400
+ post_stream_normalized(
401
+ output_frames=output_frames[
402
+ preprocessor.shift_alpha:
403
+ ],
404
+ ori_shape=video_ori.shape,
405
+ model_video_shape=input_shape,
406
+ writer=video_save_writer,
407
+ crop_flag=False,
408
+ videos_input_ori=None,
409
+ video_ori=video_ori,
410
+ mask_ori=mask_ori,
411
+ output_bbox=None,
412
+ write_to=True,
413
+ )
414
+
415
+ current_batch += 1
416
+
417
+ # -------------------------------------------------
418
+ # Progress
419
+ # -------------------------------------------------
420
+
421
+ if total_frames:
422
+
423
+ processed = min(
424
+ current_batch * (121 - preprocessor.shift_alpha),
425
+ total_frames,
426
+ )
427
+
428
+ progress(
429
+ processed / total_frames,
430
+ desc=f"Processing video: {processed}/{total_frames} frames",
431
+ )
432
+
433
+ # -----------------------------------------------------
434
+ # Close writer
435
+ # -----------------------------------------------------
436
+
437
+ if video_save_writer is not None:
438
+ video_save_writer.Close()
439
+ video_save_writer = None
440
+
441
+ if not output_path.exists():
442
+ raise gr.Error("EraserDiT did not produce an output video.")
443
+
444
+ print("Finished:", output_path)
445
+
446
+ return str(output_path)
447
+
448
+
449
+ # ---------------------------------------------------------
450
+ # Gradio UI
451
+ # ---------------------------------------------------------
452
+
453
+ with gr.Blocks(
454
+ title="EraserDiT Video Inpainting"
455
+ ) as demo:
456
+
457
+ gr.Markdown(
458
+ """
459
+ # EraserDiT Video Inpainting
460
+
461
+ Upload a video and a corresponding mask video to remove an object
462
+ using **EraserDiT**.
463
+
464
+ The video and mask must have:
465
+
466
+ - The same resolution
467
+ - The same number of frames
468
+ - The same frame rate
469
+ - MP4-compatible video encoding
470
+
471
+ The mask should contain the area that should be removed.
472
+ """
473
+ )
474
+
475
+ with gr.Row():
476
+
477
+ with gr.Column():
478
+
479
+ input_video = gr.Video(
480
+ label="Input Video",
481
+ sources=["upload"],
482
+ type="filepath",
483
+ )
484
+
485
+ input_mask = gr.Video(
486
+ label="Mask Video",
487
+ sources=["upload"],
488
+ type="filepath",
489
+ )
490
+
491
+ prompt = gr.Textbox(
492
+ label="Prompt",
493
+ value="A natural continuation of the surrounding video scene.",
494
+ placeholder="Describe the scene after removing the masked object...",
495
+ lines=3,
496
+ )
497
+
498
+ submit = gr.Button(
499
+ "Run EraserDiT",
500
+ variant="primary",
501
+ )
502
+
503
+ with gr.Column():
504
+
505
+ output_video = gr.Video(
506
+ label="Output Video",
507
+ interactive=False,
508
+ )
509
+
510
+ submit.click(
511
+ fn=process_video,
512
+ inputs=[
513
+ input_video,
514
+ input_mask,
515
+ prompt,
516
+ ],
517
+ outputs=output_video,
518
+ )
519
+
520
+ gr.Markdown(
521
+ """
522
+ ### Notes
523
+
524
+ EraserDiT is a large video diffusion model. The official project
525
+ reports **over 60 GB VRAM for 2K video**, so a high-memory GPU is
526
+ recommended.
527
+
528
+ Model: `jieeliu/EraserDiT`
529
+ """
530
+ )
531
+
532
+
533
+ if __name__ == "__main__":
534
+ demo.queue(
535
+ max_size=1,
536
+ ).launch(
537
+ show_error=True,
538
+ )