mirror of
https://github.com/wiltodelta/remove-ai-watermarks.git
synced 2026-08-10 08:00:32 +02:00
Fix what the verification pass found in the knob removal
An adversarial review of52b2c11(five independent audits, each finding put to two skeptics, plus a completeness critic) found four defects that commit introduced and several stale claims it should have caught. The install hint no longer installs -- again. Folding five hints into one INVISIBLE_EXTRA constant dropped the shell quoting the originals had, so the printed remediation was `pip install remove-ai-watermarks[qwen-zimage]`. Bare brackets are a glob in zsh, the macOS default shell: it dies with "no matches found" before pip runs. That is the exact failure52b2c11existed to stop producing, reintroduced in a different form by a bulk replace. The constant is quoted now, and a test asserts the quotes rather than the bare substring -- the old assertions passed either way, which is why nothing caught it. Three tests were not guarding what they claimed: - The commit's headline behaviour change, per-profile polish resolution inside the engine, had no test at all. Rebinding resolve_adaptive_polish to the pre-commit `bool(value)` left the full suite green. Now covered by a test that drives the real engine and observes whether humanizer.adaptive_polish ran; that mutation now fails it. - TestAvailability still asserted the pre-commit (torch, diffusers) contract, so in a diffusion-only environment it was simply wrong, and comparing each gate to a tuple copied from itself could never catch the two gates disagreeing -- the drift the shared REMOVAL_MODULES was introduced to prevent. Replaced with a test that simulates each module's absence and requires BOTH gates to close. - Both CUDA-refusal guards skipped in every environment, including CI: they were gated on the diffusion stack, which no CI job installs. The refusal fires before any torch attribute is read, so they now run everywhere; only the dtype assertion keeps its skip. Also: the retired-knob test covered `invisible` but not `all` or `batch`, though all three declared those options separately; and smoke_matrix.py still called remove_watermark(region=...), a parameter52b2c11deleted, with the resulting TypeError swallowed into a skip by a broad except. Stale documentation the previous sweep missed: known-limitations still described an MPS out-of-memory fallback and a lighter-pipeline escape that no code can produce; module-internals declared Canny thresholds of 100/200 as a compatibility contract while the code uses 13/64, attributed enable_model_cpu_offload to deleted profiles, and still warned that the engine and CLI defaults differ (this commit's predecessor made them identical); cli.md gated `all` on the `diffusion` extra; python-api claimed "cuda" was the only accepted explicit device when "auto" is too. The claim that `device` is not a parameter was wrong in both module-internals and .claude/rules/development.md -- it is one, deliberately, and now says so. `--cpu-offload` help and the pipeline's CUDA guard both still pointed at MPS. Not fixed here, reported instead -- both are outside this repo: - ComfyUI-remove-ai-watermarks nodes.py:332 passes num_inference_steps and guidance_scale (plus min_resolution/upscaler frombf4bfc1). distribute.yml's comfyui job runs on every release and fails the release if the node sync fails, so 0.25.0 needs that node updated first. - raiw-app modal_app.py:422-425 forwards the same two kwargs into remove_watermark. Latent: it is pinned to1a77e24and nothing supplies a value today, so it fires on the next pin bump. pre-commit: 1) maintain.sh - exit 0 (1093 tests, Pyright 0 errors, no vulnerabilities); 2) /simplify - not re-run, this commit is the applied output of a five-dimension adversarial review; 3) docs sync - grepped MPS/mps, the extras names and every symbol touched across README, docs/, scripts/, .claude/; updated 6 docs; 4) CLAUDE.md - corrected the device claim in .claude/rules/development.md and added the shell-quoting rule Verified by execution, not assertion: smoke_matrix --quick 51 pass / 0 fail, _knob_rows driven directly 10 pass / 0 fail / 7 skip (no CUDA), the install hint rendered and round-tripped through zsh, and each new test confirmed to fail under the mutation it is meant to catch. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
52b2c115e8
commit
2f72257996
+60
-26
@@ -14,6 +14,7 @@ import pytest
|
||||
from remove_ai_watermarks._internal.utils import get_image_format, is_supported_format
|
||||
from remove_ai_watermarks._internal.watermark_profiles import (
|
||||
PROFILE_CHOICES,
|
||||
REMOVAL_MODULES,
|
||||
SDXL_ZIMAGE_GEMINI_STRENGTH,
|
||||
SDXL_ZIMAGE_OPENAI_STRENGTH,
|
||||
SDXL_ZIMAGE_UNKNOWN_STRENGTH,
|
||||
@@ -67,20 +68,19 @@ class TestDeviceDetection:
|
||||
here only defers a guaranteed failure to model-load time - several layers down,
|
||||
after the dependency check and the pipeline import, under a message naming
|
||||
whichever profile the internal pipeline happens to be.
|
||||
|
||||
Deliberately NOT gated on the diffusion stack. It used to be, and since no CI
|
||||
job installs diffusers or diffsynth (the dev extra pulls torch only, via
|
||||
invisible-watermark) the guard skipped in every environment it ran in --
|
||||
including the maintainer's. The refusal fires before any torch attribute is
|
||||
touched, so faking the dependency probe is enough to reach it.
|
||||
"""
|
||||
if not is_watermark_removal_available():
|
||||
pytest.skip("torch/diffusers not installed")
|
||||
import torch
|
||||
from remove_ai_watermarks._internal import watermark_remover as module
|
||||
|
||||
from remove_ai_watermarks._internal.watermark_remover import WatermarkRemover
|
||||
|
||||
for device in ("cpu", "mps", "xpu"):
|
||||
with pytest.raises(ValueError, match="CUDA-only"):
|
||||
WatermarkRemover(device=device)
|
||||
|
||||
remover = WatermarkRemover(device="cuda")
|
||||
assert remover.device == "cuda"
|
||||
assert remover.torch_dtype == torch.bfloat16
|
||||
with patch.object(module, "is_watermark_removal_available", return_value=True):
|
||||
for device in ("cpu", "mps", "xpu"):
|
||||
with pytest.raises(ValueError, match="CUDA-only"):
|
||||
module.WatermarkRemover(device=device)
|
||||
|
||||
def test_the_refusal_names_the_resolved_device_not_a_bare_none(self):
|
||||
"""``device=None`` on a CUDA-less host must report "cpu", not "None".
|
||||
@@ -88,16 +88,28 @@ class TestDeviceDetection:
|
||||
The message used to interpolate the raw argument, so the common auto-detect
|
||||
path told the user that ``'None'`` cannot run the removal.
|
||||
"""
|
||||
if not is_watermark_removal_available():
|
||||
pytest.skip("torch/diffusers not installed")
|
||||
from remove_ai_watermarks._internal import watermark_remover as module
|
||||
|
||||
with (
|
||||
patch.object(module, "is_watermark_removal_available", return_value=True),
|
||||
patch.object(module, "get_device", return_value="cpu"),
|
||||
pytest.raises(ValueError, match="'cpu' cannot run it"),
|
||||
):
|
||||
module.WatermarkRemover(device=None)
|
||||
|
||||
def test_a_cuda_remover_picks_the_profile_dtype(self):
|
||||
"""The dtype half still needs real torch, so it keeps its skip."""
|
||||
if not is_watermark_removal_available():
|
||||
pytest.skip("the qwen-zimage extra is not installed")
|
||||
import torch
|
||||
|
||||
from remove_ai_watermarks._internal.watermark_remover import WatermarkRemover
|
||||
|
||||
remover = WatermarkRemover(device="cuda")
|
||||
assert remover.device == "cuda"
|
||||
assert remover.torch_dtype == torch.bfloat16
|
||||
assert WatermarkRemover(device="cuda", pipeline="sdxl-zimage").torch_dtype == torch.float16
|
||||
|
||||
|
||||
class TestModelProfiles:
|
||||
"""Only the two CUDA-only two-stage profiles remain."""
|
||||
@@ -155,7 +167,7 @@ class TestNoReembeddedWatermark:
|
||||
|
||||
def test_sdxl_global_stage_disables_watermarker(self, monkeypatch: pytest.MonkeyPatch):
|
||||
if not is_watermark_removal_available():
|
||||
pytest.skip("torch/diffusers not installed")
|
||||
pytest.skip("the qwen-zimage extra is not installed")
|
||||
import diffusers
|
||||
|
||||
from remove_ai_watermarks._internal.sdxl_zimage_pipeline import SdxlZImagePipeline
|
||||
@@ -315,25 +327,47 @@ class TestFormatUtils:
|
||||
|
||||
|
||||
class TestAvailability:
|
||||
"""Tests for dependency availability checks."""
|
||||
"""The CLI gate and the remover precondition must answer from the same module list.
|
||||
|
||||
def test_watermark_removal_available(self):
|
||||
# Reflects the actual environment: True iff torch + diffusers (the gpu
|
||||
# extra) are importable. The default+dev CI env has no diffusers, so this
|
||||
# must not assume the full stack is present.
|
||||
import importlib.util
|
||||
Both used to hardcode (torch, diffusers) while the code moved to REMOVAL_MODULES,
|
||||
which includes diffsynth. In a torch+diffusers-only environment the assertions were
|
||||
then simply wrong -- and, worse, comparing each gate against a tuple copied from
|
||||
itself can never catch the two disagreeing, which is the drift that let the CLI pass
|
||||
an environment the run then died in.
|
||||
"""
|
||||
|
||||
expected = all(importlib.util.find_spec(m) is not None for m in ("torch", "diffusers"))
|
||||
assert is_watermark_removal_available() is expected
|
||||
|
||||
def test_invisible_is_available(self):
|
||||
def test_both_gates_agree_and_read_the_shared_module_list(self):
|
||||
import importlib.util
|
||||
|
||||
from remove_ai_watermarks.invisible_engine import is_available
|
||||
|
||||
expected = all(importlib.util.find_spec(m) is not None for m in ("torch", "diffusers"))
|
||||
assert "diffsynth" in REMOVAL_MODULES
|
||||
expected = all(importlib.util.find_spec(m) is not None for m in REMOVAL_MODULES)
|
||||
assert is_watermark_removal_available() is expected
|
||||
assert is_available() is expected
|
||||
|
||||
def test_a_missing_module_closes_both_gates(self, monkeypatch: pytest.MonkeyPatch):
|
||||
"""Discriminating, not vacuous: it must FAIL if either gate stops requiring one.
|
||||
|
||||
Comparing the live answer to a tuple derived from the same constant passes on
|
||||
any host -- with the full stack (True == True) and with none of it
|
||||
(False == False). Simulate each module's absence instead.
|
||||
"""
|
||||
import remove_ai_watermarks.invisible_engine as engine_module
|
||||
from remove_ai_watermarks._internal import watermark_remover as remover_module
|
||||
|
||||
for missing in REMOVAL_MODULES:
|
||||
present = {name: name != missing for name in REMOVAL_MODULES}
|
||||
monkeypatch.setattr(
|
||||
"remove_ai_watermarks.optional_deps.module_available",
|
||||
lambda *names, _p=present: all(_p.get(n, True) for n in names),
|
||||
)
|
||||
# The remover probes at import time, so drive its cached flags directly.
|
||||
monkeypatch.setattr(remover_module, "_HAS_TORCH", missing != "torch")
|
||||
monkeypatch.setattr(remover_module, "_HAS_REMOVAL_MODULES", missing == "torch")
|
||||
assert engine_module.is_available() is False, f"engine gate ignores a missing {missing}"
|
||||
assert remover_module.is_watermark_removal_available() is False, f"remover gate ignores a missing {missing}"
|
||||
|
||||
|
||||
# ── Platform-specific path handling ─────────────────────────────────
|
||||
|
||||
|
||||
Reference in New Issue
Block a user