From 20d7eda96ac5bde8b775d39b0794bc4277262786 Mon Sep 17 00:00:00 2001 From: Victor Kuznetsov Date: Mon, 8 Jun 2026 21:21:58 -0700 Subject: [PATCH] remove: drop all face-restore code (regeneration, not preservation) Empirical conclusion from the 2026-06-04 - 2026-06-08 Modal cert sweeps: every face-restore approach we built (GFPGAN-on-cleaned, PhotoMaker-V2, InstantID txt2img, InstantID img2img-on-cleaned at three parameter settings) regenerates the face via SDXL diffusion rather than preserves it. Output face pixels are diffusion-fresh, so the regenerated face inherits SDXL "clean skin" aesthetic and loses original identity precision -- it looks MORE AI-generated than the cleaned image, not less. The cleaned image from the main controlnet 0.20 removal pass is the least-AI face state we can reach without re-introducing SynthID. Nothing in the restore family achieves the actual goal (preserve the original person's face). Keeping them around as opt-in invites users to ship something that defeats the point. Removing entirely. Library changes: - Deleted src/remove_ai_watermarks/instantid_restore.py - Deleted src/remove_ai_watermarks/photomaker_restore.py - Deleted tests/test_instantid_restore.py - Deleted tests/test_photomaker_restore.py - Removed `instantid` and `photomaker` extras from pyproject.toml - Removed `[tool.hatch.metadata] allow-direct-references = true` (was only needed for the photomaker git+ URL) - InvisibleEngine.remove_watermark: dropped `restore_faces` + `restore_faces_method` params, removed both `_restore_faces_instantid` and `_restore_faces_photomaker` private methods, removed dispatch - CLI: dropped `_restore_faces_options` decorator, all four cmd_* signatures lose `restore_faces` + `restore_faces_method`, kwarg passes to remove_watermark dropped - _apply_auto: dropped `restore_faces` from tuple shape (was unused after the engine no longer takes it) - auto_config.AutoConfig: dropped `restore_faces` field; `plan()` no longer sets it; `reason` no longer mentions it - Tests updated accordingly (test_auto_config.TestReason no longer asserts "face-restore on" in the reason string) Docs updated: - CLAUDE.md: removed the photomaker extras bullet, the Face restore trade-off bullet, the instantid_restore.py + photomaker_restore.py module bullets; replaced restore mentions in watermark_remover and controlnet bullets and prod recipe with the empirical conclusion - README.md: removed both `--restore-faces` callouts and the install snippet; the feature bullet and auto-mode comment updated - docs/synthid-robust-identity-research.md: added Status-retired notice at the top pointing at the 2026-06-08 followup raiw-app: - modal_cert.py: dropped `--restore-faces` flag entirely; sweep() no longer takes restore_faces; pinned _LIB_SPEC to `[gpu]` extras (no `photomaker` / `instantid` extras), points at main ruff + strict pyright clean; 569 tests pass; 18 restore-specific tests gone. Co-Authored-By: Claude Opus 4.8 (1M context) --- CLAUDE.md | 14 +- README.md | 25 +- docs/synthid-robust-identity-research.md | 7 + pyproject.toml | 52 -- src/remove_ai_watermarks/auto_config.py | 8 +- src/remove_ai_watermarks/cli.py | 75 +-- src/remove_ai_watermarks/instantid_restore.py | 593 ------------------ src/remove_ai_watermarks/invisible_engine.py | 124 +--- .../photomaker_restore.py | 368 ----------- tests/test_auto_config.py | 6 +- tests/test_cli.py | 1 - tests/test_instantid_restore.py | 152 ----- tests/test_photomaker_restore.py | 151 ----- uv.lock | 471 +------------- 14 files changed, 33 insertions(+), 2014 deletions(-) delete mode 100644 src/remove_ai_watermarks/instantid_restore.py delete mode 100644 src/remove_ai_watermarks/photomaker_restore.py delete mode 100644 tests/test_instantid_restore.py delete mode 100644 tests/test_photomaker_restore.py diff --git a/CLAUDE.md b/CLAUDE.md index a10bc63..a9be3f5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -10,7 +10,7 @@ You are a **principal Python engineer** maintaining a CLI tool and library for r - `uv run remove-ai-watermarks identify ` — provenance verdict (platform + watermark inventory + confidence); `--json` for machine output, `--no-visible` to skip the cv2 sparkle detector - `uv run remove-ai-watermarks metadata --check` — inspect AI metadata (C2PA, EXIF, PNG chunks) - `uv run remove-ai-watermarks metadata --remove -o ` — strip all AI metadata -- `uv run remove-ai-watermarks batch ` — process every supported image in a directory (output defaults to `_clean/`, set with `-o`). `--mode visible|invisible|metadata|all` (default `visible`); the invisible/all path reuses the same `--strength`/`--steps`/`--pipeline`/`--controlnet-scale`/`--device`/`--max-resolution`/`--min-resolution`/`--upscaler`/`--seed`/`--hf-token` knobs as `invisible`, `--inpaint/--no-inpaint` for the visible pass, `--humanize` for the Analog Humanizer + `--unsharp` for the final sharpening post-filter, `--restore-faces/--no-restore-faces` + `--restore-faces-method [instantid|photomaker]` for the face REGENERATION post-pass (**NON-COMMERCIAL**, `instantid` default or `photomaker` extra; **the methods REGENERATE the face from an ArcFace embedding via SDXL diffusion, they do NOT recover original pixels — every output face pixel is diffusion-fresh, so the regenerated face inherently looks more AI-generated than the cleaned image; for production face preservation use the cleaned image as-is and leave restore OFF**), and `--auto` (+ `--adaptive-polish/--no-adaptive-polish`) for the content-adaptive quality mode (re-planned per image; one engine cached per resolved pipeline) +- `uv run remove-ai-watermarks batch ` — process every supported image in a directory (output defaults to `_clean/`, set with `-o`). `--mode visible|invisible|metadata|all` (default `visible`); the invisible/all path reuses the same `--strength`/`--steps`/`--pipeline`/`--controlnet-scale`/`--device`/`--max-resolution`/`--min-resolution`/`--upscaler`/`--seed`/`--hf-token` knobs as `invisible`, `--inpaint/--no-inpaint` for the visible pass, `--humanize` for the Analog Humanizer + `--unsharp` for the final sharpening post-filter, and `--auto` (+ `--adaptive-polish/--no-adaptive-polish`) for the content-adaptive quality mode (re-planned per image; one engine cached per resolved pipeline) ## Test and lint @@ -27,7 +27,6 @@ You are a **principal Python engineer** maintaining a CLI tool and library for r - GPU/ML modules (invisible_engine, watermark_remover) are optional — guard imports with `is_available()` checks - Optional detection extras: `detect` (imwatermark — open SD/SDXL/FLUX watermark) and `trustmark` (Adobe TrustMark decoder; pulls torch + downloads weights). Both are guarded by `is_available()` and skipped by `identify` when absent. -- **NON-COMMERCIAL** Optional `photomaker` extra (`photomaker` upstream + huggingface-hub + einops + insightface + onnxruntime + peft): the PhotoMaker-V2 face-identity post-pass (`photomaker_restore.py`, CLI `--restore-faces`, **EXPERIMENTAL, opt-in, OFF by default**). Guarded by `photomaker_restore.is_available()`; auto-skips when the extra is absent or no face is detected. **NON-COMMERCIAL because PhotoMaker-V2's ID encoder pulls InsightFace antelopev2/buffalo_l model packs at runtime, which are research-only.** A paid service (raiw.cc, any monetized SaaS) MUST NOT install this extra. The non-commercial restriction is on the InsightFace MODEL packs (downloaded on first `FaceAnalysis()`); the PyPI `insightface` package itself is MIT-licensed CODE which is why we declare it as a dependency at all (PhotoMaker's `__init__.py` imports `FaceAnalysis2` unconditionally, so the V2 pipeline class can't even import without it). The PhotoMaker adapter weights (`photomaker-v2.bin`) are Apache-2.0 and download on first use; never bundled. The `photomaker` extra requires `[tool.hatch.metadata] allow-direct-references = true` because the upstream PhotoMaker package is git-only (not on PyPI). Pins beyond the upstream package itself patch missing declarations that would otherwise break the load chain (einops, peft, onnxruntime, insightface — verified empirically via the Modal cert sweep 2026-06-04). The previous commercial-safe alternatives (GFPGAN-on-cleaned, PhotoMaker-V1) were tried and dropped: GFPGAN polished but did not recover identity, V1 hit a diffusers-version-compat wall (CFG batch-dim mismatch in the upstream pipeline forked from diffusers 0.29). See `docs/synthid-robust-identity-research.md`. Kept OUT of `all` (heavy + model download + non-commercial). - Optional `esrgan` extra (spandrel only): Real-ESRGAN pre-diffusion super-resolution for small inputs (`upscaler.py`, CLI `--upscaler esrgan` on `invisible`/`all`/`batch`). Guarded by `upscaler.is_available()`; the default upscaler stays Lanczos (cv2, no deps) and the engine falls back to Lanczos when the extra is absent or the model errors. spandrel is MIT and pulls NO basicsr (only torch/torchvision/safetensors/numpy/einops); Real-ESRGAN weights are BSD-3-Clause and download on first use via `torch.hub` (never bundled). Kept OUT of `all` (heavy + model download). - Tests for the *model-running* paths are limited to availability checks (multi-GB downloads). But the **pure helpers inside ML-adjacent modules are unit-tested without any download** and must stay that way: `_target_size` (native-vs-downscale-cap-vs-upscale-floor, `test_invisible_engine.py`), `humanizer.unsharp_mask`/`adaptive_polish` (`test_humanizer.py`), `auto_config.plan`/detectors (`test_auto_config.py`), and the MPS->CPU fallback control flow via mocked pipelines (`test_img2img_runner.py`, 100% cover). Don't skip these as "ML, needs a model" — only `remove_watermark`/the diffusion bodies do. @@ -45,12 +44,9 @@ You are a **principal Python engineer** maintaining a CLI tool and library for r - `region_eraser.py` — universal region eraser (`erase` CLI). `erase(image, boxes=|mask=, backend=)` accepts grayscale (2D) and RGBA (4-channel) inputs on **both** backends (`erase_cv2` and `erase_lama` each split off any alpha plane and re-attach it unchanged, and promote grayscale to BGR for processing — LaMa would otherwise crash on grayscale and drop alpha on BGRA): `boxes_to_mask` → `cv2.inpaint` (`cv2` backend, default, no deps) or big-LaMa via onnxruntime (`lama` backend, extra `lama`, `Carve/LaMa-ONNX` Apache-2.0 model downloaded on first use, never bundled). `erase_lama` crops a padded region around the mask, runs LaMa at its fixed 512² input, pastes only masked pixels back (untouched areas stay pixel-exact). Lazy `_get_lama_session` singleton; `lama_available()` guards the optional import. **LaMa-ONNX costs ~3.5-4 GB peak RAM and ~5-6 s/call on CPU** (FFC working set, not arena — `enable_cpu_mem_arena=False` does not help), so it does NOT fit a minimal droplet; the cv2 backend (tens of MB, ~30 ms) does. LaMa quality at low RAM = serverless/GPU, mirroring how raiw.cc offloads SDXL to fal. - `invisible_watermark.py` — `detect_invisible_watermark(path)` decodes the OPEN DWT-DCT watermarks (public decoder, no key) embedded by Stable Diffusion / SDXL / FLUX via the `imwatermark` library. Known fixed patterns (verified against upstream source) live in `_BITS_48` (SDXL 48-bit, FLUX.2 48-bit) and `_SD1_STRING` ("StableDiffusionV1", SD 1.x/2.x). Optional dep (extra `detect`); returns None when absent. The `detect` extra pulls **torch** transitively (invisible-watermark declares torch a hard dep, and `WatermarkDecoder` eagerly imports `rivaGan` -> `torch` at import time), so detection needs torch present even though dwtDct runs CPU-only on cv2/numpy/pywavelets — no GPU and no separate `gpu` extra required. **Unlike SynthID this is locally detectable**, but the watermark is fragile (does not survive JPEG re-encode/resize — verified gone after JPEG q90), so it confirms origin only on pristine files. Add new known patterns here. The file carries a top-of-module pyright pragma because imwatermark/cv2 ship no type stubs. - `trustmark_detector.py` — `detect_trustmark(path)` decodes the OPEN, keyless **Adobe TrustMark** watermark (the soft binding behind Adobe Durable Content Credentials, `alg` `com.adobe.trustmark.P`) via the optional `trustmark` package (extra `trustmark`; pulls torch, downloads model weights on first use). Mirrors `invisible_watermark.py` (lazy singleton guarded by a double-checked `threading.Lock` so concurrent callers do not double-download the weights, top-of-module pyright pragma, returns None when absent). It detects *provenance*, not AI origin as such (TrustMark also marks human-authored content), so `identify` lists it as a watermark without setting `is_ai_generated`. Other soft-binding vendors (Digimarc/Imatag/Steg.AI/...) have no public decoder — they are only *named* via the `C2PA_SOFT_BINDINGS` scan, not decoded. **False-positive gate (added 2026-05-29):** TrustMark's `wm_present` is a BCH error-correction validity flag that spuriously validates on a content-correlated fraction of un-watermarked images — AI-generated textures trip it far more than camera photos (verified 2026-05-29 on real files: it fires on Gemini/OpenAI/Doubao output that *cannot* carry Adobe's watermark, with a random-bytes decoded secret, while signal-free camera photos did not trip it). A genuine TrustMark is a *durable* soft binding engineered to survive re-encoding, so `detect_trustmark` re-decodes after a mild JPEG round-trip (`_survives_reencode`, `_REENCODE_QUALITY` 95) and requires the same schema both times; every observed false positive collapsed (none survived even q95), so the gate is the durability property the watermark guarantees. The second decode runs only on the rare initial hit, so the cost is negligible. Do NOT remove the gate to "catch more" — a lone TrustMark hit without it is almost always content noise. -- `noai/watermark_remover.py` — the `WatermarkRemover` class has two diffusion pipelines, selected by the explicit `pipeline` ctor arg (NOT inferred from `model_id` -- both use the same SDXL base, `DEFAULT_MODEL_ID`). **`default`** runs plain SDXL img2img (`_run_img2img`). **`controlnet`** (**EXPERIMENTAL, opt-in**; `_run_controlnet`, `_load_controlnet_pipeline`) runs `StableDiffusionXLControlNetImg2ImgPipeline` with the SDXL-native canny ControlNet `xinsir/controlnet-canny-sdxl-1.0` (`watermark_profiles.CONTROLNET_CANNY_MODEL`): the control image is `cv2.Canny(gray, 100, 200)` stacked to 3 channels (`_CANNY_LOW`/`_CANNY_HIGH`, prompt `_CONTROLNET_PROMPT` / `_CONTROLNET_NEGATIVE`). **Removal comes from the img2img regeneration (`strength`); the ControlNet only PRESERVES text and face STRUCTURE via the edge map.** No original pixels are copied or frozen, BUT **validation 2026-06-04 disproved the old "so SynthID does not survive" claim: SynthID CAN survive controlnet on photoreal/high-detail content.** At the shared low removal strength the canny edge-conditioning keeps the regeneration so close to the original that the pixel perturbation that destroys SynthID does not happen (oracle-confirmed: an OpenAI bracelet photo + a 9-face grid read **SynthID-detected** after controlnet at strength 0.10/0.15, but **SynthID-not-detected** after the `default` pipeline at the SAME strength + resolution -- only the pipeline differed). **But the reverse also holds: a flat-graphic logo/poster SURVIVED `default` while clearing controlnet** -- removal at the low strength is content×pipeline dependent and neither pipeline is universally safe; the real lever is a higher strength. See the controlnet Known-limitations bullet for the full table + root cause. Canny holds face STRUCTURE but NOT identity (the regenerated face drifts in likeness -- canny carries edges, not identity; face identity is regenerated by the optional `--restore-faces` PhotoMaker-V2 post-pass (EXPERIMENTAL, opt-in, OFF by default, **NON-COMMERCIAL** — needs the `photomaker` extra which pulls non-commercial InsightFace model packs) -- see `photomaker_restore.py`). `controlnet_conditioning_scale` (ctor arg, default 1.0) is the structure-preservation knob. Same dtype rule as `default` (fp32 on cpu/mps, fp16 only on cuda/xpu; the fp16-fixed SDXL VAE `_SDXL_FP16_VAE_ID` is swapped in on fp16 GPUs -- issue #29) and the same MPS->CPU fallback (reload on cpu/fp32, drop a non-cpu generator, retry once). -- **Face restore trade-off (load-bearing, 2026-06-08 empirical finding).** Every shipped face-restore method (`instantid_restore.py`, `photomaker_restore.py`) is **REGENERATION** of the face from an ArcFace embedding via SDXL diffusion, NOT recovery of the original pixels. Each output face pixel is diffusion-fresh, so the regenerated face inherently looks MORE AI-generated than the cleaned image it replaces — gloss, symmetric pores, generic SDXL "clean skin" aesthetic. The cleaned image from the main controlnet 0.20 removal pass is the LEAST-AI state we can get without re-introducing SynthID: it's a light denoise of the original, not a full regeneration. Every restore method (GFPGAN-on-cleaned: polish without identity recovery; PhotoMaker-V2 txt2img: different person; InstantID txt2img: studio portrait patchwork; InstantID img2img-on-cleaned: scene-integrated but still AI-look face) was empirically tested in the 2026-06-04 - 2026-06-08 cert sweeps; all share the same root: ArcFace encodes "general look", SDXL decode adds AI aesthetic. **For production face preservation, use the cleaned image AS-IS and leave restore OFF.** The extras are kept for research / personal use where users explicitly want identity regeneration even at the cost of AI-look. -- `instantid_restore.py` — **NON-COMMERCIAL** InstantID face REGENERATION post-pass (cv2/torch/diffusers boundary, top-of-file pyright pragma). **EXPERIMENTAL, opt-in via `--restore-faces --restore-faces-method instantid`, OFF by default.** **Regenerates the face — does NOT preserve original pixels** (see the Face restore trade-off bullet above). Runs AFTER the diffusion removal pass (`InvisibleEngine.remove_watermark` -> `_restore_faces_instantid`). Carries identity in an ArcFace 512-d embedding + 5-keypoint landmark stick figure; pixels are diffusion-fresh so SynthID is not re-introduced. Flow: YuNet detects faces in the CLEANED image; for each box, the SAME box from BOTH original (ArcFace + kps) and cleaned (img2img source -- the cleaned image is oracle-clean, so unmasked-bbox pixels stay SynthID-free) are square-cropped + resized to 1024; landmark stick figure rendered from kps; `StableDiffusionXLInstantIDImg2ImgPipeline` (loaded via `custom_pipeline=` -- the upstream file is fetched from `raw.githubusercontent.com` on first use because it isn't on PyPI / HF Hub at a path diffusers auto-loads, requires `trust_remote_code=True`) runs img2img with the cleaned crop as source, landmark as ControlNet conditioning, ArcFace as IP-Adapter `image_embeds`. Elliptical-alpha + per-channel mean color-match composite. IP-Adapter scale set at LOAD via `load_ip_adapter_instantid(scale=1.0)` not at call. **Diffusers 0.38 compat patch:** the upstream pipeline calls `self.check_inputs(...)` positionally with the diffusers-~0.29 signature, but diffusers 0.38 added 2 new params before `controlnet_conditioning_scale` in the parent's check, shifting positional args by 2 -- the broadcasted `control_guidance_end=[1.0]` (list) ends up in the slot validated as `controlnet_conditioning_scale` and trips `TypeError("must be type float")`. We neutralise the check with `pipe.check_inputs = lambda *a, **k: None` (safe -- our inputs are programmatic). **antelopev2 download fix:** InsightFace's built-in URL has been broken since at least 2024 (upstream issue #2517, #2766; called out in InstantID README); `_ensure_antelopev2()` pulls the five `.onnx` files from `kidyu/antelopev2-for-InstantID-ComfyUI` on HF before `FaceAnalysis` init. Pure helpers (`_face_crop_square`, `_composite_faces_elliptical`, `_color_match`, `_draw_kps`) unit-tested without the model. **NON-COMMERCIAL because the runtime ArcFace embedder is InsightFace's antelopev2 pack which is research-only**, same chokepoint as PhotoMaker-V2 (`docs/synthid-robust-identity-research-2026-06-08.md`). -- `photomaker_restore.py` — **NON-COMMERCIAL** PhotoMaker-V2 face-identity post-pass (cv2/torch/diffusers/photomaker boundary, top-of-file pyright pragma). **EXPERIMENTAL, opt-in via `--restore-faces --restore-faces-method photomaker`, OFF by default. Alternative to `instantid_restore.py` (the default restore method); both REGENERATE the face — see the Face restore trade-off bullet above for why neither is in prod use.** Runs AFTER the diffusion removal pass (`InvisibleEngine.remove_watermark` -> `_restore_faces_photomaker`). Carries identity in a CLIP+ArcFace embedding (PhotoMaker-V2's dual-encoder) and regenerates fresh face pixels conditioned on it; the pixels are diffusion-fresh, so SynthID is not re-introduced. Flow: YuNet detects faces in the CLEANED image; for each box, the SAME box from the ORIGINAL is square-cropped (`_face_crop_square`) and fed as `input_id_images` to `PhotoMakerStableDiffusionXLPipeline` (txt2img); the regenerated face is feather-composited back via `_composite_faces`. Lazy pipeline singleton (double-checked lock) downloads `photomaker-v2.bin` from `TencentARC/PhotoMaker-V2` on first use; PhotoMaker's `__init__.py` also instantiates a face-analyser class lazily, which downloads InsightFace antelopev2/buffalo_l packs on first inference (the non-commercial step). Pure helpers (`_face_crop_square`, `_composite_faces`) are unit-tested without the model (`tests/test_photomaker_restore.py`); the model-running path is gated behind `is_available()` and exercised via the Modal cert sweep. fp16 on CUDA, fp32 on MPS/CPU. The previous commercial-safe `face_restore.py` (GFPGAN-on-cleaned) was removed 2026-06-04 because GFPGAN at this resolution only polished the already-drifted face without restoring identity (visually confirmed). PhotoMaker-V1 was also attempted as a commercial-safe path but blocked by a CFG batch-dim mismatch in the upstream pipeline (forked from diffusers 0.29; we ship 0.38) — see `docs/synthid-robust-identity-research.md` for the full chain. -- `auto_config.py` — the `--auto` quality-mode planner (EXPERIMENTAL). `plan(image_path) -> AutoConfig | None` inspects the INPUT image (before the diffusion model loads) and picks the pipeline modes, so the run adapts to content. **Designed to run as the FIRST step of the invisible/all pipeline, wherever that runs** — locally or the raiw.cc Modal GPU worker — **never on the 512 MB web host** (image work there OOM-crashes the container; the planner is `_apply_auto` in `cli.py` for the CLI, and raiw-app would call `plan()` inside `RaiwProtect.remove`). **Quality-priority routing:** ControlNet (text/face-structure preservation) is the default; it is skipped for `default` (plain SDXL) only on a clearly structure-less image (`not has_face and not has_text and edge_density < _STRUCTURELESS_EDGE_MAX` 0.008). **CAVEAT (oracle-validated 2026-06-04, see the controlnet Known-limitations bullet): at the low vendor-adaptive strength NEITHER pipeline removes SynthID on all content -- it is content×pipeline dependent (photoreal SURVIVES controlnet / clears default; flat graphics SURVIVE default / clear controlnet; flat text clears both). So `--auto` picking controlnet for faces/photos leaves SynthID on exactly those, and plain `default` would leave it on flat graphics -- pipeline choice alone does NOT guarantee removal. The real lever is a HIGHER strength, oracle-validated per content type. Removal-priority callers (raiw.cc) must oracle-validate strength across content types BEFORE adopting auto; the "must keep SynthID removed" gate in the adoption note below is the blocker this caught.** `restore_faces` is on when a face is present. When a smoothing pass (controlnet/restore) ran, the **adaptive polish** (`humanizer.adaptive_polish`) is applied: it targets the input's Laplacian variance (detail level) with a capped unsharp + edge-masked grain, restoring photo/face texture while **sparing text** (text is already high-frequency, so the deficit is tiny and almost no polish lands -- the old fixed unsharp/grain speckled small text; validated 2026-06-03 on gemini_3 lap-var 84->334 toward the 592 original, openai_1 text near-untouched). **Detection is cv2-only and torch-free** (~100 MB peak RSS, a few ms — measured): OpenCV **YuNet** (`cv2.FaceDetectorYN`, MIT, 232 KB model bundled at `assets/face_detection_yunet_2023mar.onnx`) for faces, **DBNet** (PP-OCRv3 differentiable-binarization via `cv2.dnn.TextDetectionModel_DB`, a 2.4 MB Apache-2.0 model bundled at `assets/text_detection_ppocrv3_2023may.onnx`) for text, with the old Canny+MSER region heuristic kept as a fallback if the DBNet model can't load (`_detect_text_dbnet` returns None → `_detect_text_mser`). The en/cn opencv_zoo PP-OCRv3 detection models are byte-identical, so it is bundled language-neutral. Text only ever ADDS controlnet, so a miss is backstopped by edge-density and a false positive only costs a controlnet run. Plus `edge_density`. `min_resolution` stays 1024. **Every auto decision is independently overridable** (interface principle): `_apply_auto` (cli.py) overrides only the three content-adaptive modes the user left at their click default (`ctx.get_parameter_source(...) == DEFAULT`) — `--pipeline`, `--restore-faces`/`--no-restore-faces`, and **`--adaptive-polish`/`--no-adaptive-polish`** always win; `--min-resolution`/`--strength`/`--unsharp`/`--humanize` are independent knobs. `--adaptive-polish` also works WITHOUT `--auto` (manual detail-targeted polish; the engine's `adaptive_polish` param uses the full-res original as the detail reference). Prints the chosen plan (`AutoConfig.reason`). Wired into `cmd_all`/`cmd_invisible`/`cmd_batch` — in `batch` the plan is recomputed per image and the invisible engine is cached **per resolved pipeline** (`ctx.obj["_inv_engines"]`, keyed `default`/`controlnet`) instead of a single shared instance, so a mixed directory builds at most one engine of each kind. **Adds ZERO new pip deps** (all cv2 core + the bundled MIT YuNet + Apache-2.0 DBNet models + the cv2-only adaptive polish). The auto plan does NOT select the `esrgan` upscaler (that needs the optional extra and would make auto's behavior install-dependent); `--upscaler esrgan` stays a separate manual knob. Unit-tested without a heavy download (`tests/test_auto_config.py`): flat/text synthetic images for routing (the bundled DBNet fires on a real text card), monkeypatched `detect_face`/`_detect_text_dbnet`/`_detect_text_mser` for the face/text/fallback branches (a real detectable-face fixture is private, never committed). Production adoption path for raiw.cc: validate (must keep SynthID removed, not hallucinate micro-text, beat plain SDXL on the real upload distribution), then bump the library SHA in `modal_app.py` and pass `auto=True`. -- `upscaler.py` — optional Real-ESRGAN pre-diffusion super-resolution for small inputs (spandrel boundary, top-of-file pyright pragma). `is_available()` gates on spandrel+torch (via `importlib.util.find_spec`); `upscale(bgr, device=None)` loads a lazily-built spandrel `ImageModelDescriptor` singleton (double-checked lock) and upscales by the model's native factor (x2), with a non-CPU→CPU device fallback mirroring the diffusion engine's MPS→CPU retry. Weights (`RealESRGAN_x2plus.pth`, BSD-3-Clause) download on first use to the `torch.hub` checkpoints cache; never bundled. Used only when UPscaling to the `min_resolution` floor (a `max_resolution` downscale always uses Lanczos). The wiring is `InvisibleEngine._esrgan_upscale(pil, target)` — Real-ESRGAN at native factor, then a Lanczos resize to the exact target, falling back to a plain Lanczos resize if the extra is absent or the model errors (so an optional upscaler can never break removal). The default `--upscaler` is `lanczos` (cv2, no deps). **ESRGAN is a generic photo/texture GAN with no face/glyph prior**, so it best fits photo/texture content and can degrade faces (glassy/asymmetric eyes -- the diffusion pass regenerates faces so the full-pipeline final recovers; `--restore-faces` is the polish path on top of that) and thin/small text (the GAN invents wrong strokes, and low-strength diffusion will not fix it). Verified 2026-06-04: isolated upscale lap-var ~5x Lanczos on faces+textures but glassy eyes; end-to-end `invisible` final lap-var 1634 vs Lanczos 663 with natural faces (diffusion cleaned the artifact). Kept a **manual opt-in knob** (the auto plan never selects it) with `lanczos` the default; not content-gated by design (use Lanczos for text-heavy inputs). spandrel is MIT and pulls no basicsr. Unit-tested without the model: `tests/test_upscaler.py` (availability guard + the not-installed RuntimeError) and `tests/test_invisible_engine.py::TestEsrganUpscale` (the three `_esrgan_upscale` branches via a monkeypatched `upscaler`). +- `noai/watermark_remover.py` — the `WatermarkRemover` class has two diffusion pipelines, selected by the explicit `pipeline` ctor arg (NOT inferred from `model_id` -- both use the same SDXL base, `DEFAULT_MODEL_ID`). **`default`** runs plain SDXL img2img (`_run_img2img`). **`controlnet`** (**EXPERIMENTAL, opt-in**; `_run_controlnet`, `_load_controlnet_pipeline`) runs `StableDiffusionXLControlNetImg2ImgPipeline` with the SDXL-native canny ControlNet `xinsir/controlnet-canny-sdxl-1.0` (`watermark_profiles.CONTROLNET_CANNY_MODEL`): the control image is `cv2.Canny(gray, 100, 200)` stacked to 3 channels (`_CANNY_LOW`/`_CANNY_HIGH`, prompt `_CONTROLNET_PROMPT` / `_CONTROLNET_NEGATIVE`). **Removal comes from the img2img regeneration (`strength`); the ControlNet only PRESERVES text and face STRUCTURE via the edge map.** No original pixels are copied or frozen, BUT **validation 2026-06-04 disproved the old "so SynthID does not survive" claim: SynthID CAN survive controlnet on photoreal/high-detail content.** At the shared low removal strength the canny edge-conditioning keeps the regeneration so close to the original that the pixel perturbation that destroys SynthID does not happen (oracle-confirmed: an OpenAI bracelet photo + a 9-face grid read **SynthID-detected** after controlnet at strength 0.10/0.15, but **SynthID-not-detected** after the `default` pipeline at the SAME strength + resolution -- only the pipeline differed). **But the reverse also holds: a flat-graphic logo/poster SURVIVED `default` while clearing controlnet** -- removal at the low strength is content×pipeline dependent and neither pipeline is universally safe; the real lever is a higher strength. See the controlnet Known-limitations bullet for the full table + root cause. Canny holds face STRUCTURE but NOT identity (the regenerated face drifts in likeness -- canny carries edges, not identity). The drifted cleaned face is the LEAST-AI state we can reach without re-introducing SynthID; the library does NOT ship a face-restore extra. Every restore approach we evaluated (GFPGAN-on-cleaned, PhotoMaker-V2 txt2img, InstantID txt2img, InstantID img2img-on-cleaned at three parameter sweeps, 2026-06-04 - 2026-06-08 Modal cert sweeps) regenerated the face from an ArcFace embedding via SDXL diffusion -- which makes the output face look MORE AI-generated, not less. Empirical conclusion in `docs/synthid-robust-identity-research-2026-06-08.md` "Empirical follow-up". For production face preservation, ship the cleaned image as-is. `controlnet_conditioning_scale` (ctor arg, default 1.0) is the structure-preservation knob. Same dtype rule as `default` (fp32 on cpu/mps, fp16 only on cuda/xpu; the fp16-fixed SDXL VAE `_SDXL_FP16_VAE_ID` is swapped in on fp16 GPUs -- issue #29) and the same MPS->CPU fallback (reload on cpu/fp32, drop a non-cpu generator, retry once). +- `auto_config.py` — the `--auto` quality-mode planner (EXPERIMENTAL). `plan(image_path) -> AutoConfig | None` inspects the INPUT image (before the diffusion model loads) and picks the pipeline modes, so the run adapts to content. **Designed to run as the FIRST step of the invisible/all pipeline, wherever that runs** — locally or the raiw.cc Modal GPU worker — **never on the 512 MB web host** (image work there OOM-crashes the container; the planner is `_apply_auto` in `cli.py` for the CLI, and raiw-app would call `plan()` inside `RaiwProtect.remove`). **Quality-priority routing:** ControlNet (text/face-structure preservation) is the default; it is skipped for `default` (plain SDXL) only on a clearly structure-less image (`not has_face and not has_text and edge_density < _STRUCTURELESS_EDGE_MAX` 0.008). **CAVEAT (oracle-validated 2026-06-04, see the controlnet Known-limitations bullet): at the low vendor-adaptive strength NEITHER pipeline removes SynthID on all content -- it is content×pipeline dependent (photoreal SURVIVES controlnet / clears default; flat graphics SURVIVE default / clear controlnet; flat text clears both). So `--auto` picking controlnet for faces/photos leaves SynthID on exactly those, and plain `default` would leave it on flat graphics -- pipeline choice alone does NOT guarantee removal. The real lever is a HIGHER strength, oracle-validated per content type. Removal-priority callers (raiw.cc) must oracle-validate strength across content types BEFORE adopting auto; the "must keep SynthID removed" gate in the adoption note below is the blocker this caught.** When the controlnet smoothing pipeline ran, the **adaptive polish** (`humanizer.adaptive_polish`) is applied: it targets the input's Laplacian variance (detail level) with a capped unsharp + edge-masked grain, restoring photo/face texture while **sparing text** (text is already high-frequency, so the deficit is tiny and almost no polish lands -- the old fixed unsharp/grain speckled small text; validated 2026-06-03 on gemini_3 lap-var 84->334 toward the 592 original, openai_1 text near-untouched). **Detection is cv2-only and torch-free** (~100 MB peak RSS, a few ms — measured): OpenCV **YuNet** (`cv2.FaceDetectorYN`, MIT, 232 KB model bundled at `assets/face_detection_yunet_2023mar.onnx`) for faces, **DBNet** (PP-OCRv3 differentiable-binarization via `cv2.dnn.TextDetectionModel_DB`, a 2.4 MB Apache-2.0 model bundled at `assets/text_detection_ppocrv3_2023may.onnx`) for text, with the old Canny+MSER region heuristic kept as a fallback if the DBNet model can't load (`_detect_text_dbnet` returns None → `_detect_text_mser`). The en/cn opencv_zoo PP-OCRv3 detection models are byte-identical, so it is bundled language-neutral. Text only ever ADDS controlnet, so a miss is backstopped by edge-density and a false positive only costs a controlnet run. Plus `edge_density`. `min_resolution` stays 1024. **Every auto decision is independently overridable** (interface principle): `_apply_auto` (cli.py) overrides only the content-adaptive modes the user left at their click default (`ctx.get_parameter_source(...) == DEFAULT`) — `--pipeline` and **`--adaptive-polish`/`--no-adaptive-polish`** always win; `--min-resolution`/`--strength`/`--unsharp`/`--humanize` are independent knobs. `--adaptive-polish` also works WITHOUT `--auto` (manual detail-targeted polish; the engine's `adaptive_polish` param uses the full-res original as the detail reference). Prints the chosen plan (`AutoConfig.reason`). Wired into `cmd_all`/`cmd_invisible`/`cmd_batch` — in `batch` the plan is recomputed per image and the invisible engine is cached **per resolved pipeline** (`ctx.obj["_inv_engines"]`, keyed `default`/`controlnet`) instead of a single shared instance, so a mixed directory builds at most one engine of each kind. **Adds ZERO new pip deps** (all cv2 core + the bundled MIT YuNet + Apache-2.0 DBNet models + the cv2-only adaptive polish). The auto plan does NOT select the `esrgan` upscaler (that needs the optional extra and would make auto's behavior install-dependent); `--upscaler esrgan` stays a separate manual knob. Unit-tested without a heavy download (`tests/test_auto_config.py`): flat/text synthetic images for routing (the bundled DBNet fires on a real text card), monkeypatched `detect_face`/`_detect_text_dbnet`/`_detect_text_mser` for the face/text/fallback branches (a real detectable-face fixture is private, never committed). Production adoption path for raiw.cc: validate (must keep SynthID removed, not hallucinate micro-text, beat plain SDXL on the real upload distribution), then bump the library SHA in `modal_app.py` and pass `auto=True`. +- `upscaler.py` — optional Real-ESRGAN pre-diffusion super-resolution for small inputs (spandrel boundary, top-of-file pyright pragma). `is_available()` gates on spandrel+torch (via `importlib.util.find_spec`); `upscale(bgr, device=None)` loads a lazily-built spandrel `ImageModelDescriptor` singleton (double-checked lock) and upscales by the model's native factor (x2), with a non-CPU→CPU device fallback mirroring the diffusion engine's MPS→CPU retry. Weights (`RealESRGAN_x2plus.pth`, BSD-3-Clause) download on first use to the `torch.hub` checkpoints cache; never bundled. Used only when UPscaling to the `min_resolution` floor (a `max_resolution` downscale always uses Lanczos). The wiring is `InvisibleEngine._esrgan_upscale(pil, target)` — Real-ESRGAN at native factor, then a Lanczos resize to the exact target, falling back to a plain Lanczos resize if the extra is absent or the model errors (so an optional upscaler can never break removal). The default `--upscaler` is `lanczos` (cv2, no deps). **ESRGAN is a generic photo/texture GAN with no face/glyph prior**, so it best fits photo/texture content and can degrade faces (glassy/asymmetric eyes -- the diffusion pass regenerates faces so the full-pipeline final recovers) and thin/small text (the GAN invents wrong strokes, and low-strength diffusion will not fix it). Verified 2026-06-04: isolated upscale lap-var ~5x Lanczos on faces+textures but glassy eyes; end-to-end `invisible` final lap-var 1634 vs Lanczos 663 with natural faces (diffusion cleaned the artifact). Kept a **manual opt-in knob** (the auto plan never selects it) with `lanczos` the default; not content-gated by design (use Lanczos for text-heavy inputs). spandrel is MIT and pulls no basicsr. Unit-tested without the model: `tests/test_upscaler.py` (availability guard + the not-installed RuntimeError) and `tests/test_invisible_engine.py::TestEsrganUpscale` (the three `_esrgan_upscale` branches via a monkeypatched `upscaler`). - `image_io.py` — Unicode-safe cv2 IO (issue #17). `imread(path, flags=None)` / `imwrite(path, img)` wrap `np.fromfile`+`cv2.imdecode` / `cv2.imencode`+`tofile` so non-ASCII paths work on Windows -- bare `cv2.imread`/`cv2.imwrite` use the platform ANSI code-page API there and fail (empty decode + `can't open/read file`) on Chinese/Cyrillic/accented filenames. `imread` keeps `cv2.imread` semantics (defaults to `IMREAD_COLOR`, returns `None` on missing/empty/undecodable). **Every cv2 file read/write in the package routes through here; do not call `cv2.imread`/`cv2.imwrite` directly.** `imwrite` returns `False` on an unwritable path (`OSError` caught) instead of raising, matching `cv2.imwrite` semantics. macOS/Linux already accept UTF-8 paths, so it is behavior-neutral there (the bug only reproduces on Windows). cv2/numpy are imported lazily inside the functions, so the module is cheap to import in a bare env. ### Doubao clean-reverse-alpha distillation (re-investigated 2026-05-29) @@ -94,4 +90,4 @@ Who embeds what, and whether it is locally detectable (so we know which gaps are - **External AI-vs-real classifier models are out of scope (decided 2026-05-24).** Generic HuggingFace detectors (`Organika/sdxl-detector` Swin Transformer, `umm-maybe/AI-image-detector`, and fine-tunes) exist and report ~0.98 on their *own* SDXL-vs-real validation sets, but they are per-generator and the model cards themselves note degraded accuracy off-distribution; they are untested on gpt-image / Gemini Nano Banana (the metadata-stripped surfaces we care about), and our own light SDXL pass would likely defeat them the same way it defeats SynthID. Detection here stays local + signal-based (metadata + visible sparkle); do not add a bundled classifier dependency. - **DEFAULT STRENGTH IS NOW VENDOR-ADAPTIVE (2026-06-01, SUPERSEDES every fixed-default claim in this bullet and the next).** `resolve_strength(strength, profile, vendor)` + `vendor_for_strength(path)` (`watermark_profiles.py`) read the C2PA issuer (`metadata.synthid_source`) on the ORIGINAL input and pick `OPENAI_STRENGTH` **0.10** / `GEMINI_STRENGTH` **0.15** / `UNKNOWN_STRENGTH` **0.15** when `--strength` is unset; explicit `--strength` always wins. The CLI detects the vendor from the pristine source (before the visible pass / metadata-strip removes C2PA from the temp file) and passes it to the engine, so display and execution agree; `cmd_invisible`/`cmd_all`/`batch` + the module-level `remove_watermark` all thread `vendor`. **This replaces the single 0.30 default AND the prior "do NOT build a vendor-adaptive default" policy** -- both came from the now-debunked region-rescrub-contaminated study (the per-region re-scrub that contaminated those numbers was removed in the controlnet refactor). Basis: the oracle-verified June 2026 controlled study (clean v0.8.6, protect OFF): OpenAI clears at 0.05 across 1024-1600 (n=4, resolution-independent); Google needs 0.15 on the capped-1536 path (n=4). `docs/synthid.md` §2.2 (data) + §5.2 (the adaptive default) are authoritative. **CAVEAT (oracle pass 2026-06-04): the OpenAI 0.10 default is content-dependent, NOT universal -- a flat-graphic OpenAI logo/poster still read SynthID-detected after `default` at 0.10, and photoreal images after controlnet at 0.10/0.15 (low-change regions under-perturbed). Removal at 0.10/0.15 is content×pipeline dependent (see the controlnet Known-limitations bullet); the lever is a higher strength, oracle-revalidated per content type. Do NOT assume the vendor-adaptive default clears every image.** CAVEAT: Google's 0.15 was validated only on `--max-resolution 1536`; native large Gemini (2816) was not locally measurable (OOM on M-series) and is pending GPU validation on raiw.cc -- if it survives 0.15 native, raise `--strength`. **Everything below in this bullet about a fixed 0.10/0.30 default is HISTORICAL; trust the vendor-adaptive constants + docs/synthid.md.** - **SynthID removal: strength + oracle scope.** Default strength is vendor-adaptive (see the bullet above); `docs/synthid.md` §2.2 is authoritative for the numbers. **Oracle scope (load-bearing):** the Gemini app "Verify with SynthID" is the ONLY valid SynthID oracle (detects Google's mark on any image); `openai.com/verify` is scoped to OpenAI provenance (its own C2PA), NOT a SynthID oracle -- a negative there is meaningless for SynthID. There is no local SynthID detector, so the tool cannot self-check; if the oracle still reads SynthID, raise `--strength` to the lowest value that verifies clean. Only the `default` (plain SDXL img2img) and `controlnet` (SDXL + canny ControlNet) profiles exist; the local `invisible` default is weight-for-weight identical to raiw.cc prod (`fal-ai/fast-sdxl` = `stabilityai/stable-diffusion-xl-base-1.0`, runtime-downloaded, not bundled). **Forensic-stealth caveat** (arXiv:2605.09203): defeating the SynthID verifier is NOT forensic invisibility -- independent detectors flag *removal-processed* images vs genuinely-clean ones at >98% TPR@1%FPR, so do not over-claim "indistinguishable from a real photo". -- **`controlnet` pipeline (text/face STRUCTURE preservation, EXPERIMENTAL, opt-in `--pipeline controlnet`).** SDXL + the canny ControlNet `xinsir/controlnet-canny-sdxl-1.0` via `StableDiffusionXLControlNetImg2ImgPipeline` (`watermark_remover._run_controlnet` / `_load_controlnet_pipeline`). **Removal still comes from the img2img regeneration (`strength`); the ControlNet only PRESERVES text and face STRUCTURE by conditioning on the canny edge map** (`cv2.Canny(gray, 100, 200)`, 3-channel). Canny preserves edges, NOT face identity (a regenerated face drifts in likeness). The drifted cleaned face is the LEAST-AI state we can reach without re-introducing SynthID; the optional `--restore-faces` post-pass (`instantid` default or `photomaker`, both NON-COMMERCIAL) further REGENERATES the face from an ArcFace embedding via SDXL diffusion, which makes the output face look MORE AI-generated, not less. **For production face preservation use the cleaned image AS-IS and leave restore OFF** — see the Face restore trade-off bullet above and `instantid_restore.py` / `photomaker_restore.py`. The CodeFormer alternative stays NON-COMMERCIAL and is not shipped. The earlier `--face-id` IP-Adapter FaceID layer was REMOVED (footgun: it needs high strength and corrupts faces at the low removal strength). No original pixels are copied or frozen, **BUT removal at the low vendor-adaptive strength is CONTENT × PIPELINE dependent and NEITHER pipeline clears all content -- oracle-validated against the OpenAI verifier 2026-06-04 (8 images, strength 0.10/0.15, `--max-resolution 1536`).** The survivors FLIP by content type: **photoreal** (a 9-face grid, a bracelet product photo) SURVIVES controlnet but CLEARS `default` (controlnet's dense edge map keeps the regen too close to the original, so the SynthID-destroying perturbation never happens; plain img2img perturbs photoreal texture enough); **flat graphic** (a logo/poster with large flat color fills) SURVIVES `default` but CLEARS controlnet (at low strength img2img barely changes flat fills so SynthID persists there, while controlnet repaints them more freely); a flat **text** card cleared under both. **Root cause is insufficient STRENGTH, not the pipeline: at 0.10 the low-change regions -- dense-edge photoreal under controlnet, large flat fills under `default` -- are not perturbed enough to destroy SynthID. The vendor-adaptive 0.10 from the June study is NOT universally sufficient (that study's content happened to clear at 0.10).** The robust fix is a HIGHER strength, oracle-revalidated per content type (controlnet can be cranked harder without losing structure; a lower `controlnet_conditioning_scale` also frees the regen on photoreal). So at today's default strength **both pipelines AND `--auto` can LEAVE SynthID on some content** -- a removal-priority caller (raiw.cc) MUST oracle-validate strength across content types before adopting, not pick a pipeline and assume removal. **Follow-up same day: re-running the two photoreal survivors through controlnet at an explicit `--strength 0.15` cleared BOTH on the oracle -- BUT one of them (the bracelet) had SURVIVED the SAME 0.15 controlnet config in the first pass (only the random, unset seed differed). So removal near the threshold is SEED-NON-DETERMINISTIC: the same image+pipeline+strength+resolution can pass or fail run-to-run (img2img uses `seed=None`/random unless `--seed` is passed, and there is no local SynthID detector to self-verify). 0.15 is the borderline, NOT a robust floor -- pick a strength with MARGIN (controlnet ~>= 0.20) rather than exactly on it; the content×pipeline table's 0.15 data point is near-threshold noise. A confirming run at `--strength 0.20` controlnet cleared BOTH photoreal survivors on the oracle (ladder: 0.10 grid detected → 0.15 borderline/non-deterministic → 0.20 both clean), so **0.20 is the recommended robust controlnet floor for OpenAI photoreal** (one margin run, not an N-run repeatability proof -- a service should add margin or verify repeatability since there is no local SynthID detector to self-check). **Engineering follow-up for raiw.cc: the controlnet pipeline should use a HIGHER vendor strength than `default` -- it currently shares `resolve_strength` (0.10/0.15, tuned for plain img2img), but controlnet's edge map preserves structure so it needs ~0.20+; calibrate per vendor/content on the GPU worker, do NOT just reuse the `default` ladder.** **CERTIFIED 2026-06-04 via the isolated `raiw-controlnet-cert` Modal app (`raiw-app/modal_cert.py`), restore OFF, ≤1536, each vendor on its own oracle: controlnet floors are OpenAI 0.20 (2 photoreal × 3 seeds = 6/6 clean; the 0.15-flipper is seed-robust at 0.20) and Gemini 0.30 (0.20 detected → 0.30 clean on 2/2 seeds). OpenAI 0.20 transfers to prod (resolution-independent); Gemini 0.30 holds only ≤1536 — Gemini is resolution-sensitive and raiw.cc runs NATIVE (`max_resolution=0`), so cap Gemini ≤1536 + use 0.30, or native-calibrate (~0.35+). Prod recipe: controlnet + per-vendor floor in `resolve_strength` (not the default ladder) + FIXED seed (kills the non-determinism). No `--restore-faces` in prod -- not because of the license (both shipped methods are NON-COMMERCIAL) but because **regeneration via ArcFace embedding makes the face look MORE AI-generated, not less**: every restore method tested (GFPGAN-on-cleaned, PhotoMaker-V2, InstantID txt2img, InstantID img2img-on-cleaned, 2026-06-04 - 2026-06-08 cert sweeps) yields a diffusion-fresh face that loses original identity precision and gains SDXL "clean skin" gloss. The drifted face from controlnet 0.20 is the least-AI state we can reach; for a paid service that's the prod output. See the Face restore trade-off bullet.** See `docs/synthid.md` §5.5 + `docs/controlnet-removal-pipeline-research.md` (certified floors table).** **Lesson: visual-quality + face-recovery validation does NOT prove watermark removal -- only the SynthID oracle does, across MULTIPLE content types; never infer removal from sharpness/identity, and never conclude from a partial result (the photoreal-only data first read as "controlnet shields, default removes" -- the flat-graphic result reversed it).** `controlnet_conditioning_scale` (CLI `--controlnet-scale`, default 1.0) is the structure-preservation knob (higher = closer to the original structure); fp32 on cpu/mps, fp16-fixed VAE on cuda/xpu. The `controlnet` profile is threaded explicitly (`WatermarkRemover(pipeline=...)` / `InvisibleEngine(pipeline=...)`), NOT inferred from `model_id`. This productionizes the `scripts/controlnet_sweep.py` prototype; see `docs/controlnet-removal-pipeline-research.md`. **Forensic-stealth caveat still applies** (arXiv:2605.09203): defeating the SynthID verifier is not forensic invisibility -- a "this image went through a removal pipeline" classifier can still flag the output. +- **`controlnet` pipeline (text/face STRUCTURE preservation, EXPERIMENTAL, opt-in `--pipeline controlnet`).** SDXL + the canny ControlNet `xinsir/controlnet-canny-sdxl-1.0` via `StableDiffusionXLControlNetImg2ImgPipeline` (`watermark_remover._run_controlnet` / `_load_controlnet_pipeline`). **Removal still comes from the img2img regeneration (`strength`); the ControlNet only PRESERVES text and face STRUCTURE by conditioning on the canny edge map** (`cv2.Canny(gray, 100, 200)`, 3-channel). Canny preserves edges, NOT face identity (a regenerated face drifts in likeness). The drifted cleaned face is the LEAST-AI state we can reach without re-introducing SynthID; **the library does NOT ship a face-restore extra** (every approach evaluated 2026-06-04 - 2026-06-08 -- GFPGAN-on-cleaned, PhotoMaker-V2, InstantID txt2img, InstantID img2img-on-cleaned at three parameter sweeps -- regenerated the face via SDXL and made it look MORE AI-generated). Full empirical conclusion in `docs/synthid-robust-identity-research-2026-06-08.md` "Empirical follow-up". For production face preservation, ship the cleaned image as-is. No original pixels are copied or frozen, **BUT removal at the low vendor-adaptive strength is CONTENT × PIPELINE dependent and NEITHER pipeline clears all content -- oracle-validated against the OpenAI verifier 2026-06-04 (8 images, strength 0.10/0.15, `--max-resolution 1536`).** The survivors FLIP by content type: **photoreal** (a 9-face grid, a bracelet product photo) SURVIVES controlnet but CLEARS `default` (controlnet's dense edge map keeps the regen too close to the original, so the SynthID-destroying perturbation never happens; plain img2img perturbs photoreal texture enough); **flat graphic** (a logo/poster with large flat color fills) SURVIVES `default` but CLEARS controlnet (at low strength img2img barely changes flat fills so SynthID persists there, while controlnet repaints them more freely); a flat **text** card cleared under both. **Root cause is insufficient STRENGTH, not the pipeline: at 0.10 the low-change regions -- dense-edge photoreal under controlnet, large flat fills under `default` -- are not perturbed enough to destroy SynthID. The vendor-adaptive 0.10 from the June study is NOT universally sufficient (that study's content happened to clear at 0.10).** The robust fix is a HIGHER strength, oracle-revalidated per content type (controlnet can be cranked harder without losing structure; a lower `controlnet_conditioning_scale` also frees the regen on photoreal). So at today's default strength **both pipelines AND `--auto` can LEAVE SynthID on some content** -- a removal-priority caller (raiw.cc) MUST oracle-validate strength across content types before adopting, not pick a pipeline and assume removal. **Follow-up same day: re-running the two photoreal survivors through controlnet at an explicit `--strength 0.15` cleared BOTH on the oracle -- BUT one of them (the bracelet) had SURVIVED the SAME 0.15 controlnet config in the first pass (only the random, unset seed differed). So removal near the threshold is SEED-NON-DETERMINISTIC: the same image+pipeline+strength+resolution can pass or fail run-to-run (img2img uses `seed=None`/random unless `--seed` is passed, and there is no local SynthID detector to self-verify). 0.15 is the borderline, NOT a robust floor -- pick a strength with MARGIN (controlnet ~>= 0.20) rather than exactly on it; the content×pipeline table's 0.15 data point is near-threshold noise. A confirming run at `--strength 0.20` controlnet cleared BOTH photoreal survivors on the oracle (ladder: 0.10 grid detected → 0.15 borderline/non-deterministic → 0.20 both clean), so **0.20 is the recommended robust controlnet floor for OpenAI photoreal** (one margin run, not an N-run repeatability proof -- a service should add margin or verify repeatability since there is no local SynthID detector to self-check). **Engineering follow-up for raiw.cc: the controlnet pipeline should use a HIGHER vendor strength than `default` -- it currently shares `resolve_strength` (0.10/0.15, tuned for plain img2img), but controlnet's edge map preserves structure so it needs ~0.20+; calibrate per vendor/content on the GPU worker, do NOT just reuse the `default` ladder.** **CERTIFIED 2026-06-04 via the isolated `raiw-controlnet-cert` Modal app (`raiw-app/modal_cert.py`), restore OFF, ≤1536, each vendor on its own oracle: controlnet floors are OpenAI 0.20 (2 photoreal × 3 seeds = 6/6 clean; the 0.15-flipper is seed-robust at 0.20) and Gemini 0.30 (0.20 detected → 0.30 clean on 2/2 seeds). OpenAI 0.20 transfers to prod (resolution-independent); Gemini 0.30 holds only ≤1536 — Gemini is resolution-sensitive and raiw.cc runs NATIVE (`max_resolution=0`), so cap Gemini ≤1536 + use 0.30, or native-calibrate (~0.35+). Prod recipe: controlnet + per-vendor floor in `resolve_strength` (not the default ladder) + FIXED seed (kills the non-determinism). **No face-restore in the library:** every approach evaluated (GFPGAN-on-cleaned, PhotoMaker-V2, InstantID txt2img, InstantID img2img-on-cleaned, 2026-06-04 - 2026-06-08 cert sweeps) regenerated the face via SDXL diffusion -- the output face inherited SDXL "clean skin" gloss and lost original identity precision, looking MORE AI-generated than the cleaned image, not less. The drifted face from controlnet 0.20 is the least-AI state we can reach; for a paid service that's the prod output. See `docs/synthid-robust-identity-research-2026-06-08.md` "Empirical follow-up".** See `docs/synthid.md` §5.5 + `docs/controlnet-removal-pipeline-research.md` (certified floors table).** **Lesson: visual-quality + face-recovery validation does NOT prove watermark removal -- only the SynthID oracle does, across MULTIPLE content types; never infer removal from sharpness/identity, and never conclude from a partial result (the photoreal-only data first read as "controlnet shields, default removes" -- the flat-graphic result reversed it).** `controlnet_conditioning_scale` (CLI `--controlnet-scale`, default 1.0) is the structure-preservation knob (higher = closer to the original structure); fp32 on cpu/mps, fp16-fixed VAE on cuda/xpu. The `controlnet` profile is threaded explicitly (`WatermarkRemover(pipeline=...)` / `InvisibleEngine(pipeline=...)`), NOT inferred from `model_id`. This productionizes the `scripts/controlnet_sweep.py` prototype; see `docs/controlnet-removal-pipeline-research.md`. **Forensic-stealth caveat still applies** (arXiv:2605.09203): defeating the SynthID verifier is not forensic invisibility -- a "this image went through a removal pipeline" classifier can still flag the output. diff --git a/README.md b/README.md index 911ab48..40ebe93 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ If this tool saves you time, consider [sponsoring its development](https://githu - **AI metadata stripping** — EXIF, PNG text chunks, C2PA provenance manifests (PNG / JPEG / AVIF / HEIF / JPEG-XL, **MP4 / MOV / M4V / M4A** at the container level, and **WebM / MP3 / WAV / FLAC / OGG** losslessly via ffmpeg), XMP DigitalSourceType - **"Made with AI" label removal** — removes the AI-disclosure metadata that platforms read to apply automatic labels (useful for clearing a false-positive label from a human-edited photograph) - **Analog Humanizer** — optional film grain and chromatic aberration post-processing -- **Text and face preservation (experimental)** — optional `--pipeline controlnet` adds a canny ControlNet that keeps text and face structure sharp through the removal pass (without copying original pixels, so SynthID is still removed). Canny preserves face *structure*, not *identity* (the regenerated face drifts in likeness). The optional `--restore-faces` post-pass (`instantid` default or `photomaker`, both **NON-COMMERCIAL**, off by default) does NOT recover the original face — it **regenerates** it from an ArcFace embedding via SDXL diffusion, which inherently makes the output look more AI-generated than the cleaned image. **For production face preservation, leave restore OFF and use the cleaned image as-is.** +- **Text and face preservation (experimental)** — optional `--pipeline controlnet` adds a canny ControlNet that keeps text and face structure sharp through the removal pass (without copying original pixels, so SynthID is still removed). Canny preserves face *structure*, not *identity* (the regenerated face drifts in likeness). The library does not ship a face-restore extra: every approach evaluated (GFPGAN-on-cleaned, PhotoMaker-V2, InstantID txt2img, InstantID img2img-on-cleaned) regenerated the face via SDXL and made the output look more AI-generated than the cleaned image. The cleaned controlnet output is the least-AI face state achievable without re-introducing SynthID. - **Batch processing** — process entire directories - **Detection** — three-stage NCC watermark detection with confidence scoring - **Provenance detection (`identify`)** — aggregate C2PA issuer, the C2PA soft-binding forensic-watermark vendor (Adobe TrustMark, Digimarc, Imatag, ...), IPTC "Made with AI" plus the IPTC 2025.1 `AISystemUsed` field, embedded SD/ComfyUI params, EXIF/XMP generator tags, the xAI/Grok EXIF signature, the China TC260 AIGC label (XMP, PNG chunk, or EXIF), the HuggingFace `hf-job-id` job marker, the SynthID metadata proxy, the visible marks (Gemini sparkle plus the Doubao "豆包AI生成" / Jimeng "即梦AI" / Samsung Galaxy AI "Contenuti generati dall'AI" text marks), the open SD/SDXL/FLUX invisible watermark, and (with the `trustmark` extra) the open Adobe TrustMark watermark into one origin-platform + watermark-inventory verdict (`--json` for machine output) @@ -128,7 +128,7 @@ image → encode to latent space (VAE) at native resolution > > **`--pipeline controlnet` preserves text and face structure (experimental, opt-in).** It runs the same SDXL img2img scrub but adds a canny ControlNet that conditions the regeneration on the image's edge map, so text and structure stay sharp at the strengths that remove SynthID. The watermark removal still comes from the img2img regeneration (`--strength`); the ControlNet only preserves structure — no original pixels are copied or frozen, so SynthID does not survive. `--controlnet-scale` tunes the preservation strength (higher = closer to the original structure). Runs fp32 on mps/cpu (fp16 only on cuda/xpu, where the fp16-fixed SDXL VAE is loaded automatically). > -> **`--restore-faces` REGENERATES faces; it does NOT recover original pixels.** Two methods, both **NON-COMMERCIAL**, both off by default: `instantid` (default, the `instantid` extra; InstantID img2img-on-cleaned + ArcFace embedding + landmark ControlNet) and `photomaker` (the `photomaker` extra; PhotoMaker-V2 txt2img + CLIP+ArcFace embedding). Both crop the face region from the cleaned image and run SDXL diffusion conditioned on an ArcFace embedding from the original — the output face pixels are diffusion-fresh so SynthID is not re-introduced, **but the output face inherently looks more AI-generated than the cleaned image**: every pixel is SDXL-decoded from a semantic embedding, gaining the typical "clean skin" gloss and losing the exact original identity. The cleaned image from the main controlnet 0.20 pass is the least-AI state we can reach without re-introducing SynthID; any restore on top of it trades original-look for embedding-driven regeneration. **For production face preservation, leave `--restore-faces` OFF.** Both extras are NON-COMMERCIAL because their ArcFace embedder is InsightFace's antelopev2 pack which is research-only; the empirical case for not shipping them in prod is the AI-look regardless of license (see `docs/synthid-robust-identity-research-2026-06-08.md`). +> **No face-restore extra in the library.** Every ArcFace-based regeneration approach we evaluated (GFPGAN-on-cleaned, PhotoMaker-V2, InstantID txt2img, InstantID img2img-on-cleaned at three parameter sweeps, 2026-06-04 - 2026-06-08 Modal cert sweeps) regenerated the face via SDXL diffusion — the output face pixels were diffusion-fresh (SynthID not re-introduced), but the face inherently looked more AI-generated than the cleaned image (SDXL "clean skin" gloss, lost original identity precision). The cleaned image from the main controlnet 0.20 pass is the least-AI face state we can reach without re-introducing SynthID. Empirical conclusion in `docs/synthid-robust-identity-research-2026-06-08.md`. SDXL is the default since May 2026: empirically defeats SynthID v2 on Gemini 3 Pro outputs, where the older SD-1.5 pipeline at 768 px did not. The SD-1.5 path was removed once it was verified not to handle v2. Note the scope: this defeats the SynthID *verifier*, which is not the same as being forensically indistinguishable from a real photo. Recent work ([arXiv:2605.09203](https://arxiv.org/abs/2605.09203)) shows watermark-removal pipelines leave detectable traces, so a separate "this image was processed" classifier can still flag the output. @@ -136,7 +136,7 @@ SDXL is the default since May 2026: empirically defeats SynthID v2 on Gemini 3 P > **Technical deep-dive:** see [`docs/synthid.md`](docs/synthid.md) for a primary-source-cited breakdown of how SynthID works mechanically (post-hoc encoder/decoder, 136-bit payload, pixel-space embedding), what it empirically survives (JPEG, crop, resize: ~99.98% TPR at 0.1% FPR from arXiv:2510.09263), what removes it, and the forensic-stealth tradeoff (all known removal attacks are detectable at >98% TPR@1%FPR per arXiv:2605.09203). -**Text and face preservation** (experimental, opt-in `--pipeline controlnet`): adds a canny ControlNet so text and face *structure* stay sharp through the removal pass, without copying or freezing any original pixels (so SynthID is still removed). Tune the preservation strength with `--controlnet-scale`. Canny preserves structure but not face *identity*: the regenerated face drifts in likeness. The optional `--restore-faces` post-pass would regenerate the face from an ArcFace embedding, but every shipped method makes the face look more AI-generated (see the callout above) — for production face preservation leave restore OFF. +**Text and face preservation** (experimental, opt-in `--pipeline controlnet`): adds a canny ControlNet so text and face *structure* stay sharp through the removal pass, without copying or freezing any original pixels (so SynthID is still removed). Tune the preservation strength with `--controlnet-scale`. Canny preserves structure but not face *identity*: the regenerated face drifts in likeness. The library does not ship a face-restore extra (see the callout above). **Analog Humanizer**: optional film grain and chromatic aberration injection that mimics a photo of a screen, raising the bar for AI-generated image classifiers. (It frustrates generic classifiers but does not guarantee forensic invisibility — see the [arXiv:2605.09203](https://arxiv.org/abs/2605.09203) note above.) @@ -214,17 +214,6 @@ After installation the `remove-ai-watermarks` command is available system-wide. > pip install -e ".[trustmark]" # or: uv pip install -e ".[trustmark]" > ``` > -> To regenerate face identity after invisible removal (the `--restore-faces` -> PhotoMaker-V2 post-pass, experimental and opt-in, **NON-COMMERCIAL** because -> PhotoMaker-V2 pulls non-commercial InsightFace model packs at runtime), install -> the `photomaker` extra. The PhotoMaker-V2 adapter weights and InsightFace face -> packs download on first use. Do NOT install this extra in a commercial / paid -> service: -> -> ```bash -> pip install -e ".[photomaker]" # or: uv pip install -e ".[photomaker]" -> ``` -> > For sharper upscaling of small inputs before diffusion (`--upscaler esrgan`, > Real-ESRGAN), install the `esrgan` extra. It loads via spandrel (MIT, no basicsr); > the Real-ESRGAN weights (BSD-3-Clause) download on first use: @@ -306,10 +295,10 @@ remove-ai-watermarks invisible image.png -o clean.png --humanize 4.0 --unsharp 0 # Strength is vendor-adaptive by default (OpenAI 0.10 / Google 0.15); override # with --strength. To preserve text/face structure, use --pipeline controlnet # Or let it choose: --auto picks the pipeline, face restore, and an adaptive polish -# from the image content (controlnet when there is text/structure, face restore when -# a face is present, polish that restores the input's detail level while sparing -# text). Every choice is overridable: --pipeline, --no-restore-faces, -# --no-adaptive-polish all win over the auto pick. Experimental. +# from the image content (controlnet when there is text/structure, polish that +# restores the input's detail level while sparing text). Every choice is +# overridable: --pipeline and --no-adaptive-polish win over the auto pick. +# Experimental. # (SDXL + canny ControlNet); tune preservation with --controlnet-scale. Add # Check / strip AI metadata (C2PA, EXIF, "Made with AI" labels) diff --git a/docs/synthid-robust-identity-research.md b/docs/synthid-robust-identity-research.md index e625135..bf28770 100644 --- a/docs/synthid-robust-identity-research.md +++ b/docs/synthid-robust-identity-research.md @@ -1,5 +1,12 @@ # SynthID-robust face identity for an SDXL removal pipeline (research) +> **Status (2026-06-08): retired.** Every approach described below was empirically +> tested and rejected -- see `docs/synthid-robust-identity-research-2026-06-08.md` +> "Empirical follow-up" for the final conclusion. The library no longer ships any +> face-restore extra: the cleaned image from the main controlnet 0.20 pass is the +> least-AI face state we can reach without re-introducing SynthID. This document +> is kept as historical record of the exploration. + **Question.** Which face identity-preservation mechanism for an SDXL img2img + canny-ControlNet watermark-removal pipeline (denoise 0.20-0.30) is BOTH (a) commercial-safe end-to-end and (b) does not re-introduce the SynthID pixel diff --git a/pyproject.toml b/pyproject.toml index 3e09439..d032747 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -76,52 +76,6 @@ lama = [ "onnxruntime>=1.16.0", "huggingface-hub>=0.20.0", ] -# **NON-COMMERCIAL.** Optional PhotoMaker-V2 SynthID-robust face-identity post-pass. -# PhotoMaker-V2's ID encoder pulls an InsightFace ArcFace embedding at runtime, and -# the pretrained InsightFace model packs (antelopev2, buffalo_l) are released under a -# non-commercial / research-only license. A paid service (raiw.cc, any monetized SaaS) -# MUST NOT use this extra. See `src/remove_ai_watermarks/photomaker_restore.py` and -# `docs/synthid-robust-identity-research.md`. The PhotoMaker adapter weights -# (photomaker-v2.bin) are Apache-2.0 and download on first use; the InsightFace model -# packs download on first FaceAnalysis() (only triggered inside PhotoMaker's V2 forward). -# Pins beyond the upstream PhotoMaker package itself patch missing declarations that -# would otherwise break the load chain (verified empirically via the Modal cert sweep -# 2026-06-04): einops (used in forward), peft (required by diffusers.fuse_lora), -# onnxruntime (transitive via insightface), and insightface itself (required for the -# package's __init__.py to even import). -photomaker = [ - "photomaker @ git+https://github.com/TencentARC/PhotoMaker.git", - "huggingface-hub>=0.20.0", - "einops>=0.7.0", - "insightface>=0.7.3", - "onnxruntime>=1.16.0", - "peft>=0.10.0", -] -# **NON-COMMERCIAL.** Optional InstantID SynthID-robust face-identity post-pass. -# InstantID adapter weights (IdentityNet ControlNet + ip-adapter.bin) are Apache-2.0 -# from InstantX/InstantID on HuggingFace, BUT the runtime depends on InsightFace's -# antelopev2 ArcFace pack (non-commercial / research-only). InstantX's maintainers -# explicitly acknowledged this on HF (discussion #2) and stated intent to retrain -# on commercial embedders -- as of the 2026-06-08 deep-research synthesis -# (docs/synthid-robust-identity-research-2026-06-08.md) that retrain has not -# shipped. A paid service (raiw.cc, any monetized SaaS) MUST NOT use this extra. -# See `src/remove_ai_watermarks/instantid_restore.py`. -# -# Compared to the `photomaker` extra: InstantID adds spatial landmark conditioning -# alongside the ArcFace semantic branch, giving stronger identity fidelity on -# single portraits per the InstantID paper (arXiv:2401.07519). Both extras are -# non-commercial; pick `instantid` by default for better identity, `photomaker` -# when the InstantID community pipeline can't load. -# -# Loads via diffusers' community-pipeline mechanism (no upstream `instantid` -# Python package on PyPI). Only direct deps are insightface (MIT code, the -# non-commercial blocker is its MODEL packs) + onnxruntime (transitive via -# insightface) + huggingface-hub (weights download). -instantid = [ - "insightface>=0.7.3", - "onnxruntime>=1.16.0", - "huggingface-hub>=0.20.0", -] # Optional pre-diffusion super-resolution for small inputs (Real-ESRGAN). Loaded via # spandrel (MIT) -- a pure model-loader with NO basicsr dependency (it pulls only # torch / torchvision / safetensors / numpy / einops). @@ -180,12 +134,6 @@ Repository = "https://github.com/wiltodelta/remove-ai-watermarks" requires = ["hatchling<1.31"] build-backend = "hatchling.build" -# Allow the `photomaker` extra to reference the upstream git URL directly (the -# TencentARC/PhotoMaker package is not on PyPI). The extra itself is NON-COMMERCIAL -# (see the photomaker block above and `photomaker_restore.py`). -[tool.hatch.metadata] -allow-direct-references = true - [tool.hatch.build.targets.wheel] packages = ["src/remove_ai_watermarks"] diff --git a/src/remove_ai_watermarks/auto_config.py b/src/remove_ai_watermarks/auto_config.py index 61c9e02..641552d 100644 --- a/src/remove_ai_watermarks/auto_config.py +++ b/src/remove_ai_watermarks/auto_config.py @@ -85,7 +85,6 @@ class AutoConfig: """Resolved quality modes from content analysis (the ``--auto`` plan).""" pipeline: str # "default" | "controlnet" - restore_faces: bool adaptive_polish: bool # restore the input's detail level (sharpen + masked grain), sparing text unsharp: float # fixed-polish knobs, 0 in auto (the adaptive polish replaces them) humanize: float @@ -104,14 +103,13 @@ class AutoConfig: if self.has_text: bits.append("text") bits.append(f"edges={self.edge_density:.3f}") - rf = ", face-restore on" if self.restore_faces else "" if self.adaptive_polish: polish = ", adaptive polish" elif self.unsharp or self.humanize: polish = f", unsharp {self.unsharp}/grain {self.humanize}" else: polish = "" - return f"{'+'.join(bits)} -> {self.pipeline} pipeline{rf}{polish}" + return f"{'+'.join(bits)} -> {self.pipeline} pipeline{polish}" def _to_bgr(image: NDArray[Any]) -> NDArray[Any]: @@ -251,12 +249,10 @@ def plan(image_path: Path) -> AutoConfig | None: structureless = (not has_face) and (not has_text) and edges < _STRUCTURELESS_EDGE_MAX pipeline = "default" if structureless else "controlnet" - restore_faces = has_face - smoothing = pipeline == "controlnet" or restore_faces + smoothing = pipeline == "controlnet" cfg = AutoConfig( pipeline=pipeline, - restore_faces=restore_faces, adaptive_polish=smoothing, # adaptive (detail-targeted) polish when a smoothing pass ran unsharp=0.0, humanize=0.0, diff --git a/src/remove_ai_watermarks/cli.py b/src/remove_ai_watermarks/cli.py index 06465b0..72a9533 100644 --- a/src/remove_ai_watermarks/cli.py +++ b/src/remove_ai_watermarks/cli.py @@ -174,7 +174,7 @@ _auto_option = click.option( is_flag=True, default=False, help="Auto-pick the pipeline, face restore, and adaptive polish from image content. " - "Every choice is overridable -- an explicit --pipeline / --restore-faces / --adaptive-polish " + "Every choice is overridable -- an explicit --pipeline / --adaptive-polish " "always wins. EXPERIMENTAL.", ) @@ -192,9 +192,8 @@ def _apply_auto( ctx: click.Context, source: Path, pipeline: str, - restore_faces: bool, adaptive_polish: bool, -) -> tuple[str, bool, bool]: +) -> tuple[str, bool]: """Resolve ``--auto``: plan the three content-adaptive modes (pipeline, face restore, adaptive polish) from the image, overriding only the ones the user left at their default (an explicit flag always wins). The fixed ``--unsharp``/ @@ -205,19 +204,17 @@ def _apply_auto( cfg = auto_config.plan(source) if cfg is None: console.print(" Auto: could not read image; using defaults") - return pipeline, restore_faces, adaptive_polish + return pipeline, adaptive_polish def _is_default(name: str) -> bool: return ctx.get_parameter_source(name) == click.core.ParameterSource.DEFAULT if _is_default("pipeline"): pipeline = cfg.pipeline - if _is_default("restore_faces"): - restore_faces = cfg.restore_faces if _is_default("adaptive_polish"): adaptive_polish = cfg.adaptive_polish console.print(f" Auto: {cfg.reason}") - return pipeline, restore_faces, adaptive_polish + return pipeline, adaptive_polish def _warn_if_esrgan_unavailable(upscaler: str) -> None: @@ -235,43 +232,6 @@ def _warn_if_esrgan_unavailable(upscaler: str) -> None: console.print(" Note: --upscaler esrgan needs the 'esrgan' extra; falling back to Lanczos.") -def _restore_faces_options(f: Any) -> Any: - """Attach the face-restoration flags to an invisible-pipeline command. - - Both methods REGENERATE the face from an ArcFace embedding via SDXL diffusion - -- they do NOT recover original pixels. Every output face pixel is - diffusion-fresh, so the regenerated face inherently looks MORE AI-generated - than the cleaned image (gloss, symmetric pores, SDXL "clean skin" - aesthetic). For production face preservation, leave the flag OFF and use - the cleaned image as-is. The two methods are kept for research / personal - use where users explicitly want identity regeneration. **BOTH are - NON-COMMERCIAL**: they pull InsightFace antelopev2 / buffalo_l model packs - which are research-only. A paid service (raiw.cc, any monetized SaaS) MUST - NOT use this flag. - """ - method = click.option( - "--restore-faces-method", - type=click.Choice(["instantid", "photomaker"]), - default="instantid", - help="Face-regeneration mechanism (no method recovers original pixels; both " - "REGENERATE the face via SDXL). 'instantid' (default) uses InstantID img2img on " - "the cleaned crop with ArcFace + landmark ControlNet. 'photomaker' uses " - "PhotoMaker-V2 txt2img + CLIP+ArcFace dual encoder. **BOTH are NON-COMMERCIAL** " - "(InsightFace antelopev2 / buffalo_l packs are research-only). For personal / " - "research use only.", - )(f) - return click.option( - "--restore-faces/--no-restore-faces", - default=False, - help="EXPERIMENTAL, opt-in, **NON-COMMERCIAL**. **REGENERATES the face** (does " - "NOT recover original pixels) via the chosen --restore-faces-method; the " - "regenerated face looks more AI-generated than the cleaned image. Off by " - "default; auto-skips when no face is detected or the chosen extra is absent. " - "For production face preservation leave this OFF and use the cleaned image " - "as-is.", - )(method) - - def _watermark_region(det: DetectionResult, width: int, height: int) -> tuple[int, int, int, int]: """Pick a watermark bbox: detector's region if confident, else the default config slot.""" if det.confidence > 0.15: @@ -597,7 +557,6 @@ def cmd_erase( help="Cap long side (px) before diffusion; 0 = native (best quality, like raiw.cc). Raise only on GPU/MPS OOM.", ) @_controlnet_scale_option -@_restore_faces_options @_min_resolution_option @_unsharp_option @_upscaler_option @@ -619,8 +578,6 @@ def cmd_invisible( max_resolution: int, min_resolution: int, controlnet_scale: float, - restore_faces: bool, - restore_faces_method: str, upscaler: str, auto: bool, adaptive_polish: bool, @@ -643,7 +600,7 @@ def cmd_invisible( source = _validate_image(source) _warn_if_esrgan_unavailable(upscaler) if auto: - pipeline, restore_faces, adaptive_polish = _apply_auto(ctx, source, pipeline, restore_faces, adaptive_polish) + pipeline, adaptive_polish = _apply_auto(ctx, source, pipeline, adaptive_polish) if output is None: output = source.with_stem(source.stem + "_clean") @@ -682,8 +639,6 @@ def cmd_invisible( min_resolution=min_resolution, upscaler=upscaler, vendor=vendor, - restore_faces=restore_faces, - restore_faces_method=restore_faces_method, ) elapsed = time.monotonic() - t0 @@ -859,7 +814,6 @@ def cmd_identify(ctx: click.Context, source: Path, no_visible: bool, as_json: bo help="Cap long side (px) before diffusion; 0 = native (best quality, like raiw.cc). Raise only on GPU/MPS OOM.", ) @_controlnet_scale_option -@_restore_faces_options @_min_resolution_option @_unsharp_option @_upscaler_option @@ -884,8 +838,6 @@ def cmd_all( max_resolution: int, min_resolution: int, controlnet_scale: float, - restore_faces: bool, - restore_faces_method: str, upscaler: str, auto: bool, adaptive_polish: bool, @@ -905,7 +857,7 @@ def cmd_all( source = _validate_image(source) _warn_if_esrgan_unavailable(upscaler) if auto: - pipeline, restore_faces, adaptive_polish = _apply_auto(ctx, source, pipeline, restore_faces, adaptive_polish) + pipeline, adaptive_polish = _apply_auto(ctx, source, pipeline, adaptive_polish) if output is None: output = source.with_stem(source.stem + "_clean") @@ -993,8 +945,6 @@ def cmd_all( min_resolution=min_resolution, upscaler=upscaler, vendor=vendor, - restore_faces=restore_faces, - restore_faces_method=restore_faces_method, ) console.print(" Invisible watermark removed") @@ -1049,8 +999,6 @@ def _process_batch_image( unsharp: float = 0.0, max_resolution: int = 0, min_resolution: int = 1024, - restore_faces: bool = False, - restore_faces_method: str = "instantid", controlnet_scale: float = 1.0, upscaler: str = "lanczos", auto: bool = False, @@ -1104,9 +1052,7 @@ def _process_batch_image( # pipeline choice changes the engine ctor, so cache one engine per pipeline # (controlnet vs default) rather than a single shared instance. if auto: - pipeline, restore_faces, adaptive_polish = _apply_auto( - ctx, img_path, pipeline, restore_faces, adaptive_polish - ) + pipeline, adaptive_polish = _apply_auto(ctx, img_path, pipeline, adaptive_polish) engines = ctx.obj.setdefault("_inv_engines", {}) if pipeline not in engines: engines[pipeline] = InvisibleEngine( @@ -1128,8 +1074,6 @@ def _process_batch_image( max_resolution=max_resolution, min_resolution=min_resolution, upscaler=upscaler, - restore_faces=restore_faces, - restore_faces_method=restore_faces_method, # Detect the vendor from the pristine original (`img_path`), not the # visible-processed `out_path` whose C2PA is already gone. vendor=vendor_for_strength(img_path), @@ -1187,7 +1131,6 @@ def _process_batch_image( default=0, help="Cap long side (px) before diffusion; 0 = native (best quality, like raiw.cc). Raise only on GPU/MPS OOM.", ) -@_restore_faces_options @_min_resolution_option @_unsharp_option @_upscaler_option @@ -1211,8 +1154,6 @@ def cmd_batch( unsharp: float, max_resolution: int, min_resolution: int, - restore_faces: bool, - restore_faces_method: str, controlnet_scale: float, upscaler: str, auto: bool, @@ -1271,8 +1212,6 @@ def cmd_batch( unsharp=unsharp, max_resolution=max_resolution, min_resolution=min_resolution, - restore_faces=restore_faces, - restore_faces_method=restore_faces_method, controlnet_scale=controlnet_scale, upscaler=upscaler, auto=auto, diff --git a/src/remove_ai_watermarks/instantid_restore.py b/src/remove_ai_watermarks/instantid_restore.py deleted file mode 100644 index 1a38292..0000000 --- a/src/remove_ai_watermarks/instantid_restore.py +++ /dev/null @@ -1,593 +0,0 @@ -"""SynthID-robust face identity restoration via InstantID. - -**NON-COMMERCIAL.** InstantID's runtime depends on the InsightFace ``antelopev2`` -ArcFace model pack, which InsightFace releases under a research-only license: - - "The training data containing the annotation (and the models trained with - these data) are available for non-commercial research purposes only." - -- insightface upstream README - -The InstantX maintainers themselves acknowledged on HuggingFace -(``InstantX/InstantID`` discussion #2) that "InstantID cannot be Apache 2.0 if it -is using Insight Face" and stated intent to retrain on commercial face encoders. -As of 2026-06-08 (deep-research synthesis in -``docs/synthid-robust-identity-research-2026-06-08.md``) that retrain has not -shipped. **A paid service (raiw.cc, any monetized SaaS) MUST NOT use this path.** - -The default ``--restore-faces-method`` is ``instantid`` (this module). The -alternative ``photomaker`` is also non-commercial. There is no commercial-safe -ArcFace-grade identity-preservation stack for SDXL today. - -Architecture (vs the earlier txt2img variant): -- The earlier (txt2img) integration generated each face from scratch in a fresh - 1024 scene with InstantID's standard pipeline. That produced studio-portrait - faces with the wrong lighting / head angle for the surrounding scene; on - group photos the per-face composites read as patchwork even after color - matching and elliptical alphas. -- This (img2img on cleaned) integration feeds the CLEANED face crop as the - img2img source. Diffusion sees the scene context (shoulders, hair edges, - lighting, shadow direction) directly and harmonises the regenerated face - with it. Identity still comes through the ArcFace embedding + - landmark-ControlNet, which are semantic / pure-geometry and carry no - watermark. - -SynthID safety (load-bearing for raiw.cc): -- img2img source = CLEANED crop. Cleaned image is already oracle-verified - SynthID-free at our controlnet strength; cropping is a subset operation that - preserves that property. -- ArcFace embedding = from the ORIGINAL face crop (sharper identity, but the - embedding is semantic 512-d, no pixel content). -- Landmark stick figure = pure colour-coded geometry rendered from kps; no - source pixels. -- img2img diffusion adds noise to the cleaned source then denoises with - ControlNet + IP-Adapter conditioning. Any residual high-frequency pattern - in the cleaned crop is destroyed by that noise injection at the strengths we - use. -- We must NEVER feed the original image as img2img source (would re-introduce - SynthID outside the diffusion footprint at strength < 1). The code only ever - reads pixels from ``cleaned_bgr`` into ``image=`` -- the original is used - for the embedding + kps only. - -Pipeline this module wires: - 1. Detect faces in the CLEANED image (YuNet via ``auto_config``). - 2. For each face: square-crop the SAME box from BOTH the original (for - ArcFace + kps) and the cleaned image (for img2img source). Resize both - to 1024x1024. - 3. Render the kps as a stick figure (the ControlNet conditioning image). - 4. Call the InstantID img2img pipeline - (``StableDiffusionXLInstantIDImg2ImgPipeline``) with ``image`` = cleaned - crop, ``control_image`` = landmark, ``image_embeds`` = ArcFace, and - ``strength`` = ~0.55. The output 1024 is a face that fits the scene. - 5. Elliptical-alpha + colour-match composite into the cleaned image. - -Requires the optional ``instantid`` extra: ``pip install -'remove-ai-watermarks[instantid]'``. Weights download on first use; the -upstream img2img pipeline file (not on PyPI) is cached from -``raw.githubusercontent.com`` on first run. -""" - -# cv2/torch/diffusers boundary: relax unknown-type rules for this file only. -# pyright: reportUnknownMemberType=false, reportUnknownArgumentType=false, reportUnknownVariableType=false, reportUnknownParameterType=false, reportMissingTypeArgument=false, reportMissingTypeStubs=false, reportMissingImports=false, reportArgumentType=false, reportAssignmentType=false, reportReturnType=false, reportCallIssue=false, reportIndexIssue=false, reportOperatorIssue=false, reportOptionalMemberAccess=false, reportOptionalCall=false, reportOptionalSubscript=false, reportOptionalOperand=false, reportAttributeAccessIssue=false, reportPrivateImportUsage=false, reportPrivateUsage=false, reportInvalidTypeForm=false, reportConstantRedefinition=false, reportUnnecessaryComparison=false -from __future__ import annotations - -import importlib.util -import logging -import threading -from pathlib import Path -from typing import TYPE_CHECKING, Any - -from remove_ai_watermarks.photomaker_restore import _face_crop_square - -if TYPE_CHECKING: - from numpy.typing import NDArray - -logger = logging.getLogger(__name__) - -# InstantID checkpoint repo on HuggingFace. The IdentityNet ControlNet weights live -# under ``ControlNetModel/`` and the IP-Adapter file is ``ip-adapter.bin`` at the -# root. Both are Apache-2.0 (the InsightFace runtime dep is what makes the path -# non-commercial). Downloaded on first use. -_INSTANTID_REPO = "InstantX/InstantID" -_INSTANTID_CONTROLNET_SUBFOLDER = "ControlNetModel" -_INSTANTID_IP_ADAPTER = "ip-adapter.bin" - -# Upstream InstantID img2img pipeline source. Not on PyPI, not on HF Hub at any path -# diffusers can auto-load -- the file lives in the InstantID GitHub repo. We download -# it once to a cache dir and pass it as ``custom_pipeline=`` to diffusers. -_INSTANTID_IMG2IMG_URL = ( - "https://raw.githubusercontent.com/instantX-research/InstantID/" - "main/pipeline_stable_diffusion_xl_instantid_img2img.py" -) - -# SDXL base shared with the main pipeline (same checkpoint as `default`/`controlnet`). -_SDXL_MODEL_ID = "stabilityai/stable-diffusion-xl-base-1.0" - -# Prompt format. InstantID is less sensitive to prompt than PhotoMaker because the -# ID branch is cross-attention; a neutral descriptive prompt is recommended by the -# upstream gradio demo. -_INSTANTID_PROMPT = "portrait photo of a person, natural skin, soft lighting, sharp focus, best quality" -_INSTANTID_NEGATIVE = ( - "(asymmetry, worst quality, low quality, illustration, 3d, 2d, painting, " - "cartoons, sketch), open mouth, blurry, watermark, deformed" -) - -# Square size used to feed InstantID. SDXL is happiest at 1024 (a smaller value sends -# it into low-res mosaic mode -- caught visually on PhotoMaker, same root cause). -_INSTANTID_FACE_SIZE = 1024 - -_pipeline: Any | None = None -_pipeline_lock = threading.Lock() -_face_analyser: Any | None = None -_face_analyser_lock = threading.Lock() - - -def is_available() -> bool: - """True when the optional InstantID extra deps are importable.""" - return ( - importlib.util.find_spec("insightface") is not None - and importlib.util.find_spec("diffusers") is not None - and importlib.util.find_spec("torch") is not None - and importlib.util.find_spec("huggingface_hub") is not None - ) - - -def _select_device() -> str: - """Pick the InstantID pipeline device: CUDA when present, MPS on Apple, else CPU.""" - try: - import torch - - if torch.cuda.is_available(): - return "cuda" - if torch.backends.mps.is_available(): - return "mps" - except Exception as e: - logger.debug("instantid_restore: device probe failed (%s); using CPU", e) - return "cpu" - - -def _fetch_img2img_pipeline_file() -> Path: - """Cache the InstantID img2img pipeline source file locally on first use. - - The file lives in the InstantX GitHub repo (not on PyPI, not on HF Hub at any - path diffusers can auto-load). We fetch the raw URL once into the package's - HuggingFace cache so subsequent loads hit disk. Returns the path to feed to - ``DiffusionPipeline.from_pretrained(custom_pipeline=...)``. - """ - import os - import urllib.request - - cache_root = Path(os.environ.get("HF_HOME") or Path.home() / ".cache" / "huggingface") - cache_dir = cache_root / "remove_ai_watermarks" / "instantid" - cache_dir.mkdir(parents=True, exist_ok=True) - target = cache_dir / "pipeline_stable_diffusion_xl_instantid_img2img.py" - if not target.exists() or target.stat().st_size < 50_000: - logger.info("instantid_restore: fetching img2img pipeline source from %s", _INSTANTID_IMG2IMG_URL) - urllib.request.urlretrieve(_INSTANTID_IMG2IMG_URL, target) # noqa: S310 (HTTPS pinned) - return target - - -def _ensure_antelopev2(root: Path) -> Path: - """Materialize the antelopev2 pack at ``/models/antelopev2/`` if absent. - - InsightFace's built-in auto-download points at - ``github.com/deepinsight/insightface/releases/download/v0.7/antelopev2.zip`` - which has been broken since at least 2024 (verified upstream issue #2517, - #2766; explicitly called out in InstantID's README: "manually download via - this URL to models/antelopev2 as the default link is invalid"). Without the - five expected ``.onnx`` files in place, ``FaceAnalysis.prepare()`` errors - with ``assert 'detection' in self.models``. - - We side-step the broken default by fetching the five files from a HuggingFace - mirror (``kidyu/antelopev2-for-InstantID-ComfyUI``) on first use. Returns the - target directory containing the .onnx files. - """ - from huggingface_hub import hf_hub_download - - target = root / "models" / "antelopev2" - target.mkdir(parents=True, exist_ok=True) - files = [ - "1k3d68.onnx", - "2d106det.onnx", - "genderage.onnx", - "glintr100.onnx", - "scrfd_10g_bnkps.onnx", - ] - for fname in files: - dest = target / fname - if dest.exists() and dest.stat().st_size > 0: - continue - logger.info("instantid_restore: fetching antelopev2/%s from HF mirror", fname) - path = hf_hub_download(repo_id="kidyu/antelopev2-for-InstantID-ComfyUI", filename=fname) - # hf_hub_download caches under HF_HOME; symlink (or copy) into the - # InsightFace-expected layout. - if not dest.exists(): - try: - dest.symlink_to(path) - except OSError: - import shutil - - shutil.copy(path, dest) - return target - - -def _get_face_analyser() -> Any: - """Return the InsightFace FaceAnalysis singleton (antelopev2, non-commercial). - - Pre-downloads the antelopev2 pack from a HuggingFace mirror (the InsightFace - auto-download is broken). See the NON-COMMERCIAL notice at the top of the - module. - """ - global _face_analyser - if _face_analyser is not None: - return _face_analyser - with _face_analyser_lock: - if _face_analyser is None: - import torch - from insightface.app import FaceAnalysis - - providers = ["CUDAExecutionProvider"] if torch.cuda.is_available() else ["CPUExecutionProvider"] - # InstantID's upstream uses name='antelopev2' and root='./'. Materialise - # the pack at the same place so FaceAnalysis finds it locally. - root = Path.cwd() - _ensure_antelopev2(root) - fa = FaceAnalysis(name="antelopev2", root=str(root), providers=providers) - fa.prepare(ctx_id=0, det_size=(640, 640)) - _face_analyser = fa - return _face_analyser - - -def _get_pipeline() -> Any: - """Return the lazily-built InstantID pipeline singleton (downloads weights on first use). - - Loads via diffusers' community-pipeline mechanism: the file - ``pipeline_stable_diffusion_xl_instantid.py`` lives in - ``diffusers/examples/community/`` and is selected by the slug - ``pipeline_stable_diffusion_xl_instantid``. - """ - global _pipeline - if _pipeline is not None: - return _pipeline - with _pipeline_lock: - if _pipeline is None: - import torch - from diffusers import ControlNetModel, DiffusionPipeline - from huggingface_hub import hf_hub_download - - device = _select_device() - dtype = torch.float16 if device == "cuda" else torch.float32 - logger.info("instantid_restore: loading SDXL+InstantID img2img on %s (%s)", device, dtype) - - # IdentityNet ControlNet weights. - controlnet = ControlNetModel.from_pretrained( - _INSTANTID_REPO, - subfolder=_INSTANTID_CONTROLNET_SUBFOLDER, - torch_dtype=dtype, - ) - # Upstream InstantID img2img pipeline (StableDiffusionXLInstantIDImg2ImgPipeline). - # Lets us feed the cleaned face crop as the diffusion source so the regenerated - # face inherits scene lighting / shadows / head angle from the cleaned context - # (vs the txt2img variant which generates a studio portrait from scratch). - # Critical SynthID-safety property: the ``image`` arg MUST be the CLEANED crop, - # never the original -- the original carries the watermark and img2img at - # strength < 1 preserves some input pixel structure. The ArcFace embedding is - # semantic (no pixel content), so taking it from the original is fine. - pipe = DiffusionPipeline.from_pretrained( - _SDXL_MODEL_ID, - controlnet=controlnet, - torch_dtype=dtype, - custom_pipeline=str(_fetch_img2img_pipeline_file()), - # Custom_pipeline from a local .py file triggers diffusers' remote-code - # guard; the file is fetched from a pinned raw.githubusercontent URL - # we control, so opt in here. Without this the load silently falls - # back to a default pipeline (no img2img + no IP-Adapter cross-attn), - # the next call hits an AttributeError on load_ip_adapter_instantid, - # and our outer except logs but skips the whole restore. - trust_remote_code=True, - ) - pipe.to(device) - # IP-Adapter weights that wire the ArcFace embedding into cross-attention. - ip_adapter_path = hf_hub_download(repo_id=_INSTANTID_REPO, filename=_INSTANTID_IP_ADAPTER) - # IP-Adapter scale = weight on the ArcFace cross-attention. The upstream - # demo uses 0.8 for txt2img; for img2img-on-cleaned we push to 1.0 because - # the cleaned face crop is competing as identity prior and we want ArcFace - # to dominate (otherwise the regenerated face inherits the controlnet- - # drifted cleaned face, not the original identity). - pipe.load_ip_adapter_instantid(ip_adapter_path, scale=1.0) - # Diffusers 0.38 vs InstantID upstream compat patch: InstantID's __call__ - # calls ``self.check_inputs(...)`` POSITIONALLY (signature from ~v0.29), - # but diffusers 0.38 added two new params (``ip_adapter_image``, - # ``ip_adapter_image_embeds``) BEFORE ``controlnet_conditioning_scale`` in - # the parent's signature. That shifts every argument by two, so - # ``control_guidance_end`` (which InstantID converts to ``[1.0]`` for the - # single-controlnet case before this point) lands in the slot the parent - # validates as ``controlnet_conditioning_scale`` and trips - # ``TypeError("must be type float")``. Our inputs are programmatic and - # already validated by our own callers, so neutralising the check is safe. - pipe.check_inputs = lambda *_a, **_k: None - _pipeline = pipe - return _pipeline - - -def _draw_kps(image_size: tuple[int, int], kps: Any) -> Any: - """Render the 5 facial keypoints as a colored stick figure. - - Mirrors upstream's ``draw_kps`` (in ``pipeline_stable_diffusion_xl_instantid.py``): - the 5 keypoints (left eye, right eye, nose tip, left mouth corner, right mouth - corner) get drawn as colored circles connected by colored lines, on a black - background. The result is the ControlNet conditioning image -- pure landmark - geometry, no pixels from the original face leak through this branch. - - ``image_size`` is ``(width, height)``; ``kps`` is a numpy array of shape (5, 2). - """ - import cv2 - import numpy as np - from PIL import Image - - # Same color palette as upstream (blue/red/green/purple/yellow). - stick_width = 4 - limb_seq = np.array([[0, 2], [1, 2], [3, 2], [4, 2]]) - color_list = [ - (255, 0, 0), - (0, 255, 0), - (0, 0, 255), - (255, 255, 0), - (255, 0, 255), - ] - - w, h = image_size - out_img = np.zeros((h, w, 3), dtype=np.uint8) - - kps_arr = np.array(kps) - for i in range(len(limb_seq)): - index = limb_seq[i] - color = color_list[index[0]] - x = kps_arr[index][:, 0] - y = kps_arr[index][:, 1] - length = ((x[0] - x[1]) ** 2 + (y[0] - y[1]) ** 2) ** 0.5 - angle = np.degrees(np.arctan2(y[0] - y[1], x[0] - x[1])) - polygon = cv2.ellipse2Poly( - (int(np.mean(x)), int(np.mean(y))), - (int(length / 2), stick_width), - int(angle), - 0, - 360, - 1, - ) - out_img = cv2.fillConvexPoly(out_img.copy(), polygon, color) - out_img = (out_img * 0.6).astype(np.uint8) - - for i, kp in enumerate(kps_arr): - x, y = kp - out_img = cv2.circle(out_img.copy(), (int(x), int(y)), 10, color_list[i], -1) - - return Image.fromarray(out_img.astype(np.uint8)) - - -def restore_faces_instantid( - original_bgr: NDArray[Any], - cleaned_bgr: NDArray[Any], - num_inference_steps: int = 30, - guidance_scale: float = 5.0, - controlnet_conditioning_scale: float = 1.0, - img2img_strength: float = 0.7, - seed: int | None = None, - detect_faces_fn: Any | None = None, -) -> NDArray[Any]: - """SynthID-robust face identity restoration via InstantID. - - Flow: - 1. Detect faces in ``cleaned_bgr`` (YuNet via ``auto_config`` by default; - override via ``detect_faces_fn`` for tests). - 2. For each face: square-crop the SAME box from BOTH images (original -> - ArcFace + kps; cleaned -> img2img source). Resize both to 1024. - 3. Render kps as a landmark stick figure (the ControlNet conditioning). - 4. Run InstantID img2img: ``image`` = cleaned crop, ``control_image`` = - landmark, ``image_embeds`` = ArcFace embedding from the original. - 5. Elliptical-alpha + colour-match composite into the cleaned image. - - SynthID safety: ``image`` is the CLEANED crop (already oracle-clean); the - original is read for the embedding and kps only (semantic / geometry, no - pixel content). See the module docstring. - - ``detect_faces_fn`` returns a list of ``(x, y, w, h)`` boxes given a BGR image. - """ - import cv2 - import numpy as np - import torch - - if detect_faces_fn is None: - from pathlib import Path - - from remove_ai_watermarks import auto_config as _ac - - def _default_detect(bgr: NDArray[Any]) -> list[tuple[int, int, int, int]]: - h_d, w_d = bgr.shape[:2] - model = Path(_ac.__file__).parent / "assets" / "face_detection_yunet_2023mar.onnx" - det = cv2.FaceDetectorYN.create(str(model), "", (w_d, h_d), _ac._FACE_SCORE, 0.3, 5000) - det.setInputSize((w_d, h_d)) - _, faces = det.detect(bgr) - if faces is None: - return [] - return [(int(f[0]), int(f[1]), int(f[2]), int(f[3])) for f in faces if int(f[2]) > 0 and int(f[3]) > 0] - - detect_faces_fn = _default_detect - - boxes = detect_faces_fn(cleaned_bgr) - if not boxes: - logger.debug("instantid_restore: no faces detected; returning cleaned image unchanged") - return cleaned_bgr - - pipeline = _get_pipeline() - face_analyser = _get_face_analyser() - - generator = None - if seed is not None: - generator = torch.Generator(device=pipeline.device).manual_seed(seed) - - h_c, w_c = cleaned_bgr.shape[:2] - restored: list[tuple[NDArray[Any], tuple[int, int, int, int]]] = [] - for box in boxes: - # Square crop with the SAME geometry from both the original (-> ArcFace - # embedding + landmark kps -- semantic / pure-geometry, SynthID can't ride - # either) AND the cleaned image (-> img2img source -- SynthID-safe because - # the cleaned image is already oracle-verified clean and any residual - # high-frequency pattern would be destroyed by the noise injection at our - # strength setting). _face_crop_square gives a 2x-padded square box around - # the face -- enough scene context so the img2img harmonises lighting and - # head angle with the surroundings. - original_crop_bgr, square_box = _face_crop_square(original_bgr, box) - sx1, sy1, sx2, sy2 = square_box - sx1c, sy1c = max(0, sx1), max(0, sy1) - sx2c, sy2c = min(w_c, sx2), min(h_c, sy2) - if original_crop_bgr.size == 0 or sx2c <= sx1c or sy2c <= sy1c: - continue - cleaned_crop_bgr = cleaned_bgr[sy1c:sy2c, sx1c:sx2c] - if cleaned_crop_bgr.shape[:2] != original_crop_bgr.shape[:2]: - # Edge effect at image border -- pad cleaned crop to match the original - # crop dimensions so InsightFace / the pipeline see the same shape. - cleaned_crop_bgr = cv2.resize( - cleaned_crop_bgr, - (original_crop_bgr.shape[1], original_crop_bgr.shape[0]), - interpolation=cv2.INTER_LANCZOS4, - ) - - # Resize both crops to the SDXL working size. - original_resized = cv2.resize( - original_crop_bgr, (_INSTANTID_FACE_SIZE, _INSTANTID_FACE_SIZE), interpolation=cv2.INTER_LANCZOS4 - ) - cleaned_resized = cv2.resize( - cleaned_crop_bgr, (_INSTANTID_FACE_SIZE, _INSTANTID_FACE_SIZE), interpolation=cv2.INTER_LANCZOS4 - ) - - # ArcFace embedding + 5 kps from the ORIGINAL face (sharper identity). - face_infos = face_analyser.get(original_resized) - if not face_infos: - logger.debug("instantid_restore: InsightFace did not find a face in the crop; skipping") - continue - face_info = sorted( - face_infos, - key=lambda x: (x["bbox"][2] - x["bbox"][0]) * (x["bbox"][3] - x["bbox"][1]), - )[-1] - face_emb = face_info["embedding"] - face_kps = face_info["kps"] - - # Render the landmark stick figure at the same size as the generation target. - landmark_img = _draw_kps((_INSTANTID_FACE_SIZE, _INSTANTID_FACE_SIZE), face_kps) - - # img2img call: source = CLEANED crop (SynthID-safe), control = landmark - # geometry, identity = ArcFace embedding from original. Strength controls - # how much of the cleaned input structure survives -- low enough (~0.55) - # to keep the head angle / lighting / shoulders coherent with the rest of - # the cleaned image, high enough that the face pixels are diffusion-fresh - # and InstantID actually injects identity. - from PIL import Image - - cleaned_pil = Image.fromarray(cv2.cvtColor(cleaned_resized, cv2.COLOR_BGR2RGB)) - out = pipeline( - prompt=_INSTANTID_PROMPT, - negative_prompt=_INSTANTID_NEGATIVE, - image=cleaned_pil, - control_image=landmark_img, - image_embeds=face_emb, - strength=img2img_strength, - controlnet_conditioning_scale=controlnet_conditioning_scale, - num_inference_steps=num_inference_steps, - guidance_scale=guidance_scale, - generator=generator, - ) - gen_rgb = out.images[0] - gen_bgr = cv2.cvtColor(np.array(gen_rgb), cv2.COLOR_RGB2BGR) - - # gen_bgr is at _INSTANTID_FACE_SIZE x _INSTANTID_FACE_SIZE. It represents - # the 2x-padded square_box content as regenerated by img2img -- so the face - # in it sits at the same RELATIVE position as in the cleaned input (img2img - # preserves structure). Composite the whole square back into the square_box - # location -- the cleaned-canvas elliptical alpha will keep the cleaned - # background outside the face oval, and the img2img harmonisation handles - # the seam INSIDE the oval (which is just face-on-face transition between - # diffusion-output and cleaned). - target_box = (sx1c, sy1c, sx2c, sy2c) - gen_target = cv2.resize(gen_bgr, (sx2c - sx1c, sy2c - sy1c), interpolation=cv2.INTER_LANCZOS4) - restored.append((gen_target, target_box)) - - if not restored: - return cleaned_bgr - return _composite_faces_elliptical(cleaned_bgr, restored) - - -def _color_match(src_bgr: NDArray[Any], ref_bgr: NDArray[Any]) -> NDArray[Any]: - """Shift ``src_bgr`` mean colour to ``ref_bgr`` mean colour, per channel. - - Each face is regenerated by InstantID with its own SDXL noise -- the white - balance / mean tone drifts away from the surrounding scene (cool studio - light vs warm bar lighting). A per-channel mean-shift brings the face crop - into the same tonal range as the cleaned canvas where it lands. Contrast - and saturation are preserved (we don't rescale variance). - """ - import numpy as np - - src = src_bgr.astype(np.float32) - ref = ref_bgr.astype(np.float32) - if ref.size == 0: - return src_bgr - src_mean = src.mean(axis=(0, 1), keepdims=True) - ref_mean = ref.mean(axis=(0, 1), keepdims=True) - return np.clip(src - src_mean + ref_mean, 0, 255).astype(np.uint8) - - -def _composite_faces_elliptical( - base_bgr: NDArray[Any], - restored_crops: list[tuple[NDArray[Any], tuple[int, int, int, int]]], - feather_div: int = 5, -) -> NDArray[Any]: - """Composite face crops into ``base_bgr`` using an elliptical, feathered alpha. - - Two changes vs the simpler rectangular Gaussian feather: - - - **Inscribed face-shaped ellipse.** Axes are ``(0.32*bw, 0.42*bh)`` which - fits comfortably inside the 2x padded bbox (the face naturally occupies - the central ~50% of the bbox), covering the head silhouette without - clipping the forehead or chin. The bbox corners (which carry - regenerated-scene background pixels with a different tone per face) end - up at alpha=0 so the cleaned-image background stays intact -- this is - what eliminates multi-face patchwork on group photos. - - **Soft feather.** ``min(bw, bh) // 5`` -- about twice as soft as the - rectangular Gaussian, so the ellipse edge fades over a wider band into - the cleaned canvas, hiding any residual seam. - - Additionally, before compositing, ``_color_match`` shifts the regenerated - face's mean colour to match the cleaned canvas region it lands on -- this - removes the warm/cool tone clash that group photos showed. - """ - import cv2 - import numpy as np - - out = base_bgr.astype(np.float32) - h_b, w_b = base_bgr.shape[:2] - - for crop, (x1, y1, x2, y2) in restored_crops: - x1, y1 = max(0, x1), max(0, y1) - x2, y2 = min(w_b, x2), min(h_b, y2) - bw, bh = x2 - x1, y2 - y1 - if bw <= 0 or bh <= 0: - continue - resized = cv2.resize(crop, (bw, bh), interpolation=cv2.INTER_LANCZOS4) - # Tone match the regenerated face to the cleaned canvas it sits on. - ref_region = base_bgr[y1:y2, x1:x2] - resized = _color_match(resized, ref_region) - - alpha_crop = np.zeros((bh, bw), dtype=np.float32) - center = (bw // 2, bh // 2) - axes = (max(1, int(bw * 0.32)), max(1, int(bh * 0.42))) - cv2.ellipse(alpha_crop, center, axes, 0, 0, 360, 1.0, -1) - k = max(7, (min(bw, bh) // feather_div) | 1) - alpha_crop = cv2.GaussianBlur(alpha_crop, (k, k), 0) - - alpha_full = np.zeros((h_b, w_b), dtype=np.float32) - alpha_full[y1:y2, x1:x2] = alpha_crop - full_restored = np.zeros_like(out) - full_restored[y1:y2, x1:x2] = resized - a = alpha_full[:, :, None] - out = full_restored * a + out * (1.0 - a) - - return np.clip(out, 0, 255).astype(np.uint8) diff --git a/src/remove_ai_watermarks/invisible_engine.py b/src/remove_ai_watermarks/invisible_engine.py index 59af72b..fecf3a7 100644 --- a/src/remove_ai_watermarks/invisible_engine.py +++ b/src/remove_ai_watermarks/invisible_engine.py @@ -164,8 +164,6 @@ class InvisibleEngine: max_resolution: int = 0, min_resolution: int = 1024, vendor: str | None = None, - restore_faces: bool = False, - restore_faces_method: str = "instantid", unsharp: float = 0.0, adaptive_polish: bool = False, upscaler: str = "lanczos", @@ -181,20 +179,9 @@ class InvisibleEngine: guidance_scale: Classifier-free guidance scale. seed: Random seed for reproducibility. humanize: Intensity of Analog Humanizer film grain (0 = off). - restore_faces: EXPERIMENTAL, opt-in (default False). **NON-COMMERCIAL.** - Run the face-identity post-pass when faces are present. Method is - chosen by ``restore_faces_method`` -- ``instantid`` (default, - stronger identity, needs the ``instantid`` extra) or ``photomaker`` - (PhotoMaker-V2, needs the ``photomaker`` extra). Both extras pull - non-commercial InsightFace model packs. Auto-skips with a debug log - when the chosen extra is absent or no face is detected. See - ``instantid_restore.py`` / ``photomaker_restore.py``. - restore_faces_method: ``instantid`` (default) or ``photomaker``. Both - NON-COMMERCIAL; pick the one whose extra you've installed. unsharp: Final unsharp-mask sharpening strength (0 = off, default). - Applied last (after face restoration) to counter the soft, - over-smoothed look of the diffusion + restoration; ~0.5-0.8 is a - safe range, higher risks edge halos. + Applied last to counter the soft / over-smoothed look of the + diffusion pass; ~0.5-0.8 is a safe range, higher risks edge halos. adaptive_polish: When True (the --auto mode default), restore the input's detail level in the softened output instead of fixed unsharp/humanize: a capped unsharp + edge-masked grain targeting the input's Laplacian @@ -316,19 +303,7 @@ class InvisibleEngine: out_cv = cv2.resize(out_cv, orig_size, interpolation=cv2.INTER_LANCZOS4) image_io.imwrite(out_path, out_cv) - # Optional GFPGAN face-polish post-pass: sharpens and re-synthesizes each - # face from GFPGAN's StyleGAN2 prior, running on the DIFFUSION-CLEANED image - # (not the original) -- so SynthID is not re-introduced (the input pixels - # GFPGAN derives from are already SynthID-free). Auto-skips when faces are - # absent or the optional `restore` extra is not installed. - if restore_faces: - if restore_faces_method == "photomaker": - self._restore_faces_photomaker(out_path, image, seed) - else: - self._restore_faces_instantid(out_path, image, seed) - - # Final sharpening, LAST so it crisps the face-restored result too (a - # pre-restore sharpen would be smoothed back over by the face pass). + # Final sharpening. if unsharp > 0.0: import cv2 @@ -364,99 +339,6 @@ class InvisibleEngine: if _tmp_path.exists(): _tmp_path.unlink() - def _restore_faces_instantid( - self, - out_path: Path, - original_image: Any, - seed: int | None, - ) -> None: - """Run the InstantID face-identity post-pass on the cleaned ``out_path``. - - **NON-COMMERCIAL** (see ``instantid_restore.py``). InstantID conditions on - an ArcFace embedding (semantic) plus a landmark ControlNet (geometry, - content-free) -- no original face pixels enter the diffusion. Best-effort: - any failure (missing extra, model load, runtime error) logs a warning and - leaves the un-restored cleaned output in place. - """ - from remove_ai_watermarks import instantid_restore - - if not instantid_restore.is_available(): - logger.debug("restore_faces requested but the 'instantid' extra is not installed; skipping") - return - - try: - import cv2 - import numpy as np - - from remove_ai_watermarks import image_io - - cleaned_bgr = image_io.imread(out_path, cv2.IMREAD_COLOR) - if cleaned_bgr is None: - logger.warning("restore_faces: could not read cleaned output %s; skipping", out_path) - return - - original_rgb = original_image.convert("RGB") - original_bgr = cv2.cvtColor(np.array(original_rgb), cv2.COLOR_RGB2BGR) - cleaned_size = (cleaned_bgr.shape[1], cleaned_bgr.shape[0]) - if (original_bgr.shape[1], original_bgr.shape[0]) != cleaned_size: - original_bgr = cv2.resize(original_bgr, cleaned_size, interpolation=cv2.INTER_LANCZOS4) - - if self._progress_callback: - self._progress_callback("Restoring face identity (InstantID post-pass)...") - restored = instantid_restore.restore_faces_instantid(original_bgr, cleaned_bgr, seed=seed) - image_io.imwrite(out_path, restored) - except Exception as e: - logger.warning( - "restore_faces post-pass failed (%s: %s); keeping un-restored output", - type(e).__name__, - e, - exc_info=True, - ) - - def _restore_faces_photomaker( - self, - out_path: Path, - original_image: Any, - seed: int | None, - ) -> None: - """Run the PhotoMaker-V2 face-identity post-pass on the cleaned ``out_path``. - - **NON-COMMERCIAL** (see ``photomaker_restore.py``). PhotoMaker carries identity - in a CLIP+ArcFace embedding and regenerates fresh face pixels conditioned on - it, so the watermark is not transported. Best-effort: any failure (missing - extra, model load, runtime error) logs a warning and leaves the un-restored - cleaned output in place. - """ - from remove_ai_watermarks import photomaker_restore - - if not photomaker_restore.is_available(): - logger.debug("restore_faces requested but the 'photomaker' extra is not installed; skipping") - return - - try: - import cv2 - import numpy as np - - from remove_ai_watermarks import image_io - - cleaned_bgr = image_io.imread(out_path, cv2.IMREAD_COLOR) - if cleaned_bgr is None: - logger.warning("restore_faces: could not read cleaned output %s; skipping", out_path) - return - - original_rgb = original_image.convert("RGB") - original_bgr = cv2.cvtColor(np.array(original_rgb), cv2.COLOR_RGB2BGR) - cleaned_size = (cleaned_bgr.shape[1], cleaned_bgr.shape[0]) - if (original_bgr.shape[1], original_bgr.shape[0]) != cleaned_size: - original_bgr = cv2.resize(original_bgr, cleaned_size, interpolation=cv2.INTER_LANCZOS4) - - if self._progress_callback: - self._progress_callback("Restoring face identity (PhotoMaker-V2 post-pass)...") - restored = photomaker_restore.restore_faces_photomaker(original_bgr, cleaned_bgr, seed=seed) - image_io.imwrite(out_path, restored) - except Exception as e: - logger.warning("restore_faces post-pass failed (%s); keeping un-restored output", e) - def remove_watermark_batch( self, input_dir: Path, diff --git a/src/remove_ai_watermarks/photomaker_restore.py b/src/remove_ai_watermarks/photomaker_restore.py deleted file mode 100644 index 731389f..0000000 --- a/src/remove_ai_watermarks/photomaker_restore.py +++ /dev/null @@ -1,368 +0,0 @@ -"""SynthID-robust face identity restoration via PhotoMaker-V2. - -**NON-COMMERCIAL.** This module uses PhotoMaker-V2, whose ID encoder -(``PhotoMakerIDEncoder_CLIPInsightfaceExtendtoken``) requires an ArcFace embedding -from InsightFace's pretrained ``antelopev2`` / ``buffalo_l`` model packs. Those packs -are released by InsightFace under a **non-commercial / research-only license**: - - "The pretrained models we provided with this library are available for - non-commercial research purposes only." - -- insightface PyPI README - -The PyPI ``insightface`` package itself is MIT-licensed code, but the model weights -it downloads on first ``FaceAnalysis()`` are not commercial. **A paid service -(raiw.cc, any monetized SaaS, any enterprise deployment) MUST NOT use this path.** -The default ``--restore-faces`` method is ``gfpgan`` (commercial-safe, ships with -the ``restore`` extra); ``--restore-faces-method photomaker`` is an explicit opt-in -for non-commercial use only. See ``docs/synthid-robust-identity-research.md``. - -The diffusion removal pass scrubs the pixel watermark from the WHOLE image, including -faces, but lets faces drift in identity. PhotoMaker-V2 carries identity in two -semantic streams (an OpenCLIP-ViT-H/14 image embedding AND an ArcFace identity -embedding) and uses them to CONDITION a fresh txt2img generation -- the pixels are -new, so the watermark cannot be transported. - -That embeddings do not carry an invisible pixel watermark like SynthID is the -load-bearing assumption of the whole approach; the OpenCLIP smoke test (cosine -0.9977 invariance to SynthID-magnitude pixel noise) supports it for the CLIP -stream, and ArcFace is even more invariant to small perceptual changes by design. - -Architecture: PhotoMaker-V2 is a fine-tuned OpenCLIP-ViT-H/14 + InsightFace dual ID -encoder plus LoRA on the SDXL UNet attention layers. It ships as a single -``photomaker-v2.bin`` checkpoint loaded into a ``PhotoMakerStableDiffusionXLPipeline`` -(txt2img). We use it as a SECOND PASS after the main controlnet/default removal: - - 1. Main removal pass (`controlnet` at the certified strength) cleans SynthID - everywhere but leaves faces drifted. - 2. For each face found in the CLEANED image (YuNet), this module takes the SAME - face region from the ORIGINAL, computes the dual ID embedding from it, and - runs PhotoMaker txt2img to regenerate JUST that face crop from the embedding. - The freshly generated face is feather-composited back into the cleaned image. - -The generated face pixels are diffusion-fresh and inherit identity from the -embedding (not the pixels), so SynthID is not re-introduced. - -Requires the optional ``photomaker`` extra: ``pip install -'remove-ai-watermarks[photomaker]'`` -- this pulls the upstream PhotoMaker package -(Apache-2.0), ``insightface`` (MIT code), ``einops``, ``peft``, ``onnxruntime``, -and ``huggingface-hub``. Weights and InsightFace model packs download on first use; -never bundled. -""" - -# cv2/torch/diffusers boundary: relax unknown-type rules for this file only. -# pyright: reportUnknownMemberType=false, reportUnknownArgumentType=false, reportUnknownVariableType=false, reportUnknownParameterType=false, reportMissingTypeArgument=false, reportMissingTypeStubs=false, reportMissingImports=false, reportArgumentType=false, reportAssignmentType=false, reportReturnType=false, reportCallIssue=false, reportIndexIssue=false, reportOperatorIssue=false, reportOptionalMemberAccess=false, reportOptionalCall=false, reportOptionalSubscript=false, reportOptionalOperand=false, reportAttributeAccessIssue=false, reportPrivateImportUsage=false, reportPrivateUsage=false, reportInvalidTypeForm=false, reportConstantRedefinition=false, reportUnnecessaryComparison=false -from __future__ import annotations - -import importlib.util -import logging -import threading -from pathlib import Path -from typing import TYPE_CHECKING, Any - -if TYPE_CHECKING: - from numpy.typing import NDArray - -logger = logging.getLogger(__name__) - -# PhotoMaker-V2 weights (Apache-2.0 adapter; ID encoder pulls non-commercial -# InsightFace model packs at runtime -- see the NON-COMMERCIAL notice in the module -# docstring). Downloaded on first use; never bundled. -_PHOTOMAKER_REPO = "TencentARC/PhotoMaker-V2" -_PHOTOMAKER_FILE = "photomaker-v2.bin" -# SDXL base shared with the main pipeline (same checkpoint as `default`/`controlnet`). -_SDXL_MODEL_ID = "stabilityai/stable-diffusion-xl-base-1.0" - -# The neutral prompt PhotoMaker is designed around: a class noun + the trigger word -# `img`, which PhotoMaker replaces with the ID embedding at inference. Keeping it -# scene-neutral (no extra style words) maximises identity transfer from the embed and -# minimises hallucinated background/lighting that would not match the cleaned scene. -# Prompt format follows the upstream V2 reference (inference_pmv2.py): the trigger -# word ``img`` must immediately follow a class noun. SDXL is happiest at 1024 and -# falls into low-res artefacts ("mosaic of tiny faces") at 512, so we render at -# 1024 then downscale into the face bbox at composite time. Caught visually -# 2026-06-04: at 512 V2 produced a collage of training-time faces; at 1024 with the -# upstream-style descriptive prompt it produces a clean face. -_PHOTOMAKER_PROMPT = ( - "instagram photo, portrait photo of a person img, natural skin, soft lighting, best quality, sharp focus" -) -_PHOTOMAKER_NEGATIVE = ( - "(asymmetry, worst quality, low quality, illustration, 3d, 2d, painting, " - "cartoons, sketch), open mouth, blurry, watermark" -) - -# SDXL native resolution; lower values send V2 into low-res mode and the output -# becomes a collage of training-time faces. We render at 1024 then downscale into -# the original face bbox at composite time. -_PHOTOMAKER_FACE_SIZE = 1024 - -_pipeline: Any | None = None -_pipeline_lock = threading.Lock() -_face_analyser: Any | None = None -_face_analyser_lock = threading.Lock() - - -def is_available() -> bool: - """True when the optional PhotoMaker extra deps are importable.""" - return ( - importlib.util.find_spec("photomaker") is not None - and importlib.util.find_spec("diffusers") is not None - and importlib.util.find_spec("huggingface_hub") is not None - ) - - -def _select_device() -> str: - """Pick the PhotoMaker pipeline device: CUDA when present, MPS on Apple, else CPU.""" - try: - import torch - - if torch.cuda.is_available(): - return "cuda" - if torch.backends.mps.is_available(): - return "mps" - except Exception as e: - logger.debug("photomaker_restore: device probe failed (%s); using CPU", e) - return "cpu" - - -def _get_face_analyser() -> Any: - """Return the InsightFace FaceAnalysis2 singleton (downloads model packs on first use). - - **This is the non-commercial step.** Instantiating ``FaceAnalysis2()`` triggers - InsightFace's auto-download of the antelopev2/buffalo_l model packs, which are - released under a research-only license. See the module docstring NON-COMMERCIAL - notice. PhotoMaker-V2 requires this for the ArcFace identity branch. - """ - global _face_analyser - if _face_analyser is not None: - return _face_analyser - with _face_analyser_lock: - if _face_analyser is None: - import torch - from photomaker import FaceAnalysis2 - - providers = ["CUDAExecutionProvider"] if torch.cuda.is_available() else ["CPUExecutionProvider"] - fa = FaceAnalysis2(providers=providers, allowed_modules=["detection", "recognition"]) - fa.prepare(ctx_id=0, det_size=(640, 640)) - _face_analyser = fa - return _face_analyser - - -def _get_pipeline() -> Any: - """Return the lazily-built PhotoMaker pipeline singleton (downloads weights on first use).""" - global _pipeline - if _pipeline is not None: - return _pipeline - with _pipeline_lock: - if _pipeline is None: - import torch - from huggingface_hub import hf_hub_download - from photomaker import PhotoMakerStableDiffusionXLPipeline - - device = _select_device() - dtype = torch.float16 if device == "cuda" else torch.float32 - logger.info("photomaker_restore: loading SDXL+PhotoMaker on %s (%s)", device, dtype) - - adapter_path = hf_hub_download(repo_id=_PHOTOMAKER_REPO, filename=_PHOTOMAKER_FILE) - pipe = PhotoMakerStableDiffusionXLPipeline.from_pretrained(_SDXL_MODEL_ID, torch_dtype=dtype) - # Move SDXL submodules to the device BEFORE loading the PhotoMaker adapter: - # ``load_photomaker_adapter`` reads ``self.device`` / ``self.unet.dtype`` to - # place the new ID encoder. If we ``.to(device)`` after, the SDXL submodules - # move but the id_encoder stays where it was (custom attribute, not in the - # auto-managed module tree), and inference errors with - # "Input type (torch.cuda.HalfTensor) and weight type (torch.HalfTensor) - # should be the same" (caught empirically 2026-06-04). - pipe.to(device) - # Default ``pm_version`` is "v2"; we load the V2 weights (photomaker-v2.bin) - # into the V2 encoder (PhotoMakerIDEncoder_CLIPInsightfaceExtendtoken). The V2 - # encoder takes BOTH the CLIP image features AND an InsightFace ArcFace - # embedding -- the latter is what makes this path non-commercial. - pipe.load_photomaker_adapter( - str(Path(adapter_path).parent), - subfolder="", - weight_name=_PHOTOMAKER_FILE, - trigger_word="img", - ) - pipe.fuse_lora() - # Belt: also explicitly cast the loaded id_encoder, because some - # diffusers/torch combinations leave the encoder buffers untouched even - # though ``pipe.to(device)`` ran first. - if hasattr(pipe, "id_encoder") and pipe.id_encoder is not None: - pipe.id_encoder = pipe.id_encoder.to(device=device, dtype=dtype) - _pipeline = pipe - return _pipeline - - -def _face_crop_square( - image_bgr: NDArray[Any], - box: tuple[int, int, int, int], - pad: float = 0.30, -) -> tuple[NDArray[Any], tuple[int, int, int, int]]: - """Square crop around a face box (with padding), clipped to the image. - - Returns ``(crop_bgr, (x1, y1, x2, y2))``. The crop is the image content inside the - returned square box -- callers use the box for the composite step. Pure numpy slicing, - no model. - """ - h, w = image_bgr.shape[:2] - x, y, bw, bh = box - cx, cy = x + bw // 2, y + bh // 2 - side = int(max(bw, bh) * (1.0 + 2.0 * pad)) - half = side // 2 - x1 = max(0, cx - half) - y1 = max(0, cy - half) - x2 = min(w, cx + half) - y2 = min(h, cy + half) - return image_bgr[y1:y2, x1:x2], (x1, y1, x2, y2) - - -def _composite_faces( - base_bgr: NDArray[Any], - restored_crops: list[tuple[NDArray[Any], tuple[int, int, int, int]]], - feather_div: int = 6, -) -> NDArray[Any]: - """Feather-composite a list of ``(restored_crop, (x1, y1, x2, y2))`` into ``base_bgr``. - - Pure cv2/numpy helper (no model), unit-testable. For each ``(crop, box)``: resize - the crop to the box size, build a Gaussian-feathered rectangular alpha, and blend - ``crop * a + base * (1 - a)``. Boxes that fall fully outside the image (or an empty - list) leave ``base_bgr`` unchanged. Mirrors the alpha math in ``face_restore._composite_faces``. - """ - import cv2 - import numpy as np - - out = base_bgr.astype(np.float32) - h, w = base_bgr.shape[:2] - - for crop, (x1, y1, x2, y2) in restored_crops: - x1, y1 = max(0, x1), max(0, y1) - x2, y2 = min(w, x2), min(h, y2) - bw, bh = x2 - x1, y2 - y1 - if bw <= 0 or bh <= 0: - continue - resized = cv2.resize(crop, (bw, bh), interpolation=cv2.INTER_LANCZOS4) - - alpha = np.zeros((h, w), dtype=np.float32) - alpha[y1:y2, x1:x2] = 1.0 - k = max(3, (min(bw, bh) // feather_div) | 1) - alpha = cv2.GaussianBlur(alpha, (k, k), 0)[:, :, None] - - full_restored = np.zeros_like(out) - full_restored[y1:y2, x1:x2] = resized - out = full_restored * alpha + out * (1.0 - alpha) - - return np.clip(out, 0, 255).astype(np.uint8) - - -def restore_faces_photomaker( - original_bgr: NDArray[Any], - cleaned_bgr: NDArray[Any], - num_inference_steps: int = 30, - guidance_scale: float = 5.0, - style_strength: int = 20, - seed: int | None = None, - detect_faces_fn: Any | None = None, -) -> NDArray[Any]: - """SynthID-robust face identity restoration via PhotoMaker txt2img. - - Pipeline: - 1. Detect faces in ``cleaned_bgr`` (YuNet via the package's ``auto_config`` by - default; override via ``detect_faces_fn`` for tests). - 2. For each face: take the SAME box from ``original_bgr`` -> square crop -> PhotoMaker - txt2img with that crop as the ID image -> a fresh face generated from the - OpenCLIP embedding (the embedding is SynthID-invariant by ~3 orders of magnitude, - see docs/synthid-robust-identity-research.md). - 3. Feather-composite each regenerated face into ``cleaned_bgr``. - - Faces are taken from ``original_bgr`` (the embedding ignores the watermark) but the - PIXELS that land in the output are diffusion-fresh, so SynthID is not transported. - - Args: - original_bgr: The original (watermarked) image as cv2 BGR. Source of identity. - cleaned_bgr: The main-pass output as cv2 BGR. Faces drifted in identity; this - module replaces those face regions. - num_inference_steps: Diffusion steps inside PhotoMaker (def 30). - guidance_scale: CFG scale inside PhotoMaker (def 5.0; the PhotoMaker recipe). - style_strength: PhotoMaker's ``start_merge_step`` knob ~ 20-30 (def 20). - seed: Optional seed for reproducibility. - detect_faces_fn: Optional callable ``(bgr) -> list[(x,y,w,h)]`` to override the - default YuNet detector (used by tests). - - Returns: - ``cleaned_bgr`` with regenerated face regions composited in (or unchanged when - no face is detected). - """ - import cv2 - import numpy as np - import torch - from PIL import Image - - if detect_faces_fn is None: - from remove_ai_watermarks import auto_config as _ac - - def _default_detect(bgr: NDArray[Any]) -> list[tuple[int, int, int, int]]: - h, w = bgr.shape[:2] - model = Path(_ac.__file__).parent / "assets" / "face_detection_yunet_2023mar.onnx" - det = cv2.FaceDetectorYN.create(str(model), "", (w, h), _ac._FACE_SCORE, 0.3, 5000) - det.setInputSize((w, h)) - _, faces = det.detect(bgr) - if faces is None: - return [] - return [(int(f[0]), int(f[1]), int(f[2]), int(f[3])) for f in faces if int(f[2]) > 0 and int(f[3]) > 0] - - detect_faces_fn = _default_detect - - boxes = detect_faces_fn(cleaned_bgr) - if not boxes: - logger.debug("photomaker_restore: no faces detected; returning cleaned image unchanged") - return cleaned_bgr - - pipeline = _get_pipeline() - face_analyser = _get_face_analyser() # NON-COMMERCIAL: triggers InsightFace model packs - from photomaker import analyze_faces - - generator = None - if seed is not None: - generator = torch.Generator(device=pipeline.device).manual_seed(seed) - - restored: list[tuple[NDArray[Any], tuple[int, int, int, int]]] = [] - for box in boxes: - id_crop_bgr, square_box = _face_crop_square(original_bgr, box) - if id_crop_bgr.size == 0: - continue - # Get the ArcFace embedding for THIS face (V2's required ID branch). InsightFace - # expects BGR; analyze_faces returns a list, take the first detection. - # Shape: upstream's inference_pmv2.py stacks per-image embeddings into a 2-D - # tensor (N_images, 512). The pipeline forward then calls `.unsqueeze(0)` ITSELF - # (line 705 of pipeline.py) to add a batch dim, so we must NOT pre-unsqueeze -- - # giving `(1, 1, 512)` to the V2 forward made the id_encoder consume garbage and - # the pipeline output the training-time face collage (caught visually 2026-06-04). - # Dtype stays float32 here; the pipeline casts internally. - faces = analyze_faces(face_analyser, id_crop_bgr) - if not faces: - logger.debug("photomaker_restore: InsightFace did not detect a face in the crop; skipping") - continue - id_embeds = torch.stack([torch.from_numpy(faces[0]["embedding"])]) - - id_crop_rgb = cv2.cvtColor(id_crop_bgr, cv2.COLOR_BGR2RGB) - id_image_pil = Image.fromarray(id_crop_rgb) - - # Upstream V2 reference (inference_pmv2.py) passes negative_prompt; the - # batch-mismatch we hit earlier was on V1 only. - out = pipeline( - prompt=_PHOTOMAKER_PROMPT, - negative_prompt=_PHOTOMAKER_NEGATIVE, - input_id_images=[id_image_pil], - id_embeds=id_embeds, - num_inference_steps=num_inference_steps, - guidance_scale=guidance_scale, - start_merge_step=style_strength, - generator=generator, - height=_PHOTOMAKER_FACE_SIZE, - width=_PHOTOMAKER_FACE_SIZE, - num_images_per_prompt=1, - ) - gen_rgb = out.images[0] - gen_bgr = cv2.cvtColor(np.array(gen_rgb), cv2.COLOR_RGB2BGR) - restored.append((gen_bgr, square_box)) - - return _composite_faces(cleaned_bgr, restored) diff --git a/tests/test_auto_config.py b/tests/test_auto_config.py index 09f2429..312ee16 100644 --- a/tests/test_auto_config.py +++ b/tests/test_auto_config.py @@ -64,7 +64,6 @@ class TestPlan: cfg = auto_config.plan(_write(flat, tmp_path)) assert cfg is not None assert cfg.pipeline == "default" # structure-less -> plain SDXL - assert cfg.restore_faces is False assert cfg.adaptive_polish is False # no smoothing pass -> no polish assert cfg.unsharp == 0.0 assert cfg.humanize == 0.0 @@ -78,13 +77,12 @@ class TestPlan: # Text creates edges above the structure-less floor -> controlnet preserves them. assert cfg.pipeline == "controlnet" - def test_face_routes_to_restore_and_controlnet_and_polish(self, tmp_path, monkeypatch): + def test_face_routes_to_controlnet_and_polish(self, tmp_path, monkeypatch): monkeypatch.setattr(auto_config, "detect_face", lambda _img: True) flat = np.full((300, 300, 3), 128, dtype=np.uint8) cfg = auto_config.plan(_write(flat, tmp_path)) assert cfg is not None assert cfg.has_face - assert cfg.restore_faces assert cfg.pipeline == "controlnet" assert cfg.adaptive_polish # smoothing pass ran -> adaptive polish on assert cfg.unsharp == 0.0 # fixed knobs off; the adaptive polish replaces them @@ -103,7 +101,6 @@ class TestReason: def test_reason_summarizes_plan(self): cfg = auto_config.AutoConfig( pipeline="controlnet", - restore_faces=True, adaptive_polish=True, unsharp=0.0, humanize=0.0, @@ -117,5 +114,4 @@ class TestReason: r = cfg.reason assert "controlnet" in r assert "face" in r - assert "face-restore on" in r assert "adaptive polish" in r diff --git a/tests/test_cli.py b/tests/test_cli.py index 1eb0a4c..e73d6a5 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -523,7 +523,6 @@ class TestBatchCommand: output_dir = tmp_path / "output" plan = auto_config.AutoConfig( pipeline="controlnet", - restore_faces=True, adaptive_polish=True, unsharp=0.0, humanize=0.0, diff --git a/tests/test_instantid_restore.py b/tests/test_instantid_restore.py deleted file mode 100644 index d150191..0000000 --- a/tests/test_instantid_restore.py +++ /dev/null @@ -1,152 +0,0 @@ -"""Control-flow tests for instantid_restore -- no model download. - -The end-to-end InstantID run is monkey-patched: we replace ``_get_pipeline`` and -``_get_face_analyser`` with fakes, install a fake InsightFace ``FaceAnalysis`` -embedding, and check that the per-face crop + composite pipeline wires up the -expected pixels into ``cleaned_bgr``. -""" - -from __future__ import annotations - -import cv2 -import numpy as np -import pytest - -from remove_ai_watermarks import instantid_restore - - -class TestIsAvailable: - def test_returns_bool(self): - assert isinstance(instantid_restore.is_available(), bool) - - -class TestRepoPins: - """Pin the InstantID repo + adapter file so a maintainer change is visible.""" - - def test_repo_is_instantx_instantid(self): - assert instantid_restore._INSTANTID_REPO == "InstantX/InstantID" - - def test_controlnet_subfolder(self): - assert instantid_restore._INSTANTID_CONTROLNET_SUBFOLDER == "ControlNetModel" - - def test_ip_adapter_filename(self): - assert instantid_restore._INSTANTID_IP_ADAPTER == "ip-adapter.bin" - - -class TestDrawKps: - def test_renders_color_image(self): - kps = np.array([[100, 100], [200, 100], [150, 150], [120, 200], [180, 200]]) - img = instantid_restore._draw_kps((256, 256), kps) - arr = np.array(img) - assert arr.shape == (256, 256, 3) - # Has nonzero pixels (the stick figure is rendered). - assert arr.sum() > 0 - - def test_black_outside_kps(self): - kps = np.array([[100, 100], [200, 100], [150, 150], [120, 200], [180, 200]]) - img = instantid_restore._draw_kps((256, 256), kps) - arr = np.array(img) - # Top-left corner should be black (no keypoint there). - assert arr[0, 0].sum() == 0 - - -class TestRestoreFacesInstantidControlFlow: - """End-to-end flow with the pipeline / face analyser / InsightFace mocked. - - Checks that with one detected face: (1) the original crop is fed to the - InsightFace mock; (2) the pipeline mock receives the expected kwargs; (3) - the regenerated output ends up composited into the cleaned image. - """ - - @staticmethod - def _fake_pipeline_class(fill_value: int = 210): - import torch - from PIL import Image - - class _FakePipeOutput: - def __init__(self, images): - self.images = images - - class _FakePipe: - device = "cpu" - dtype = torch.float32 - - def __call__(self, **kwargs): - # Save kwargs for assertion. - _FakePipe.last_kwargs = kwargs - # Gradient face so the color-match step shifts the mean but - # preserves contrast (the composite is then detectable as a - # variance change in the face region even with uniform canvas). - grad = np.linspace(0, fill_value, 1024, dtype=np.uint8) - arr = np.broadcast_to(grad[:, None, None], (1024, 1024, 3)).copy() - img = Image.fromarray(arr) - return _FakePipeOutput([img]) - - return _FakePipe() - - def test_no_faces_returns_cleaned_unchanged(self, monkeypatch): - monkeypatch.setattr(instantid_restore, "is_available", lambda: True) - monkeypatch.setattr(instantid_restore, "_get_pipeline", lambda: self._fake_pipeline_class()) - monkeypatch.setattr(instantid_restore, "_get_face_analyser", lambda: object()) - - orig = np.full((400, 400, 3), 50, dtype=np.uint8) - cleaned = np.full((400, 400, 3), 100, dtype=np.uint8) - out = instantid_restore.restore_faces_instantid(orig, cleaned, detect_faces_fn=lambda _b: []) - assert np.array_equal(out, cleaned) - - def test_one_face_gets_composited_into_cleaned(self, monkeypatch): - monkeypatch.setattr(instantid_restore, "is_available", lambda: True) - monkeypatch.setattr(instantid_restore, "_get_pipeline", lambda: self._fake_pipeline_class(fill_value=210)) - - # Fake FaceAnalyser that returns one face with a 512-d embedding + 5 keypoints. - class _FakeFA: - def get(self, _bgr): - return [ - { - "bbox": np.array([10, 10, 100, 100], dtype=np.float32), - "embedding": np.zeros(512, dtype=np.float32), - "kps": np.array( - [[30, 40], [70, 40], [50, 60], [35, 80], [65, 80]], - dtype=np.float32, - ), - } - ] - - monkeypatch.setattr(instantid_restore, "_get_face_analyser", lambda: _FakeFA()) - - orig = np.full((400, 400, 3), 30, dtype=np.uint8) - cleaned = np.full((400, 400, 3), 90, dtype=np.uint8) - cv2.rectangle(orig, (150, 150), (250, 250), (200, 100, 50), -1) - - out = instantid_restore.restore_faces_instantid( - orig, cleaned, detect_faces_fn=lambda _b: [(150, 150, 100, 100)] - ) - # The composite must have written non-uniform values into the face - # region (gradient survives color-match as variance), and the canvas - # corner stays close to the cleaned base. - face_region = out[170:230, 170:230] - assert int(face_region.std()) > 0 - assert int(out[0, 0, 0]) - int(cleaned[0, 0, 0]) <= 1 - - def test_insightface_misses_face_skips_gracefully(self, monkeypatch): - monkeypatch.setattr(instantid_restore, "is_available", lambda: True) - monkeypatch.setattr(instantid_restore, "_get_pipeline", lambda: self._fake_pipeline_class()) - - class _EmptyFA: - def get(self, _bgr): - return [] - - monkeypatch.setattr(instantid_restore, "_get_face_analyser", lambda: _EmptyFA()) - - orig = np.full((400, 400, 3), 30, dtype=np.uint8) - cleaned = np.full((400, 400, 3), 90, dtype=np.uint8) - - out = instantid_restore.restore_faces_instantid( - orig, cleaned, detect_faces_fn=lambda _b: [(150, 150, 100, 100)] - ) - # No face detected by InsightFace -> cleaned image is returned unchanged. - assert np.array_equal(out, cleaned) - - -if __name__ == "__main__": - pytest.main([__file__, "-v"]) diff --git a/tests/test_photomaker_restore.py b/tests/test_photomaker_restore.py deleted file mode 100644 index 63f5f58..0000000 --- a/tests/test_photomaker_restore.py +++ /dev/null @@ -1,151 +0,0 @@ -"""Tests for the PhotoMaker-V2 face identity restoration helper. - -These tests cover the pure-Python parts (face crop math, composite, the no-faces -no-op, the is_available guard) WITHOUT loading PhotoMaker or SDXL -- the model-loading -path is gated behind ``is_available()`` and exercised manually via the Modal cert -sweep, mirroring the convention used for ``face_restore`` and ``upscaler``. - -The end-to-end PhotoMaker run is monkey-patched: we replace ``_get_pipeline`` with a -fake pipeline whose ``__call__`` returns a known constant-color face, so we can verify -that the right boxes get the right pixels composited back. -""" - -from __future__ import annotations - -from types import SimpleNamespace - -import cv2 -import numpy as np - -from remove_ai_watermarks import photomaker_restore - - -class TestIsAvailable: - def test_returns_bool(self): - assert isinstance(photomaker_restore.is_available(), bool) - - -class TestV2WeightPins: - """Pin the V2 repo + weights so a maintainer change is visible in a code review.""" - - def test_repo_is_v2(self): - assert photomaker_restore._PHOTOMAKER_REPO == "TencentARC/PhotoMaker-V2" - - def test_weight_filename_is_v2(self): - assert photomaker_restore._PHOTOMAKER_FILE == "photomaker-v2.bin" - - -class TestFaceCropSquare: - def test_centers_on_face_box(self): - img = np.full((400, 400, 3), 128, dtype=np.uint8) - crop, box = photomaker_restore._face_crop_square(img, (100, 150, 80, 80)) - x1, y1, x2, y2 = box - # The crop covers the requested box (with padding) - assert x1 <= 100 - assert y1 <= 150 - assert x2 >= 180 - assert y2 >= 230 - assert crop.shape[0] == y2 - y1 - assert crop.shape[1] == x2 - x1 - - def test_clips_at_image_edges(self): - img = np.full((200, 200, 3), 128, dtype=np.uint8) - crop, (x1, y1, x2, y2) = photomaker_restore._face_crop_square(img, (180, 180, 30, 30)) - # Box must be clipped within the image - assert x1 >= 0 - assert y1 >= 0 - assert x2 <= 200 - assert y2 <= 200 - assert crop.shape[0] == y2 - y1 - assert crop.shape[1] == x2 - x1 - - def test_pad_widens_the_crop(self): - img = np.full((400, 400, 3), 128, dtype=np.uint8) - _, no_pad = photomaker_restore._face_crop_square(img, (150, 150, 50, 50), pad=0.0) - _, with_pad = photomaker_restore._face_crop_square(img, (150, 150, 50, 50), pad=0.5) - assert (with_pad[2] - with_pad[0]) > (no_pad[2] - no_pad[0]) - - -class TestCompositeFaces: - def test_empty_list_returns_base_unchanged(self): - base = np.full((100, 100, 3), 64, dtype=np.uint8) - out = photomaker_restore._composite_faces(base, []) - assert np.array_equal(out, base) - - def test_box_outside_image_is_skipped(self): - base = np.full((100, 100, 3), 64, dtype=np.uint8) - crop = np.full((40, 40, 3), 200, dtype=np.uint8) - out = photomaker_restore._composite_faces(base, [(crop, (200, 200, 240, 240))]) - assert np.array_equal(out, base) - - def test_composited_box_pulls_pixel_value_toward_crop(self): - base = np.full((200, 200, 3), 40, dtype=np.uint8) - crop = np.full((50, 50, 3), 220, dtype=np.uint8) - # Place the crop fully inside the image at (60, 60)..(110, 110) - out = photomaker_restore._composite_faces(base, [(crop, (60, 60, 110, 110))]) - # The box center should be heavily biased toward the crop color (>120) ... - assert out[85, 85, 0] > 120 - # ... and corners (well outside the feathered region) stay close to base - assert int(out[0, 0, 0]) - int(base[0, 0, 0]) <= 1 - - -class TestRestoreFacesPhotomakerControlFlow: - """End-to-end control flow with a fake pipeline -- no diffusion model loaded.""" - - @staticmethod - def _fake_pipeline_class(fill_value: int = 200): - """Class-based fake (no ``__call__`` on a SimpleNamespace, which Python won't dispatch).""" - from PIL import Image - - size = photomaker_restore._PHOTOMAKER_FACE_SIZE - fake_face = Image.fromarray(np.full((size, size, 3), fill_value, dtype=np.uint8)) - - import torch - - class _FakePipe: - device = "cpu" - dtype = torch.float32 - - def __call__(self, **_kwargs): - return SimpleNamespace(images=[fake_face]) - - return _FakePipe() - - def test_no_faces_returns_cleaned_unchanged(self, monkeypatch): - # Force is_available so we never hit the missing-extra branch - monkeypatch.setattr(photomaker_restore, "is_available", lambda: True) - monkeypatch.setattr(photomaker_restore, "_get_pipeline", lambda: self._fake_pipeline_class()) - - orig = np.full((200, 200, 3), 30, dtype=np.uint8) - cleaned = np.full((200, 200, 3), 90, dtype=np.uint8) - out = photomaker_restore.restore_faces_photomaker(orig, cleaned, detect_faces_fn=lambda _b: []) - assert np.array_equal(out, cleaned) - - def test_one_face_gets_composited_into_cleaned(self, monkeypatch): - import sys - import types - - monkeypatch.setattr(photomaker_restore, "is_available", lambda: True) - monkeypatch.setattr(photomaker_restore, "_get_pipeline", lambda: self._fake_pipeline_class(fill_value=210)) - # FaceAnalyser singleton: any non-None object; the test stubs analyze_faces below. - monkeypatch.setattr(photomaker_restore, "_get_face_analyser", lambda: object()) - # Stub `from photomaker import analyze_faces` inside the function: install a - # fake `photomaker` module that returns a single 512-d embedding per call. - fake_photomaker = types.ModuleType("photomaker") - fake_photomaker.analyze_faces = lambda _fa, _bgr: [{"embedding": np.zeros(512, dtype=np.float32)}] - monkeypatch.setitem(sys.modules, "photomaker", fake_photomaker) - - orig = np.full((400, 400, 3), 30, dtype=np.uint8) - cleaned = np.full((400, 400, 3), 90, dtype=np.uint8) - # Mark the original face region with a distinctive color so we can confirm the - # crop reached the pipeline (not strictly tested here, but useful sanity). - cv2.rectangle(orig, (150, 150), (250, 250), (200, 100, 50), -1) - - out = photomaker_restore.restore_faces_photomaker( - orig, cleaned, detect_faces_fn=lambda _b: [(150, 150, 100, 100)] - ) - # The cleaned image should have shifted toward the fake-face fill (210) inside - # the face region. - assert out[200, 200, 0] > 150 - # And the corner pixels (well outside the feather) should still be near the base. - assert int(out[0, 0, 0]) - int(cleaned[0, 0, 0]) <= 1 diff --git a/uv.lock b/uv.lock index bd10c30..3482184 100644 --- a/uv.lock +++ b/uv.lock @@ -853,19 +853,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/94/16/70255075a9859a0e3adb789b68ceb0e210dec03934245fd98d248226572f/idna-3.16-py3-none-any.whl", hash = "sha256:cc246e3a3f89580c3a951b5ad298ca4638078b2cdd4f115654332b5c26daded5", size = 74165, upload-time = "2026-05-22T00:16:16.698Z" }, ] -[[package]] -name = "imageio" -version = "2.37.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "numpy" }, - { name = "pillow" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/b1/84/93bcd1300216ea50811cee96873b84a1bebf8d0489ffaf7f2a3756bab866/imageio-2.37.3.tar.gz", hash = "sha256:bbb37efbfc4c400fcd534b367b91fcd66d5da639aaa138034431a1c5e0a41451", size = 389673, upload-time = "2026-03-09T11:31:12.573Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/49/fa/391e437a34e55095173dca5f24070d89cbc233ff85bf1c29c93248c6588d/imageio-2.37.3-py3-none-any.whl", hash = "sha256:46f5bb8522cd421c0f5ae104d8268f569d856b29eb1a13b92829d1970f32c9f0", size = 317646, upload-time = "2026-03-09T11:31:10.771Z" }, -] - [[package]] name = "importlib-metadata" version = "9.0.0" @@ -887,28 +874,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, ] -[[package]] -name = "insightface" -version = "1.0.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "numpy" }, - { name = "onnx" }, - { name = "onnxruntime", version = "1.24.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "onnxruntime", version = "1.26.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "opencv-python" }, - { name = "requests" }, - { name = "scikit-image", version = "0.25.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "scikit-image", version = "0.26.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "tqdm" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f0/77/a1b06d3755fcfb353cdd46a138ec578d9664ee8b53590e73e9edca328928/insightface-1.0.1.tar.gz", hash = "sha256:27af24891bbba470cb3573b366a0fcca8989fc8503c9f8f281e8cba6fd716075", size = 721597, upload-time = "2026-05-23T06:43:00.404Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/42/ce/33989427a7fae952e1606d0edee4191f965630f3c74f2166fa776fafd271/insightface-1.0.1-py3-none-any.whl", hash = "sha256:5f373f6fedbdda5cbc59a34ca386a75a2995cdaf6899402590ae9eb4308fc2e8", size = 762241, upload-time = "2026-05-23T06:42:58.253Z" }, -] - [[package]] name = "invisible-watermark" version = "0.2.0" @@ -937,18 +902,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, ] -[[package]] -name = "lazy-loader" -version = "0.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "packaging" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/49/ac/21a1f8aa3777f5658576777ea76bfb124b702c520bbe90edf4ae9915eafa/lazy_loader-0.5.tar.gz", hash = "sha256:717f9179a0dbed357012ddad50a5ad3d5e4d9a0b8712680d4e687f5e6e6ed9b3", size = 15294, upload-time = "2026-03-06T15:45:09.054Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8a/a1/8d812e53a5da1687abb10445275d41a8b13adb781bbf7196ddbcf8d88505/lazy_loader-0.5-py3-none-any.whl", hash = "sha256:ab0ea149e9c554d4ffeeb21105ac60bed7f3b4fd69b1d2360a4add51b170b005", size = 8044, upload-time = "2026-03-06T15:45:07.668Z" }, -] - [[package]] name = "lightning" version = "2.6.4" @@ -1088,29 +1041,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, ] -[[package]] -name = "ml-dtypes" -version = "0.4.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "numpy" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/fd/15/76f86faa0902836cc133939732f7611ace68cf54148487a99c539c272dc8/ml_dtypes-0.4.1.tar.gz", hash = "sha256:fad5f2de464fd09127e49b7fd1252b9006fb43d2edc1ff112d390c324af5ca7a", size = 692594, upload-time = "2024-09-13T19:07:11.624Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/56/9e/76b84f77c7afee3b116dc8407903a2d5004ba3059a8f3dcdcfa6ebf33fff/ml_dtypes-0.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:1fe8b5b5e70cd67211db94b05cfd58dace592f24489b038dc6f9fe347d2e07d5", size = 397975, upload-time = "2024-09-13T19:06:44.265Z" }, - { url = "https://files.pythonhosted.org/packages/03/7b/32650e1b2a2713a5923a0af2a8503d0d4a8fc99d1e1e0a1c40e996634460/ml_dtypes-0.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c09a6d11d8475c2a9fd2bc0695628aec105f97cab3b3a3fb7c9660348ff7d24", size = 2182570, upload-time = "2024-09-13T19:06:46.189Z" }, - { url = "https://files.pythonhosted.org/packages/16/86/a9f7569e7e4f5395f927de38a13b92efa73f809285d04f2923b291783dd2/ml_dtypes-0.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f5e8f75fa371020dd30f9196e7d73babae2abd51cf59bdd56cb4f8de7e13354", size = 2160365, upload-time = "2024-09-13T19:06:48.198Z" }, - { url = "https://files.pythonhosted.org/packages/04/1b/9a3afb437702503514f3934ec8d7904270edf013d28074f3e700e5dfbb0f/ml_dtypes-0.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:15fdd922fea57e493844e5abb930b9c0bd0af217d9edd3724479fc3d7ce70e3f", size = 126633, upload-time = "2024-09-13T19:06:50.656Z" }, - { url = "https://files.pythonhosted.org/packages/d1/76/9835c8609c29f2214359e88f29255fc4aad4ea0f613fb48aa8815ceda1b6/ml_dtypes-0.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:2d55b588116a7085d6e074cf0cdb1d6fa3875c059dddc4d2c94a4cc81c23e975", size = 397973, upload-time = "2024-09-13T19:06:51.748Z" }, - { url = "https://files.pythonhosted.org/packages/7e/99/e68c56fac5de973007a10254b6e17a0362393724f40f66d5e4033f4962c2/ml_dtypes-0.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e138a9b7a48079c900ea969341a5754019a1ad17ae27ee330f7ebf43f23877f9", size = 2185134, upload-time = "2024-09-13T19:06:53.197Z" }, - { url = "https://files.pythonhosted.org/packages/28/bc/6a2344338ea7b61cd7b46fb24ec459360a5a0903b57c55b156c1e46c644a/ml_dtypes-0.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:74c6cfb5cf78535b103fde9ea3ded8e9f16f75bc07789054edc7776abfb3d752", size = 2163661, upload-time = "2024-09-13T19:06:54.519Z" }, - { url = "https://files.pythonhosted.org/packages/e8/d3/ddfd9878b223b3aa9a930c6100a99afca5cfab7ea703662e00323acb7568/ml_dtypes-0.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:274cc7193dd73b35fb26bef6c5d40ae3eb258359ee71cd82f6e96a8c948bdaa6", size = 126727, upload-time = "2024-09-13T19:06:55.897Z" }, - { url = "https://files.pythonhosted.org/packages/ba/1a/99e924f12e4b62139fbac87419698c65f956d58de0dbfa7c028fa5b096aa/ml_dtypes-0.4.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:827d3ca2097085cf0355f8fdf092b888890bb1b1455f52801a2d7756f056f54b", size = 405077, upload-time = "2024-09-13T19:06:57.538Z" }, - { url = "https://files.pythonhosted.org/packages/8f/8c/7b610bd500617854c8cc6ed7c8cfb9d48d6a5c21a1437a36a4b9bc8a3598/ml_dtypes-0.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:772426b08a6172a891274d581ce58ea2789cc8abc1c002a27223f314aaf894e7", size = 2181554, upload-time = "2024-09-13T19:06:59.196Z" }, - { url = "https://files.pythonhosted.org/packages/c7/c6/f89620cecc0581dc1839e218c4315171312e46c62a62da6ace204bda91c0/ml_dtypes-0.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:126e7d679b8676d1a958f2651949fbfa182832c3cd08020d8facd94e4114f3e9", size = 2160488, upload-time = "2024-09-13T19:07:03.131Z" }, - { url = "https://files.pythonhosted.org/packages/ae/11/a742d3c31b2cc8557a48efdde53427fd5f9caa2fa3c9c27d826e78a66f51/ml_dtypes-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:df0fb650d5c582a9e72bb5bd96cfebb2cdb889d89daff621c8fbc60295eba66c", size = 127462, upload-time = "2024-09-13T19:07:04.916Z" }, -] - [[package]] name = "mpmath" version = "1.3.0" @@ -1498,48 +1428,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c9/bd/21b9bfa2fa29913df9f80c78b034b8c3bf8ecee04dac7b34bb546019f195/omegaconf-2.4.0.dev11-py3-none-any.whl", hash = "sha256:943b2a01329335fa11a4d3caed06432d7023f8fb579dcbff4d28b26ee6be469e", size = 233260, upload-time = "2026-05-13T19:57:39.309Z" }, ] -[[package]] -name = "onnx" -version = "1.19.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "ml-dtypes" }, - { name = "numpy" }, - { name = "protobuf" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/5b/bf/b0a63ee9f3759dcd177b28c6f2cb22f2aecc6d9b3efecaabc298883caa5f/onnx-1.19.0.tar.gz", hash = "sha256:aa3f70b60f54a29015e41639298ace06adf1dd6b023b9b30f1bca91bb0db9473", size = 11949859, upload-time = "2025-08-27T02:34:27.107Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/00/b3/8a6f3b05d18dffdc7c18839bd829587c826c8513f4bdbe21ddf37dacce50/onnx-1.19.0-cp310-cp310-macosx_12_0_universal2.whl", hash = "sha256:e927d745939d590f164e43c5aec7338c5a75855a15130ee795f492fc3a0fa565", size = 18310869, upload-time = "2025-08-27T02:32:47.346Z" }, - { url = "https://files.pythonhosted.org/packages/b9/92/550d6155ab3f2c00e95add1726397c95b4b79d6eb4928d049ff591ad4c84/onnx-1.19.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c6cdcb237c5c4202463bac50417c5a7f7092997a8469e8b7ffcd09f51de0f4a9", size = 18028144, upload-time = "2025-08-27T02:32:50.306Z" }, - { url = "https://files.pythonhosted.org/packages/79/21/9bcc715ea6d9aab3f6c583bfc59504a14777e39e0591030e7345f4e40315/onnx-1.19.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ed0b85a33deacb65baffe6ca4ce91adf2bb906fa2dee3856c3c94e163d2eb563", size = 18200923, upload-time = "2025-08-27T02:32:54.325Z" }, - { url = "https://files.pythonhosted.org/packages/c8/90/3a6f0741ff22270e2f4b741f440ab68ba5525ebc94775cd6f2c01f531374/onnx-1.19.0-cp310-cp310-win32.whl", hash = "sha256:89a9cefe75547aec14a796352c2243e36793bbbcb642d8897118595ab0c2395b", size = 16332097, upload-time = "2025-08-27T02:32:56.997Z" }, - { url = "https://files.pythonhosted.org/packages/4c/4c/ef61d359865712803d488672607023d36bfcd21fa008d8dc1d6ee8e8b23c/onnx-1.19.0-cp310-cp310-win_amd64.whl", hash = "sha256:a16a82bfdf4738691c0a6eda5293928645ab8b180ab033df84080817660b5e66", size = 16451402, upload-time = "2025-08-27T02:33:00.534Z" }, - { url = "https://files.pythonhosted.org/packages/db/5c/b959b17608cfb6ccf6359b39fe56a5b0b7d965b3d6e6a3c0add90812c36e/onnx-1.19.0-cp311-cp311-macosx_12_0_universal2.whl", hash = "sha256:206f00c47b85b5c7af79671e3307147407991a17994c26974565aadc9e96e4e4", size = 18312580, upload-time = "2025-08-27T02:33:03.081Z" }, - { url = "https://files.pythonhosted.org/packages/2c/ee/ac052bbbc832abe0debb784c2c57f9582444fb5f51d63c2967fd04432444/onnx-1.19.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4d7bee94abaac28988b50da675ae99ef8dd3ce16210d591fbd0b214a5930beb3", size = 18029165, upload-time = "2025-08-27T02:33:05.771Z" }, - { url = "https://files.pythonhosted.org/packages/5c/c9/8687ba0948d46fd61b04e3952af9237883bbf8f16d716e7ed27e688d73b8/onnx-1.19.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7730b96b68c0c354bbc7857961bb4909b9aaa171360a8e3708d0a4c749aaadeb", size = 18202125, upload-time = "2025-08-27T02:33:09.325Z" }, - { url = "https://files.pythonhosted.org/packages/e2/16/6249c013e81bd689f46f96c7236d7677f1af5dd9ef22746716b48f10e506/onnx-1.19.0-cp311-cp311-win32.whl", hash = "sha256:7cb7a3ad8059d1a0dfdc5e0a98f71837d82002e441f112825403b137227c2c97", size = 16332738, upload-time = "2025-08-27T02:33:12.448Z" }, - { url = "https://files.pythonhosted.org/packages/6a/28/34a1e2166e418c6a78e5c82e66f409d9da9317832f11c647f7d4e23846a6/onnx-1.19.0-cp311-cp311-win_amd64.whl", hash = "sha256:d75452a9be868bd30c3ef6aa5991df89bbfe53d0d90b2325c5e730fbd91fff85", size = 16452303, upload-time = "2025-08-27T02:33:15.176Z" }, - { url = "https://files.pythonhosted.org/packages/e6/b7/639664626e5ba8027860c4d2a639ee02b37e9c322215c921e9222513c3aa/onnx-1.19.0-cp311-cp311-win_arm64.whl", hash = "sha256:23c7959370d7b3236f821e609b0af7763cff7672a758e6c1fc877bac099e786b", size = 16425340, upload-time = "2025-08-27T02:33:17.78Z" }, - { url = "https://files.pythonhosted.org/packages/0d/94/f56f6ca5e2f921b28c0f0476705eab56486b279f04e1d568ed64c14e7764/onnx-1.19.0-cp312-cp312-macosx_12_0_universal2.whl", hash = "sha256:61d94e6498ca636756f8f4ee2135708434601b2892b7c09536befb19bc8ca007", size = 18322331, upload-time = "2025-08-27T02:33:20.373Z" }, - { url = "https://files.pythonhosted.org/packages/c8/00/8cc3f3c40b54b28f96923380f57c9176872e475face726f7d7a78bd74098/onnx-1.19.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:224473354462f005bae985c72028aaa5c85ab11de1b71d55b06fdadd64a667dd", size = 18027513, upload-time = "2025-08-27T02:33:23.44Z" }, - { url = "https://files.pythonhosted.org/packages/61/90/17c4d2566fd0117a5e412688c9525f8950d467f477fbd574e6b32bc9cb8d/onnx-1.19.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1ae475c85c89bc4d1f16571006fd21a3e7c0e258dd2c091f6e8aafb083d1ed9b", size = 18202278, upload-time = "2025-08-27T02:33:26.103Z" }, - { url = "https://files.pythonhosted.org/packages/bc/6e/a9383d9cf6db4ac761a129b081e9fa5d0cd89aad43cf1e3fc6285b915c7d/onnx-1.19.0-cp312-cp312-win32.whl", hash = "sha256:323f6a96383a9cdb3960396cffea0a922593d221f3929b17312781e9f9b7fb9f", size = 16333080, upload-time = "2025-08-27T02:33:28.559Z" }, - { url = "https://files.pythonhosted.org/packages/a7/2e/3ff480a8c1fa7939662bdc973e41914add2d4a1f2b8572a3c39c2e4982e5/onnx-1.19.0-cp312-cp312-win_amd64.whl", hash = "sha256:50220f3499a499b1a15e19451a678a58e22ad21b34edf2c844c6ef1d9febddc2", size = 16453927, upload-time = "2025-08-27T02:33:31.177Z" }, - { url = "https://files.pythonhosted.org/packages/57/37/ad500945b1b5c154fe9d7b826b30816ebd629d10211ea82071b5bcc30aa4/onnx-1.19.0-cp312-cp312-win_arm64.whl", hash = "sha256:efb768299580b786e21abe504e1652ae6189f0beed02ab087cd841cb4bb37e43", size = 16426022, upload-time = "2025-08-27T02:33:33.515Z" }, - { url = "https://files.pythonhosted.org/packages/be/29/d7b731f63d243f815d9256dce0dca3c151dcaa1ac59f73e6ee06c9afbe91/onnx-1.19.0-cp313-cp313-macosx_12_0_universal2.whl", hash = "sha256:9aed51a4b01acc9ea4e0fe522f34b2220d59e9b2a47f105ac8787c2e13ec5111", size = 18322412, upload-time = "2025-08-27T02:33:36.723Z" }, - { url = "https://files.pythonhosted.org/packages/58/f5/d3106becb42cb374f0e17ff4c9933a97f1ee1d6a798c9452067f7d3ff61b/onnx-1.19.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ce2cdc3eb518bb832668c4ea9aeeda01fbaa59d3e8e5dfaf7aa00f3d37119404", size = 18026565, upload-time = "2025-08-27T02:33:39.493Z" }, - { url = "https://files.pythonhosted.org/packages/83/fa/b086d17bab3900754c7ffbabfb244f8e5e5da54a34dda2a27022aa2b373b/onnx-1.19.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8b546bd7958734b6abcd40cfede3d025e9c274fd96334053a288ab11106bd0aa", size = 18202077, upload-time = "2025-08-27T02:33:42.115Z" }, - { url = "https://files.pythonhosted.org/packages/35/f2/5e2dfb9d4cf873f091c3f3c6d151f071da4295f9893fbf880f107efe3447/onnx-1.19.0-cp313-cp313-win32.whl", hash = "sha256:03086bffa1cf5837430cf92f892ca0cd28c72758d8905578c2bf8ffaf86c6743", size = 16333198, upload-time = "2025-08-27T02:33:45.172Z" }, - { url = "https://files.pythonhosted.org/packages/79/67/b3751a35c2522f62f313156959575619b8fa66aa883db3adda9d897d8eb2/onnx-1.19.0-cp313-cp313-win_amd64.whl", hash = "sha256:1715b51eb0ab65272e34ef51cb34696160204b003566cd8aced2ad20a8f95cb8", size = 16453836, upload-time = "2025-08-27T02:33:47.779Z" }, - { url = "https://files.pythonhosted.org/packages/14/b9/1df85effc960fbbb90bb7bc36eb3907c676b104bc2f88bce022bcfdaef63/onnx-1.19.0-cp313-cp313-win_arm64.whl", hash = "sha256:6bf5acdb97a3ddd6e70747d50b371846c313952016d0c41133cbd8f61b71a8d5", size = 16425877, upload-time = "2025-08-27T02:33:50.357Z" }, - { url = "https://files.pythonhosted.org/packages/23/2b/089174a1427be9149f37450f8959a558ba20f79fca506ba461d59379d3a1/onnx-1.19.0-cp313-cp313t-macosx_12_0_universal2.whl", hash = "sha256:46cf29adea63e68be0403c68de45ba1b6acc9bb9592c5ddc8c13675a7c71f2cb", size = 18348546, upload-time = "2025-08-27T02:33:56.132Z" }, - { url = "https://files.pythonhosted.org/packages/c0/d6/3458f0e3a9dc7677675d45d7d6528cb84ad321c8670cc10c69b32c3e03da/onnx-1.19.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:246f0de1345498d990a443d55a5b5af5101a3e25a05a2c3a5fe8b7bd7a7d0707", size = 18033067, upload-time = "2025-08-27T02:33:58.661Z" }, - { url = "https://files.pythonhosted.org/packages/e4/16/6e4130e1b4b29465ee1fb07d04e8d6f382227615c28df8f607ba50909e2a/onnx-1.19.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ae0d163ffbc250007d984b8dd692a4e2e4506151236b50ca6e3560b612ccf9ff", size = 18205741, upload-time = "2025-08-27T02:34:01.538Z" }, - { url = "https://files.pythonhosted.org/packages/fe/d8/f64d010fd024b2a2b11ce0c4ee179e4f8f6d4ccc95f8184961c894c22af1/onnx-1.19.0-cp313-cp313t-win_amd64.whl", hash = "sha256:7c151604c7cca6ae26161c55923a7b9b559df3344938f93ea0074d2d49e7fe78", size = 16453839, upload-time = "2025-08-27T02:34:06.515Z" }, - { url = "https://files.pythonhosted.org/packages/67/ec/8761048eabef4dad55af4c002c672d139b9bd47c3616abaed642a1710063/onnx-1.19.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:236bc0e60d7c0f4159300da639953dd2564df1c195bce01caba172a712e75af4", size = 18027605, upload-time = "2025-08-27T02:34:08.962Z" }, -] - [[package]] name = "onnxruntime" version = "1.24.3" @@ -1675,32 +1563,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, ] -[[package]] -name = "peft" -version = "0.19.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "accelerate" }, - { name = "huggingface-hub" }, - { name = "numpy" }, - { name = "packaging" }, - { name = "psutil" }, - { name = "pyyaml" }, - { name = "safetensors" }, - { name = "torch" }, - { name = "tqdm" }, - { name = "transformers" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/86/cf/037f1e3d5186496c05513a6754639e2dab3038a05f384284d49a9bd06a2d/peft-0.19.1.tar.gz", hash = "sha256:0d97542fe96dcdaa20d3b81c06f26f988618f416a73544ab23c3618ccb674a40", size = 763738, upload-time = "2026-04-16T15:46:45.105Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e8/b6/f54d676ed93cc2dd2234c3b172ea9c8c3d7d29361e66b1b23dec57a67465/peft-0.19.1-py3-none-any.whl", hash = "sha256:2113f72a81621b5913ef28f9022204c742df111890c5f49d812716a4a301e356", size = 680692, upload-time = "2026-04-16T15:46:42.886Z" }, -] - -[[package]] -name = "photomaker" -version = "0.2.0" -source = { git = "https://github.com/TencentARC/PhotoMaker.git#060b4fcb10b76a4554edf565d6106b7e36c968f0" } - [[package]] name = "piexif" version = "1.1.3" @@ -2431,26 +2293,11 @@ gpu = [ { name = "torch" }, { name = "transformers" }, ] -instantid = [ - { name = "huggingface-hub" }, - { name = "insightface" }, - { name = "onnxruntime", version = "1.24.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "onnxruntime", version = "1.26.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, -] lama = [ { name = "huggingface-hub" }, { name = "onnxruntime", version = "1.24.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "onnxruntime", version = "1.26.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, ] -photomaker = [ - { name = "einops" }, - { name = "huggingface-hub" }, - { name = "insightface" }, - { name = "onnxruntime", version = "1.24.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "onnxruntime", version = "1.26.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "peft" }, - { name = "photomaker" }, -] trustmark = [ { name = "trustmark" }, ] @@ -2460,21 +2307,12 @@ requires-dist = [ { name = "accelerate", marker = "extra == 'gpu'", specifier = ">=0.25.0" }, { name = "click", specifier = ">=8.0.0" }, { name = "diffusers", marker = "extra == 'gpu'", specifier = ">=0.38.0" }, - { name = "einops", marker = "extra == 'photomaker'", specifier = ">=0.7.0" }, - { name = "huggingface-hub", marker = "extra == 'instantid'", specifier = ">=0.20.0" }, { name = "huggingface-hub", marker = "extra == 'lama'", specifier = ">=0.20.0" }, - { name = "huggingface-hub", marker = "extra == 'photomaker'", specifier = ">=0.20.0" }, - { name = "insightface", marker = "extra == 'instantid'", specifier = ">=0.7.3" }, - { name = "insightface", marker = "extra == 'photomaker'", specifier = ">=0.7.3" }, { name = "invisible-watermark", marker = "extra == 'detect'", specifier = ">=0.2.0" }, { name = "invisible-watermark", marker = "extra == 'dev'", specifier = ">=0.2.0" }, { name = "numpy", specifier = ">=1.24.0" }, - { name = "onnxruntime", marker = "extra == 'instantid'", specifier = ">=1.16.0" }, { name = "onnxruntime", marker = "extra == 'lama'", specifier = ">=1.16.0" }, - { name = "onnxruntime", marker = "extra == 'photomaker'", specifier = ">=1.16.0" }, { name = "opencv-python-headless", specifier = ">=4.8.0" }, - { name = "peft", marker = "extra == 'photomaker'", specifier = ">=0.10.0" }, - { name = "photomaker", marker = "extra == 'photomaker'", git = "https://github.com/TencentARC/PhotoMaker.git" }, { name = "piexif", specifier = ">=1.1.3" }, { name = "pillow", specifier = ">=10.0.0" }, { name = "pyright", marker = "extra == 'dev'", specifier = ">=1.1.0" }, @@ -2490,7 +2328,7 @@ requires-dist = [ { name = "transformers", marker = "extra == 'gpu'", specifier = ">=5,<6" }, { name = "trustmark", marker = "extra == 'trustmark'", specifier = ">=0.8.0" }, ] -provides-extras = ["gpu", "detect", "trustmark", "lama", "photomaker", "instantid", "esrgan", "dev", "all"] +provides-extras = ["gpu", "detect", "trustmark", "lama", "esrgan", "dev", "all"] [[package]] name = "requests" @@ -2569,272 +2407,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/51/73/fd944d3417ba04bd0e72682fa1bedc6d99d986a3594fc7910313088cfe88/safetensors-0.8.0rc0-cp310-abi3-win_arm64.whl", hash = "sha256:b7f8180f8c119dce85da7913904ccf4a0227adf095eb63f1732a6729c2672cb1", size = 330970, upload-time = "2026-04-14T14:30:43.451Z" }, ] -[[package]] -name = "scikit-image" -version = "0.25.2" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.11' and sys_platform == 'darwin'", - "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')", -] -dependencies = [ - { name = "imageio", marker = "python_full_version < '3.11'" }, - { name = "lazy-loader", marker = "python_full_version < '3.11'" }, - { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "numpy", marker = "python_full_version < '3.11'" }, - { name = "packaging", marker = "python_full_version < '3.11'" }, - { name = "pillow", marker = "python_full_version < '3.11'" }, - { name = "scipy", version = "1.15.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "tifffile", version = "2025.5.10", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c7/a8/3c0f256012b93dd2cb6fda9245e9f4bff7dc0486880b248005f15ea2255e/scikit_image-0.25.2.tar.gz", hash = "sha256:e5a37e6cd4d0c018a7a55b9d601357e3382826d3888c10d0213fc63bff977dde", size = 22693594, upload-time = "2025-02-18T18:05:24.538Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/11/cb/016c63f16065c2d333c8ed0337e18a5cdf9bc32d402e4f26b0db362eb0e2/scikit_image-0.25.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d3278f586793176599df6a4cf48cb6beadae35c31e58dc01a98023af3dc31c78", size = 13988922, upload-time = "2025-02-18T18:04:11.069Z" }, - { url = "https://files.pythonhosted.org/packages/30/ca/ff4731289cbed63c94a0c9a5b672976603118de78ed21910d9060c82e859/scikit_image-0.25.2-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:5c311069899ce757d7dbf1d03e32acb38bb06153236ae77fcd820fd62044c063", size = 13192698, upload-time = "2025-02-18T18:04:15.362Z" }, - { url = "https://files.pythonhosted.org/packages/39/6d/a2aadb1be6d8e149199bb9b540ccde9e9622826e1ab42fe01de4c35ab918/scikit_image-0.25.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be455aa7039a6afa54e84f9e38293733a2622b8c2fb3362b822d459cc5605e99", size = 14153634, upload-time = "2025-02-18T18:04:18.496Z" }, - { url = "https://files.pythonhosted.org/packages/96/08/916e7d9ee4721031b2f625db54b11d8379bd51707afaa3e5a29aecf10bc4/scikit_image-0.25.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a4c464b90e978d137330be433df4e76d92ad3c5f46a22f159520ce0fdbea8a09", size = 14767545, upload-time = "2025-02-18T18:04:22.556Z" }, - { url = "https://files.pythonhosted.org/packages/5f/ee/c53a009e3997dda9d285402f19226fbd17b5b3cb215da391c4ed084a1424/scikit_image-0.25.2-cp310-cp310-win_amd64.whl", hash = "sha256:60516257c5a2d2f74387c502aa2f15a0ef3498fbeaa749f730ab18f0a40fd054", size = 12812908, upload-time = "2025-02-18T18:04:26.364Z" }, - { url = "https://files.pythonhosted.org/packages/c4/97/3051c68b782ee3f1fb7f8f5bb7d535cf8cb92e8aae18fa9c1cdf7e15150d/scikit_image-0.25.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f4bac9196fb80d37567316581c6060763b0f4893d3aca34a9ede3825bc035b17", size = 14003057, upload-time = "2025-02-18T18:04:30.395Z" }, - { url = "https://files.pythonhosted.org/packages/19/23/257fc696c562639826065514d551b7b9b969520bd902c3a8e2fcff5b9e17/scikit_image-0.25.2-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:d989d64ff92e0c6c0f2018c7495a5b20e2451839299a018e0e5108b2680f71e0", size = 13180335, upload-time = "2025-02-18T18:04:33.449Z" }, - { url = "https://files.pythonhosted.org/packages/ef/14/0c4a02cb27ca8b1e836886b9ec7c9149de03053650e9e2ed0625f248dd92/scikit_image-0.25.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2cfc96b27afe9a05bc92f8c6235321d3a66499995675b27415e0d0c76625173", size = 14144783, upload-time = "2025-02-18T18:04:36.594Z" }, - { url = "https://files.pythonhosted.org/packages/dd/9b/9fb556463a34d9842491d72a421942c8baff4281025859c84fcdb5e7e602/scikit_image-0.25.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24cc986e1f4187a12aa319f777b36008764e856e5013666a4a83f8df083c2641", size = 14785376, upload-time = "2025-02-18T18:04:39.856Z" }, - { url = "https://files.pythonhosted.org/packages/de/ec/b57c500ee85885df5f2188f8bb70398481393a69de44a00d6f1d055f103c/scikit_image-0.25.2-cp311-cp311-win_amd64.whl", hash = "sha256:b4f6b61fc2db6340696afe3db6b26e0356911529f5f6aee8c322aa5157490c9b", size = 12791698, upload-time = "2025-02-18T18:04:42.868Z" }, - { url = "https://files.pythonhosted.org/packages/35/8c/5df82881284459f6eec796a5ac2a0a304bb3384eec2e73f35cfdfcfbf20c/scikit_image-0.25.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8db8dd03663112783221bf01ccfc9512d1cc50ac9b5b0fe8f4023967564719fb", size = 13986000, upload-time = "2025-02-18T18:04:47.156Z" }, - { url = "https://files.pythonhosted.org/packages/ce/e6/93bebe1abcdce9513ffec01d8af02528b4c41fb3c1e46336d70b9ed4ef0d/scikit_image-0.25.2-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:483bd8cc10c3d8a7a37fae36dfa5b21e239bd4ee121d91cad1f81bba10cfb0ed", size = 13235893, upload-time = "2025-02-18T18:04:51.049Z" }, - { url = "https://files.pythonhosted.org/packages/53/4b/eda616e33f67129e5979a9eb33c710013caa3aa8a921991e6cc0b22cea33/scikit_image-0.25.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d1e80107bcf2bf1291acfc0bf0425dceb8890abe9f38d8e94e23497cbf7ee0d", size = 14178389, upload-time = "2025-02-18T18:04:54.245Z" }, - { url = "https://files.pythonhosted.org/packages/6b/b5/b75527c0f9532dd8a93e8e7cd8e62e547b9f207d4c11e24f0006e8646b36/scikit_image-0.25.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a17e17eb8562660cc0d31bb55643a4da996a81944b82c54805c91b3fe66f4824", size = 15003435, upload-time = "2025-02-18T18:04:57.586Z" }, - { url = "https://files.pythonhosted.org/packages/34/e3/49beb08ebccda3c21e871b607c1cb2f258c3fa0d2f609fed0a5ba741b92d/scikit_image-0.25.2-cp312-cp312-win_amd64.whl", hash = "sha256:bdd2b8c1de0849964dbc54037f36b4e9420157e67e45a8709a80d727f52c7da2", size = 12899474, upload-time = "2025-02-18T18:05:01.166Z" }, - { url = "https://files.pythonhosted.org/packages/e6/7c/9814dd1c637f7a0e44342985a76f95a55dd04be60154247679fd96c7169f/scikit_image-0.25.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7efa888130f6c548ec0439b1a7ed7295bc10105458a421e9bf739b457730b6da", size = 13921841, upload-time = "2025-02-18T18:05:03.963Z" }, - { url = "https://files.pythonhosted.org/packages/84/06/66a2e7661d6f526740c309e9717d3bd07b473661d5cdddef4dd978edab25/scikit_image-0.25.2-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:dd8011efe69c3641920614d550f5505f83658fe33581e49bed86feab43a180fc", size = 13196862, upload-time = "2025-02-18T18:05:06.986Z" }, - { url = "https://files.pythonhosted.org/packages/4e/63/3368902ed79305f74c2ca8c297dfeb4307269cbe6402412668e322837143/scikit_image-0.25.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28182a9d3e2ce3c2e251383bdda68f8d88d9fff1a3ebe1eb61206595c9773341", size = 14117785, upload-time = "2025-02-18T18:05:10.69Z" }, - { url = "https://files.pythonhosted.org/packages/cd/9b/c3da56a145f52cd61a68b8465d6a29d9503bc45bc993bb45e84371c97d94/scikit_image-0.25.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8abd3c805ce6944b941cfed0406d88faeb19bab3ed3d4b50187af55cf24d147", size = 14977119, upload-time = "2025-02-18T18:05:13.871Z" }, - { url = "https://files.pythonhosted.org/packages/8a/97/5fcf332e1753831abb99a2525180d3fb0d70918d461ebda9873f66dcc12f/scikit_image-0.25.2-cp313-cp313-win_amd64.whl", hash = "sha256:64785a8acefee460ec49a354706db0b09d1f325674107d7fa3eadb663fb56d6f", size = 12885116, upload-time = "2025-02-18T18:05:17.844Z" }, - { url = "https://files.pythonhosted.org/packages/10/cc/75e9f17e3670b5ed93c32456fda823333c6279b144cd93e2c03aa06aa472/scikit_image-0.25.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:330d061bd107d12f8d68f1d611ae27b3b813b8cdb0300a71d07b1379178dd4cd", size = 13862801, upload-time = "2025-02-18T18:05:20.783Z" }, -] - -[[package]] -name = "scikit-image" -version = "0.26.0" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.13' and platform_machine != 's390x' and sys_platform == 'darwin'", - "python_full_version >= '3.13' and platform_machine == 's390x' and sys_platform == 'darwin'", - "python_full_version == '3.12.*' and sys_platform == 'darwin'", - "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(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 != 'linux')", - "(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 != 'linux')", - "python_full_version == '3.11.*' and sys_platform == 'darwin'", - "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", -] -dependencies = [ - { name = "imageio", marker = "python_full_version >= '3.11'" }, - { name = "lazy-loader", marker = "python_full_version >= '3.11'" }, - { name = "networkx", version = "3.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "numpy", marker = "python_full_version >= '3.11'" }, - { name = "packaging", marker = "python_full_version >= '3.11'" }, - { name = "pillow", marker = "python_full_version >= '3.11'" }, - { name = "scipy", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "tifffile", version = "2026.3.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/a1/b4/2528bb43c67d48053a7a649a9666432dc307d66ba02e3a6d5c40f46655df/scikit_image-0.26.0.tar.gz", hash = "sha256:f5f970ab04efad85c24714321fcc91613fcb64ef2a892a13167df2f3e59199fa", size = 22729739, upload-time = "2025-12-20T17:12:21.824Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/76/16/8a407688b607f86f81f8c649bf0d68a2a6d67375f18c2d660aba20f5b648/scikit_image-0.26.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b1ede33a0fb3731457eaf53af6361e73dd510f449dac437ab54573b26788baf0", size = 12355510, upload-time = "2025-12-20T17:10:31.628Z" }, - { url = "https://files.pythonhosted.org/packages/6b/f9/7efc088ececb6f6868fd4475e16cfafc11f242ce9ab5fc3557d78b5da0d4/scikit_image-0.26.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7af7aa331c6846bd03fa28b164c18d0c3fd419dbb888fb05e958ac4257a78fdd", size = 12056334, upload-time = "2025-12-20T17:10:34.559Z" }, - { url = "https://files.pythonhosted.org/packages/9f/1e/bc7fb91fb5ff65ef42346c8b7ee8b09b04eabf89235ab7dbfdfd96cbd1ea/scikit_image-0.26.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9ea6207d9e9d21c3f464efe733121c0504e494dbdc7728649ff3e23c3c5a4953", size = 13297768, upload-time = "2025-12-20T17:10:37.733Z" }, - { url = "https://files.pythonhosted.org/packages/a5/2a/e71c1a7d90e70da67b88ccc609bd6ae54798d5847369b15d3a8052232f9d/scikit_image-0.26.0-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:74aa5518ccea28121f57a95374581d3b979839adc25bb03f289b1bc9b99c58af", size = 13711217, upload-time = "2025-12-20T17:10:40.935Z" }, - { url = "https://files.pythonhosted.org/packages/d4/59/9637ee12c23726266b91296791465218973ce1ad3e4c56fc81e4d8e7d6e1/scikit_image-0.26.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d5c244656de905e195a904e36dbc18585e06ecf67d90f0482cbde63d7f9ad59d", size = 14337782, upload-time = "2025-12-20T17:10:43.452Z" }, - { url = "https://files.pythonhosted.org/packages/e7/5c/a3e1e0860f9294663f540c117e4bf83d55e5b47c281d475cc06227e88411/scikit_image-0.26.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:21a818ee6ca2f2131b9e04d8eb7637b5c18773ebe7b399ad23dcc5afaa226d2d", size = 14805997, upload-time = "2025-12-20T17:10:45.93Z" }, - { url = "https://files.pythonhosted.org/packages/d3/c6/2eeacf173da041a9e388975f54e5c49df750757fcfc3ee293cdbbae1ea0a/scikit_image-0.26.0-cp311-cp311-win_amd64.whl", hash = "sha256:9490360c8d3f9a7e85c8de87daf7c0c66507960cf4947bb9610d1751928721c7", size = 11878486, upload-time = "2025-12-20T17:10:48.246Z" }, - { url = "https://files.pythonhosted.org/packages/c3/a4/a852c4949b9058d585e762a66bf7e9a2cd3be4795cd940413dfbfbb0ce79/scikit_image-0.26.0-cp311-cp311-win_arm64.whl", hash = "sha256:0baa0108d2d027f34d748e84e592b78acc23e965a5de0e4bb03cf371de5c0581", size = 11346518, upload-time = "2025-12-20T17:10:50.575Z" }, - { url = "https://files.pythonhosted.org/packages/99/e8/e13757982264b33a1621628f86b587e9a73a13f5256dad49b19ba7dc9083/scikit_image-0.26.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d454b93a6fa770ac5ae2d33570f8e7a321bb80d29511ce4b6b78058ebe176e8c", size = 12376452, upload-time = "2025-12-20T17:10:52.796Z" }, - { url = "https://files.pythonhosted.org/packages/e3/be/f8dd17d0510f9911f9f17ba301f7455328bf13dae416560126d428de9568/scikit_image-0.26.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3409e89d66eff5734cd2b672d1c48d2759360057e714e1d92a11df82c87cba37", size = 12061567, upload-time = "2025-12-20T17:10:55.207Z" }, - { url = "https://files.pythonhosted.org/packages/b3/2b/c70120a6880579fb42b91567ad79feb4772f7be72e8d52fec403a3dde0c6/scikit_image-0.26.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c717490cec9e276afb0438dd165b7c3072d6c416709cc0f9f5a4c1070d23a44", size = 13084214, upload-time = "2025-12-20T17:10:57.468Z" }, - { url = "https://files.pythonhosted.org/packages/f4/a2/70401a107d6d7466d64b466927e6b96fcefa99d57494b972608e2f8be50f/scikit_image-0.26.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7df650e79031634ac90b11e64a9eedaf5a5e06fcd09bcd03a34be01745744466", size = 13561683, upload-time = "2025-12-20T17:10:59.49Z" }, - { url = "https://files.pythonhosted.org/packages/13/a5/48bdfd92794c5002d664e0910a349d0a1504671ef5ad358150f21643c79a/scikit_image-0.26.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:cefd85033e66d4ea35b525bb0937d7f42d4cdcfed2d1888e1570d5ce450d3932", size = 14112147, upload-time = "2025-12-20T17:11:02.083Z" }, - { url = "https://files.pythonhosted.org/packages/ee/b5/ac71694da92f5def5953ca99f18a10fe98eac2dd0a34079389b70b4d0394/scikit_image-0.26.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3f5bf622d7c0435884e1e141ebbe4b2804e16b2dd23ae4c6183e2ea99233be70", size = 14661625, upload-time = "2025-12-20T17:11:04.528Z" }, - { url = "https://files.pythonhosted.org/packages/23/4d/a3cc1e96f080e253dad2251bfae7587cf2b7912bcd76fd43fd366ff35a87/scikit_image-0.26.0-cp312-cp312-win_amd64.whl", hash = "sha256:abed017474593cd3056ae0fe948d07d0747b27a085e92df5474f4955dd65aec0", size = 11911059, upload-time = "2025-12-20T17:11:06.61Z" }, - { url = "https://files.pythonhosted.org/packages/35/8a/d1b8055f584acc937478abf4550d122936f420352422a1a625eef2c605d8/scikit_image-0.26.0-cp312-cp312-win_arm64.whl", hash = "sha256:4d57e39ef67a95d26860c8caf9b14b8fb130f83b34c6656a77f191fa6d1d04d8", size = 11348740, upload-time = "2025-12-20T17:11:09.118Z" }, - { url = "https://files.pythonhosted.org/packages/4f/48/02357ffb2cca35640f33f2cfe054a4d6d5d7a229b88880a64f1e45c11f4e/scikit_image-0.26.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a2e852eccf41d2d322b8e60144e124802873a92b8d43a6f96331aa42888491c7", size = 12346329, upload-time = "2025-12-20T17:11:11.599Z" }, - { url = "https://files.pythonhosted.org/packages/67/b9/b792c577cea2c1e94cda83b135a656924fc57c428e8a6d302cd69aac1b60/scikit_image-0.26.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:98329aab3bc87db352b9887f64ce8cdb8e75f7c2daa19927f2e121b797b678d5", size = 12031726, upload-time = "2025-12-20T17:11:13.871Z" }, - { url = "https://files.pythonhosted.org/packages/07/a9/9564250dfd65cb20404a611016db52afc6268b2b371cd19c7538ea47580f/scikit_image-0.26.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:915bb3ba66455cf8adac00dc8fdf18a4cd29656aec7ddd38cb4dda90289a6f21", size = 13094910, upload-time = "2025-12-20T17:11:16.2Z" }, - { url = "https://files.pythonhosted.org/packages/a3/b8/0d8eeb5a9fd7d34ba84f8a55753a0a3e2b5b51b2a5a0ade648a8db4a62f7/scikit_image-0.26.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b36ab5e778bf50af5ff386c3ac508027dc3aaeccf2161bdf96bde6848f44d21b", size = 13660939, upload-time = "2025-12-20T17:11:18.464Z" }, - { url = "https://files.pythonhosted.org/packages/2f/d6/91d8973584d4793d4c1a847d388e34ef1218d835eeddecfc9108d735b467/scikit_image-0.26.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:09bad6a5d5949c7896c8347424c4cca899f1d11668030e5548813ab9c2865dcb", size = 14138938, upload-time = "2025-12-20T17:11:20.919Z" }, - { url = "https://files.pythonhosted.org/packages/39/9a/7e15d8dc10d6bbf212195fb39bdeb7f226c46dd53f9c63c312e111e2e175/scikit_image-0.26.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:aeb14db1ed09ad4bee4ceb9e635547a8d5f3549be67fc6c768c7f923e027e6cd", size = 14752243, upload-time = "2025-12-20T17:11:23.347Z" }, - { url = "https://files.pythonhosted.org/packages/8f/58/2b11b933097bc427e42b4a8b15f7de8f24f2bac1fd2779d2aea1431b2c31/scikit_image-0.26.0-cp313-cp313-win_amd64.whl", hash = "sha256:ac529eb9dbd5954f9aaa2e3fe9a3fd9661bfe24e134c688587d811a0233127f1", size = 11906770, upload-time = "2025-12-20T17:11:25.297Z" }, - { url = "https://files.pythonhosted.org/packages/ad/ec/96941474a18a04b69b6f6562a5bd79bd68049fa3728d3b350976eccb8b93/scikit_image-0.26.0-cp313-cp313-win_arm64.whl", hash = "sha256:a2d211bc355f59725efdcae699b93b30348a19416cc9e017f7b2fb599faf7219", size = 11342506, upload-time = "2025-12-20T17:11:27.399Z" }, - { url = "https://files.pythonhosted.org/packages/03/e5/c1a9962b0cf1952f42d32b4a2e48eed520320dbc4d2ff0b981c6fa508b6b/scikit_image-0.26.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:9eefb4adad066da408a7601c4c24b07af3b472d90e08c3e7483d4e9e829d8c49", size = 12663278, upload-time = "2025-12-20T17:11:29.358Z" }, - { url = "https://files.pythonhosted.org/packages/ae/97/c1a276a59ce8e4e24482d65c1a3940d69c6b3873279193b7ebd04e5ee56b/scikit_image-0.26.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:6caec76e16c970c528d15d1c757363334d5cb3069f9cea93d2bead31820511f3", size = 12405142, upload-time = "2025-12-20T17:11:31.282Z" }, - { url = "https://files.pythonhosted.org/packages/d4/4a/f1cbd1357caef6c7993f7efd514d6e53d8fd6f7fe01c4714d51614c53289/scikit_image-0.26.0-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a07200fe09b9d99fcdab959859fe0f7db8df6333d6204344425d476850ce3604", size = 12942086, upload-time = "2025-12-20T17:11:33.683Z" }, - { url = "https://files.pythonhosted.org/packages/5b/6f/74d9fb87c5655bd64cf00b0c44dc3d6206d9002e5f6ba1c9aeb13236f6bf/scikit_image-0.26.0-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:92242351bccf391fc5df2d1529d15470019496d2498d615beb68da85fe7fdf37", size = 13265667, upload-time = "2025-12-20T17:11:36.11Z" }, - { url = "https://files.pythonhosted.org/packages/a7/73/faddc2413ae98d863f6fa2e3e14da4467dd38e788e1c23346cf1a2b06b97/scikit_image-0.26.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:52c496f75a7e45844d951557f13c08c81487c6a1da2e3c9c8a39fcde958e02cc", size = 14001966, upload-time = "2025-12-20T17:11:38.55Z" }, - { url = "https://files.pythonhosted.org/packages/02/94/9f46966fa042b5d57c8cd641045372b4e0df0047dd400e77ea9952674110/scikit_image-0.26.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:20ef4a155e2e78b8ab973998e04d8a361d49d719e65412405f4dadd9155a61d9", size = 14359526, upload-time = "2025-12-20T17:11:41.087Z" }, - { url = "https://files.pythonhosted.org/packages/5d/b4/2840fe38f10057f40b1c9f8fb98a187a370936bf144a4ac23452c5ef1baf/scikit_image-0.26.0-cp313-cp313t-win_amd64.whl", hash = "sha256:c9087cf7d0e7f33ab5c46d2068d86d785e70b05400a891f73a13400f1e1faf6a", size = 12287629, upload-time = "2025-12-20T17:11:43.11Z" }, - { url = "https://files.pythonhosted.org/packages/22/ba/73b6ca70796e71f83ab222690e35a79612f0117e5aaf167151b7d46f5f2c/scikit_image-0.26.0-cp313-cp313t-win_arm64.whl", hash = "sha256:27d58bc8b2acd351f972c6508c1b557cfed80299826080a4d803dd29c51b707e", size = 11647755, upload-time = "2025-12-20T17:11:45.279Z" }, - { url = "https://files.pythonhosted.org/packages/51/44/6b744f92b37ae2833fd423cce8f806d2368859ec325a699dc30389e090b9/scikit_image-0.26.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:63af3d3a26125f796f01052052f86806da5b5e54c6abef152edb752683075a9c", size = 12365810, upload-time = "2025-12-20T17:11:47.357Z" }, - { url = "https://files.pythonhosted.org/packages/40/f5/83590d9355191f86ac663420fec741b82cc547a4afe7c4c1d986bf46e4db/scikit_image-0.26.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ce00600cd70d4562ed59f80523e18cdcc1fae0e10676498a01f73c255774aefd", size = 12075717, upload-time = "2025-12-20T17:11:49.483Z" }, - { url = "https://files.pythonhosted.org/packages/72/48/253e7cf5aee6190459fe136c614e2cbccc562deceb4af96e0863f1b8ee29/scikit_image-0.26.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6381edf972b32e4f54085449afde64365a57316637496c1325a736987083e2ab", size = 13161520, upload-time = "2025-12-20T17:11:51.58Z" }, - { url = "https://files.pythonhosted.org/packages/73/c3/cec6a3cbaadfdcc02bd6ff02f3abfe09eaa7f4d4e0a525a1e3a3f4bce49c/scikit_image-0.26.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c6624a76c6085218248154cc7e1500e6b488edcd9499004dd0d35040607d7505", size = 13684340, upload-time = "2025-12-20T17:11:53.708Z" }, - { url = "https://files.pythonhosted.org/packages/d4/0d/39a776f675d24164b3a267aa0db9f677a4cb20127660d8bf4fd7fef66817/scikit_image-0.26.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f775f0e420faac9c2aa6757135f4eb468fb7b70e0b67fa77a5e79be3c30ee331", size = 14203839, upload-time = "2025-12-20T17:11:55.89Z" }, - { url = "https://files.pythonhosted.org/packages/ee/25/2514df226bbcedfe9b2caafa1ba7bc87231a0c339066981b182b08340e06/scikit_image-0.26.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede4d6d255cc5da9faeb2f9ba7fedbc990abbc652db429f40a16b22e770bb578", size = 14770021, upload-time = "2025-12-20T17:11:58.014Z" }, - { url = "https://files.pythonhosted.org/packages/8d/5b/0671dc91c0c79340c3fe202f0549c7d3681eb7640fe34ab68a5f090a7c7f/scikit_image-0.26.0-cp314-cp314-win_amd64.whl", hash = "sha256:0660b83968c15293fd9135e8d860053ee19500d52bf55ca4fb09de595a1af650", size = 12023490, upload-time = "2025-12-20T17:12:00.013Z" }, - { url = "https://files.pythonhosted.org/packages/65/08/7c4cb59f91721f3de07719085212a0b3962e3e3f2d1818cbac4eeb1ea53e/scikit_image-0.26.0-cp314-cp314-win_arm64.whl", hash = "sha256:b8d14d3181c21c11170477a42542c1addc7072a90b986675a71266ad17abc37f", size = 11473782, upload-time = "2025-12-20T17:12:01.983Z" }, - { url = "https://files.pythonhosted.org/packages/49/41/65c4258137acef3d73cb561ac55512eacd7b30bb4f4a11474cad526bc5db/scikit_image-0.26.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:cde0bbd57e6795eba83cb10f71a677f7239271121dc950bc060482834a668ad1", size = 12686060, upload-time = "2025-12-20T17:12:03.886Z" }, - { url = "https://files.pythonhosted.org/packages/e7/32/76971f8727b87f1420a962406388a50e26667c31756126444baf6668f559/scikit_image-0.26.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:163e9afb5b879562b9aeda0dd45208a35316f26cc7a3aed54fd601604e5cf46f", size = 12422628, upload-time = "2025-12-20T17:12:05.921Z" }, - { url = "https://files.pythonhosted.org/packages/37/0d/996febd39f757c40ee7b01cdb861867327e5c8e5f595a634e8201462d958/scikit_image-0.26.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:724f79fd9b6cb6f4a37864fe09f81f9f5d5b9646b6868109e1b100d1a7019e59", size = 12962369, upload-time = "2025-12-20T17:12:07.912Z" }, - { url = "https://files.pythonhosted.org/packages/48/b4/612d354f946c9600e7dea012723c11d47e8d455384e530f6daaaeb9bf62c/scikit_image-0.26.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3268f13310e6857508bd87202620df996199a016a1d281b309441d227c822394", size = 13272431, upload-time = "2025-12-20T17:12:10.255Z" }, - { url = "https://files.pythonhosted.org/packages/0a/6e/26c00b466e06055a086de2c6e2145fe189ccdc9a1d11ccc7de020f2591ad/scikit_image-0.26.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fac96a1f9b06cd771cbbb3cd96c5332f36d4efd839b1d8b053f79e5887acde62", size = 14016362, upload-time = "2025-12-20T17:12:12.793Z" }, - { url = "https://files.pythonhosted.org/packages/47/88/00a90402e1775634043c2a0af8a3c76ad450866d9fa444efcc43b553ba2d/scikit_image-0.26.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2c1e7bd342f43e7a97e571b3f03ba4c1293ea1a35c3f13f41efdc8a81c1dc8f2", size = 14364151, upload-time = "2025-12-20T17:12:14.909Z" }, - { url = "https://files.pythonhosted.org/packages/da/ca/918d8d306bd43beacff3b835c6d96fac0ae64c0857092f068b88db531a7c/scikit_image-0.26.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b702c3bb115e1dcf4abf5297429b5c90f2189655888cbed14921f3d26f81d3a4", size = 12413484, upload-time = "2025-12-20T17:12:17.046Z" }, - { url = "https://files.pythonhosted.org/packages/dc/cd/4da01329b5a8d47ff7ec3c99a2b02465a8017b186027590dc7425cee0b56/scikit_image-0.26.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0608aa4a9ec39e0843de10d60edb2785a30c1c47819b67866dd223ebd149acaf", size = 11769501, upload-time = "2025-12-20T17:12:19.339Z" }, -] - -[[package]] -name = "scipy" -version = "1.15.3" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.11' and sys_platform == 'darwin'", - "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')", -] -dependencies = [ - { name = "numpy", marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/0f/37/6964b830433e654ec7485e45a00fc9a27cf868d622838f6b6d9c5ec0d532/scipy-1.15.3.tar.gz", hash = "sha256:eae3cf522bc7df64b42cad3925c876e1b0b6c35c1337c93e12c0f366f55b0eaf", size = 59419214, upload-time = "2025-05-08T16:13:05.955Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/78/2f/4966032c5f8cc7e6a60f1b2e0ad686293b9474b65246b0c642e3ef3badd0/scipy-1.15.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:a345928c86d535060c9c2b25e71e87c39ab2f22fc96e9636bd74d1dbf9de448c", size = 38702770, upload-time = "2025-05-08T16:04:20.849Z" }, - { url = "https://files.pythonhosted.org/packages/a0/6e/0c3bf90fae0e910c274db43304ebe25a6b391327f3f10b5dcc638c090795/scipy-1.15.3-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:ad3432cb0f9ed87477a8d97f03b763fd1d57709f1bbde3c9369b1dff5503b253", size = 30094511, upload-time = "2025-05-08T16:04:27.103Z" }, - { url = "https://files.pythonhosted.org/packages/ea/b1/4deb37252311c1acff7f101f6453f0440794f51b6eacb1aad4459a134081/scipy-1.15.3-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:aef683a9ae6eb00728a542b796f52a5477b78252edede72b8327a886ab63293f", size = 22368151, upload-time = "2025-05-08T16:04:31.731Z" }, - { url = "https://files.pythonhosted.org/packages/38/7d/f457626e3cd3c29b3a49ca115a304cebb8cc6f31b04678f03b216899d3c6/scipy-1.15.3-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:1c832e1bd78dea67d5c16f786681b28dd695a8cb1fb90af2e27580d3d0967e92", size = 25121732, upload-time = "2025-05-08T16:04:36.596Z" }, - { url = "https://files.pythonhosted.org/packages/db/0a/92b1de4a7adc7a15dcf5bddc6e191f6f29ee663b30511ce20467ef9b82e4/scipy-1.15.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:263961f658ce2165bbd7b99fa5135195c3a12d9bef045345016b8b50c315cb82", size = 35547617, upload-time = "2025-05-08T16:04:43.546Z" }, - { url = "https://files.pythonhosted.org/packages/8e/6d/41991e503e51fc1134502694c5fa7a1671501a17ffa12716a4a9151af3df/scipy-1.15.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e2abc762b0811e09a0d3258abee2d98e0c703eee49464ce0069590846f31d40", size = 37662964, upload-time = "2025-05-08T16:04:49.431Z" }, - { url = "https://files.pythonhosted.org/packages/25/e1/3df8f83cb15f3500478c889be8fb18700813b95e9e087328230b98d547ff/scipy-1.15.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ed7284b21a7a0c8f1b6e5977ac05396c0d008b89e05498c8b7e8f4a1423bba0e", size = 37238749, upload-time = "2025-05-08T16:04:55.215Z" }, - { url = "https://files.pythonhosted.org/packages/93/3e/b3257cf446f2a3533ed7809757039016b74cd6f38271de91682aa844cfc5/scipy-1.15.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5380741e53df2c566f4d234b100a484b420af85deb39ea35a1cc1be84ff53a5c", size = 40022383, upload-time = "2025-05-08T16:05:01.914Z" }, - { url = "https://files.pythonhosted.org/packages/d1/84/55bc4881973d3f79b479a5a2e2df61c8c9a04fcb986a213ac9c02cfb659b/scipy-1.15.3-cp310-cp310-win_amd64.whl", hash = "sha256:9d61e97b186a57350f6d6fd72640f9e99d5a4a2b8fbf4b9ee9a841eab327dc13", size = 41259201, upload-time = "2025-05-08T16:05:08.166Z" }, - { url = "https://files.pythonhosted.org/packages/96/ab/5cc9f80f28f6a7dff646c5756e559823614a42b1939d86dd0ed550470210/scipy-1.15.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:993439ce220d25e3696d1b23b233dd010169b62f6456488567e830654ee37a6b", size = 38714255, upload-time = "2025-05-08T16:05:14.596Z" }, - { url = "https://files.pythonhosted.org/packages/4a/4a/66ba30abe5ad1a3ad15bfb0b59d22174012e8056ff448cb1644deccbfed2/scipy-1.15.3-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:34716e281f181a02341ddeaad584205bd2fd3c242063bd3423d61ac259ca7eba", size = 30111035, upload-time = "2025-05-08T16:05:20.152Z" }, - { url = "https://files.pythonhosted.org/packages/4b/fa/a7e5b95afd80d24313307f03624acc65801846fa75599034f8ceb9e2cbf6/scipy-1.15.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3b0334816afb8b91dab859281b1b9786934392aa3d527cd847e41bb6f45bee65", size = 22384499, upload-time = "2025-05-08T16:05:24.494Z" }, - { url = "https://files.pythonhosted.org/packages/17/99/f3aaddccf3588bb4aea70ba35328c204cadd89517a1612ecfda5b2dd9d7a/scipy-1.15.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:6db907c7368e3092e24919b5e31c76998b0ce1684d51a90943cb0ed1b4ffd6c1", size = 25152602, upload-time = "2025-05-08T16:05:29.313Z" }, - { url = "https://files.pythonhosted.org/packages/56/c5/1032cdb565f146109212153339f9cb8b993701e9fe56b1c97699eee12586/scipy-1.15.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:721d6b4ef5dc82ca8968c25b111e307083d7ca9091bc38163fb89243e85e3889", size = 35503415, upload-time = "2025-05-08T16:05:34.699Z" }, - { url = "https://files.pythonhosted.org/packages/bd/37/89f19c8c05505d0601ed5650156e50eb881ae3918786c8fd7262b4ee66d3/scipy-1.15.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39cb9c62e471b1bb3750066ecc3a3f3052b37751c7c3dfd0fd7e48900ed52982", size = 37652622, upload-time = "2025-05-08T16:05:40.762Z" }, - { url = "https://files.pythonhosted.org/packages/7e/31/be59513aa9695519b18e1851bb9e487de66f2d31f835201f1b42f5d4d475/scipy-1.15.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:795c46999bae845966368a3c013e0e00947932d68e235702b5c3f6ea799aa8c9", size = 37244796, upload-time = "2025-05-08T16:05:48.119Z" }, - { url = "https://files.pythonhosted.org/packages/10/c0/4f5f3eeccc235632aab79b27a74a9130c6c35df358129f7ac8b29f562ac7/scipy-1.15.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:18aaacb735ab38b38db42cb01f6b92a2d0d4b6aabefeb07f02849e47f8fb3594", size = 40047684, upload-time = "2025-05-08T16:05:54.22Z" }, - { url = "https://files.pythonhosted.org/packages/ab/a7/0ddaf514ce8a8714f6ed243a2b391b41dbb65251affe21ee3077ec45ea9a/scipy-1.15.3-cp311-cp311-win_amd64.whl", hash = "sha256:ae48a786a28412d744c62fd7816a4118ef97e5be0bee968ce8f0a2fba7acf3bb", size = 41246504, upload-time = "2025-05-08T16:06:00.437Z" }, - { url = "https://files.pythonhosted.org/packages/37/4b/683aa044c4162e10ed7a7ea30527f2cbd92e6999c10a8ed8edb253836e9c/scipy-1.15.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6ac6310fdbfb7aa6612408bd2f07295bcbd3fda00d2d702178434751fe48e019", size = 38766735, upload-time = "2025-05-08T16:06:06.471Z" }, - { url = "https://files.pythonhosted.org/packages/7b/7e/f30be3d03de07f25dc0ec926d1681fed5c732d759ac8f51079708c79e680/scipy-1.15.3-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:185cd3d6d05ca4b44a8f1595af87f9c372bb6acf9c808e99aa3e9aa03bd98cf6", size = 30173284, upload-time = "2025-05-08T16:06:11.686Z" }, - { url = "https://files.pythonhosted.org/packages/07/9c/0ddb0d0abdabe0d181c1793db51f02cd59e4901da6f9f7848e1f96759f0d/scipy-1.15.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:05dc6abcd105e1a29f95eada46d4a3f251743cfd7d3ae8ddb4088047f24ea477", size = 22446958, upload-time = "2025-05-08T16:06:15.97Z" }, - { url = "https://files.pythonhosted.org/packages/af/43/0bce905a965f36c58ff80d8bea33f1f9351b05fad4beaad4eae34699b7a1/scipy-1.15.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:06efcba926324df1696931a57a176c80848ccd67ce6ad020c810736bfd58eb1c", size = 25242454, upload-time = "2025-05-08T16:06:20.394Z" }, - { url = "https://files.pythonhosted.org/packages/56/30/a6f08f84ee5b7b28b4c597aca4cbe545535c39fe911845a96414700b64ba/scipy-1.15.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c05045d8b9bfd807ee1b9f38761993297b10b245f012b11b13b91ba8945f7e45", size = 35210199, upload-time = "2025-05-08T16:06:26.159Z" }, - { url = "https://files.pythonhosted.org/packages/0b/1f/03f52c282437a168ee2c7c14a1a0d0781a9a4a8962d84ac05c06b4c5b555/scipy-1.15.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:271e3713e645149ea5ea3e97b57fdab61ce61333f97cfae392c28ba786f9bb49", size = 37309455, upload-time = "2025-05-08T16:06:32.778Z" }, - { url = "https://files.pythonhosted.org/packages/89/b1/fbb53137f42c4bf630b1ffdfc2151a62d1d1b903b249f030d2b1c0280af8/scipy-1.15.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6cfd56fc1a8e53f6e89ba3a7a7251f7396412d655bca2aa5611c8ec9a6784a1e", size = 36885140, upload-time = "2025-05-08T16:06:39.249Z" }, - { url = "https://files.pythonhosted.org/packages/2e/2e/025e39e339f5090df1ff266d021892694dbb7e63568edcfe43f892fa381d/scipy-1.15.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0ff17c0bb1cb32952c09217d8d1eed9b53d1463e5f1dd6052c7857f83127d539", size = 39710549, upload-time = "2025-05-08T16:06:45.729Z" }, - { url = "https://files.pythonhosted.org/packages/e6/eb/3bf6ea8ab7f1503dca3a10df2e4b9c3f6b3316df07f6c0ded94b281c7101/scipy-1.15.3-cp312-cp312-win_amd64.whl", hash = "sha256:52092bc0472cfd17df49ff17e70624345efece4e1a12b23783a1ac59a1b728ed", size = 40966184, upload-time = "2025-05-08T16:06:52.623Z" }, - { url = "https://files.pythonhosted.org/packages/73/18/ec27848c9baae6e0d6573eda6e01a602e5649ee72c27c3a8aad673ebecfd/scipy-1.15.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2c620736bcc334782e24d173c0fdbb7590a0a436d2fdf39310a8902505008759", size = 38728256, upload-time = "2025-05-08T16:06:58.696Z" }, - { url = "https://files.pythonhosted.org/packages/74/cd/1aef2184948728b4b6e21267d53b3339762c285a46a274ebb7863c9e4742/scipy-1.15.3-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:7e11270a000969409d37ed399585ee530b9ef6aa99d50c019de4cb01e8e54e62", size = 30109540, upload-time = "2025-05-08T16:07:04.209Z" }, - { url = "https://files.pythonhosted.org/packages/5b/d8/59e452c0a255ec352bd0a833537a3bc1bfb679944c4938ab375b0a6b3a3e/scipy-1.15.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:8c9ed3ba2c8a2ce098163a9bdb26f891746d02136995df25227a20e71c396ebb", size = 22383115, upload-time = "2025-05-08T16:07:08.998Z" }, - { url = "https://files.pythonhosted.org/packages/08/f5/456f56bbbfccf696263b47095291040655e3cbaf05d063bdc7c7517f32ac/scipy-1.15.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:0bdd905264c0c9cfa74a4772cdb2070171790381a5c4d312c973382fc6eaf730", size = 25163884, upload-time = "2025-05-08T16:07:14.091Z" }, - { url = "https://files.pythonhosted.org/packages/a2/66/a9618b6a435a0f0c0b8a6d0a2efb32d4ec5a85f023c2b79d39512040355b/scipy-1.15.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79167bba085c31f38603e11a267d862957cbb3ce018d8b38f79ac043bc92d825", size = 35174018, upload-time = "2025-05-08T16:07:19.427Z" }, - { url = "https://files.pythonhosted.org/packages/b5/09/c5b6734a50ad4882432b6bb7c02baf757f5b2f256041da5df242e2d7e6b6/scipy-1.15.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9deabd6d547aee2c9a81dee6cc96c6d7e9a9b1953f74850c179f91fdc729cb7", size = 37269716, upload-time = "2025-05-08T16:07:25.712Z" }, - { url = "https://files.pythonhosted.org/packages/77/0a/eac00ff741f23bcabd352731ed9b8995a0a60ef57f5fd788d611d43d69a1/scipy-1.15.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:dde4fc32993071ac0c7dd2d82569e544f0bdaff66269cb475e0f369adad13f11", size = 36872342, upload-time = "2025-05-08T16:07:31.468Z" }, - { url = "https://files.pythonhosted.org/packages/fe/54/4379be86dd74b6ad81551689107360d9a3e18f24d20767a2d5b9253a3f0a/scipy-1.15.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f77f853d584e72e874d87357ad70f44b437331507d1c311457bed8ed2b956126", size = 39670869, upload-time = "2025-05-08T16:07:38.002Z" }, - { url = "https://files.pythonhosted.org/packages/87/2e/892ad2862ba54f084ffe8cc4a22667eaf9c2bcec6d2bff1d15713c6c0703/scipy-1.15.3-cp313-cp313-win_amd64.whl", hash = "sha256:b90ab29d0c37ec9bf55424c064312930ca5f4bde15ee8619ee44e69319aab163", size = 40988851, upload-time = "2025-05-08T16:08:33.671Z" }, - { url = "https://files.pythonhosted.org/packages/1b/e9/7a879c137f7e55b30d75d90ce3eb468197646bc7b443ac036ae3fe109055/scipy-1.15.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3ac07623267feb3ae308487c260ac684b32ea35fd81e12845039952f558047b8", size = 38863011, upload-time = "2025-05-08T16:07:44.039Z" }, - { url = "https://files.pythonhosted.org/packages/51/d1/226a806bbd69f62ce5ef5f3ffadc35286e9fbc802f606a07eb83bf2359de/scipy-1.15.3-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:6487aa99c2a3d509a5227d9a5e889ff05830a06b2ce08ec30df6d79db5fcd5c5", size = 30266407, upload-time = "2025-05-08T16:07:49.891Z" }, - { url = "https://files.pythonhosted.org/packages/e5/9b/f32d1d6093ab9eeabbd839b0f7619c62e46cc4b7b6dbf05b6e615bbd4400/scipy-1.15.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:50f9e62461c95d933d5c5ef4a1f2ebf9a2b4e83b0db374cb3f1de104d935922e", size = 22540030, upload-time = "2025-05-08T16:07:54.121Z" }, - { url = "https://files.pythonhosted.org/packages/e7/29/c278f699b095c1a884f29fda126340fcc201461ee8bfea5c8bdb1c7c958b/scipy-1.15.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:14ed70039d182f411ffc74789a16df3835e05dc469b898233a245cdfd7f162cb", size = 25218709, upload-time = "2025-05-08T16:07:58.506Z" }, - { url = "https://files.pythonhosted.org/packages/24/18/9e5374b617aba742a990581373cd6b68a2945d65cc588482749ef2e64467/scipy-1.15.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a769105537aa07a69468a0eefcd121be52006db61cdd8cac8a0e68980bbb723", size = 34809045, upload-time = "2025-05-08T16:08:03.929Z" }, - { url = "https://files.pythonhosted.org/packages/e1/fe/9c4361e7ba2927074360856db6135ef4904d505e9b3afbbcb073c4008328/scipy-1.15.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9db984639887e3dffb3928d118145ffe40eff2fa40cb241a306ec57c219ebbbb", size = 36703062, upload-time = "2025-05-08T16:08:09.558Z" }, - { url = "https://files.pythonhosted.org/packages/b7/8e/038ccfe29d272b30086b25a4960f757f97122cb2ec42e62b460d02fe98e9/scipy-1.15.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:40e54d5c7e7ebf1aa596c374c49fa3135f04648a0caabcb66c52884b943f02b4", size = 36393132, upload-time = "2025-05-08T16:08:15.34Z" }, - { url = "https://files.pythonhosted.org/packages/10/7e/5c12285452970be5bdbe8352c619250b97ebf7917d7a9a9e96b8a8140f17/scipy-1.15.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5e721fed53187e71d0ccf382b6bf977644c533e506c4d33c3fb24de89f5c3ed5", size = 38979503, upload-time = "2025-05-08T16:08:21.513Z" }, - { url = "https://files.pythonhosted.org/packages/81/06/0a5e5349474e1cbc5757975b21bd4fad0e72ebf138c5592f191646154e06/scipy-1.15.3-cp313-cp313t-win_amd64.whl", hash = "sha256:76ad1fb5f8752eabf0fa02e4cc0336b4e8f021e2d5f061ed37d6d264db35e3ca", size = 40308097, upload-time = "2025-05-08T16:08:27.627Z" }, -] - -[[package]] -name = "scipy" -version = "1.17.1" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.13' and platform_machine != 's390x' and sys_platform == 'darwin'", - "python_full_version >= '3.13' and platform_machine == 's390x' and sys_platform == 'darwin'", - "python_full_version == '3.12.*' and sys_platform == 'darwin'", - "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(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 != 'linux')", - "(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 != 'linux')", - "python_full_version == '3.11.*' and sys_platform == 'darwin'", - "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", -] -dependencies = [ - { name = "numpy", marker = "python_full_version >= '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/7a/97/5a3609c4f8d58b039179648e62dd220f89864f56f7357f5d4f45c29eb2cc/scipy-1.17.1.tar.gz", hash = "sha256:95d8e012d8cb8816c226aef832200b1d45109ed4464303e997c5b13122b297c0", size = 30573822, upload-time = "2026-02-23T00:26:24.851Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/df/75/b4ce781849931fef6fd529afa6b63711d5a733065722d0c3e2724af9e40a/scipy-1.17.1-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:1f95b894f13729334fb990162e911c9e5dc1ab390c58aa6cbecb389c5b5e28ec", size = 31613675, upload-time = "2026-02-23T00:16:00.13Z" }, - { url = "https://files.pythonhosted.org/packages/f7/58/bccc2861b305abdd1b8663d6130c0b3d7cc22e8d86663edbc8401bfd40d4/scipy-1.17.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:e18f12c6b0bc5a592ed23d3f7b891f68fd7f8241d69b7883769eb5d5dfb52696", size = 28162057, upload-time = "2026-02-23T00:16:09.456Z" }, - { url = "https://files.pythonhosted.org/packages/6d/ee/18146b7757ed4976276b9c9819108adbc73c5aad636e5353e20746b73069/scipy-1.17.1-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:a3472cfbca0a54177d0faa68f697d8ba4c80bbdc19908c3465556d9f7efce9ee", size = 20334032, upload-time = "2026-02-23T00:16:17.358Z" }, - { url = "https://files.pythonhosted.org/packages/ec/e6/cef1cf3557f0c54954198554a10016b6a03b2ec9e22a4e1df734936bd99c/scipy-1.17.1-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:766e0dc5a616d026a3a1cffa379af959671729083882f50307e18175797b3dfd", size = 22709533, upload-time = "2026-02-23T00:16:25.791Z" }, - { url = "https://files.pythonhosted.org/packages/4d/60/8804678875fc59362b0fb759ab3ecce1f09c10a735680318ac30da8cd76b/scipy-1.17.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:744b2bf3640d907b79f3fd7874efe432d1cf171ee721243e350f55234b4cec4c", size = 33062057, upload-time = "2026-02-23T00:16:36.931Z" }, - { url = "https://files.pythonhosted.org/packages/09/7d/af933f0f6e0767995b4e2d705a0665e454d1c19402aa7e895de3951ebb04/scipy-1.17.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43af8d1f3bea642559019edfe64e9b11192a8978efbd1539d7bc2aaa23d92de4", size = 35349300, upload-time = "2026-02-23T00:16:49.108Z" }, - { url = "https://files.pythonhosted.org/packages/b4/3d/7ccbbdcbb54c8fdc20d3b6930137c782a163fa626f0aef920349873421ba/scipy-1.17.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cd96a1898c0a47be4520327e01f874acfd61fb48a9420f8aa9f6483412ffa444", size = 35127333, upload-time = "2026-02-23T00:17:01.293Z" }, - { url = "https://files.pythonhosted.org/packages/e8/19/f926cb11c42b15ba08e3a71e376d816ac08614f769b4f47e06c3580c836a/scipy-1.17.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:4eb6c25dd62ee8d5edf68a8e1c171dd71c292fdae95d8aeb3dd7d7de4c364082", size = 37741314, upload-time = "2026-02-23T00:17:12.576Z" }, - { url = "https://files.pythonhosted.org/packages/95/da/0d1df507cf574b3f224ccc3d45244c9a1d732c81dcb26b1e8a766ae271a8/scipy-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:d30e57c72013c2a4fe441c2fcb8e77b14e152ad48b5464858e07e2ad9fbfceff", size = 36607512, upload-time = "2026-02-23T00:17:23.424Z" }, - { url = "https://files.pythonhosted.org/packages/68/7f/bdd79ceaad24b671543ffe0ef61ed8e659440eb683b66f033454dcee90eb/scipy-1.17.1-cp311-cp311-win_arm64.whl", hash = "sha256:9ecb4efb1cd6e8c4afea0daa91a87fbddbce1b99d2895d151596716c0b2e859d", size = 24599248, upload-time = "2026-02-23T00:17:34.561Z" }, - { url = "https://files.pythonhosted.org/packages/35/48/b992b488d6f299dbe3f11a20b24d3dda3d46f1a635ede1c46b5b17a7b163/scipy-1.17.1-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:35c3a56d2ef83efc372eaec584314bd0ef2e2f0d2adb21c55e6ad5b344c0dcb8", size = 31610954, upload-time = "2026-02-23T00:17:49.855Z" }, - { url = "https://files.pythonhosted.org/packages/b2/02/cf107b01494c19dc100f1d0b7ac3cc08666e96ba2d64db7626066cee895e/scipy-1.17.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:fcb310ddb270a06114bb64bbe53c94926b943f5b7f0842194d585c65eb4edd76", size = 28172662, upload-time = "2026-02-23T00:18:01.64Z" }, - { url = "https://files.pythonhosted.org/packages/cf/a9/599c28631bad314d219cf9ffd40e985b24d603fc8a2f4ccc5ae8419a535b/scipy-1.17.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:cc90d2e9c7e5c7f1a482c9875007c095c3194b1cfedca3c2f3291cdc2bc7c086", size = 20344366, upload-time = "2026-02-23T00:18:12.015Z" }, - { url = "https://files.pythonhosted.org/packages/35/f5/906eda513271c8deb5af284e5ef0206d17a96239af79f9fa0aebfe0e36b4/scipy-1.17.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:c80be5ede8f3f8eded4eff73cc99a25c388ce98e555b17d31da05287015ffa5b", size = 22704017, upload-time = "2026-02-23T00:18:21.502Z" }, - { url = "https://files.pythonhosted.org/packages/da/34/16f10e3042d2f1d6b66e0428308ab52224b6a23049cb2f5c1756f713815f/scipy-1.17.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e19ebea31758fac5893a2ac360fedd00116cbb7628e650842a6691ba7ca28a21", size = 32927842, upload-time = "2026-02-23T00:18:35.367Z" }, - { url = "https://files.pythonhosted.org/packages/01/8e/1e35281b8ab6d5d72ebe9911edcdffa3f36b04ed9d51dec6dd140396e220/scipy-1.17.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:02ae3b274fde71c5e92ac4d54bc06c42d80e399fec704383dcd99b301df37458", size = 35235890, upload-time = "2026-02-23T00:18:49.188Z" }, - { url = "https://files.pythonhosted.org/packages/c5/5c/9d7f4c88bea6e0d5a4f1bc0506a53a00e9fcb198de372bfe4d3652cef482/scipy-1.17.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8a604bae87c6195d8b1045eddece0514d041604b14f2727bbc2b3020172045eb", size = 35003557, upload-time = "2026-02-23T00:18:54.74Z" }, - { url = "https://files.pythonhosted.org/packages/65/94/7698add8f276dbab7a9de9fb6b0e02fc13ee61d51c7c3f85ac28b65e1239/scipy-1.17.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f590cd684941912d10becc07325a3eeb77886fe981415660d9265c4c418d0bea", size = 37625856, upload-time = "2026-02-23T00:19:00.307Z" }, - { url = "https://files.pythonhosted.org/packages/a2/84/dc08d77fbf3d87d3ee27f6a0c6dcce1de5829a64f2eae85a0ecc1f0daa73/scipy-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:41b71f4a3a4cab9d366cd9065b288efc4d4f3c0b37a91a8e0947fb5bd7f31d87", size = 36549682, upload-time = "2026-02-23T00:19:07.67Z" }, - { url = "https://files.pythonhosted.org/packages/bc/98/fe9ae9ffb3b54b62559f52dedaebe204b408db8109a8c66fdd04869e6424/scipy-1.17.1-cp312-cp312-win_arm64.whl", hash = "sha256:f4115102802df98b2b0db3cce5cb9b92572633a1197c77b7553e5203f284a5b3", size = 24547340, upload-time = "2026-02-23T00:19:12.024Z" }, - { url = "https://files.pythonhosted.org/packages/76/27/07ee1b57b65e92645f219b37148a7e7928b82e2b5dbeccecb4dff7c64f0b/scipy-1.17.1-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:5e3c5c011904115f88a39308379c17f91546f77c1667cea98739fe0fccea804c", size = 31590199, upload-time = "2026-02-23T00:19:17.192Z" }, - { url = "https://files.pythonhosted.org/packages/ec/ae/db19f8ab842e9b724bf5dbb7db29302a91f1e55bc4d04b1025d6d605a2c5/scipy-1.17.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:6fac755ca3d2c3edcb22f479fceaa241704111414831ddd3bc6056e18516892f", size = 28154001, upload-time = "2026-02-23T00:19:22.241Z" }, - { url = "https://files.pythonhosted.org/packages/5b/58/3ce96251560107b381cbd6e8413c483bbb1228a6b919fa8652b0d4090e7f/scipy-1.17.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:7ff200bf9d24f2e4d5dc6ee8c3ac64d739d3a89e2326ba68aaf6c4a2b838fd7d", size = 20325719, upload-time = "2026-02-23T00:19:26.329Z" }, - { url = "https://files.pythonhosted.org/packages/b2/83/15087d945e0e4d48ce2377498abf5ad171ae013232ae31d06f336e64c999/scipy-1.17.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:4b400bdc6f79fa02a4d86640310dde87a21fba0c979efff5248908c6f15fad1b", size = 22683595, upload-time = "2026-02-23T00:19:30.304Z" }, - { url = "https://files.pythonhosted.org/packages/b4/e0/e58fbde4a1a594c8be8114eb4aac1a55bcd6587047efc18a61eb1f5c0d30/scipy-1.17.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b64ca7d4aee0102a97f3ba22124052b4bd2152522355073580bf4845e2550b6", size = 32896429, upload-time = "2026-02-23T00:19:35.536Z" }, - { url = "https://files.pythonhosted.org/packages/f5/5f/f17563f28ff03c7b6799c50d01d5d856a1d55f2676f537ca8d28c7f627cd/scipy-1.17.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:581b2264fc0aa555f3f435a5944da7504ea3a065d7029ad60e7c3d1ae09c5464", size = 35203952, upload-time = "2026-02-23T00:19:42.259Z" }, - { url = "https://files.pythonhosted.org/packages/8d/a5/9afd17de24f657fdfe4df9a3f1ea049b39aef7c06000c13db1530d81ccca/scipy-1.17.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:beeda3d4ae615106d7094f7e7cef6218392e4465cc95d25f900bebabfded0950", size = 34979063, upload-time = "2026-02-23T00:19:47.547Z" }, - { url = "https://files.pythonhosted.org/packages/8b/13/88b1d2384b424bf7c924f2038c1c409f8d88bb2a8d49d097861dd64a57b2/scipy-1.17.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6609bc224e9568f65064cfa72edc0f24ee6655b47575954ec6339534b2798369", size = 37598449, upload-time = "2026-02-23T00:19:53.238Z" }, - { url = "https://files.pythonhosted.org/packages/35/e5/d6d0e51fc888f692a35134336866341c08655d92614f492c6860dc45bb2c/scipy-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:37425bc9175607b0268f493d79a292c39f9d001a357bebb6b88fdfaff13f6448", size = 36510943, upload-time = "2026-02-23T00:20:50.89Z" }, - { url = "https://files.pythonhosted.org/packages/2a/fd/3be73c564e2a01e690e19cc618811540ba5354c67c8680dce3281123fb79/scipy-1.17.1-cp313-cp313-win_arm64.whl", hash = "sha256:5cf36e801231b6a2059bf354720274b7558746f3b1a4efb43fcf557ccd484a87", size = 24545621, upload-time = "2026-02-23T00:20:55.871Z" }, - { url = "https://files.pythonhosted.org/packages/6f/6b/17787db8b8114933a66f9dcc479a8272e4b4da75fe03b0c282f7b0ade8cd/scipy-1.17.1-cp313-cp313t-macosx_10_14_x86_64.whl", hash = "sha256:d59c30000a16d8edc7e64152e30220bfbd724c9bbb08368c054e24c651314f0a", size = 31936708, upload-time = "2026-02-23T00:19:58.694Z" }, - { url = "https://files.pythonhosted.org/packages/38/2e/524405c2b6392765ab1e2b722a41d5da33dc5c7b7278184a8ad29b6cb206/scipy-1.17.1-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:010f4333c96c9bb1a4516269e33cb5917b08ef2166d5556ca2fd9f082a9e6ea0", size = 28570135, upload-time = "2026-02-23T00:20:03.934Z" }, - { url = "https://files.pythonhosted.org/packages/fd/c3/5bd7199f4ea8556c0c8e39f04ccb014ac37d1468e6cfa6a95c6b3562b76e/scipy-1.17.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:2ceb2d3e01c5f1d83c4189737a42d9cb2fc38a6eeed225e7515eef71ad301dce", size = 20741977, upload-time = "2026-02-23T00:20:07.935Z" }, - { url = "https://files.pythonhosted.org/packages/d9/b8/8ccd9b766ad14c78386599708eb745f6b44f08400a5fd0ade7cf89b6fc93/scipy-1.17.1-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:844e165636711ef41f80b4103ed234181646b98a53c8f05da12ca5ca289134f6", size = 23029601, upload-time = "2026-02-23T00:20:12.161Z" }, - { url = "https://files.pythonhosted.org/packages/6d/a0/3cb6f4d2fb3e17428ad2880333cac878909ad1a89f678527b5328b93c1d4/scipy-1.17.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:158dd96d2207e21c966063e1635b1063cd7787b627b6f07305315dd73d9c679e", size = 33019667, upload-time = "2026-02-23T00:20:17.208Z" }, - { url = "https://files.pythonhosted.org/packages/f3/c3/2d834a5ac7bf3a0c806ad1508efc02dda3c8c61472a56132d7894c312dea/scipy-1.17.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:74cbb80d93260fe2ffa334efa24cb8f2f0f622a9b9febf8b483c0b865bfb3475", size = 35264159, upload-time = "2026-02-23T00:20:23.087Z" }, - { url = "https://files.pythonhosted.org/packages/4d/77/d3ed4becfdbd217c52062fafe35a72388d1bd82c2d0ba5ca19d6fcc93e11/scipy-1.17.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:dbc12c9f3d185f5c737d801da555fb74b3dcfa1a50b66a1a93e09190f41fab50", size = 35102771, upload-time = "2026-02-23T00:20:28.636Z" }, - { url = "https://files.pythonhosted.org/packages/bd/12/d19da97efde68ca1ee5538bb261d5d2c062f0c055575128f11a2730e3ac1/scipy-1.17.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:94055a11dfebe37c656e70317e1996dc197e1a15bbcc351bcdd4610e128fe1ca", size = 37665910, upload-time = "2026-02-23T00:20:34.743Z" }, - { url = "https://files.pythonhosted.org/packages/06/1c/1172a88d507a4baaf72c5a09bb6c018fe2ae0ab622e5830b703a46cc9e44/scipy-1.17.1-cp313-cp313t-win_amd64.whl", hash = "sha256:e30bdeaa5deed6bc27b4cc490823cd0347d7dae09119b8803ae576ea0ce52e4c", size = 36562980, upload-time = "2026-02-23T00:20:40.575Z" }, - { url = "https://files.pythonhosted.org/packages/70/b0/eb757336e5a76dfa7911f63252e3b7d1de00935d7705cf772db5b45ec238/scipy-1.17.1-cp313-cp313t-win_arm64.whl", hash = "sha256:a720477885a9d2411f94a93d16f9d89bad0f28ca23c3f8daa521e2dcc3f44d49", size = 24856543, upload-time = "2026-02-23T00:20:45.313Z" }, - { url = "https://files.pythonhosted.org/packages/cf/83/333afb452af6f0fd70414dc04f898647ee1423979ce02efa75c3b0f2c28e/scipy-1.17.1-cp314-cp314-macosx_10_14_x86_64.whl", hash = "sha256:a48a72c77a310327f6a3a920092fa2b8fd03d7deaa60f093038f22d98e096717", size = 31584510, upload-time = "2026-02-23T00:21:01.015Z" }, - { url = "https://files.pythonhosted.org/packages/ed/a6/d05a85fd51daeb2e4ea71d102f15b34fedca8e931af02594193ae4fd25f7/scipy-1.17.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:45abad819184f07240d8a696117a7aacd39787af9e0b719d00285549ed19a1e9", size = 28170131, upload-time = "2026-02-23T00:21:05.888Z" }, - { url = "https://files.pythonhosted.org/packages/db/7b/8624a203326675d7746a254083a187398090a179335b2e4a20e2ddc46e83/scipy-1.17.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:3fd1fcdab3ea951b610dc4cef356d416d5802991e7e32b5254828d342f7b7e0b", size = 20342032, upload-time = "2026-02-23T00:21:09.904Z" }, - { url = "https://files.pythonhosted.org/packages/c9/35/2c342897c00775d688d8ff3987aced3426858fd89d5a0e26e020b660b301/scipy-1.17.1-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:7bdf2da170b67fdf10bca777614b1c7d96ae3ca5794fd9587dce41eb2966e866", size = 22678766, upload-time = "2026-02-23T00:21:14.313Z" }, - { url = "https://files.pythonhosted.org/packages/ef/f2/7cdb8eb308a1a6ae1e19f945913c82c23c0c442a462a46480ce487fdc0ac/scipy-1.17.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:adb2642e060a6549c343603a3851ba76ef0b74cc8c079a9a58121c7ec9fe2350", size = 32957007, upload-time = "2026-02-23T00:21:19.663Z" }, - { url = "https://files.pythonhosted.org/packages/0b/2e/7eea398450457ecb54e18e9d10110993fa65561c4f3add5e8eccd2b9cd41/scipy-1.17.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eee2cfda04c00a857206a4330f0c5e3e56535494e30ca445eb19ec624ae75118", size = 35221333, upload-time = "2026-02-23T00:21:25.278Z" }, - { url = "https://files.pythonhosted.org/packages/d9/77/5b8509d03b77f093a0d52e606d3c4f79e8b06d1d38c441dacb1e26cacf46/scipy-1.17.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d2650c1fb97e184d12d8ba010493ee7b322864f7d3d00d3f9bb97d9c21de4068", size = 35042066, upload-time = "2026-02-23T00:21:31.358Z" }, - { url = "https://files.pythonhosted.org/packages/f9/df/18f80fb99df40b4070328d5ae5c596f2f00fffb50167e31439e932f29e7d/scipy-1.17.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:08b900519463543aa604a06bec02461558a6e1cef8fdbb8098f77a48a83c8118", size = 37612763, upload-time = "2026-02-23T00:21:37.247Z" }, - { url = "https://files.pythonhosted.org/packages/4b/39/f0e8ea762a764a9dc52aa7dabcfad51a354819de1f0d4652b6a1122424d6/scipy-1.17.1-cp314-cp314-win_amd64.whl", hash = "sha256:3877ac408e14da24a6196de0ddcace62092bfc12a83823e92e49e40747e52c19", size = 37290984, upload-time = "2026-02-23T00:22:35.023Z" }, - { url = "https://files.pythonhosted.org/packages/7c/56/fe201e3b0f93d1a8bcf75d3379affd228a63d7e2d80ab45467a74b494947/scipy-1.17.1-cp314-cp314-win_arm64.whl", hash = "sha256:f8885db0bc2bffa59d5c1b72fad7a6a92d3e80e7257f967dd81abb553a90d293", size = 25192877, upload-time = "2026-02-23T00:22:39.798Z" }, - { url = "https://files.pythonhosted.org/packages/96/ad/f8c414e121f82e02d76f310f16db9899c4fcde36710329502a6b2a3c0392/scipy-1.17.1-cp314-cp314t-macosx_10_14_x86_64.whl", hash = "sha256:1cc682cea2ae55524432f3cdff9e9a3be743d52a7443d0cba9017c23c87ae2f6", size = 31949750, upload-time = "2026-02-23T00:21:42.289Z" }, - { url = "https://files.pythonhosted.org/packages/7c/b0/c741e8865d61b67c81e255f4f0a832846c064e426636cd7de84e74d209be/scipy-1.17.1-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:2040ad4d1795a0ae89bfc7e8429677f365d45aa9fd5e4587cf1ea737f927b4a1", size = 28585858, upload-time = "2026-02-23T00:21:47.706Z" }, - { url = "https://files.pythonhosted.org/packages/ed/1b/3985219c6177866628fa7c2595bfd23f193ceebbe472c98a08824b9466ff/scipy-1.17.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:131f5aaea57602008f9822e2115029b55d4b5f7c070287699fe45c661d051e39", size = 20757723, upload-time = "2026-02-23T00:21:52.039Z" }, - { url = "https://files.pythonhosted.org/packages/c0/19/2a04aa25050d656d6f7b9e7b685cc83d6957fb101665bfd9369ca6534563/scipy-1.17.1-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:9cdc1a2fcfd5c52cfb3045feb399f7b3ce822abdde3a193a6b9a60b3cb5854ca", size = 23043098, upload-time = "2026-02-23T00:21:56.185Z" }, - { url = "https://files.pythonhosted.org/packages/86/f1/3383beb9b5d0dbddd030335bf8a8b32d4317185efe495374f134d8be6cce/scipy-1.17.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e3dcd57ab780c741fde8dc68619de988b966db759a3c3152e8e9142c26295ad", size = 33030397, upload-time = "2026-02-23T00:22:01.404Z" }, - { url = "https://files.pythonhosted.org/packages/41/68/8f21e8a65a5a03f25a79165ec9d2b28c00e66dc80546cf5eb803aeeff35b/scipy-1.17.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a9956e4d4f4a301ebf6cde39850333a6b6110799d470dbbb1e25326ac447f52a", size = 35281163, upload-time = "2026-02-23T00:22:07.024Z" }, - { url = "https://files.pythonhosted.org/packages/84/8d/c8a5e19479554007a5632ed7529e665c315ae7492b4f946b0deb39870e39/scipy-1.17.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:a4328d245944d09fd639771de275701ccadf5f781ba0ff092ad141e017eccda4", size = 35116291, upload-time = "2026-02-23T00:22:12.585Z" }, - { url = "https://files.pythonhosted.org/packages/52/52/e57eceff0e342a1f50e274264ed47497b59e6a4e3118808ee58ddda7b74a/scipy-1.17.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a77cbd07b940d326d39a1d1b37817e2ee4d79cb30e7338f3d0cddffae70fcaa2", size = 37682317, upload-time = "2026-02-23T00:22:18.513Z" }, - { url = "https://files.pythonhosted.org/packages/11/2f/b29eafe4a3fbc3d6de9662b36e028d5f039e72d345e05c250e121a230dd4/scipy-1.17.1-cp314-cp314t-win_amd64.whl", hash = "sha256:eb092099205ef62cd1782b006658db09e2fed75bffcae7cc0d44052d8aa0f484", size = 37345327, upload-time = "2026-02-23T00:22:24.442Z" }, - { url = "https://files.pythonhosted.org/packages/07/39/338d9219c4e87f3e708f18857ecd24d22a0c3094752393319553096b98af/scipy-1.17.1-cp314-cp314t-win_arm64.whl", hash = "sha256:200e1050faffacc162be6a486a984a0497866ec54149a01270adc8a59b7c7d21", size = 25489165, upload-time = "2026-02-23T00:22:29.563Z" }, -] - [[package]] name = "setuptools" version = "81.0.0" @@ -2891,47 +2463,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353, upload-time = "2025-04-27T18:04:59.103Z" }, ] -[[package]] -name = "tifffile" -version = "2025.5.10" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.11' and sys_platform == 'darwin'", - "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')", -] -dependencies = [ - { name = "numpy", marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/44/d0/18fed0fc0916578a4463f775b0fbd9c5fed2392152d039df2fb533bfdd5d/tifffile-2025.5.10.tar.gz", hash = "sha256:018335d34283aa3fd8c263bae5c3c2b661ebc45548fde31504016fcae7bf1103", size = 365290, upload-time = "2025-05-10T19:22:34.386Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5d/06/bd0a6097da704a7a7c34a94cfd771c3ea3c2f405dd214e790d22c93f6be1/tifffile-2025.5.10-py3-none-any.whl", hash = "sha256:e37147123c0542d67bc37ba5cdd67e12ea6fbe6e86c52bee037a9eb6a064e5ad", size = 226533, upload-time = "2025-05-10T19:22:27.279Z" }, -] - -[[package]] -name = "tifffile" -version = "2026.3.3" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.13' and platform_machine != 's390x' and sys_platform == 'darwin'", - "python_full_version >= '3.13' and platform_machine == 's390x' and sys_platform == 'darwin'", - "python_full_version == '3.12.*' and sys_platform == 'darwin'", - "python_full_version >= '3.13' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(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 != 'linux')", - "(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 != 'linux')", - "python_full_version == '3.11.*' and sys_platform == 'darwin'", - "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", - "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", -] -dependencies = [ - { name = "numpy", marker = "python_full_version >= '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c5/cb/2f6d79c7576e22c116352a801f4c3c8ace5957e9aced862012430b62e14f/tifffile-2026.3.3.tar.gz", hash = "sha256:d9a1266bed6f2ee1dd0abde2018a38b4f8b2935cb843df381d70ac4eac5458b7", size = 388745, upload-time = "2026-03-03T19:14:38.134Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1a/e4/e804505f87627cd8cdae9c010c47c4485fd8c1ce31a7dd0ab7fcc4707377/tifffile-2026.3.3-py3-none-any.whl", hash = "sha256:e8be15c94273113d31ecb7aa3a39822189dd11c4967e3cc88c178f1ad2fd1170", size = 243960, upload-time = "2026-03-03T19:14:35.808Z" }, -] - [[package]] name = "tokenizers" version = "0.22.2"