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="
|
| 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 = "
|
| 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=
|
| 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 |
-
|
|
|
|
|
|
|
| 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 |
-
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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="
|
|
|
|
| 19 |
parser.add_argument("-y", "--yes", action="store_true", help="Run without confirmation")
|
| 20 |
-
parser.add_argument("--detect-mode", choices=["fast", "balanced", "sensitive"], default="
|
| 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
|
| 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 = [
|