Abdo96 commited on
Commit
4e782dc
Β·
verified Β·
1 Parent(s): 7975369

Upload 19 files

Browse files
Files changed (3) hide show
  1. DEPLOY_HF.md +2 -1
  2. README.md +3 -2
  3. app.py +154 -25
DEPLOY_HF.md CHANGED
@@ -87,7 +87,8 @@ If you hit the 10 MB file limit, make sure you did **not** commit `venv/` or any
87
 
88
  ## Using it
89
 
90
- 1. Upload a video.
 
91
  2. In **"What to blur?"** type the regions, e.g. `face. license plate.`
92
  3. Click **Preview Detection** to confirm the right regions are caught.
93
  4. Click **Process Video**, then download the result.
 
87
 
88
  ## Using it
89
 
90
+ 1. Upload a video. (Longer than the limit? Open **βœ‚οΈ Trim** and choose a
91
+ segment β€” over-length clips are trimmed automatically.)
92
  2. In **"What to blur?"** type the regions, e.g. `face. license plate.`
93
  3. Click **Preview Detection** to confirm the right regions are caught.
94
  4. Click **Process Video**, then download the result.
README.md CHANGED
@@ -37,8 +37,9 @@ Full step-by-step with troubleshooting: see **[`DEPLOY_HF.md`](DEPLOY_HF.md)**.
37
 
38
  > ZeroGPU gives each request the GPU for a limited time, so this app defaults to
39
  > a reduced processing resolution (480p), fast models (Grounding DINO *tiny* +
40
- > SAM 2 *small*), and a short per-clip length cap. Raise these for a paid GPU β€”
41
- > see "Tuning" below.
 
42
 
43
  ---
44
 
 
37
 
38
  > ZeroGPU gives each request the GPU for a limited time, so this app defaults to
39
  > a reduced processing resolution (480p), fast models (Grounding DINO *tiny* +
40
+ > SAM 2 *small*), and a short per-clip length cap. **Longer videos aren't
41
+ > rejected** β€” use the **βœ‚οΈ Trim** panel to pick a segment (over-length clips
42
+ > are trimmed automatically). Raise these for a paid GPU β€” see "Tuning" below.
43
 
44
  ---
45
 
app.py CHANGED
@@ -12,6 +12,7 @@ import os
12
  import sys
13
  import time
14
  import tempfile
 
15
  from pathlib import Path
16
 
17
  # ─── ZeroGPU / Hugging Face Spaces support ─────────────────────
