stevenlearns commited on
Commit
821976c
·
verified ·
1 Parent(s): 7c8fbd2

Add automatic sensitivity fallback and default vpipe cleanup

Browse files
src/videoclean/__pycache__/cli.cpython-314.pyc CHANGED
Binary files a/src/videoclean/__pycache__/cli.cpython-314.pyc and b/src/videoclean/__pycache__/cli.cpython-314.pyc differ
 
src/videoclean/__pycache__/engine.cpython-314.pyc CHANGED
Binary files a/src/videoclean/__pycache__/engine.cpython-314.pyc and b/src/videoclean/__pycache__/engine.cpython-314.pyc differ
 
src/videoclean/cli.py CHANGED
@@ -52,7 +52,7 @@ def _build_parser():
52
  clean.add_argument("--intent", default=None)
53
  clean.add_argument("--preview", action="store_true")
54
  clean.add_argument("--confirm", action="store_true")
55
- clean.add_argument("--detect-mode", choices=["fast", "balanced", "sensitive"], default="balanced")
56
  clean.add_argument("--ocr", choices=["auto", "off", "rapidocr"], default="auto")
57
  clean.add_argument("--detector", dest="detector_mode", choices=["dbnet", "hybrid"], default="dbnet")
58
  clean.add_argument("--inpaint-method", choices=["telea", "ns"], default="telea")
 
52
  clean.add_argument("--intent", default=None)
53
  clean.add_argument("--preview", action="store_true")
54
  clean.add_argument("--confirm", action="store_true")
55
+ clean.add_argument("--detect-mode", choices=["auto", "fast", "balanced", "sensitive"], default="auto")
56
  clean.add_argument("--ocr", choices=["auto", "off", "rapidocr"], default="auto")
57
  clean.add_argument("--detector", dest="detector_mode", choices=["dbnet", "hybrid"], default="dbnet")
58
  clean.add_argument("--inpaint-method", choices=["telea", "ns"], default="telea")
src/videoclean/engine.py CHANGED
@@ -41,7 +41,7 @@ class WipeEngine:
41
  self, task: str = "detext", *, verbose: bool = False, gap: int = 10,
42
  dual: bool = False, detector: Optional[TextDetector] = None,
43
  model: str = "opencv", model_options: Optional[dict] = None,
44
- detect_mode: str = "balanced", ocr: str = "auto", **legacy,
45
  ):
46
  if task not in _TASK_CLASSES:
47
  raise ValueError(f"Unknown task: {task}. Choose from: {list(_TASK_CLASSES)}")
@@ -265,8 +265,9 @@ class WipeEngine:
265
 
266
  def _build_fresh_clean_plan(self, video, result, selected_ids, snapshot, directed):
267
  selection = self._explicit_selection_kwargs(result.candidates, selected_ids, directed)
 
268
  return build_refined_wipe_plan(
269
- video, result, compute_source(video), refine=snapshot["detect_mode"] != "fast",
270
  request=snapshot, progress=lambda done, total: self._emit_progress(ProgressEvent("refine", done, total)),
271
  check_cancelled=self._check_cancelled, **selection,
272
  )
@@ -297,7 +298,9 @@ class WipeEngine:
297
  effective_targets = list(dict.fromkeys(effective_targets))
298
  normalized = {normalize_target(target) for target in effective_targets}
299
  detect_text = not requested_regions and not normalized or bool(normalized & {"subtitle", "timestamp", "watermark", "scene_text", "unknown_text"}) or bool(intent and not normalized)
300
- params = resolve_detect_params(detect_mode or self._detect_mode, has_subtitle_target="subtitle" in normalized)
 
 
301
  recognizer = self._build_recognizer(ocr or self._ocr)
302
  mode = detector_mode or "dbnet"
303
  if mode not in {"dbnet", "hybrid"}:
@@ -307,18 +310,42 @@ class WipeEngine:
307
  detector = _default_detector()
308
  if mode == "hybrid":
309
  detector = HybridTextDetector(detector)
310
- self._emit_progress(ProgressEvent("detect", 0, params["sample_count"], message=f"Scanning {params['sample_count']} sampled frames"))
311
- result = detect_clean_candidates(video, detector=detector, regions=requested_regions, detect_text=detect_text, include_logo="logo" in normalized, include_translucent_watermark="watermark" in normalized, sample_count=params["sample_count"], consistency=params["consistency"], subtitle_fallback=params["subtitle_fallback"], recognizer=recognizer)
312
- selected = select_clean_candidates(result.candidates, targets=effective_targets, intent=intent)
313
- print(f"[videoclean] found {len(result.candidates)} candidate(s); selected {len(selected)}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
314
  if not result.candidates:
315
  print("[videoclean] no text or overlay candidates were detected")
316
  write_clean_artifacts(result, selected, output)
 
317
  if confirm:
318
  selected = self._confirm_candidates(result.candidates, selected)
319
  write_clean_artifacts(result, selected, output)
320
  self._emit_progress(ProgressEvent("detect", 1, 1))
321
- return result, {candidate.id for candidate in selected}, {"intent": intent, "targets": effective_targets, "regions": requested_regions, "detect_mode": detect_mode or self._detect_mode, "ocr": ocr or self._ocr, "detector_mode": mode}, bool(targets or intent or regions or confirm)
 
 
 
 
 
322
 
323
  @staticmethod
324
  def _confirm_candidates(candidates, selected):
 
41
  self, task: str = "detext", *, verbose: bool = False, gap: int = 10,
42
  dual: bool = False, detector: Optional[TextDetector] = None,
43
  model: str = "opencv", model_options: Optional[dict] = None,
44
+ detect_mode: str = "auto", ocr: str = "auto", **legacy,
45
  ):
