diff --git a/docs/module-internals.md b/docs/module-internals.md index 1f4c892..2c94312 100644 --- a/docs/module-internals.md +++ b/docs/module-internals.md @@ -68,10 +68,14 @@ Important contracts: - `visible` and `erase` strip and verify AI metadata on a staged sibling and exit `1` with nothing published when a marker survives; they used to swallow a failed strip unless `--verbose` was set. The library `remove_visible` does the same and - raises `MetadataStripIncomplete`, as `remove_all` already did, and the diffusion - writer behind `invisible` raises when its own strip leaves a marker. + raises `MetadataStripIncomplete`, as `remove_all` already did. The diffusion + writer behind `invisible` stages the same way and raises with nothing published + (fixed 2026-10-19; it used to leave the marked file at the output path). `_internal.utils.staged_sibling` is the shared stage: it never pre-creates the - name, so the published file keeps the default permissions. + name, so the published file keeps the default permissions. These writers strip + through `metadata.strip_written_output`, which skips the readability probe: + the file is the package's own encode, and the probe's full decode doubled the + cost of the strip (decided 2026-10-19). - A quiet `metadata --check` and a successful `metadata --remove` (same for `video metadata`) end by repeating the `identify` limit: the pixel channel is untouched and a watermark such as SynthID has no local decoder once its diff --git a/pyproject.toml b/pyproject.toml index e5b59e8..13863ab 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.46.0" +version = "0.47.0" 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/server.json b/server.json index 3737a8c..05a69d9 100644 --- a/server.json +++ b/server.json @@ -3,7 +3,7 @@ "name": "io.github.wiltodelta/remove-ai-watermarks", "title": "Remove AI Watermarks", "description": "Identify and remove AI watermarks and provenance metadata from your own images and video.", - "version": "0.46.0", + "version": "0.47.0", "repository": { "url": "https://github.com/wiltodelta/remove-ai-watermarks", "source": "github" @@ -14,7 +14,7 @@ "registryType": "pypi", "registryBaseUrl": "https://pypi.org", "identifier": "remove-ai-watermarks", - "version": "0.46.0", + "version": "0.47.0", "runtimeHint": "uvx", "transport": { "type": "stdio" diff --git a/skills/remove-ai-watermarks/scripts/probe.py b/skills/remove-ai-watermarks/scripts/probe.py index 2a7cbfa..e7b0236 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, 46, 0) +MIN_CLI_VERSION = (0, 47, 0) # 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 1a18203..6596fe1 100644 --- a/src/remove_ai_watermarks/__init__.py +++ b/src/remove_ai_watermarks/__init__.py @@ -35,7 +35,7 @@ _os.environ.setdefault("TRANSFORMERS_VERBOSITY", "error") _warnings.filterwarnings("ignore", message=r".*ImageProcessorFast.*") -__version__ = "0.46.0" +__version__ = "0.47.0" __all__ = [ "BatchItemResult", diff --git a/src/remove_ai_watermarks/_internal/watermark_remover.py b/src/remove_ai_watermarks/_internal/watermark_remover.py index 06a5ef2..6c5dabc 100644 --- a/src/remove_ai_watermarks/_internal/watermark_remover.py +++ b/src/remove_ai_watermarks/_internal/watermark_remover.py @@ -212,17 +212,18 @@ class WatermarkRemover: import numpy as np from remove_ai_watermarks import image_io + from remove_ai_watermarks._internal.utils import staged_sibling + from remove_ai_watermarks.metadata import strip_written_output output_path.parent.mkdir(parents=True, exist_ok=True) bgr = np.ascontiguousarray(np.asarray(image.convert("RGB"))[:, :, ::-1]) - if not image_io.imwrite(str(output_path), bgr, display_tags_from=display_tags_from): - image.save(output_path) - from remove_ai_watermarks.metadata import strip_and_verify - - # Verified, so a marker that survives fails the run instead of shipping with it. - _, leftover = strip_and_verify(output_path, output_path, keep_standard=True) - if leftover: - raise RuntimeError(f"AI metadata survived the strip: {', '.join(sorted(leftover))}") + # Staged and verified, so a marker that survives fails the run with nothing + # published at output_path. + with staged_sibling(output_path) as staged: + if not image_io.imwrite(str(staged), bgr, display_tags_from=display_tags_from): + image.save(staged) + if leftover := strip_written_output(staged): + raise RuntimeError(f"AI metadata survived the strip: {', '.join(sorted(leftover))}") def remove_watermark( self, diff --git a/src/remove_ai_watermarks/api.py b/src/remove_ai_watermarks/api.py index 39f7ec4..d1ef1ee 100644 --- a/src/remove_ai_watermarks/api.py +++ b/src/remove_ai_watermarks/api.py @@ -157,10 +157,9 @@ def _write_visible_result( raise OSError(f"failed to write output (is the destination writable?): {out_path}") if strip_metadata: - from remove_ai_watermarks.metadata import strip_and_verify + from remove_ai_watermarks.metadata import strip_written_output - _, leftover = strip_and_verify(staged, staged) - if leftover: + if leftover := strip_written_output(staged): raise MetadataStripIncomplete(set(leftover)) @@ -464,7 +463,7 @@ def remove_all( import tempfile from remove_ai_watermarks import image_io, watermark_registry - from remove_ai_watermarks.metadata import strip_and_verify + from remove_ai_watermarks.metadata import strip_written_output def say(stage: str, detail: str) -> None: if progress is not None: @@ -519,8 +518,7 @@ def remove_all( # ── 3. AI metadata ── # Read the pristine ORIGINAL for provenance above and the STAGED file here: # the visible pass has already dropped this file's C2PA. - _, leftover = strip_and_verify(staged, staged) - if leftover: + if leftover := strip_written_output(staged): # Before the write, on purpose -- see MetadataStripIncomplete. raise MetadataStripIncomplete(set(leftover)) say("metadata", "stripped") diff --git a/src/remove_ai_watermarks/cli.py b/src/remove_ai_watermarks/cli.py index b1af397..d40f8c7 100644 --- a/src/remove_ai_watermarks/cli.py +++ b/src/remove_ai_watermarks/cli.py @@ -467,7 +467,7 @@ def _write_output_or_exit( published, so a marker that survives exits 1 with nothing written. Both commands using this used to swallow a failed strip unless ``--verbose`` was set.""" from remove_ai_watermarks._internal.utils import staged_sibling - from remove_ai_watermarks.metadata import strip_and_verify + from remove_ai_watermarks.metadata import strip_written_output output.parent.mkdir(parents=True, exist_ok=True) with staged_sibling(output) as staged: @@ -477,7 +477,7 @@ def _write_output_or_exit( if not strip_metadata: return try: - _, leftover = strip_and_verify(staged, staged) + leftover = strip_written_output(staged) except (OSError, RuntimeError, ValueError) as e: console.print(f" Error: failed to strip AI metadata from {output}: {e}") raise SystemExit(1) from e diff --git a/src/remove_ai_watermarks/metadata.py b/src/remove_ai_watermarks/metadata.py index c846c4f..f8cd9ac 100644 --- a/src/remove_ai_watermarks/metadata.py +++ b/src/remove_ai_watermarks/metadata.py @@ -1968,6 +1968,25 @@ def strip_and_verify( """ if (reason := unreadable_reason(source_path)) is not None: raise ValueError(f"cannot read {source_path.name}: {reason}") + return _strip_and_rescan(source_path, output_path, keep_standard=keep_standard) + + +def strip_written_output(path: Path, *, keep_standard: bool = True) -> dict[str, str]: + """Strip ``path`` in place and return the markers that survived. + + For a file whose pixels this run has already decoded or written itself, so the + readability probe that :func:`strip_and_verify` runs first would only decode + them a second time. + """ + return _strip_and_rescan(path, path, keep_standard=keep_standard)[1] + + +def _strip_and_rescan( + source_path: Path, + output_path: Path | None, + *, + keep_standard: bool, +) -> tuple[Path, dict[str, str]]: out = remove_ai_metadata(source_path, output_path, keep_standard=keep_standard) remaining = get_ai_metadata(out) if not remaining: diff --git a/tests/test_api.py b/tests/test_api.py index 5a18988..44017c3 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -430,7 +430,7 @@ class TestRemoveAllLibrary: from remove_ai_watermarks import api, invisible_engine, metadata monkeypatch.setattr(invisible_engine, "is_available", lambda: False) - monkeypatch.setattr(metadata, "strip_and_verify", lambda src, dst: (dst, {"c2pa"})) + monkeypatch.setattr(metadata, "strip_written_output", lambda _path: {"c2pa": "survived"}) out = tmp_path / "clean.png" with pytest.raises(api.MetadataStripIncomplete, match="c2pa"): diff --git a/tests/test_cli.py b/tests/test_cli.py index be0e12d..a3d608a 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -675,10 +675,7 @@ class TestAllCommand: # The no-signal gate lives in the library now; patch it there, not on the # CLI module, or the diffusion stage runs for real. patch("remove_ai_watermarks.api._SourceEvidence.has_invisible_target", return_value=False), - patch( - "remove_ai_watermarks.metadata.strip_and_verify", - return_value=(tmp_path / "intermediate.png", {"c2pa": True}), - ), + patch("remove_ai_watermarks.metadata.strip_written_output", return_value={"c2pa": "survived"}), ): result = runner.invoke(main, ["all", str(sample_png), "-o", str(output)]) diff --git a/tests/test_fail_closed.py b/tests/test_fail_closed.py index f04fdfa..a203f23 100644 --- a/tests/test_fail_closed.py +++ b/tests/test_fail_closed.py @@ -239,6 +239,29 @@ class TestSkippedStepsAreFailures: remover = object.__new__(WatermarkRemover) # the writer needs no model state with pytest.raises(RuntimeError, match="survived"): remover._write_output(Image.new("RGB", (16, 16)), tmp_path / "out.png") + assert list(tmp_path.iterdir()) == [], "an output that kept its AI metadata was published" + + @pytest.mark.parametrize("command", ["erase", "remove_visible", "invisible_writer"]) + def test_an_output_the_package_just_wrote_is_not_probed_again(self, tmp_path, clean_png, monkeypatch, command): + """The readability probe decodes every pixel; on our own encode it is pure waste.""" + from PIL import Image + + from remove_ai_watermarks._internal.watermark_remover import WatermarkRemover + from remove_ai_watermarks.api import remove_visible + + probed: list[Path] = [] + real = metadata.unreadable_reason + monkeypatch.setattr(metadata, "unreadable_reason", lambda path: probed.append(path) or real(path)) + out = tmp_path / "out.png" + if command == "erase": + result = _invoke(["erase", "--region", "2,2,10,10", str(clean_png), "-o", str(out)]) + assert result.exit_code == 0, result.output + elif command == "remove_visible": + remove_visible(clean_png, out, write_noop=True) + else: + object.__new__(WatermarkRemover)._write_output(Image.new("RGB", (16, 16)), out) + assert out.exists() + assert [p for p in probed if p.name != clean_png.name] == [] def test_metadata_remove_reports_a_missing_ffmpeg_without_a_traceback(self, tmp_path, monkeypatch): monkeypatch.setattr(shutil, "which", lambda _name: None) diff --git a/uv.lock b/uv.lock index 560f717..0bce126 100644 --- a/uv.lock +++ b/uv.lock @@ -825,7 +825,7 @@ name = "cuda-bindings" version = "13.4.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cuda-pathfinder" }, + { name = "cuda-pathfinder", marker = "sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/f1/3a/06569edfd5db020c1ff7c0361268a3eaa60ead0c0ccca460fef69d68022f/cuda_bindings-13.4.3-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df8b3767facca8acde216460df684dfc3826d519096c13adfd3030a55870dc79", size = 6494098, upload-time = "2026-09-23T02:21:50.472Z" }, @@ -858,43 +858,43 @@ wheels = [ [package.optional-dependencies] cublas = [ - { 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'" }, + { 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')" }, ] cudart = [ - { name = "nvidia-cuda-runtime", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, + { name = "nvidia-cuda-runtime", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, ] cufft = [ - { name = "nvidia-cufft", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, - { name = "nvidia-nvjitlink", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, + { 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')" }, ] cufile = [ - { name = "nvidia-cufile", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, + { name = "nvidia-cufile", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, ] cupti = [ - { name = "nvidia-cuda-cupti", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, + { name = "nvidia-cuda-cupti", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, ] curand = [ - { name = "nvidia-curand", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, + { name = "nvidia-curand", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, ] cusolver = [ - { 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'" }, + { 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')" }, ] cusparse = [ - { name = "nvidia-cusparse", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, - { name = "nvidia-nvjitlink", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, + { 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')" }, ] nvjitlink = [ - { name = "nvidia-nvjitlink", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, + { name = "nvidia-nvjitlink", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, ] nvrtc = [ - { name = "nvidia-cuda-nvrtc", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, + { name = "nvidia-cuda-nvrtc", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, ] nvtx = [ - { name = "nvidia-nvtx", marker = "platform_machine == 'aarch64' or platform_machine == 'x86_64'" }, + { name = "nvidia-nvtx", marker = "(platform_machine == 'aarch64' and sys_platform == 'linux') or (platform_machine == 'x86_64' and sys_platform == 'linux')" }, ] [[package]] @@ -1221,8 +1221,8 @@ name = "httpcore2" version = "2.13.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "h11" }, - { name = "truststore" }, + { name = "h11", marker = "sys_platform != 'emscripten'" }, + { name = "truststore", marker = "sys_platform != 'emscripten'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/cb/f3/1db7aa2bc2524062192bb0e0323969492d1883152a232fe36eea65f4e35c/httpcore2-2.13.1.tar.gz", hash = "sha256:e0aa977abe17e69a3b820a24542a6fa88702676d83880b8d194dcd18408e5103", size = 68071, upload-time = "2026-09-23T07:47:22.372Z" } wheels = [ @@ -1964,7 +1964,7 @@ name = "nvidia-cublas" version = "13.1.1.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-cuda-nvrtc" }, + { name = "nvidia-cuda-nvrtc", marker = "sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, ] 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" }, @@ -2003,7 +2003,7 @@ name = "nvidia-cudnn-cu13" version = "9.24.0.43" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-cublas" }, + { name = "nvidia-cublas", marker = "sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, ] 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" }, @@ -2015,7 +2015,7 @@ name = "nvidia-cufft" version = "12.0.0.61" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-nvjitlink" }, + { name = "nvidia-nvjitlink", marker = "sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, ] 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" }, @@ -2045,9 +2045,9 @@ name = "nvidia-cusolver" version = "12.0.4.66" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-cublas" }, - { name = "nvidia-cusparse" }, - { name = "nvidia-nvjitlink" }, + { 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'" }, ] 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" }, @@ -2059,7 +2059,7 @@ name = "nvidia-cusparse" version = "12.6.3.3" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "nvidia-nvjitlink" }, + { name = "nvidia-nvjitlink", marker = "sys_platform != 'darwin' and sys_platform != 'emscripten' and sys_platform != 'win32'" }, ] 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" }, @@ -3203,7 +3203,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" }, + { name = "numpy", marker = "python_full_version < '3.12'" }, ] 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 = [ @@ -3279,7 +3279,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" }, + { name = "numpy", marker = "python_full_version >= '3.12'" }, ] 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" } wheels = [ @@ -3479,7 +3479,7 @@ wheels = [ [[package]] name = "remove-ai-watermarks" -version = "0.46.0" +version = "0.47.0" source = { editable = "." } dependencies = [ { name = "c2pa-python" }, @@ -4473,8 +4473,8 @@ name = "uvicorn" version = "0.54.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "click" }, - { name = "h11" }, + { name = "click", marker = "sys_platform != 'emscripten'" }, + { name = "h11", marker = "sys_platform != 'emscripten'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/da/34/30e9280707135d2cfc589dfff3cb796bd07a3aeb1a3e415ba09dd89d7bb4/uvicorn-0.54.0.tar.gz", hash = "sha256:a2e33cbfaa0306f8e6b0c13e0cb89d7d7a2da3e62b90c66e18c33d9807b28620", size = 112283, upload-time = "2026-09-25T06:52:37.601Z" } wheels = [