@@ -259,6 +260,47 @@ CUSTOM_CSS = """
259
 
260
  # ─── Processing Functions ──────────────────────────────────────
261
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
262
  @spaces.GPU(duration=PROCESS_DURATION)
263
  def process_video(
264
  video_file,
@@ -270,6 +312,9 @@ def process_video(
270
  keyframe_interval,
271
  detection_threshold,
272
  model_quality,
 
 
 
273
  progress=gr.Progress()
274
  ):
275
  """Main video processing function"""
@@ -302,26 +347,33 @@ def process_video(
302
  else:
303
  video_path = video_file.name if hasattr(video_file, 'name') else str(video_file)
304
 
305
- # Generate output path
306
- output_dir = tempfile.mkdtemp(prefix="blur_output_")
307
- output_name = f"blurred_{Path(video_path).stem}.mp4"
308
- output_path = os.path.join(output_dir, output_name)
309
-
310
- # Enforce a max duration so a single GPU call fits inside the
311
- # ZeroGPU time budget (long clips would otherwise time out).
312
  max_secs = getattr(config, "max_video_duration", 600)
313
  try:
314
- info = pipeline.video_processor.get_video_info(video_path)
315
- if info.duration and info.duration > max_secs:
316
- msg = (
317
- f"⏱️ Video is {info.duration:.0f}s but the limit here is "
318
- f"{max_secs}s. Please trim it, or raise max_video_duration "
319
- f"/ use a larger GPU."
320
- )
321
- gr.Warning(msg)
322
- return None, "❌ " + msg
323
  except Exception:
324
- pass # if probing fails, let the pipeline surface the real error
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
325
 
326
  # Process
327
  result_path = pipeline.process_video(
@@ -377,9 +429,18 @@ def preview_detection(video_file, text_prompt, frame_slider):
377
 
378
 
379
  def get_video_info_text(video_file):
380
- """Get video info when uploaded"""
 
 
381
  if video_file is None:
382
- return "No video uploaded", gr.Slider(maximum=0, value=0)
 
 
 
 
 
 
 
383
 
384
  try:
385
  video_path = video_file if isinstance(video_file, str) else video_file.name
@@ -395,10 +456,37 @@ def get_video_info_text(video_file):
395
  )
396
 
397
  max_frame = max(0, info.total_frames - 1)
398
- return text, gr.Slider(maximum=max_frame, value=0)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
399
 
400
  except Exception as e:
401
- return f"❌ Error reading video: {e}", gr.Slider(maximum=0, value=0)
 
 
 
 
 
 
 
402
 
403
 
404
  # ─── Build UI ──────────────────────────────────────────────────
@@ -416,7 +504,7 @@ def create_app():
416
  limits_note = (
417
  f"<p style='font-size:0.8em;color:#8a8aa8;'>Running on ZeroGPU Β· "
418
  f"processed at {config.video.max_resolution}p Β· clips up to "
419
- f"{config.max_video_duration}s</p>"
420
  )
421
 
422
  with gr.Blocks(
@@ -449,6 +537,35 @@ def create_app():
449
  # Video Info
450
  video_info_text = gr.Markdown("No video uploaded")
451
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
452
  # Prompt Input β€” this is where the user WRITES the regions to blur
453
  text_prompt = gr.Textbox(
454
  label="✏️ What to blur? (Text Prompt)",
@@ -591,16 +708,27 @@ def create_app():
591
  ### Running on ZeroGPU
592
  - The first run loads the models onto the GPU, so it's a bit slower; later runs are faster.
593
  - Each run gets the GPU for a limited time, so clips are processed at a reduced resolution and capped in length by default for speed.
 
594
  - Want longer / higher-res? Upgrade the Space GPU and raise `max_video_duration`, `max_resolution` and the `@spaces.GPU(duration=...)` budget.
595
  """)
596
 
597
  # ── Event Handlers ──
598
 
599
- # Video upload β†’ show info
600
  video_input.change(
601
  fn=get_video_info_text,
602
  inputs=[video_input],
603
- outputs=[video_info_text, frame_slider]
 
 
 
 
 
 
 
 
 
 
604
  )
605
 
606
  # Preview button
@@ -616,7 +744,8 @@ def create_app():
616
  inputs=[
617
  video_input, text_prompt, blur_type, blur_strength,
618
  edge_feather, processing_mode, keyframe_interval,
619
- detection_threshold, model_quality
 
620
  ],
621
  outputs=[video_output, status_text]
622
  )
 
12
  import sys
13
  import time
14
  import tempfile
15
+ import subprocess
16
  from pathlib import Path
17
 
18
  # ─── ZeroGPU / Hugging Face Spaces support ─────────────────────
 
260
 
261
  # ─── Processing Functions ──────────────────────────────────────
262
 
263
+ def trim_clip(video_path, start, length):
264
+ """Cut [start, start+length] seconds from a video with ffmpeg.
265
+
266
+ Returns the path to a new .mp4. Re-encodes (fast preset) so the cut is
267
+ frame-accurate. This is CPU-only work (no GPU needed).
268
+ """
269
+ start = max(0.0, float(start))
270
+ length = max(0.1, float(length))
271
+ out_dir = tempfile.mkdtemp(prefix="trim_")
272
+ out_path = os.path.join(out_dir, f"trimmed_{Path(video_path).stem}.mp4")
273
+ cmd = [
274
+ "ffmpeg", "-y",
275
+ "-ss", str(start),
276
+ "-i", video_path,
277
+ "-t", str(length),
278
+ "-c:v", "libx264", "-preset", "veryfast", "-crf", "20",
279
+ "-c:a", "aac",
280
+ "-movflags", "+faststart",
281
+ out_path,
282
+ ]
283
+ proc = subprocess.run(cmd, capture_output=True, text=True)
284
+ if proc.returncode != 0 or not os.path.exists(out_path):
285
+ raise RuntimeError((proc.stderr or "ffmpeg failed")[-400:])
286
+ return out_path
287
+
288
+
289
+ def preview_trim(video_file, trim_start, trim_length):
290
+ """Produce a preview of the trimmed segment so the user can confirm it."""
291
+ if video_file is None:
292
+ gr.Warning("⚠️ Please upload a video first!")
293
+ return None
294
+ try:
295
+ video_path = video_file if isinstance(video_file, str) else video_file.name
296
+ cap = getattr(config, "max_video_duration", 600)
297
+ length = min(float(trim_length) if trim_length else cap, cap)
298
+ return trim_clip(video_path, trim_start, length)
299
+ except Exception as e:
300
+ gr.Warning(f"Trim error: {e}")
301
+ return None
302
+
303
+
304
  @spaces.GPU(duration=PROCESS_DURATION)