46
  if task not in _TASK_CLASSES:
47
  raise ValueError(f"Unknown task: {task}. Choose from: {list(_TASK_CLASSES)}")
 
265
 
266
  def _build_fresh_clean_plan(self, video, result, selected_ids, snapshot, directed):
267
  selection = self._explicit_selection_kwargs(result.candidates, selected_ids, directed)
268
+ effective_mode = snapshot.get("effective_detect_mode", snapshot["detect_mode"])
269
  return build_refined_wipe_plan(
270
+ video, result, compute_source(video), refine=effective_mode != "fast",
271
  request=snapshot, progress=lambda done, total: self._emit_progress(ProgressEvent("refine", done, total)),
272
  check_cancelled=self._check_cancelled, **selection,
273
  )
 
298
  effective_targets = list(dict.fromkeys(effective_targets))
299
  normalized = {normalize_target(target) for target in effective_targets}
300
  detect_text = not requested_regions and not normalized or bool(normalized & {"subtitle", "timestamp", "watermark", "scene_text", "unknown_text"}) or bool(intent and not normalized)
301
+ requested_mode = detect_mode or self._detect_mode
302
+ if requested_mode not in {"auto", "fast", "balanced", "sensitive"}:
303
+ raise ValueError("detect_mode must be auto, fast, balanced, or sensitive")
304
  recognizer = self._build_recognizer(ocr or self._ocr)
305
  mode = detector_mode or "dbnet"
306
  if mode not in {"dbnet", "hybrid"}:
 
310
  detector = _default_detector()
311
  if mode == "hybrid":
312
  detector = HybridTextDetector(detector)
313
+ def run_pass(mode_name):
314
+ params = resolve_detect_params(mode_name, has_subtitle_target="subtitle" in normalized)
315
+ self._emit_progress(ProgressEvent("detect", 0, params["sample_count"], message=f"Scanning {params['sample_count']} sampled frames ({mode_name})"))
316
+ detected = detect_clean_candidates(
317
+ video, detector=detector, regions=requested_regions,
318
+ detect_text=detect_text, include_logo="logo" in normalized,
319
+ include_translucent_watermark="watermark" in normalized,
320
+ sample_count=params["sample_count"], consistency=params["consistency"],
321
+ subtitle_fallback=params["subtitle_fallback"], recognizer=recognizer,
322
+ )
323
+ chosen = select_clean_candidates(detected.candidates, targets=effective_targets, intent=intent)
324
+ return detected, chosen, mode_name
325
+
326
+ if requested_mode == "auto":
327
+ result, selected, effective_mode = run_pass("balanced")
328
+ if not result.candidates:
329
+ print("[videoclean] no candidates in balanced mode; retrying sensitive mode")
330
+ self._emit_progress(ProgressEvent("detect", 0, 1, message="Retrying with sensitive mode"))
331
+ result, selected, effective_mode = run_pass("sensitive")
332
+ else:
333
+ result, selected, effective_mode = run_pass(requested_mode)
334
+ print(f"[videoclean] {effective_mode} mode found {len(result.candidates)} candidate(s); selected {len(selected)}")
335
  if not result.candidates:
336
  print("[videoclean] no text or overlay candidates were detected")
337
  write_clean_artifacts(result, selected, output)
338
+
339
  if confirm:
340
  selected = self._confirm_candidates(result.candidates, selected)
341
  write_clean_artifacts(result, selected, output)
342
  self._emit_progress(ProgressEvent("detect", 1, 1))
343
+ snapshot = {
344
+ "intent": intent, "targets": effective_targets, "regions": requested_regions,
345
+ "detect_mode": requested_mode, "effective_detect_mode": effective_mode,
346
+ "ocr": ocr or self._ocr, "detector_mode": mode,
347
+ }
348
+ return result, {candidate.id for candidate in selected}, snapshot, bool(targets or intent or regions or confirm)
349
 
350
  @staticmethod
351
  def _confirm_candidates(candidates, selected):
tests/__pycache__/test_auto_detect.cpython-314-pytest-9.1.1.pyc ADDED
Binary file (9.52 kB). View file
 
