mirror of
https://github.com/wiltodelta/remove-ai-watermarks.git
synced 2026-10-15 14:00:54 +02:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4241a01659 | ||
|
|
c37378b347 | ||
|
|
8cb99b73b2 | ||
|
|
c45e6db3e4 | ||
|
|
37fb080c74 | ||
|
|
954298aa14 | ||
|
|
10c7775990 | ||
|
|
3f6447b23d |
No files matched your search
@@ -79,6 +79,8 @@ The same rule applies to install hints: name the extra that actually makes the c
|
||||
|
||||
Run `bash maintain.sh` from the repository root. The authoritative type gate is scoped to `src/`; full-project Pyright can exhaust Node memory on the ML dependency graph.
|
||||
|
||||
The full gate outlasts the agent's 10-minute foreground shell cap, which kills it mid-suite. Run it detached under `nohup caffeinate -dimu` with its exit code written to a file and poll that file. To stop a stray run, kill its own PID, never `pkill -f pytest`, which also takes down other sessions' suites on the machine.
|
||||
|
||||
The security scanner's exit status is authoritative, including a failure after a success
|
||||
message. `tests/test_maintenance.py` checks clean, vulnerable, and broken scanner
|
||||
runs through the real shell entry point, and pins the CI `security` job to the
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
---
|
||||
name: provider-oracles
|
||||
description: Run hash-bound Google, OpenAI, Microsoft, or Meta watermark-oracle checks for this repository. Use for provider verification, multi-account Gemini checks, isolated Playwright checks, or recording oracle evidence.
|
||||
metadata:
|
||||
internal: true
|
||||
---
|
||||
|
||||
# Provider oracles
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
---
|
||||
name: signal-discovery
|
||||
description: Discover missing AI provenance and watermark coverage in the remove-ai-watermarks library from fresh or historical corpora, then validate detection and removal parity. Use for corpus gap mining, periodic signal audits, new-provider discovery, or deciding whether old retained data must be replayed. Do not use for ordinary per-file identification or removal.
|
||||
metadata:
|
||||
internal: true
|
||||
---
|
||||
|
||||
# Signal discovery
|
||||
|
||||
@@ -26,6 +26,11 @@ SkillsMP indexes public GitHub `SKILL.md` files and does not need a separate
|
||||
upload. ClawHub and the Claude community marketplace are explicit publishes;
|
||||
see [Release and distribution](release-and-distribution.md#agent-skill).
|
||||
|
||||
Every maintainer skill under `.claude/skills/` carries `metadata.internal: true`.
|
||||
The skills CLI discovers every `SKILL.md` in that directory, so without the flag
|
||||
`npx skills add` offers it and skills.sh lists it beside the published skill.
|
||||
`tests/test_agent_skill.py` fails on a project skill that lacks it.
|
||||
|
||||
## Where the skill can actually be listed
|
||||
|
||||
Most skill catalogs are install registries, not discovery engines. A listing
|
||||
|
||||
@@ -400,12 +400,20 @@ global Qwen stage is tiled; the face stage runs after tile blending.
|
||||
every profile, streaming weights instead of pinning them. It reduces CUDA memory
|
||||
pressure at the cost of speed.
|
||||
|
||||
It reaches the global stack of `qwen-zimage` only. That stack is each
|
||||
profile's own, and `chroma-zimage` and `sdxl-zimage` load theirs with a plain
|
||||
`.to(device)`. On a card below the face-stage residency floor, where the face
|
||||
stack was already offloading, the flag changes nothing at all for those two: the
|
||||
run behaves as if it were absent. The library says so on the way past, as a
|
||||
warning ahead of the model load.
|
||||
It reaches the global stack of `qwen-zimage` and `sdxl-zimage`. That stack is
|
||||
each profile's own, and `chroma-zimage` loads its stack with a plain `.to(device)`.
|
||||
On a card below the face-stage residency floor, where the face stack was already
|
||||
offloading, the flag changes nothing at all for that profile: the run behaves as
|
||||
if it were absent. The library says so on the way past, as a warning ahead of the
|
||||
model load.
|
||||
|
||||
`sdxl-zimage` streams its global stack without being asked on a card below
|
||||
`RESIDENT_SDXL_GLOBAL_MIN_VRAM_GIB` (12 GiB). Its UNet and Canny ControlNet run
|
||||
together on every step, so the stack does not fit an 8 GiB card even with
|
||||
model-level offload. A resident load there does not always fail loudly: under
|
||||
the Windows display driver, including WSL2, CUDA can spill the overflow into
|
||||
shared system memory, and the run goes on at a fraction of the speed instead of
|
||||
raising an out-of-memory error.
|
||||
|
||||
Residency is otherwise chosen from the card's total VRAM. On a card large enough
|
||||
to hold a stack, offloading is pure waste: DiffSynth drops weights to the meta
|
||||
|
||||
+49
-12
@@ -2185,27 +2185,27 @@ Regression coverage:
|
||||
|
||||
### CPU offload
|
||||
|
||||
CPU offload is enabled only when requested. Nothing calls Diffusers'
|
||||
`enable_model_cpu_offload` any more -- that belonged to the deleted single-stage
|
||||
profiles. `--cpu-offload` sets both residency fields, and they do not travel the
|
||||
same distance: the face field is read by the shared base and so applies to every
|
||||
profile, while the global field is read by `QwenZImagePipeline` alone.
|
||||
`ChromaZImagePipeline` and `SdxlZImagePipeline` load their global stack with a
|
||||
plain `.to(device)` and never consult it.
|
||||
Nothing calls Diffusers' `enable_model_cpu_offload` any more -- that belonged to
|
||||
the deleted single-stage profiles. `--cpu-offload` sets both residency fields, and
|
||||
they do not travel the same distance: the face field is read by the shared base and
|
||||
so applies to every profile, while the global field is read by `QwenZImagePipeline`
|
||||
and `SdxlZImagePipeline`. `ChromaZImagePipeline` loads its global stack with a
|
||||
plain `.to(device)` and never consults it.
|
||||
|
||||
`GLOBAL_OFFLOAD_PROFILES` in `watermark_profiles.py` is the list, and
|
||||
`global_offload_supported` the question; `WatermarkRemover._warn_if_global_offload_unsupported`
|
||||
warns ahead of the model load rather than at construction, because `auto` picks
|
||||
its engine per-image. An explicit `auto` preload uses the qwen-zimage fallback and
|
||||
therefore keeps global offload active before that resolution. Below
|
||||
`RESIDENT_FACE_MODEL_MIN_VRAM_GIB` the flag is a no-op for the other two profiles --
|
||||
the face stack was offloading anyway and the global one does not ask -- which is
|
||||
what the warning exists to say before a download starts.
|
||||
`RESIDENT_FACE_MODEL_MIN_VRAM_GIB` the flag is a no-op for `chroma-zimage` -- the
|
||||
face stack was offloading anyway and the global one does not ask -- which is what
|
||||
the warning exists to say before a download starts.
|
||||
|
||||
Residency is otherwise chosen from the card's total VRAM, once per stack:
|
||||
`resolve_global_model_residency` gates the mandatory Qwen stack at
|
||||
`RESIDENT_GLOBAL_MODEL_MIN_VRAM_GIB` and `resolve_face_model_residency` gates the
|
||||
optional Z-Image stack at `RESIDENT_FACE_MODEL_MIN_VRAM_GIB`.
|
||||
`RESIDENT_GLOBAL_MODEL_MIN_VRAM_GIB`, `resolve_sdxl_global_residency` gates the
|
||||
SDXL stack at `RESIDENT_SDXL_GLOBAL_MIN_VRAM_GIB`, and `resolve_face_model_residency`
|
||||
gates the optional Z-Image stack at `RESIDENT_FACE_MODEL_MIN_VRAM_GIB`.
|
||||
|
||||
Below the global floor, `_qwen_vram_config` streams the stack from disk, which is
|
||||
what makes a 20B model runnable on a consumer card. At or above it, streaming is
|
||||
@@ -2222,6 +2222,43 @@ The resident config deliberately passes no `"disk"` value anywhere. DiffSynth la
|
||||
`disk_offload` once, from `offload_dtype`, so leaving the sentinel in place while
|
||||
pointing every device at CUDA would keep the meta-drop and re-read.
|
||||
|
||||
Sequential SDXL offload receives the current CUDA device index explicitly, so
|
||||
its hooks use the same GPU as the residency probes, input tensors, and seeded
|
||||
generators. Omitting the index makes Diffusers select GPU 0 even after
|
||||
`torch.cuda.set_device(1)`. `test_streaming_uses_the_current_cuda_device` in
|
||||
`test_cpu_offload.py` covers automatic and forced streaming on nonzero devices.
|
||||
|
||||
The SDXL stack (fp16 UNet, Canny ControlNet, both text encoders, the fp16-fix VAE)
|
||||
is ~8.8 GiB of weights, and the UNet and the ControlNet run together on every
|
||||
denoising step. Below the 12 GiB floor `_load_global` fuses the Lightning LoRA on
|
||||
the CPU and then calls `enable_sequential_cpu_offload`; the order matters, because
|
||||
the offload hooks stream their own copy of each weight and a later fuse would never
|
||||
reach it. Measured on an RTX 5060 Laptop (8 GiB, WSL2, 16 GiB system RAM) in
|
||||
October 2026, one 1672x941 global pass at strength 0.15:
|
||||
|
||||
| Placement | Warm pass | Peak VRAM | Peak system RAM |
|
||||
|---|---|---|---|
|
||||
| resident `.to(device)` (previous behaviour) | did not finish in 10 min | 7.9 GiB used, spilling | -- |
|
||||
| `enable_model_cpu_offload` | 237 s | 9.2 GiB reserved on an 8 GiB card | 12.4 GiB |
|
||||
| group offload, block level, CUDA stream | 92 s, or hung | 12.4 GiB reserved | 15.6 GiB |
|
||||
| group offload, leaf level, CUDA stream | 7.8 s, or hung | 5.6 GiB | 15.7 GiB |
|
||||
| group offload, leaf level, `low_cpu_mem_usage` | 21.4 s | 5.6 GiB | 9.4 GiB |
|
||||
| `enable_sequential_cpu_offload` | 12.8 s | 3-4 GiB | 8.5 GiB |
|
||||
|
||||
The resident and model-offload rows did not fail: under the Windows display driver
|
||||
CUDA falls back to shared system memory, so PyTorch reserved more than the card
|
||||
holds and the run crawled instead of raising. The streamed group-offload variants
|
||||
pin every weight in host memory up front; on a 16 GiB machine that left the run
|
||||
finishing or hanging depending on what else held RAM, which rules them out as a
|
||||
default. All completed placements produced bit-identical images.
|
||||
|
||||
Fusing on the CPU instead of on CUDA rounds 788 of the UNet's 3256 fused tensors
|
||||
differently, by at most one fp16 ulp (1.2e-4). On the pass above that moved the
|
||||
output by a mean of 0.16 levels (PSNR 54.3 dB against a CUDA-fused, equally
|
||||
streamed run). Cards at or above the floor keep the CUDA fuse and therefore the
|
||||
previous pixels; only a streamed run, automatic or forced with `--cpu-offload`,
|
||||
sees the difference.
|
||||
|
||||
Regression coverage:
|
||||
|
||||
- [`test_cpu_offload.py`](../tests/test_cpu_offload.py)
|
||||
|
||||
+1
-1
@@ -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.45.0"
|
||||
version = "0.45.1"
|
||||
description = "AI watermark remover for visible, invisible, and provenance marks in images and video"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.11,<3.15"
|
||||
|
||||
@@ -35,7 +35,7 @@ _os.environ.setdefault("TRANSFORMERS_VERBOSITY", "error")
|
||||
_warnings.filterwarnings("ignore", message=r".*ImageProcessorFast.*")
|
||||
|
||||
|
||||
__version__ = "0.45.0"
|
||||
__version__ = "0.45.1"
|
||||
|
||||
__all__ = [
|
||||
"BatchItemResult",
|
||||
|
||||
@@ -47,6 +47,12 @@ _LATENT_GRID = 8
|
||||
# The reference graph loads the Qwen LoRA at 0.8; carrying that number to a different
|
||||
# LoRA on a different architecture would be imitation, not parity.
|
||||
SDXL_STEPS = 4
|
||||
# Below this floor the global stack streams its weights from CPU layer by layer; at or
|
||||
# above it the stack stays resident. The fp16 UNet, Canny ControlNet, both text
|
||||
# encoders and the VAE are ~8.8 GiB of weights, and the UNet and the ControlNet run
|
||||
# together on every step, so model-level offload cannot get under an 8 GiB card.
|
||||
# Benchmark in docs/module-internals.md, "CPU offload".
|
||||
RESIDENT_SDXL_GLOBAL_MIN_VRAM_GIB = 12.0
|
||||
|
||||
|
||||
def sdxl_target_size(width: int, height: int) -> tuple[int, int]:
|
||||
@@ -54,6 +60,17 @@ def sdxl_target_size(width: int, height: int) -> tuple[int, int]:
|
||||
return _target_size(width, height, _LATENT_GRID)
|
||||
|
||||
|
||||
def resolve_sdxl_global_residency(
|
||||
requested: bool | None,
|
||||
*,
|
||||
total_memory_gib: float,
|
||||
) -> bool:
|
||||
"""Keep the SDXL global stack resident when explicitly requested or safely sized."""
|
||||
if requested is not None:
|
||||
return requested
|
||||
return total_memory_gib >= RESIDENT_SDXL_GLOBAL_MIN_VRAM_GIB
|
||||
|
||||
|
||||
@dataclass
|
||||
class SdxlZImagePipeline(TwoStageZImagePipeline):
|
||||
"""Lazy runtime for the SDXL global stage plus the inherited face stage."""
|
||||
@@ -64,6 +81,12 @@ class SdxlZImagePipeline(TwoStageZImagePipeline):
|
||||
super().__post_init__()
|
||||
self._sdxl_pipe: Any = None
|
||||
|
||||
def _keep_global_models_resident(self) -> bool:
|
||||
return resolve_sdxl_global_residency(
|
||||
self.keep_global_models_on_device,
|
||||
total_memory_gib=self._total_vram_gib(),
|
||||
)
|
||||
|
||||
def _load_global(self) -> Any:
|
||||
if self._sdxl_pipe is not None:
|
||||
return self._sdxl_pipe
|
||||
@@ -89,11 +112,19 @@ class SdxlZImagePipeline(TwoStageZImagePipeline):
|
||||
variant="fp16",
|
||||
add_watermarker=False,
|
||||
**token,
|
||||
).to(self.device)
|
||||
)
|
||||
resident = self._keep_global_models_resident()
|
||||
if resident:
|
||||
pipe = pipe.to(self.device)
|
||||
pipe.load_lora_weights(hf_hub_download(SDXL_LIGHTNING_MODEL_ID, SDXL_LIGHTNING_PATTERN, **token))
|
||||
pipe.fuse_lora()
|
||||
# SDXL-Lightning is distilled against trailing timestep spacing.
|
||||
pipe.scheduler = EulerDiscreteScheduler.from_config(pipe.scheduler.config, timestep_spacing="trailing")
|
||||
if not resident:
|
||||
# After the fuse: the offload hooks keep their own CPU copy of each weight
|
||||
# and stream that copy, so fusing later would never reach what runs.
|
||||
self._progress("Streaming the SDXL stack from CPU (card below the residency floor)...")
|
||||
pipe.enable_sequential_cpu_offload(device=self.device, gpu_id=torch.cuda.current_device())
|
||||
self._sdxl_pipe = pipe
|
||||
return pipe
|
||||
|
||||
|
||||
@@ -63,13 +63,13 @@ PROFILE_ADAPTIVE_POLISH = {
|
||||
|
||||
# Which profiles let ``--cpu-offload`` reach their GLOBAL stack. The face stage
|
||||
# honours the flag everywhere, because residency lives on the shared base, but the
|
||||
# global stack is each profile's own: only qwen-zimage streams it, through
|
||||
# ``_qwen_vram_config``. chroma-zimage and sdxl-zimage load theirs with a plain
|
||||
# ``.to(device)``, so on a card below the face-stage residency floor the flag has
|
||||
# nothing left to change and the run behaves as if it were absent. Data rather than
|
||||
# a subclass attribute, because the caller that must warn cannot import a profile
|
||||
# module without pulling in Diffusers.
|
||||
GLOBAL_OFFLOAD_PROFILES = frozenset({QWEN_ZIMAGE_PROFILE})
|
||||
# global stack is each profile's own: qwen-zimage streams it through
|
||||
# ``_qwen_vram_config`` and sdxl-zimage through Diffusers sequential offload.
|
||||
# chroma-zimage loads its stack with a plain ``.to(device)``, so on a card below the
|
||||
# face-stage residency floor the flag has nothing left to change there and the run
|
||||
# behaves as if it were absent. Data rather than a subclass attribute, because the
|
||||
# caller that must warn cannot import a profile module without pulling in Diffusers.
|
||||
GLOBAL_OFFLOAD_PROFILES = frozenset({QWEN_ZIMAGE_PROFILE, SDXL_ZIMAGE_PROFILE})
|
||||
|
||||
SDXL_LIGHTNING_MODEL_ID = "ByteDance/SDXL-Lightning"
|
||||
SDXL_LIGHTNING_PATTERN = "sdxl_lightning_4step_lora.safetensors"
|
||||
|
||||
@@ -175,9 +175,9 @@ class WatermarkRemover:
|
||||
return
|
||||
logger.warning(
|
||||
"--cpu-offload does not reach the global stack of the '%s' pipeline: only "
|
||||
"qwen-zimage streams its global model between CUDA calls. The face stage "
|
||||
"still honours the flag, so below the face-stage residency floor this run "
|
||||
"behaves as if --cpu-offload were absent.",
|
||||
"qwen-zimage and sdxl-zimage stream their global model between CUDA calls. "
|
||||
"The face stage still honours the flag, so below the face-stage residency "
|
||||
"floor this run behaves as if --cpu-offload were absent.",
|
||||
self.model_profile,
|
||||
)
|
||||
|
||||
|
||||
@@ -308,8 +308,9 @@ _cpu_offload_option = click.option(
|
||||
"Offload model components to CPU between CUDA calls instead of keeping the "
|
||||
"whole pipeline in VRAM, at the cost of speed. Forces the face stack to "
|
||||
"offload instead of using automatic residency, on every profile. It reaches "
|
||||
"the global stack of qwen-zimage only: chroma-zimage and sdxl-zimage keep "
|
||||
"theirs resident either way."
|
||||
"the global stack of qwen-zimage and sdxl-zimage, which also stream theirs "
|
||||
"automatically on a card too small to hold it; chroma-zimage keeps its "
|
||||
"global stack resident either way."
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -172,7 +172,8 @@ class InvisibleEngine:
|
||||
cpu_offload: Offload model components to CPU between CUDA calls instead
|
||||
of keeping the whole pipeline in VRAM, at the cost of speed. Forces
|
||||
the face stack to offload instead of using automatic residency, on
|
||||
every profile; reaches the global stack of qwen-zimage only.
|
||||
every profile; reaches the global stack of qwen-zimage and
|
||||
sdxl-zimage, not chroma-zimage.
|
||||
CUDA only.
|
||||
"""
|
||||
|
||||
|
||||
@@ -55,6 +55,16 @@ def test_skill_frontmatter_matches_agent_skills_spec() -> None:
|
||||
assert "remove-ai-watermarks" in description
|
||||
|
||||
|
||||
def test_project_skills_stay_out_of_public_skill_discovery() -> None:
|
||||
# `npx skills add` and skills.sh discover every SKILL.md under .claude/skills;
|
||||
# metadata.internal hides a maintainer-only skill from that listing.
|
||||
project_skills = sorted((ROOT / ".claude" / "skills").glob("*/SKILL.md"))
|
||||
assert project_skills
|
||||
for path in project_skills:
|
||||
frontmatter = _frontmatter(path.read_text(encoding="utf-8"))
|
||||
assert re.search(r"(?m)^metadata:\n internal: true$", frontmatter), path
|
||||
|
||||
|
||||
def test_plugin_and_marketplace_names_agree() -> None:
|
||||
plugin = json.loads(PLUGIN.read_text(encoding="utf-8"))
|
||||
marketplace = json.loads(MARKETPLACE.read_text(encoding="utf-8"))
|
||||
|
||||
+135
-11
@@ -1,20 +1,27 @@
|
||||
"""Unit tests for how far --cpu-offload travels, per profile.
|
||||
|
||||
The flag sets two residency fields. The face one is read by the shared base and
|
||||
reaches every profile; the global one is read by ``QwenZImagePipeline`` alone, so
|
||||
on ``chroma-zimage`` and ``sdxl-zimage`` it never touches the stack the user is
|
||||
trying to fit. ``WatermarkRemover`` says so before the model load.
|
||||
reaches every profile; the global one is read by ``QwenZImagePipeline`` and
|
||||
``SdxlZImagePipeline``, so on ``chroma-zimage`` it never touches the stack the user
|
||||
is trying to fit. ``WatermarkRemover`` says so before the model load.
|
||||
|
||||
Both halves are exercised with an uninitialized remover, so the core CI matrix
|
||||
needs no diffusion dependency, model download, or GPU.
|
||||
needs no diffusion dependency, model download, or GPU. The SDXL loader placement is
|
||||
driven through stubbed Diffusers constructors and skips without the diffusion extra.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from remove_ai_watermarks._internal.sdxl_zimage_pipeline import (
|
||||
RESIDENT_SDXL_GLOBAL_MIN_VRAM_GIB,
|
||||
resolve_sdxl_global_residency,
|
||||
)
|
||||
from remove_ai_watermarks._internal.watermark_profiles import (
|
||||
AUTO_PROFILE,
|
||||
CHROMA_ZIMAGE_PROFILE,
|
||||
@@ -29,7 +36,7 @@ from remove_ai_watermarks._internal.watermark_remover import WatermarkRemover
|
||||
OFFLOAD_SUPPORT_CASES = (
|
||||
(QWEN_ZIMAGE_PROFILE, True),
|
||||
(CHROMA_ZIMAGE_PROFILE, False),
|
||||
(SDXL_ZIMAGE_PROFILE, False),
|
||||
(SDXL_ZIMAGE_PROFILE, True),
|
||||
# Auto preloads its concrete Qwen fallback before an image vendor is known.
|
||||
(AUTO_PROFILE, True),
|
||||
)
|
||||
@@ -69,18 +76,20 @@ class TestGlobalOffloadSupport:
|
||||
|
||||
|
||||
class TestUnsupportedProfileWarns:
|
||||
@pytest.mark.parametrize("profile", [CHROMA_ZIMAGE_PROFILE, SDXL_ZIMAGE_PROFILE])
|
||||
def test_it_names_the_profile_and_the_flag(self, profile: str, caplog: pytest.LogCaptureFixture) -> None:
|
||||
def test_it_names_the_profile_and_the_flag(self, caplog: pytest.LogCaptureFixture) -> None:
|
||||
with caplog.at_level(logging.WARNING):
|
||||
_remover(profile, cpu_offload=True)._warn_if_global_offload_unsupported()
|
||||
_remover(CHROMA_ZIMAGE_PROFILE, cpu_offload=True)._warn_if_global_offload_unsupported()
|
||||
assert len(caplog.records) == 1
|
||||
message = caplog.records[0].getMessage()
|
||||
assert "--cpu-offload" in message
|
||||
assert profile in message
|
||||
assert CHROMA_ZIMAGE_PROFILE in message
|
||||
|
||||
def test_qwen_zimage_is_quiet(self, caplog: pytest.LogCaptureFixture) -> None:
|
||||
@pytest.mark.parametrize("profile", [QWEN_ZIMAGE_PROFILE, SDXL_ZIMAGE_PROFILE])
|
||||
def test_a_profile_that_streams_its_global_stack_is_quiet(
|
||||
self, profile: str, caplog: pytest.LogCaptureFixture
|
||||
) -> None:
|
||||
with caplog.at_level(logging.WARNING):
|
||||
_remover(QWEN_ZIMAGE_PROFILE, cpu_offload=True)._warn_if_global_offload_unsupported()
|
||||
_remover(profile, cpu_offload=True)._warn_if_global_offload_unsupported()
|
||||
assert caplog.records == []
|
||||
|
||||
def test_a_run_that_did_not_ask_for_offload_is_quiet(self, caplog: pytest.LogCaptureFixture) -> None:
|
||||
@@ -132,3 +141,118 @@ class TestUnsupportedProfileWarns:
|
||||
with pytest.raises(ImportError):
|
||||
remover._load_qwen_zimage_pipeline()
|
||||
assert order == ["warned", "loaded"]
|
||||
|
||||
|
||||
class TestSdxlGlobalResidency:
|
||||
@pytest.mark.parametrize(
|
||||
("requested", "total_memory_gib", "expected"),
|
||||
[
|
||||
# Unset: the card decides, and an 8 GiB card cannot hold the stack.
|
||||
(None, 7.6, False),
|
||||
(None, RESIDENT_SDXL_GLOBAL_MIN_VRAM_GIB, True),
|
||||
(None, 23.5, True),
|
||||
# An unreadable card reports 0.0 and must stream rather than spill.
|
||||
(None, 0.0, False),
|
||||
# Explicit requests win in both directions.
|
||||
(False, 79.2, False),
|
||||
(True, 7.6, True),
|
||||
],
|
||||
)
|
||||
def test_request_wins_and_the_card_decides_otherwise(
|
||||
self, requested: bool | None, total_memory_gib: float, expected: bool
|
||||
) -> None:
|
||||
assert resolve_sdxl_global_residency(requested, total_memory_gib=total_memory_gib) is expected
|
||||
|
||||
|
||||
class _RecordingSdxlPipe:
|
||||
"""Stands in for the Diffusers SDXL pipeline and records placement calls in order."""
|
||||
|
||||
def __init__(self, calls: list[Any]) -> None:
|
||||
self.calls = calls
|
||||
self.scheduler = MagicMock()
|
||||
|
||||
def to(self, device: str) -> _RecordingSdxlPipe:
|
||||
self.calls.append(("to", device))
|
||||
return self
|
||||
|
||||
def load_lora_weights(self, _path: str) -> None:
|
||||
self.calls.append(("load_lora_weights",))
|
||||
|
||||
def fuse_lora(self) -> None:
|
||||
self.calls.append(("fuse_lora",))
|
||||
|
||||
def enable_sequential_cpu_offload(self, *, device: str, gpu_id: int | None = None) -> None:
|
||||
self.calls.append(("enable_sequential_cpu_offload", device, gpu_id))
|
||||
|
||||
|
||||
class TestSdxlGlobalPlacement:
|
||||
@staticmethod
|
||||
def _load(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
*,
|
||||
requested: bool | None,
|
||||
total_memory_gib: float,
|
||||
current_device: int = 0,
|
||||
) -> list[Any]:
|
||||
diffusers = pytest.importorskip("diffusers")
|
||||
torch = pytest.importorskip("torch")
|
||||
monkeypatch.setattr(torch.cuda, "current_device", lambda: current_device)
|
||||
from remove_ai_watermarks._internal.sdxl_zimage_pipeline import SdxlZImagePipeline
|
||||
|
||||
calls: list[Any] = []
|
||||
monkeypatch.setattr(diffusers.ControlNetModel, "from_pretrained", lambda *a, **k: MagicMock())
|
||||
monkeypatch.setattr(diffusers.AutoencoderKL, "from_pretrained", lambda *a, **k: MagicMock())
|
||||
monkeypatch.setattr(
|
||||
diffusers.StableDiffusionXLControlNetImg2ImgPipeline,
|
||||
"from_pretrained",
|
||||
lambda *a, **k: _RecordingSdxlPipe(calls),
|
||||
)
|
||||
monkeypatch.setattr("huggingface_hub.hf_hub_download", lambda *a, **k: "lora.safetensors")
|
||||
monkeypatch.setattr(diffusers.EulerDiscreteScheduler, "from_config", lambda *a, **k: MagicMock())
|
||||
|
||||
pipeline = SdxlZImagePipeline(device="cuda", torch_dtype=None, keep_global_models_on_device=requested)
|
||||
monkeypatch.setattr(type(pipeline), "_require_cuda", lambda self: None)
|
||||
monkeypatch.setattr(type(pipeline), "_total_vram_gib", lambda self: total_memory_gib)
|
||||
pipeline._load_global()
|
||||
return calls
|
||||
|
||||
def test_a_small_card_streams_the_fused_stack_instead_of_moving_it(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
calls = self._load(monkeypatch, requested=None, total_memory_gib=7.6)
|
||||
# The offload hooks stream their own copy of each weight, so they must follow the fuse.
|
||||
assert calls == [("load_lora_weights",), ("fuse_lora",), ("enable_sequential_cpu_offload", "cuda", 0)]
|
||||
|
||||
def test_a_large_card_keeps_the_stack_resident_as_before(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
calls = self._load(monkeypatch, requested=None, total_memory_gib=23.5)
|
||||
assert calls == [("to", "cuda"), ("load_lora_weights",), ("fuse_lora",)]
|
||||
|
||||
def test_cpu_offload_streams_even_on_a_large_card(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
calls = self._load(monkeypatch, requested=False, total_memory_gib=79.2)
|
||||
assert ("to", "cuda") not in calls
|
||||
assert calls[-1] == ("enable_sequential_cpu_offload", "cuda", 0)
|
||||
|
||||
@pytest.mark.parametrize("current_device", [1, 2])
|
||||
@pytest.mark.parametrize(("requested", "total_memory_gib"), [(None, 7.6), (False, 79.2)])
|
||||
def test_streaming_uses_the_current_cuda_device(
|
||||
self, monkeypatch: pytest.MonkeyPatch, current_device: int, requested: bool | None, total_memory_gib: float
|
||||
) -> None:
|
||||
calls = self._load(
|
||||
monkeypatch,
|
||||
requested=requested,
|
||||
total_memory_gib=total_memory_gib,
|
||||
current_device=current_device,
|
||||
)
|
||||
assert ("to", "cuda") not in calls
|
||||
assert calls[-1] == ("enable_sequential_cpu_offload", "cuda", current_device)
|
||||
|
||||
def test_cpu_offload_reaches_the_sdxl_stack(self, monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
from remove_ai_watermarks._internal import sdxl_zimage_pipeline
|
||||
|
||||
captured: dict[str, object] = {}
|
||||
|
||||
class Recorder:
|
||||
def __init__(self, **kwargs: object) -> None:
|
||||
captured.update(kwargs)
|
||||
|
||||
monkeypatch.setattr(sdxl_zimage_pipeline, "SdxlZImagePipeline", Recorder)
|
||||
_remover(SDXL_ZIMAGE_PROFILE, cpu_offload=True)._load_qwen_zimage_pipeline()
|
||||
assert captured["keep_global_models_on_device"] is False
|
||||
@@ -192,7 +192,7 @@ class TestNoReembeddedWatermark:
|
||||
# from_config would otherwise resolve the mock's config as a repo id.
|
||||
monkeypatch.setattr(diffusers.EulerDiscreteScheduler, "from_config", lambda *a, **k: MagicMock())
|
||||
|
||||
pipeline = SdxlZImagePipeline(device="cuda", torch_dtype=None)
|
||||
pipeline = SdxlZImagePipeline(device="cuda", torch_dtype=None, keep_global_models_on_device=True)
|
||||
monkeypatch.setattr(type(pipeline), "_require_cuda", lambda self: None)
|
||||
pipeline._load_global()
|
||||
|
||||
|
||||
@@ -2098,7 +2098,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "openai"
|
||||
version = "3.22.1"
|
||||
version = "3.24.0"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
dependencies = [
|
||||
{ name = "anyio" },
|
||||
@@ -2108,9 +2108,9 @@ dependencies = [
|
||||
{ name = "sniffio" },
|
||||
{ name = "typing-extensions" },
|
||||
]
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/e7/db/b135830095e2ae120ea04b0a53d3a1644eb65554385a8ef4c9cb4de3f5fd/openai-3.22.1.tar.gz", hash = "sha256:2575f961e4c1df8761f8eeb4d78e967de3f9b9f91b22f7120d60e393e970094d", size = 1751626, upload-time = "2026-09-30T00:25:55.548Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/73/4f/e57670227cb7b61362d8f9bfba54d4e9f7bde799798c342782788bc12d6c/openai-3.24.0.tar.gz", hash = "sha256:1e7463f7d78773ab2ce4fe85710481aa5bd5ffefd54c8de4b067506cd2d42895", size = 1797218, upload-time = "2026-10-02T15:55:18.851Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/32/74/e3b77d1157c8b9ad815a87d5719545097e5edb06156869c1657fbb6b797b/openai-3.22.1-py3-none-any.whl", hash = "sha256:43ff3de5457a8beb4afa177d4e0ba6841cfadd5219ebc62297fde6c8a9d401a6", size = 2090965, upload-time = "2026-09-30T00:25:53.524Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/f2/01/32baec3bdb83d36da27281d7eeac2647241345cf8ff538bc21d1013ddeec/openai-3.24.0-py3-none-any.whl", hash = "sha256:b59703a3e3b5df78b269e90049d383a1dda58c90104cc4be59db21078db4b7a4", size = 2127166, upload-time = "2026-10-02T15:55:16.228Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3065,11 +3065,11 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "python-dotenv"
|
||||
version = "1.2.3"
|
||||
version = "1.2.4"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/6a/53/ed9d74092561d4b01a2ef1349d52cdbc135e526c245f366b089cfca6de49/python_dotenv-1.2.3.tar.gz", hash = "sha256:a20a594dabeaa385725aa239d5244871c143ecb356add8a20fcf23773a6c3a35", size = 58945, upload-time = "2026-08-16T16:54:54.067Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/74/26/2fbeedb218a787a5eea551c7532cac4e009f83d689dd2faa0d0353473f86/python_dotenv-1.2.4.tar.gz", hash = "sha256:f0d53e69935a851c0dcc78f3ab7aaccd8cabef0b92382b576b824212902873c0", size = 60824, upload-time = "2026-10-01T05:36:10Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/17/c5c6b53ddc18f297992099b3d9ec16c855c0ccc83263a21fe4d1c625ec6c/python_dotenv-1.2.3-py3-none-any.whl", hash = "sha256:904552145e8bfed22162c09dab1c2b9b54fefa7b23ba780f4f26ca0316b0f0d9", size = 22780, upload-time = "2026-08-16T16:54:52.473Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/60/d1/38f3a3405989a89ac18390803e70c6ad7c7760da4f9b83cbeca0c44a0c72/python_dotenv-1.2.4-py3-none-any.whl", hash = "sha256:42269a8a5b3fd54ffa6f3d84b18abed50064717576b4ecf03dc4a55d8aa04fdc", size = 23266, upload-time = "2026-10-01T05:36:08.633Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3346,7 +3346,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "remove-ai-watermarks"
|
||||
version = "0.45.0"
|
||||
version = "0.45.1"
|
||||
source = { editable = "." }
|
||||
dependencies = [
|
||||
{ name = "c2pa-python" },
|
||||
@@ -3603,27 +3603,27 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "ruff"
|
||||
version = "0.16.9"
|
||||
version = "0.16.10"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/96/bf/c935ca98e73fe8ce65b87ef08a280c0c1e85295d569228c15e87d8fdfaf1/ruff-0.16.9.tar.gz", hash = "sha256:12b625c6cfba78d285d9f48eda5f053374f1e53cb10ef17342a383750db99161", size = 4948764, upload-time = "2026-09-24T20:37:49.416Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/c4/49/23802c45f093eb14bde54b141d2b2f058edfa63a06db7beed047308cc08f/ruff-0.16.10.tar.gz", hash = "sha256:eff4728c4eaae93f0955cd264d24b2ab348e74bf59986ccf282ba6dc16b3b017", size = 4958724, upload-time = "2026-10-01T18:03:21.697Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0d/26/df51322b52ee1ada7eff2d071ea09d11d5c2d1dcc9f02594f5785c4d1635/ruff-0.16.9-py3-none-linux_armv6l.whl", hash = "sha256:95e6f022090368ab3b824c36276839c53b2adf1a3f4c09fefc33dfc400f6da96", size = 10082922, upload-time = "2026-09-24T20:37:13.045Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/a5/27/7bf51f5a7aa375e9f339280a303aab44ca75dc1525f1cdc5991761685b0f/ruff-0.16.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:a5f27be168556594a86d2f415db0cf43f5291917849318f873c7e2791f7a8c67", size = 10236360, upload-time = "2026-09-24T20:37:16.049Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/b6/63/09659283f92f02dff45809da194a70da2f688d87c55d8875c4fae3536072/ruff-0.16.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:1632eb1d6197f33bd00b1acbc5b71009e89a8895c158e2d2b03a834fac964ab6", size = 9892940, upload-time = "2026-09-24T20:37:17.957Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/24/58/98de1b72ec172f5f8f1731236fe21585b3998bf7dfe9fcc44ae9ba626012/ruff-0.16.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b3f951b14d865d5952c89d40a5ca07e87abe24fa5453299878411e127748fb1c", size = 10032114, upload-time = "2026-09-24T20:37:19.942Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/7d/f1e17c54ab59d4bad1dce8ee3e22a7a1d0ef4745240decacdcf3832b5bb2/ruff-0.16.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:447fc07e1573afff7cb02803462b12b6c8ece7cf10e2cd78565fa6d7a1c0bf8d", size = 9910227, upload-time = "2026-09-24T20:37:21.872Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/34/19/436f647a65075bbd3bab2668b3bdaa5120559b294694018cdcefabbbf30b/ruff-0.16.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8a3e039a6a40ed976c491722b60e0ae4a4aa1a86057f540ee7a37a5d19ae9120", size = 10547484, upload-time = "2026-09-24T20:37:24.229Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/03/59/38430a6bc2f6d8095447ac39625cf8b6e9344a47e6c26225c8ba1bff3ffb/ruff-0.16.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4684dded7db60aa57cb118fa158630f5feade4af5782903b6053484bdf9bd129", size = 11412367, upload-time = "2026-09-24T20:37:26.307Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c8/bd/bbb6d7fc7f208c8b8c50dd5dc8206e4cfdb1e7a8fb852606a3adf370c880/ruff-0.16.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d29c934357e45642fda2f34c0b1f4025b4a6c01e15e4bf0016879d60078a142c", size = 10869787, upload-time = "2026-09-24T20:37:28.35Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/bc/b8/9c543074918061abbefc3bd139bee22de35abedb00dde0f2d27288838962/ruff-0.16.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a21713e629d3e5bdb2f5c2def1cc7f04f47fa8e1a7eb0571b4a28e1da64bc728", size = 10406494, upload-time = "2026-09-24T20:37:30.624Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/35/7a/5a8851bd146e7ccf8fd4b003f6c75c11f8fbdb0e60673b45097986b6bf41/ruff-0.16.9-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:7baa24ef5fc8e77aa93879e1d3f43754a01ae488e869f1ae30cf431afd4d2452", size = 10590083, upload-time = "2026-09-24T20:37:32.439Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/87/f0/4c3467188f23f806960b46fa76575a7cd0514c9ba90562650b71efc96980/ruff-0.16.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:a41aac6230aadfaa133bdfa1614488531ffa3e0837567ae04c0da2058a9c0f9e", size = 10119151, upload-time = "2026-09-24T20:37:34.581Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/15/34/5a4def5adea572ce6aea0bb64f21f928ee317b80e0db747d7979b01d7261/ruff-0.16.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:c2529fb5896d49115b0e9aa8f887490b34bbe76baf879ec2264ac59406869ce7", size = 9911544, upload-time = "2026-09-24T20:37:36.796Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/62/5d/d15ebea7499eef9373318c0ee6ca127832927c6529731f6d48e18dce7ca9/ruff-0.16.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:41e3870277694177429b56406d65dfbdb2c2802c52b715edaf6a0b829c69d4ee", size = 10269884, upload-time = "2026-09-24T20:37:38.857Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d1/56/c5d3cd119ded7a3c7aba0e961b69cb3df701c91662c98ad694467d060ce1/ruff-0.16.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:8adbe4e58af167f767d7b2ba5e83c42e878350796cf78c2f5e14ab9903a92588", size = 10749366, upload-time = "2026-09-24T20:37:41.042Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ac/fe/734ec7527029ac757ecf821f143c9f3fcf69149c21f53a044a899b430f5a/ruff-0.16.9-py3-none-win32.whl", hash = "sha256:0e1dbc2073624dee6618d41d0098690a7244654af746704b64759e12b6b6b385", size = 10152355, upload-time = "2026-09-24T20:37:43.025Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/14/21/26e4643629b3ebb44f0a06f9c9a53058d63d989415f63a9a3c28e2ee7f22/ruff-0.16.9-py3-none-win_amd64.whl", hash = "sha256:6bd40fec8cd4c8a3d4dd589bd8ad4e6320c13c29234159bfd959a40d529d597b", size = 10592965, upload-time = "2026-09-24T20:37:44.944Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/51/60/5fb1a39dbb5ae314d5f59bc7348a63c1d5c20f3cd83914c4b5cb0be31d2d/ruff-0.16.9-py3-none-win_arm64.whl", hash = "sha256:ed1a252039200f57a59eebc063b54beabea67bfbaaca0eeaa7f54b5fbcda2284", size = 10458649, upload-time = "2026-09-24T20:37:46.882Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2f/21/ebce22e1d90cdb2cd691026b9c6e9bec6499f0b396089481755b5d49efff/ruff-0.16.10-py3-none-linux_armv6l.whl", hash = "sha256:488b0fe3f3574210e5cf80d9f59b9e3ab17a127a8155de3f307b392589cfb511", size = 10095557, upload-time = "2026-10-01T18:02:36.072Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/cb/98/a54de85876a8b2612bfa0d84c7b9abfb39c6a3354aee7800b09c1649b9e3/ruff-0.16.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:e748ff95c934c4e978783b8e687bc174e7bd84e8ad24e3243e1ecfcda5e0282d", size = 10340577, upload-time = "2026-10-01T18:02:39.258Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/9f/16/1a5a4a2657effe4806110f2b907313802f1367fcdbb29e8122766e407fab/ruff-0.16.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3031a4a2e8e7b8a46f70be45f198c35a11ece509a94b80334d8d397a33c67550", size = 9774282, upload-time = "2026-10-01T18:02:41.79Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6e/fb/470085af734da396e68cd80fb0a3e7c109a459716ae59588c0f6fab8a17d/ruff-0.16.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:494401c86df4c4c25f69b9419605d944467ee98c42fb6ad405ef4fa40b8fb67d", size = 9920895, upload-time = "2026-10-01T18:02:44.458Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/57/de/f10cffe4f88a37ea6615ec460f01bf76f0bc1477737a35d9ee61a630a78b/ruff-0.16.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d203abc0ff2b773ee33d00ab8df0bb67046fbc7c07b119332f08b9b345cf8221", size = 9902707, upload-time = "2026-10-01T18:02:47.041Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/ff/44/3fdcedf83ae60ef837dd239170e480dce606a9142cfa2db911afdf855fd9/ruff-0.16.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bd83d1235a5258d318477bc5b576303974cbdef5df0c01a1bff14efcc23bd12a", size = 10619287, upload-time = "2026-10-01T18:02:49.485Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c1/62/02e76a5574002153618eafbb70e159468addc72a5d2c488e65cbb4639d2d/ruff-0.16.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bc2610fb269fa56dd8a68669ae470fa6272902668c0fc2ebc3aa112b2633d5b8", size = 11339942, upload-time = "2026-10-01T18:02:52.008Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/6b/c4/cde27d47ad8d4126c587e608c67c46e7feac11763f606d261a1bc8a489e6/ruff-0.16.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3e70175e29cc94c26ea296c80e470180b744b7419026898e58f520c6ab32578e", size = 10934316, upload-time = "2026-10-01T18:02:54.811Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/e4/03/17234145f302a645a123e8c3bb2411ecf4669fbc350de3b0d803230a1729/ruff-0.16.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f33f43a864a8483eebd160e713336c8bab02c934feaff0a33cf5ccb41546d09a", size = 10387968, upload-time = "2026-10-01T18:02:57.497Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/71/29/2493af60240ee7644b38a4b821f5f9c3b5a4fa3178770fe1d0c685217395/ruff-0.16.10-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:1dfc6f0088149fb6a362c1c446bcbb3fd2157b3852fe2fa68409276eab9ad9b3", size = 10537906, upload-time = "2026-10-01T18:03:00.006Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/2e/9a/f56b28f3b143bb9e518c34fb89ab191b626bca8b70dbf8af0d2e2d473572/ruff-0.16.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:6553498afc35f580f036030795810b9e6bcea31604b0fd9e8d352795473042e3", size = 10012938, upload-time = "2026-10-01T18:03:03.006Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/c2/c4/fca37362848ea4d4d80712df13632e645e7c7cfb1bdedf140699ac7a090b/ruff-0.16.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:a3b8471dea115d37f123882be852bed13403746d3a76c11de4a19ec5f9ff5a03", size = 9897945, upload-time = "2026-10-01T18:03:05.818Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/7d/c7/e0bc57664d6e0c61fd22f620af260fe9662d7e1ddb320ea7f160e76ef165/ruff-0.16.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:92e59a70bcbd9d3a5483656da906ec28edfdacfce00afd99edb8b4e9d15644be", size = 10333134, upload-time = "2026-10-01T18:03:08.25Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/21/aa/5c9f3b68737c0e4a33a1db5dfab44f7b786d96ca91a7233112ddab1e6dc2/ruff-0.16.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7ae7375f803b5520dc9f546bed7e3a0acb70b91812e9bb4b19927de22f25b77d", size = 10741702, upload-time = "2026-10-01T18:03:10.775Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/78/fa/0f9c2020dc316c53d983be011720b8157cc052b97e3f4dfa7db6d0f880a6/ruff-0.16.10-py3-none-win32.whl", hash = "sha256:2a12e01cb9156c10c466f63b46eaae5ecea28dfbd21b5836353ae498e7d1349a", size = 10139176, upload-time = "2026-10-01T18:03:13.267Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/99/29/cfb0df9448d4d4ad48c2de029ada9ebd71baa6da983a6c77ee6c6cd0fe82/ruff-0.16.10-py3-none-win_amd64.whl", hash = "sha256:97f2015c92aa97105b0eab19eb5d224884399281cfc5da86a92db4ab5e7fb2ca", size = 10584734, upload-time = "2026-10-01T18:03:16.006Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/fc/05/c16957eb287c3fc062e032619a25868d93d408a844b725bcf514f7a378ff/ruff-0.16.10-py3-none-win_arm64.whl", hash = "sha256:25a65fe998c4e6861ec079ada5826a2fc605e6cbccbe9dcd7fac1f54e791621b", size = 10366440, upload-time = "2026-10-01T18:03:19.04Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
Reference in new issue
Block a user