305
  def process_video(
306
  video_file,
 
312
  keyframe_interval,
313
  detection_threshold,
314
  model_quality,
315
+ trim_enable,
316
+ trim_start,
317
+ trim_length,
318
  progress=gr.Progress()
319
  ):
320
  """Main video processing function"""
 
347
  else:
348
  video_path = video_file.name if hasattr(video_file, 'name') else str(video_file)
349
 
350
+ # ── Trim (for long videos) ──
351
+ # Trim when the user asked to, OR automatically when the clip is longer
352
+ # than the cap β€” so long videos are handled instead of rejected.
 
 
 
 
353
  max_secs = getattr(config, "max_video_duration", 600)
354
  try:
355
+ _info = pipeline.video_processor.get_video_info(video_path)
356
+ duration = _info.duration or 0.0
 
 
 
 
 
 
 
357
  except Exception:
358
+ duration = 0.0
359
+
360
+ should_trim = bool(trim_enable) or (duration and duration > max_secs)
361
+ if should_trim:
362
+ length = min(float(trim_length) if trim_length else max_secs, max_secs)
363
+ start = max(0.0, float(trim_start) if trim_start else 0.0)
364
+ if duration and start >= duration:
365
+ start = 0.0
366
+ gradio_progress(0.02, f"βœ‚οΈ Trimming to {start:.0f}–{start + length:.0f}s...")
367
+ try:
368
+ video_path = trim_clip(video_path, start, length)
369
+ except Exception as e:
370
+ gr.Warning(f"Trim failed: {e}")
371
+ return None, f"❌ Trim failed: {e}"
372
+
373
+ # Generate output path (from the possibly-trimmed video)
374
+ output_dir = tempfile.mkdtemp(prefix="blur_output_")
375
+ output_name = f"blurred_{Path(video_path).stem}.mp4"
376
+ output_path = os.path.join(output_dir, output_name)
377
 
378
  # Process
379
  result_path = pipeline.process_video(
 
429
 
430
 
431
  def get_video_info_text(video_file):
432
+ """Get video info when uploaded, and set up the trim controls."""
433
+ cap = getattr(config, "max_video_duration", 600)
434
+
435
  if video_file is None:
436
+ return (
437
+ "No video uploaded",
438
+ gr.Slider(maximum=0, value=0), # frame_slider
439
+ gr.Checkbox(value=False), # trim_enable
440
+ gr.Slider(maximum=1, value=0), # trim_start
441
+ gr.Slider(maximum=cap, value=cap), # trim_length
442
+ "", # trim_info
443
+ )
444
 
445
  try:
446
  video_path = video_file if isinstance(video_file, str) else video_file.name
 
456
  )
457
 
458
  max_frame = max(0, info.total_frames - 1)
459
+ dur = info.duration or 0.0
460
+ too_long = bool(dur and dur > cap)
461
+ length_default = min(cap, dur) if dur else cap
462
+
463
+ trim_info = ""
464
+ if too_long:
465
+ trim_info = (
466
+ f"⏱️ This clip is **{dur:.0f}s**, longer than the **{cap}s** limit. "
467
+ f"It will be trimmed automatically β€” set the start below to choose "
468
+ f"which part to keep (defaults to the first {cap}s), and optionally "
469
+ f"**Preview trimmed clip** to check it."
470
+ )
471
+
472
+ return (
473
+ text,
474
+ gr.Slider(maximum=max_frame, value=0), # frame_slider
475
+ gr.Checkbox(value=too_long), # trim_enable (auto-on if long)
476
+ gr.Slider(maximum=max(0.1, dur), value=0), # trim_start
477
+ gr.Slider(maximum=cap, value=length_default), # trim_length
478
+ trim_info, # trim_info
479
+ )
480
 