tests/test_auto_detect.py ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from types import SimpleNamespace
2
+
3
+ from videoclean.cli import _build_parser
4
+ from videoclean.engine import WipeEngine
5
+
6
+
7
+ def test_cli_defaults_to_auto_detection():
8
+ args = _build_parser().parse_args(["clean", "input.mp4"])
9
+ assert args.detect_mode == "auto"
10
+
11
+
12
+ def test_explicit_modes_remain_available():
13
+ for mode in ("fast", "balanced", "sensitive"):
14
+ args = _build_parser().parse_args(["clean", "input.mp4", "--detect-mode", mode])
15
+ assert args.detect_mode == mode
16
+
17
+
18
+ def test_auto_mode_runs_balanced_then_sensitive(monkeypatch, tmp_path):
19
+ empty = SimpleNamespace(candidates=[])
20
+ found = SimpleNamespace(candidates=[])
21
+ calls = []
22
+
23
+ def fake_detect(*args, **kwargs):
24
+ calls.append(kwargs["sample_count"])
25
+ return empty if len(calls) == 1 else found
26
+
27
+ monkeypatch.setattr("videoclean.detect.detect_clean_candidates", fake_detect)
28
+ monkeypatch.setattr("videoclean.detect._default_detector", lambda: object())
29
+ monkeypatch.setattr("videoclean.detect.write_clean_artifacts", lambda *args: None)
30
+ monkeypatch.setattr("videoclean.engine.WipeEngine._build_recognizer", staticmethod(lambda mode: None))
31
+ engine = WipeEngine(task="clean", detect_mode="auto", ocr="off")
32
+ result, selected, snapshot, directed = engine._detect_clean(
33
+ "input.mp4", object(), [], None, [], None, "off", str(tmp_path), False, "dbnet"
34
+ )
35
+ assert calls == [50, 80]
36
+ assert snapshot["detect_mode"] == "auto"
37
+ assert snapshot["effective_detect_mode"] == "sensitive"
38
+ assert selected == set()
39
+ assert directed is False
40
+
41
+
42
+ def test_explicit_fast_does_not_retry(monkeypatch, tmp_path):
43
+ calls = []
44
+
45
+ def fake_detect(*args, **kwargs):
46
+ calls.append(kwargs["sample_count"])
47
+ return SimpleNamespace(candidates=[])
48
+
49
+ monkeypatch.setattr("videoclean.detect.detect_clean_candidates", fake_detect)
50
+ monkeypatch.setattr("videoclean.detect._default_detector", lambda: object())
51
+ monkeypatch.setattr("videoclean.detect.write_clean_artifacts", lambda *args: None)
52
+ monkeypatch.setattr("videoclean.engine.WipeEngine._build_recognizer", staticmethod(lambda mode: None))
53
+ engine = WipeEngine(task="clean", detect_mode="fast", ocr="off")
54
+ _, _, snapshot, _ = engine._detect_clean(
55
+ "input.mp4", object(), [], None, [], None, "off", str(tmp_path), False, "dbnet"
56
+ )
57
+ assert calls == [24]
58
+ assert snapshot["effective_detect_mode"] == "fast"
vpipe CHANGED
@@ -15,9 +15,10 @@ def main() -> int:
15
  parser = argparse.ArgumentParser(description="Batch-clean videos with videoclean")
16
  parser.add_argument("source", help="Video file or directory")
17
  parser.add_argument("-o", "--output", help="Output directory")
18
- parser.add_argument("--clean", action="store_true", help="Remove detected overlays")
 
19
  parser.add_argument("-y", "--yes", action="store_true", help="Run without confirmation")
20
- parser.add_argument("--detect-mode", choices=["fast", "balanced", "sensitive"], default="balanced")
21
  parser.add_argument("--inpaint-method", choices=["telea", "ns"], default="telea")
22
  parser.add_argument("--inpaint-radius", type=float, default=3.0)
23
  parser.add_argument("--inpaint-dilate", type=int, default=0)
@@ -41,7 +42,7 @@ def main() -> int:
41
 
42
  for video in videos:
43
  destination = output / f"{video.stem}.mp4"
44
- if not args.clean:
45
  shutil.copy2(video, destination)
46
  continue
47
  command = [
 
15
  parser = argparse.ArgumentParser(description="Batch-clean videos with videoclean")
16
  parser.add_argument("source", help="Video file or directory")
17
  parser.add_argument("-o", "--output", help="Output directory")
18
+ parser.add_argument("--clean", action="store_true", help="Compatibility flag; cleaning is the default")
19
+ parser.add_argument("--no-clean", "--copy", action="store_true", help="Copy files without cleaning")
20
  parser.add_argument("-y", "--yes", action="store_true", help="Run without confirmation")
21
+ parser.add_argument("--detect-mode", choices=["auto", "fast", "balanced", "sensitive"], default="auto")
22
  parser.add_argument("--inpaint-method", choices=["telea", "ns"], default="telea")
23
  parser.add_argument("--inpaint-radius", type=float, default=3.0)
24
  parser.add_argument("--inpaint-dilate", type=int, default=0)
 
42
 
43
  for video in videos:
44
  destination = output / f"{video.stem}.mp4"
45
+ if args.no_clean:
46
  shutil.copy2(video, destination)
47
  continue
48
  command = [