diff --git a/docs/cli.md b/docs/cli.md index 759e97e..2c6e0ba 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -529,9 +529,9 @@ remove-ai-watermarks invisible image.png -o clean.png \ --pipeline qwen-zimage --text-manifest verified-lines.json --force ``` -Use an explicit `qwen-zimage` or `chroma-zimage` profile with this option. For -Google provenance, `auto` resolves to SDXL, whose VAE path has not been calibrated -for verified-text restoration and is therefore rejected. +The option works with `qwen-zimage`, `sdxl-zimage`, `chroma-zimage`, and `auto`. +Each concrete profile reconstructs the donor through its own already loaded VAE; +for Google provenance, `auto` therefore keeps the normal SDXL route. ``verified: true`` may also be set by an automated operator that attests machine-verified geometry: stability-gated detector boxes inside sane caps. Such @@ -556,10 +556,11 @@ container changes remain valid while a resized or edited source fails closed. Th experimental helper `remove_ai_watermarks._internal.text_restoration.source_pixel_sha256` computes it. -This mode is supported by `qwen-zimage`, `chroma-zimage`, and `auto` at native -geometry with `humanize=0`, `unsharp=0`, and adaptive polish disabled. The legacy -`sdxl-zimage` profile does not expose a verified-text VAE donor. `all` also accepts the flag, -but its manifest must match the pixels entering the invisible stage; if visible-mark +This mode is supported by every profile at native geometry with `humanize=0` and +`unsharp=0`. An unset adaptive-polish option is forced off so verified text remains +the final pixel-writing stage; an explicit request to enable it is rejected. `all` +also accepts the flag, but its +manifest must match the pixels entering the invisible stage; if visible-mark removal changes those pixels, the hash check rejects the run. One oracle verdict does not certify another manifest, seed, model/runtime version, or output hash. diff --git a/docs/module-internals.md b/docs/module-internals.md index e07f45b..6dbc263 100644 --- a/docs/module-internals.md +++ b/docs/module-internals.md @@ -1839,8 +1839,8 @@ verification, and geometry-only operators do not need to invent text or script f When enabled, the one profile selected before model loading reconstructs the source once through its already loaded VAE, runs the ordinary global and face stages, and -calls the shared restoration compositor. Qwen and Chroma implement this donor hook; -SDXL does not. The optional Qwen-only fidelity anchor first blends 15% of the VAE +calls the shared restoration compositor. Qwen, SDXL, and Chroma implement this donor +hook. The optional Qwen-only fidelity anchor first blends 15% of the VAE reconstruction into the clean result; it is off by default because the blend returned detector-visible OpenAI SynthID in the measured poster fixtures. The compositor derives binary source and candidate silhouettes, groups nearby lines, uses LaMa for the initial @@ -1854,8 +1854,9 @@ extend 8% above and 25% below. The anchored-component gate covers clipped leadin flourishes, trailing punctuation, icons, and descenders without walking into disconnected decoration or background texture. -The stage is deliberately narrower than the engine: it rejects `sdxl-zimage`, tiles, -resolution caps, humanize, unsharp, and adaptive polish. Those combinations change +The stage is deliberately narrower than the engine: it rejects tiles, resolution +caps, humanize, unsharp, and explicitly enabled adaptive polish. An unset polish +value resolves to off in this mode, including for SDXL, because those combinations change geometry or final pixels after the verified layer and have no measured oracle result. It remains opt-in because annotations are manual and provider verdicts apply only to the exact tested output hashes, not to the mechanism in general. @@ -1879,6 +1880,22 @@ the `chroma_text_restoration_study.py` harness (kept outside this repository) under the gitignored `out/text-restoration-engine-study/` directory. +The 2026-09-15 rerun added the SDXL donor implementation and exercised all three +profiles on the same three manifests. SDXL improved text-box MAE from 11.319 to +7.970 and from 10.704 to 7.657 on the two OpenAI posters, while PSNR rose from +25.83 to 29.24 dB and from 26.17 to 29.04 dB. On the tracked Google CJK carrier +(`4affd7f27767a445db6abf741355743ba8d95108ad922c9fff045feed8492236`, already +recorded as SynthID-positive), SDXL at the calibrated 0.50 strength improved +text-box MAE from 10.738 to 8.564 and PSNR from 23.93 to 24.00 dB. The exact +restored output +`035e4919e1d84129a347006c98f19eb4431eb5772788f6d2097b94a2c6baec84` +was submitted to the Google Gemini oracle after provenance stripping preserved +its decoded pixels; the sanitized upload hash was +`3e4f06274a7b735f7d06863fd54dc8146f60aeb9cc8315f86294180deb660b82`. +The oracle returned `not_detected` on 2026-09-16. This +certifies that one carrier, manifest, seed, runtime, and output hash; it does not +turn the compositor into a provider-wide guarantee. + A matched stage-isolation check on the 18-face Gemini portrait grid confirms the division of responsibility. The visible-cleaned, metadata-stripped control and the Z-Image face-only output were both SynthID-positive; Qwen global-only and the full @@ -1986,8 +2003,9 @@ is supported by this pass, so `auto` keeps the measured provenance-cohort table. [`_internal/sdxl_zimage_pipeline.py`](../src/remove_ai_watermarks/_internal/sdxl_zimage_pipeline.py) runs the same two-stage recipe on an SDXL global pass. `SdxlZImagePipeline` -subclasses `TwoStageZImagePipeline` and implements only the global stage -(`_load_global`, `_run_global`), so the face stage is inherited rather than copied +subclasses `TwoStageZImagePipeline` and implements the global stage +(`_load_global`, `_run_global`) plus the deterministic verified-text VAE donor, so +the face stage is inherited rather than copied and cannot drift between the profiles; a test asserts the shared methods are the same objects. diff --git a/docs/python-api.md b/docs/python-api.md index 88b480f..31320c0 100644 --- a/docs/python-api.md +++ b/docs/python-api.md @@ -780,11 +780,11 @@ engine.remove_watermark( Install `remove-ai-watermarks[text-restoration]`. The manifest schema and safety constraints are documented in the CLI guide. The engine verifies its decoded RGB -hash before loading the diffusion models. Qwen and Chroma reconstruct the donor with -the VAE already loaded for the selected profile; no second generative profile runs. -The engine rejects SDXL, including `auto` when Google provenance selects it, -downscaling, and postprocessing combinations that were not evaluated. Tiling is -also rejected because the combined +hash before loading the diffusion models. Qwen, SDXL, and Chroma reconstruct the +donor with the VAE already loaded for the selected profile; no second generative +profile runs. The engine turns an unset `adaptive_polish` off in verified-text mode +and rejects explicit postprocessing or downscaling combinations that were not +evaluated. Tiling is also rejected because the combined tiled-restoration path has no provider-oracle calibration. `InvisibleOptions` exposes the same field for `remove_all`; after a visible-stage edit, the manifest must be built against the staged pixels rather than the pristine source. diff --git a/pyproject.toml b/pyproject.toml index 4af516b..11e1c9e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ # on it, including the ComfyUI node package. The console script below carries # the same weight, since users have it on PATH. name = "remove-ai-watermarks" -version = "0.40.4" +version = "0.40.5" description = "AI watermark remover for visible, invisible, and provenance marks in images and video" readme = "README.md" requires-python = ">=3.11,<3.15" diff --git a/skills/remove-ai-watermarks/references/commands.md b/skills/remove-ai-watermarks/references/commands.md index 833f81e..f6ab2b4 100644 --- a/skills/remove-ai-watermarks/references/commands.md +++ b/skills/remove-ai-watermarks/references/commands.md @@ -94,6 +94,10 @@ chroma-zimage for Microsoft, qwen-zimage for OpenAI (since CLI 0.39.0), Meta and -- so prefer it over naming an engine when the user has no reason to care. `sdxl-zimage` is the heavier alternative and `chroma-zimage` is the Apache-2.0 global stage. +Verified-text manifests work with every profile (since CLI 0.40.5). Each profile +uses its own already loaded VAE donor, so `auto` keeps the normal SDXL route for a +Google image instead of switching engines. + ```bash remove-ai-watermarks all image.png -o clean.png remove-ai-watermarks batch ./images --mode visible --output-dir ./clean diff --git a/skills/remove-ai-watermarks/scripts/probe.py b/skills/remove-ai-watermarks/scripts/probe.py index 8c54bf4..f5cb455 100644 --- a/skills/remove-ai-watermarks/scripts/probe.py +++ b/skills/remove-ai-watermarks/scripts/probe.py @@ -32,7 +32,7 @@ _GPU_MARKERS = ("GPU ", "gpu:") # told an agent its CLI was current right before it typed a value that build # rejects. tests/test_agent_skill.py pins it against the package version and # against every "(since CLI X.Y.Z)" annotation in the skill. -MIN_CLI_VERSION = (0, 40, 0) +MIN_CLI_VERSION = (0, 40, 5) # Markers a build without the pixel stack prints. The first is the current CLI's own # install hint; the rest are what older builds emit before the guard existed, and the # Homebrew formula ships exactly such a build. diff --git a/src/remove_ai_watermarks/__init__.py b/src/remove_ai_watermarks/__init__.py index a080cc6..1bf9053 100644 --- a/src/remove_ai_watermarks/__init__.py +++ b/src/remove_ai_watermarks/__init__.py @@ -33,7 +33,7 @@ _os.environ.setdefault("TRANSFORMERS_VERBOSITY", "error") _warnings.filterwarnings("ignore", message=r".*ImageProcessorFast.*") -__version__ = "0.40.4" +__version__ = "0.40.5" __all__ = [ "BatchItemResult", diff --git a/src/remove_ai_watermarks/_internal/chroma_zimage_pipeline.py b/src/remove_ai_watermarks/_internal/chroma_zimage_pipeline.py index 47dcf87..21e24d4 100644 --- a/src/remove_ai_watermarks/_internal/chroma_zimage_pipeline.py +++ b/src/remove_ai_watermarks/_internal/chroma_zimage_pipeline.py @@ -36,7 +36,7 @@ from remove_ai_watermarks._internal.two_stage_pipeline import ( _prompt_cache_path, _store_prompt_payload, _target_size, - edge_pad_to_grid, + diffusers_vae_roundtrip, requested_steps, ) @@ -182,28 +182,12 @@ class ChromaZImagePipeline(TwoStageZImagePipeline): def _vae_roundtrip(self, image: Image.Image) -> Image.Image: """Reconstruct source pixels through the already loaded Chroma VAE.""" - import torch - pipe = self._load_global() - source_width, source_height = image.size - padded = edge_pad_to_grid(image, _LATENT_GRID) - - tensor = pipe.image_processor.preprocess( - padded, - height=padded.height, - width=padded.width, - ).to(device=self.device, dtype=self.torch_dtype) - with torch.inference_mode(): - encoded = pipe.vae.encode(tensor) - if hasattr(encoded, "latent_dist"): - # A restoration donor must be deterministic. Diffusers samples this - # distribution for img2img noise initialization; its mode is the - # faithful VAE reconstruction needed here. - latents = encoded.latent_dist.mode() - elif hasattr(encoded, "latents"): - latents = encoded.latents - else: - raise AttributeError("Chroma VAE encoder output contains no latents") - decoded = pipe.vae.decode(latents, return_dict=False)[0] - reconstructed = pipe.image_processor.postprocess(decoded, output_type="pil")[0] - return reconstructed.crop((0, 0, source_width, source_height)).convert("RGB") + return diffusers_vae_roundtrip( + pipe, + image, + grid=_LATENT_GRID, + device=self.device, + dtype=self.torch_dtype, + profile_name=self.profile_name, + ) diff --git a/src/remove_ai_watermarks/_internal/sdxl_zimage_pipeline.py b/src/remove_ai_watermarks/_internal/sdxl_zimage_pipeline.py index f0b3b37..2645603 100644 --- a/src/remove_ai_watermarks/_internal/sdxl_zimage_pipeline.py +++ b/src/remove_ai_watermarks/_internal/sdxl_zimage_pipeline.py @@ -28,6 +28,7 @@ from remove_ai_watermarks._internal.two_stage_pipeline import ( TwoStageZImagePipeline, _target_size, build_canny_control_image, + diffusers_vae_roundtrip, requested_steps, ) from remove_ai_watermarks._internal.watermark_profiles import ( @@ -120,3 +121,15 @@ class SdxlZImagePipeline(TwoStageZImagePipeline): if result.size != image.size: result = result.resize(image.size, Image.Resampling.LANCZOS) return result.convert("RGB") + + def _vae_roundtrip(self, image: Image.Image) -> Image.Image: + """Reconstruct source pixels through the already loaded SDXL VAE.""" + pipe = self._load_global() + return diffusers_vae_roundtrip( + pipe, + image, + grid=_LATENT_GRID, + device=self.device, + dtype=self.torch_dtype, + profile_name=self.profile_name, + ) diff --git a/src/remove_ai_watermarks/_internal/two_stage_pipeline.py b/src/remove_ai_watermarks/_internal/two_stage_pipeline.py index aa5ce9b..c15fcea 100644 --- a/src/remove_ai_watermarks/_internal/two_stage_pipeline.py +++ b/src/remove_ai_watermarks/_internal/two_stage_pipeline.py @@ -96,6 +96,41 @@ def edge_pad_to_grid(image: Image.Image, grid: int) -> Image.Image: ) +def diffusers_vae_roundtrip( + pipe: Any, + image: Image.Image, + *, + grid: int, + device: Any, + dtype: Any, + profile_name: str, +) -> Image.Image: + """Reconstruct pixels deterministically through a Diffusers pipeline VAE.""" + import torch + + source_width, source_height = image.size + padded = edge_pad_to_grid(image, grid) + tensor = pipe.image_processor.preprocess( + padded, + height=padded.height, + width=padded.width, + ).to(device=device, dtype=dtype) + with torch.inference_mode(): + encoded = pipe.vae.encode(tensor) + if hasattr(encoded, "latent_dist"): + # A restoration donor must be deterministic. Diffusers samples this + # distribution for img2img noise initialization; its mode is the + # faithful VAE reconstruction needed here. + latents = encoded.latent_dist.mode() + elif hasattr(encoded, "latents"): + latents = encoded.latents + else: + raise AttributeError(f"{profile_name} VAE encoder output contains no latents") + decoded = pipe.vae.decode(latents, return_dict=False)[0] + reconstructed = pipe.image_processor.postprocess(decoded, output_type="pil")[0] + return reconstructed.crop((0, 0, source_width, source_height)).convert("RGB") + + def resolve_face_model_residency( requested: bool | None, *, diff --git a/src/remove_ai_watermarks/_internal/watermark_remover.py b/src/remove_ai_watermarks/_internal/watermark_remover.py index d4fe815..d8047c7 100644 --- a/src/remove_ai_watermarks/_internal/watermark_remover.py +++ b/src/remove_ai_watermarks/_internal/watermark_remover.py @@ -250,8 +250,6 @@ class WatermarkRemover: self.model_profile = resolved self.torch_dtype = _dtype_for_profile(resolved) self._qwen_zimage_pipeline = None - if text_manifest is not None and self.model_profile == SDXL_ZIMAGE_PROFILE: - raise ValueError("Verified text restoration is not supported by the sdxl-zimage profile") if text_manifest is not None and tile: raise ValueError("Verified text restoration is not calibrated with tiled diffusion") if fidelity_anchor and self.model_profile != QWEN_ZIMAGE_PROFILE: diff --git a/src/remove_ai_watermarks/cli.py b/src/remove_ai_watermarks/cli.py index 98ff66a..a7cdd1e 100644 --- a/src/remove_ai_watermarks/cli.py +++ b/src/remove_ai_watermarks/cli.py @@ -318,9 +318,8 @@ _text_manifest_option = click.option( type=click.Path(exists=True, dir_okay=False, path_type=Path), default=None, help=( - "Experimental verified-text restoration manifest. Requires qwen-zimage or " - "chroma-zimage, the text-restoration extra, native untiled geometry, and no " - "postprocessing." + "Experimental verified-text restoration manifest. Requires the text-restoration " + "extra, native untiled geometry, and no postprocessing." ), ) diff --git a/src/remove_ai_watermarks/invisible_engine.py b/src/remove_ai_watermarks/invisible_engine.py index 993a022..b5b7b87 100644 --- a/src/remove_ai_watermarks/invisible_engine.py +++ b/src/remove_ai_watermarks/invisible_engine.py @@ -19,7 +19,6 @@ from typing import TYPE_CHECKING, Any from ._internal.watermark_profiles import ( DEFAULT_PROFILE, REMOVAL_MODULES, - SDXL_ZIMAGE_PROFILE, resolve_adaptive_polish, resolve_effective_profile, resolve_seed, @@ -192,9 +191,9 @@ class InvisibleEngine: def preload(self, *, global_only: bool = False) -> None: """Eagerly load the pipeline so download progress is visible. - For ``qwen-zimage``, ``global_only=True`` loads the mandatory Qwen stage - and leaves the optional Z-Image and SAM face stack lazy until a face is - detected. Other profiles have no optional stage and ignore the flag. + For every concrete profile, ``global_only=True`` loads its mandatory + global stage and leaves the shared Z-Image and SAM face stack lazy until + a face is detected. """ self._remover.preload(global_only=global_only) @@ -269,13 +268,18 @@ class InvisibleEngine: seed = resolve_seed(seed) effective_profile = resolve_effective_profile(self._remover.configured_profile, vendor) - adaptive_polish = resolve_adaptive_polish(adaptive_polish, effective_profile) + # Verified text must be the final pixel-writing stage. SDXL normally enables + # adaptive polish by profile default, so an unset value becomes False here; + # an explicit True remains an error below instead of being silently ignored. + adaptive_polish = ( + False + if text_manifest is not None and adaptive_polish is None + else resolve_adaptive_polish(adaptive_polish, effective_profile) + ) if fidelity_anchor and text_manifest is None: raise ValueError("fidelity_anchor requires a text manifest") if text_manifest is not None: - if effective_profile == SDXL_ZIMAGE_PROFILE: - raise ValueError("--text-manifest is not supported by the sdxl-zimage profile") if tile: raise ValueError("--text-manifest is not calibrated with --tile") if max_resolution != 0: diff --git a/tests/test_invisible_engine.py b/tests/test_invisible_engine.py index e54e65c..1e6b71d 100644 --- a/tests/test_invisible_engine.py +++ b/tests/test_invisible_engine.py @@ -64,7 +64,6 @@ class TestVerifiedTextMode: manifest = tmp_path / "manifest.json" manifest.write_text("{}", encoding="utf-8") cases = ( - ("sdxl-zimage", {}, "not supported by the sdxl-zimage profile"), ("qwen-zimage", {"tile": True}, "not calibrated with --tile"), ("qwen-zimage", {"max_resolution": 1024}, "max-resolution 0"), ("qwen-zimage", {"humanize": 1.0}, "humanize=0"), @@ -87,19 +86,6 @@ class TestVerifiedTextMode: fidelity_anchor=True, ) - def test_auto_rejects_google_text_manifest_before_model_loading(self, tmp_path): - import pytest - - manifest = tmp_path / "manifest.json" - manifest.write_text("{}", encoding="utf-8") - - with pytest.raises(ValueError, match="not supported by the sdxl-zimage profile"): - self._engine("auto").remove_watermark( - tmp_path / "unused.png", - text_manifest=manifest, - vendor="google", - ) - def test_loads_and_forwards_verified_manifest(self, tmp_path, monkeypatch): import json @@ -137,9 +123,10 @@ class TestVerifiedTextMode: engine._remover.remove_watermark = fake_remove monkeypatch.setattr(region_eraser, "lama_available", lambda: True) - engine.remove_watermark(source, output, text_manifest=manifest) + engine.remove_watermark(source, output, text_manifest=manifest, vendor="google") assert seen["text_manifest"].lines[0].text == "Exact" + assert seen["vendor"] == "google" # Leak-safe default since 0.27.1: the global 15% donor blend is OFF unless # explicitly requested (measured to return detector-visible OpenAI SynthID # on poster-scale manifests; see docs/text-protection-research.md). diff --git a/tests/test_qwen_zimage_pipeline.py b/tests/test_qwen_zimage_pipeline.py index 3a6e400..884f58f 100644 --- a/tests/test_qwen_zimage_pipeline.py +++ b/tests/test_qwen_zimage_pipeline.py @@ -996,6 +996,70 @@ def test_sdxl_zimage_inherits_the_face_stage_rather_than_copying_it(): assert SdxlZImagePipeline._load_global is not QwenZImagePipeline._load_global +@pytest.mark.parametrize( + ("pipeline_module", "pipeline_class", "grid"), + [ + ("sdxl_zimage_pipeline", "SdxlZImagePipeline", 8), + ("chroma_zimage_pipeline", "ChromaZImagePipeline", 16), + ], +) +def test_diffusers_vae_roundtrip_uses_deterministic_mode_and_restores_native_size( + monkeypatch, pipeline_module, pipeline_class, grid +): + """Diffusers donors pad without distorting geometry and never sample.""" + import importlib + + import torch + + seen: dict[str, object] = {} + padded_width = 40 if grid == 8 else 48 + padded_height = 24 if grid == 8 else 32 + latent = torch.ones(1, 4, padded_height // 8, padded_width // 8) + + class LatentDistribution: + def mode(self): + seen["mode_calls"] = int(seen.get("mode_calls", 0)) + 1 + return latent + + def sample(self, *_args, **_kwargs): + raise AssertionError("A verified-text donor must not sample VAE latents") + + class FakeVae: + @staticmethod + def encode(tensor): + seen["encoded_shape"] = tuple(tensor.shape) + return type("Encoded", (), {"latent_dist": LatentDistribution()})() + + @staticmethod + def decode(received, *, return_dict): + assert received is latent + assert return_dict is False + return (torch.zeros(1, 3, padded_height, padded_width),) + + class FakeProcessor: + @staticmethod + def preprocess(image, *, height, width): + seen["preprocess"] = (image.size, height, width) + return torch.zeros(1, 3, height, width) + + @staticmethod + def postprocess(decoded, *, output_type): + assert output_type == "pil" + return [Image.new("RGB", (decoded.shape[3], decoded.shape[2]))] + + pipe = type("FakePipe", (), {"vae": FakeVae(), "image_processor": FakeProcessor()})() + module = importlib.import_module(f"remove_ai_watermarks._internal.{pipeline_module}") + pipeline = getattr(module, pipeline_class)(device="cpu", torch_dtype=torch.float32) + monkeypatch.setattr(pipeline, "_load_global", lambda: pipe) + + result = pipeline._vae_roundtrip(Image.new("RGB", (35, 19))) + + assert seen["preprocess"] == ((padded_width, padded_height), padded_height, padded_width) + assert seen["encoded_shape"] == (1, 3, padded_height, padded_width) + assert seen["mode_calls"] == 1 + assert result.size == (35, 19) + + def test_chroma_zimage_strength_uses_measured_flat_floors(): """The chroma floors come from the four-cohort oracle calibration; they must not drift from their derivation in watermark_profiles.""" @@ -1163,8 +1227,9 @@ def test_chroma_zimage_floors_to_its_own_latent_grid(): assert chroma_target_size(3, 3) == (16, 16) -def test_watermark_remover_accepts_text_manifest_for_chroma_but_not_sdxl(tmp_path, monkeypatch): - """Chroma owns a VAE donor; SDXL still fails at the public boundary.""" +@pytest.mark.parametrize("profile", ["chroma-zimage", "sdxl-zimage"]) +def test_watermark_remover_accepts_text_manifest_for_every_diffusers_profile(tmp_path, monkeypatch, profile): + """Both Diffusers profiles forward reviewed text to their VAE donor.""" from remove_ai_watermarks._internal.text_restoration import VerifiedTextLine, VerifiedTextManifest from remove_ai_watermarks._internal.watermark_remover import WatermarkRemover @@ -1173,22 +1238,18 @@ def test_watermark_remover_accepts_text_manifest_for_chroma_but_not_sdxl(tmp_pat Image.new("RGB", (96, 80)).save(source) manifest = VerifiedTextManifest("0" * 64, 96, 80, (VerifiedTextLine((4, 4, 20, 16), "x", "alphabetic"),)) - chroma = WatermarkRemover(device="cuda", pipeline="chroma-zimage") - chroma_runtime = MagicMock() - chroma_runtime.run.return_value = Image.new("RGB", (96, 80)) - monkeypatch.setattr(chroma, "_load_qwen_zimage_pipeline", lambda: chroma_runtime) + remover = WatermarkRemover(device="cuda", pipeline=profile) + runtime = MagicMock() + runtime.run.return_value = Image.new("RGB", (96, 80)) + monkeypatch.setattr(remover, "_load_qwen_zimage_pipeline", lambda: runtime) - chroma.remove_watermark(source, text_manifest=manifest) + remover.remove_watermark(source, text_manifest=manifest) - assert chroma_runtime.run.call_count == 1 - assert chroma_runtime.run.call_args.kwargs["text_manifest"] is manifest + assert runtime.run.call_count == 1 + assert runtime.run.call_args.kwargs["text_manifest"] is manifest with pytest.raises(ValueError, match=r"fidelity anchor.*qwen-zimage"): - chroma.remove_watermark(source, text_manifest=manifest, fidelity_anchor=True) - assert chroma_runtime.run.call_count == 1 - - sdxl = WatermarkRemover(device="cuda", pipeline="sdxl-zimage") - with pytest.raises(ValueError, match="not supported by the sdxl-zimage profile"): - sdxl.remove_watermark(source, text_manifest=manifest) + remover.remove_watermark(source, text_manifest=manifest, fidelity_anchor=True) + assert runtime.run.call_count == 1 def test_watermark_remover_dispatches_to_chroma_pipeline(monkeypatch): @@ -1229,58 +1290,6 @@ def test_chroma_adaptive_polish_defaults_off(): assert resolve_adaptive_polish(True, "chroma-zimage") is True -def test_chroma_vae_roundtrip_uses_deterministic_mode_and_restores_native_size(monkeypatch): - """The Chroma donor pads without distorting geometry and never samples.""" - import torch - - from remove_ai_watermarks._internal.chroma_zimage_pipeline import ChromaZImagePipeline - - seen: dict[str, object] = {} - latent = torch.ones(1, 4, 4, 6) - - class LatentDistribution: - def mode(self): - seen["mode_calls"] = int(seen.get("mode_calls", 0)) + 1 - return latent - - def sample(self, *_args, **_kwargs): - raise AssertionError("A verified-text donor must not sample VAE latents") - - class FakeVae: - @staticmethod - def encode(tensor): - seen["encoded_shape"] = tuple(tensor.shape) - return type("Encoded", (), {"latent_dist": LatentDistribution()})() - - @staticmethod - def decode(received, *, return_dict): - assert received is latent - assert return_dict is False - return (torch.zeros(1, 3, 32, 48),) - - class FakeProcessor: - @staticmethod - def preprocess(image, *, height, width): - seen["preprocess"] = (image.size, height, width) - return torch.zeros(1, 3, height, width) - - @staticmethod - def postprocess(decoded, *, output_type): - assert output_type == "pil" - return [Image.new("RGB", (decoded.shape[3], decoded.shape[2]))] - - pipe = type("FakePipe", (), {"vae": FakeVae(), "image_processor": FakeProcessor()})() - pipeline = ChromaZImagePipeline(device="cpu", torch_dtype=torch.float32) - monkeypatch.setattr(pipeline, "_load_global", lambda: pipe) - - result = pipeline._vae_roundtrip(Image.new("RGB", (35, 19))) - - assert seen["preprocess"] == ((48, 32), 32, 48) - assert seen["encoded_shape"] == (1, 3, 32, 48) - assert seen["mode_calls"] == 1 - assert result.size == (35, 19) - - def test_chroma_google_uses_one_content_agnostic_floor(): """Face detection alone cannot safely select a lower Google floor. diff --git a/uv.lock b/uv.lock index 3e20c98..0dfdb02 100644 --- a/uv.lock +++ b/uv.lock @@ -800,7 +800,7 @@ name = "cuda-bindings" version = "13.3.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cuda-pathfinder", marker = "sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "cuda-pathfinder" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/51/6b/457ca12dad3ee9bfcc9a545cfd6b64b359ba49de40f776f6e028e678f262/cuda_bindings-13.3.1-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c5879712accf6e14bb01aa5e67440eb84998b8d104b509cc7a6dc0b8f656a474", size = 6053539, upload-time = "2026-05-29T23:11:43.19Z" }, @@ -833,43 +833,43 @@ wheels = [ [package.optional-dependencies] cublas = [ - { name = "nvidia-cublas", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, - { name = "nvidia-cuda-nvrtc", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-cublas", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, + { name = "nvidia-cuda-nvrtc", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, ] cudart = [ - { name = "nvidia-cuda-runtime", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-cuda-runtime", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, ] cufft = [ - { name = "nvidia-cufft", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, - { name = "nvidia-nvjitlink", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-cufft", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, + { name = "nvidia-nvjitlink", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, ] cufile = [ - { name = "nvidia-cufile", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-cufile", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, ] cupti = [ - { name = "nvidia-cuda-cupti", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-cuda-cupti", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, ] curand = [ - { name = "nvidia-curand", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-curand", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, ] cusolver = [ - { name = "nvidia-cublas", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, - { name = "nvidia-cusolver", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, - { name = "nvidia-cusparse", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, - { name = "nvidia-nvjitlink", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-cublas", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, + { name = "nvidia-cusolver", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, + { name = "nvidia-cusparse", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, + { name = "nvidia-nvjitlink", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, ] cusparse = [ - { name = "nvidia-cusparse", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, - { name = "nvidia-nvjitlink", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-cusparse", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, + { name = "nvidia-nvjitlink", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, ] nvjitlink = [ - { name = "nvidia-nvjitlink", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-nvjitlink", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, ] nvrtc = [ - { name = "nvidia-cuda-nvrtc", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-cuda-nvrtc", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, ] nvtx = [ - { name = "nvidia-nvtx", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, + { name = "nvidia-nvtx", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, ] [[package]] @@ -1066,7 +1066,7 @@ wheels = [ [package.optional-dependencies] http = [ - { name = "aiohttp", marker = "python_full_version < '3.13'" }, + { name = "aiohttp" }, ] [[package]] @@ -1198,8 +1198,8 @@ name = "httpcore2" version = "2.12.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "h11", marker = "sys_platform != 'emscripten'" }, - { name = "truststore", marker = "sys_platform != 'emscripten'" }, + { name = "h11" }, + { name = "truststore" }, ] sdist = { url = "https://files.pythonhosted.org/packages/be/ad/f4f0e57345f1870f3e8cb624e058d7eca6e5a27d33bcc3311d9b618734cd/httpcore2-2.12.0.tar.gz", hash = "sha256:9293522bba0aa7c4c8e9e3f040c16575bd8868e155a77fa30c7a9085a5eae648", size = 67548, upload-time = "2026-08-18T13:22:08.211Z" } wheels = [ @@ -1461,15 +1461,15 @@ name = "lightning" version = "2.6.6" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "fsspec", extra = ["http"], marker = "python_full_version < '3.13'" }, - { name = "lightning-utilities", marker = "python_full_version < '3.13'" }, - { name = "packaging", marker = "python_full_version < '3.13'" }, - { name = "pytorch-lightning", marker = "python_full_version < '3.13'" }, - { name = "pyyaml", marker = "python_full_version < '3.13'" }, - { name = "torch", marker = "python_full_version < '3.13'" }, - { name = "torchmetrics", marker = "python_full_version < '3.13'" }, - { name = "tqdm", marker = "python_full_version < '3.13'" }, - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "fsspec", extra = ["http"] }, + { name = "lightning-utilities" }, + { name = "packaging" }, + { name = "pytorch-lightning" }, + { name = "pyyaml" }, + { name = "torch" }, + { name = "torchmetrics" }, + { name = "tqdm" }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/39/b8/fbf1469d917c3ed3b22e00ab3a852fb9a5f85b9dc7f7060eedc3449098e7/lightning-2.6.6.tar.gz", hash = "sha256:1c6329e241f8a85f29fedead89c1ee138b36caa08de60c260c6a7eaee81c8295", size = 659902, upload-time = "2026-09-10T09:41:03.009Z" } wheels = [ @@ -1481,8 +1481,8 @@ name = "lightning-utilities" version = "0.15.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "packaging", marker = "python_full_version < '3.13'" }, - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "packaging" }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f1/45/7fa8f56b17dc0f0a41ec70dd307ecd6787254483549843bef4c30ab5adce/lightning_utilities-0.15.3.tar.gz", hash = "sha256:792ae0204c79f6859721ac7f386c237a33b0ed06ba775009cb894e010a842033", size = 33553, upload-time = "2026-02-22T14:48:53.348Z" } wheels = [ @@ -1896,7 +1896,7 @@ name = "nvidia-cublas" version = "13.1.1.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-cuda-nvrtc", marker = "sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "nvidia-cuda-nvrtc" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/a7/a1/0bd24ee8c8d03adac032fd2909426a00c88f8c57961b1277ded97f91119f/nvidia_cublas-13.1.1.3-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:b7a210458267ac818974c53038fbec2e969d5c99f305ab15c72522fa9f001dd5", size = 542848918, upload-time = "2026-04-08T18:46:22.985Z" }, @@ -1935,7 +1935,7 @@ name = "nvidia-cudnn-cu13" version = "9.24.0.43" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-cublas", marker = "sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "nvidia-cublas" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/ca/30/7c257e3d5cb4fecb147b93895c66e29c93f8e76d74b45bb418ff0587c4ec/nvidia_cudnn_cu13-9.24.0.43-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:a6812a554a1ff0413e9c52b84c26c050380649ab9615f9c16bded368ce9f421f", size = 650976863, upload-time = "2026-07-02T16:23:39.248Z" }, @@ -1947,7 +1947,7 @@ name = "nvidia-cufft" version = "12.0.0.61" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-nvjitlink", marker = "sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "nvidia-nvjitlink" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/8b/ae/f417a75c0259e85c1d2f83ca4e960289a5f814ed0cea74d18c353d3e989d/nvidia_cufft-12.0.0.61-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2708c852ef8cd89d1d2068bdbece0aa188813a0c934db3779b9b1faa8442e5f5", size = 214053554, upload-time = "2025-09-04T08:31:38.196Z" }, @@ -1977,9 +1977,9 @@ name = "nvidia-cusolver" version = "12.0.4.66" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-cublas", marker = "sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, - { name = "nvidia-cusparse", marker = "sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, - { name = "nvidia-nvjitlink", marker = "sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "nvidia-cublas" }, + { name = "nvidia-cusparse" }, + { name = "nvidia-nvjitlink" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/c8/c3/b30c9e935fc01e3da443ec0116ed1b2a009bb867f5324d3f2d7e533e776b/nvidia_cusolver-12.0.4.66-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:02c2457eaa9e39de20f880f4bd8820e6a1cfb9f9a34f820eb12a155aa5bc92d2", size = 223467760, upload-time = "2025-09-04T08:33:04.222Z" }, @@ -1991,7 +1991,7 @@ name = "nvidia-cusparse" version = "12.6.3.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-nvjitlink", marker = "sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "nvidia-nvjitlink" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/f8/94/5c26f33738ae35276672f12615a64bd008ed5be6d1ebcb23579285d960a9/nvidia_cusparse-12.6.3.3-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:80bcc4662f23f1054ee334a15c72b8940402975e0eab63178fc7e670aa59472c", size = 162155568, upload-time = "2025-09-04T08:33:42.864Z" }, @@ -2146,7 +2146,7 @@ resolution-markers = [ "(python_full_version < '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.12' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", ] dependencies = [ - { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" } }, ] sdist = { url = "https://files.pythonhosted.org/packages/17/06/68c27a523103dad5837dc5b87e71285280c4f098c60e4fe8a8db6486ab09/opencv-python-4.11.0.86.tar.gz", hash = "sha256:03d60ccae62304860d232272e4a4fda93c39d595780cb40b161b310244b736a4", size = 95171956, upload-time = "2025-01-16T13:52:24.737Z" } wheels = [ @@ -2175,7 +2175,7 @@ resolution-markers = [ "(python_full_version == '3.13.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", ] dependencies = [ - { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, + { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" } }, ] sdist = { url = "https://files.pythonhosted.org/packages/ed/e8/c2959c0f969ea4ae96b55ad10c108c0410de4e8b3550d9941a4cfdd52703/opencv_python-4.14.0.94.tar.gz", hash = "sha256:afadc76a38a07713a4c2b4702da917cf0ed489be57e2a40a34e670189731de7d", size = 96400225, upload-time = "2026-07-28T19:23:09.546Z" } wheels = [ @@ -2206,7 +2206,7 @@ resolution-markers = [ "(python_full_version < '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.12' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", ] dependencies = [ - { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" } }, ] sdist = { url = "https://files.pythonhosted.org/packages/36/2f/5b2b3ba52c864848885ba988f24b7f105052f68da9ab0e693cc7c25b0b30/opencv-python-headless-4.11.0.86.tar.gz", hash = "sha256:996eb282ca4b43ec6a3972414de0e2331f5d9cda2b41091a49739c19fb843798", size = 95177929, upload-time = "2025-01-16T13:53:40.22Z" } wheels = [ @@ -2235,7 +2235,7 @@ resolution-markers = [ "(python_full_version == '3.13.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", ] dependencies = [ - { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, + { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" } }, ] sdist = { url = "https://files.pythonhosted.org/packages/67/8a/51ff1c6287be0b6cd3070cf095875da4ff7d6eda38507e6ada5608786a78/opencv_python_headless-4.14.0.94.tar.gz", hash = "sha256:4afa2ea1214453648be88259f035712454faa9039b686de7753569ba8eec1577", size = 96405664, upload-time = "2026-07-28T19:23:48.758Z" } wheels = [ @@ -3179,14 +3179,14 @@ name = "pytorch-lightning" version = "2.6.6" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "fsspec", extra = ["http"], marker = "python_full_version < '3.13'" }, - { name = "lightning-utilities", marker = "python_full_version < '3.13'" }, - { name = "packaging", marker = "python_full_version < '3.13'" }, - { name = "pyyaml", marker = "python_full_version < '3.13'" }, - { name = "torch", marker = "python_full_version < '3.13'" }, - { name = "torchmetrics", marker = "python_full_version < '3.13'" }, - { name = "tqdm", marker = "python_full_version < '3.13'" }, - { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "fsspec", extra = ["http"] }, + { name = "lightning-utilities" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "torch" }, + { name = "torchmetrics" }, + { name = "tqdm" }, + { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/97/e4/5025cb42311b4f8cc6d1b12cd81399ca433159f8e3831ab229c5a6c5177e/pytorch_lightning-2.6.6.tar.gz", hash = "sha256:52c52b20522dd3d3feb3bb3aae2dbe3f291e8e836d4111a74cfd72e9df018834", size = 662507, upload-time = "2026-09-10T09:41:16.684Z" } wheels = [ @@ -3205,7 +3205,7 @@ resolution-markers = [ "(python_full_version < '3.12' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.12' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", ] dependencies = [ - { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.12'" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" } }, ] sdist = { url = "https://files.pythonhosted.org/packages/5a/75/50581633d199812205ea8cdd0f6d52f12a624886b74bf1486335b67f01ff/pywavelets-1.9.0.tar.gz", hash = "sha256:148d12203377772bea452a59211d98649c8ee4a05eff019a9021853a36babdc8", size = 3938340, upload-time = "2025-08-04T16:20:04.978Z" } wheels = [ @@ -3281,7 +3281,7 @@ resolution-markers = [ "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'linux' and sys_platform != 'win32')", ] dependencies = [ - { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version == '3.12.*'" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, { name = "numpy", version = "2.3.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/25/84/a9e80e33eb114e78bd2f1829a90d72d8d4d660109c721a8aa2f0d58d7592/pywavelets-1.10.0.tar.gz", hash = "sha256:f3cb8640225a8f3fdcbc3f6f3bd112c78927c9407cbfa65a5ff303990a6a3e32", size = 3948163, upload-time = "2026-09-07T17:15:15.637Z" } @@ -3449,7 +3449,7 @@ wheels = [ [[package]] name = "remove-ai-watermarks" -version = "0.40.4" +version = "0.40.5" source = { editable = "." } dependencies = [ { name = "c2pa-python" }, @@ -4067,10 +4067,10 @@ name = "torchmetrics" version = "1.9.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "lightning-utilities", marker = "python_full_version < '3.13'" }, - { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, - { name = "packaging", marker = "python_full_version < '3.13'" }, - { name = "torch", marker = "python_full_version < '3.13'" }, + { name = "lightning-utilities" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" } }, + { name = "packaging" }, + { name = "torch" }, ] sdist = { url = "https://files.pythonhosted.org/packages/81/34/39b8b749333db56c0585d7a11fa62a283c087bb1dfc897d69fb8cedbefb1/torchmetrics-1.9.0.tar.gz", hash = "sha256:a488609948600df52d3db4fcdab02e62aab2a85ef34da67037dc3e65b8512faa", size = 581765, upload-time = "2026-03-09T17:41:22.443Z" } wheels = [ @@ -4165,13 +4165,13 @@ name = "trustmark" version = "0.9.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "einops", marker = "python_full_version < '3.13'" }, - { name = "lightning", marker = "python_full_version < '3.13'" }, - { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.13'" }, - { name = "omegaconf", marker = "python_full_version < '3.13'" }, - { name = "six", marker = "python_full_version < '3.13'" }, - { name = "torch", marker = "python_full_version < '3.13'" }, - { name = "torchvision", marker = "python_full_version < '3.13'" }, + { name = "einops" }, + { name = "lightning" }, + { name = "numpy", version = "1.26.4", source = { registry = "https://pypi.org/simple" } }, + { name = "omegaconf" }, + { name = "six" }, + { name = "torch" }, + { name = "torchvision" }, ] sdist = { url = "https://files.pythonhosted.org/packages/53/41/32e84596af94e8696c55060d55eb9eed9d99722a673d12212a1c0c4ef1d2/trustmark-0.9.2.tar.gz", hash = "sha256:bcaa3bc38ead1c0558df81932672f34e306efaed31389da57dbed100e236b28a", size = 63780, upload-time = "2026-09-08T10:58:11.601Z" }