481
  except Exception as e:
482
+ return (
483
+ f"❌ Error reading video: {e}",
484
+ gr.Slider(maximum=0, value=0),
485
+ gr.Checkbox(value=False),
486
+ gr.Slider(maximum=1, value=0),
487
+ gr.Slider(maximum=cap, value=cap),
488
+ "",
489
+ )
490
 
491
 
492
  # ─── Build UI ──────────────────────────────────────────────────
 
504
  limits_note = (
505
  f"<p style='font-size:0.8em;color:#8a8aa8;'>Running on ZeroGPU Β· "
506
  f"processed at {config.video.max_resolution}p Β· clips up to "
507
+ f"{config.max_video_duration}s (longer videos can be trimmed)</p>"
508
  )
509
 
510
  with gr.Blocks(
 
537
  # Video Info
538
  video_info_text = gr.Markdown("No video uploaded")
539
 
540
+ # ── Trim controls (for long videos) ──
541
+ with gr.Accordion("βœ‚οΈ Trim (for long videos)", open=False):
542
+ trim_info = gr.Markdown("")
543
+ trim_enable = gr.Checkbox(
544
+ label="Trim this video before processing",
545
+ value=False,
546
+ info=(
547
+ f"Clips here are limited to {config.max_video_duration}s. "
548
+ "Turn this on to process only a chosen segment. Long videos "
549
+ "are trimmed automatically."
550
+ ),
551
+ )
552
+ with gr.Row():
553
+ trim_start = gr.Slider(
554
+ minimum=0, maximum=60, value=0, step=0.5,
555
+ label="Start (seconds)",
556
+ )
557
+ trim_length = gr.Slider(
558
+ minimum=1, maximum=config.max_video_duration,
559
+ value=config.max_video_duration, step=1,
560
+ label=f"Clip length (max {config.max_video_duration}s)",
561
+ )
562
+ trim_preview_btn = gr.Button(
563
+ "βœ‚οΈ Preview trimmed clip", variant="secondary"
564
+ )
565
+ trim_preview_video = gr.Video(
566
+ label="Trimmed preview", interactive=False
567
+ )
568
+
569
  # Prompt Input β€” this is where the user WRITES the regions to blur
570
  text_prompt = gr.Textbox(
571
  label="✏️ What to blur? (Text Prompt)",
 
708
  ### Running on ZeroGPU
709
  - The first run loads the models onto the GPU, so it's a bit slower; later runs are faster.
710
  - Each run gets the GPU for a limited time, so clips are processed at a reduced resolution and capped in length by default for speed.
711
+ - **Long video?** Open **βœ‚οΈ Trim** to pick a segment. Videos over the limit are trimmed automatically (to the first part, or your chosen start).
712
  - Want longer / higher-res? Upgrade the Space GPU and raise `max_video_duration`, `max_resolution` and the `@spaces.GPU(duration=...)` budget.
713
  """)
714
 
715
  # ── Event Handlers ──
716
 
717
+ # Video upload β†’ show info + set up trim controls
718
  video_input.change(
719
  fn=get_video_info_text,
720
  inputs=[video_input],
721
+ outputs=[
722
+ video_info_text, frame_slider,
723
+ trim_enable, trim_start, trim_length, trim_info,
724
+ ]
725
+ )
726
+
727
+ # Trim preview button
728
+ trim_preview_btn.click(
729
+ fn=preview_trim,
730
+ inputs=[video_input, trim_start, trim_length],
731
+ outputs=[trim_preview_video]
732
  )
733
 
734
  # Preview button
 
744
  inputs=[
745
  video_input, text_prompt, blur_type, blur_strength,
746
  edge_feather, processing_mode, keyframe_interval,
747
+ detection_threshold, model_quality,
748
+ trim_enable, trim_start, trim_length
749
  ],
750
  outputs=[video_output, status_text]
751
  )