mirror of
https://github.com/wiltodelta/remove-ai-watermarks.git
synced 2026-08-09 23:50:40 +02:00
Verify every doc claim against the source and fix what drifted
Every code-referencing claim in the docs, the README and the rules files was checked against src/, and each finding was re-derived independently before it was applied. 35 held, 5 were false positives. Two of them were code, not text. `InvisibleOptions` promises in its docstring to mirror `InvisibleEngine`, and two defaults had silently stopped: `max_resolution=None` reached `_target_size`'s `max_resolution > 0` and raised `TypeError` on every library call that left the options alone, and `cpu_offload=True` made a library run slower than the identical CLI run. Both are fixed, and `TestInvisibleOptionsMirrorTheEngine` compares the two signatures field by field rather than pinning the two values that happen to be known. A companion assertion in `TestTargetSize` reads the engine's own declared default, so a drift on the engine side -- which the mirror check alone would accept, because both sides would still agree -- fails too. The user-facing docs: README called `invisible` GPU-optional where it raises without CUDA, and gave the image `metadata` command `video metadata`'s output rule, promising the source survives a command that overwrites it. Yuanbao was missing from the supported-mark list. `veo` was listed among the video policies that require a run anchor, though its row sets no `anchor_iou`. `known-limitations` called ControlNet the default profile and contradicted itself ninety lines below. An unescaped pipe truncated the `hailuo` table row. The `dev` extra, the CI shape, ffmpeg's role, the sdist boundary and the strength-curve range were corrected, and `remove_all`/`remove_batch`, the pill gate, `erase --keep-metadata` and `all`'s CUDA failure mode were documented. Research notes that described removed modules, extras and flags in the present tense now say so once in the page banner instead of sentence by sentence, which covers the whole page rather than the lines that happened to be noticed, and one fixture is referred to by role rather than by name. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
0261ec1bd7
commit
13095fb45c
@@ -1,6 +1,6 @@
|
||||
---
|
||||
globs: ["src/**/*.py", "tests/**/*.py", "scripts/**/*.py", "pyproject.toml", "uv.lock", "maintain.sh", ".github/workflows/*.yml"]
|
||||
description: Command contracts, project gate, typing boundaries, and model-adjacent test invariants.
|
||||
description: Command contracts, project gate, typing boundaries, model-adjacent test invariants, and the detection-path measurement rule.
|
||||
---
|
||||
|
||||
# Development invariants
|
||||
@@ -37,6 +37,11 @@ Do not classify an entire module as untestable because its main path downloads a
|
||||
stage's; `sdxl-zimage` shipped doing exactly that and crashed on every image with a
|
||||
face. When one profile inherits another's stage, guard the invariants that stage
|
||||
relies on, not just the code path.
|
||||
- the `InvisibleOptions` defaults, in `test_api.py`. When one signature promises to
|
||||
mirror another, compare them field by field rather than pinning the values you happen
|
||||
to know about, so the next field added on one side and not the other fails at the
|
||||
seam. Two of these defaults drifted in practice and neither needed a GPU to catch;
|
||||
the incident is recorded in `docs/module-internals.md`.
|
||||
|
||||
Use availability checks only for paths that actually load large models.
|
||||
|
||||
|
||||
@@ -39,7 +39,7 @@ removal.
|
||||
| Remove a registered visible AI mark from video | `video visible` | No |
|
||||
| Process a directory of videos | `video batch` | Depends on mode |
|
||||
| Remove video SynthID with the certified VAE profile | `video invisible` | Recommended |
|
||||
| Regenerate an image to disrupt invisible watermarks | `invisible` | Recommended |
|
||||
| Regenerate an image to disrupt invisible watermarks | `invisible` | Required (CUDA) |
|
||||
| Run visible, invisible, and metadata removal | `all` | Recommended |
|
||||
| Process a directory | `batch` | Depends on mode |
|
||||
|
||||
@@ -92,6 +92,8 @@ Strip metadata without running visible inpainting or diffusion:
|
||||
remove-ai-watermarks metadata image.png --remove -o clean.png
|
||||
```
|
||||
|
||||
Without `-o` this command overwrites the source in place.
|
||||
|
||||
Inspect or remove AI metadata from an MP4, MOV, M4V, WebM, MKV, AVI, or FLV
|
||||
file:
|
||||
|
||||
@@ -100,8 +102,9 @@ remove-ai-watermarks video metadata input.mp4 --check
|
||||
remove-ai-watermarks video metadata input.mp4 --remove -o clean.mp4
|
||||
```
|
||||
|
||||
The metadata command does not transcode video or audio streams. When `-o` is
|
||||
omitted it writes `<source>_clean` and preserves the original. MP4 and MOV
|
||||
The `video metadata` command does not transcode video or audio streams. Unlike
|
||||
the image command above, when `-o` is omitted it writes `<source>_clean` and
|
||||
preserves the original. MP4 and MOV
|
||||
inspection includes the native TC260 `AIGC` tag in
|
||||
`moov.udta.meta.keys/ilst`, including a `moov` placed after the media payload.
|
||||
MKV and WebM inspection reads the normative
|
||||
@@ -288,7 +291,7 @@ remove-ai-watermarks batch ./images --mode all
|
||||
Visible mark support includes:
|
||||
|
||||
- Google Gemini and Nano Banana sparkle;
|
||||
- Doubao, Jimeng, Qwen, Kling, Baidu, LibLibAI, and RunningHub labels;
|
||||
- Doubao, Jimeng, Qwen, Kling, Yuanbao, Baidu, LibLibAI, and RunningHub labels;
|
||||
- one calibrated Samsung Galaxy AI label variant.
|
||||
|
||||
Metadata and provenance inspection covers C2PA, EXIF, XMP, IPTC, common
|
||||
@@ -351,8 +354,9 @@ The high level API accepts a file path or a BGR NumPy array. For path inputs it
|
||||
also reads provenance metadata, preserves alpha, and can strip AI metadata from
|
||||
the written result.
|
||||
|
||||
See the [Python API guide](docs/python-api.md) for visible removal, provenance
|
||||
inspection, metadata stripping, and diffusion usage.
|
||||
See the [Python API guide](docs/python-api.md) for visible removal, the full
|
||||
`remove_all` and `remove_batch` pipeline, provenance inspection, metadata
|
||||
stripping, and diffusion usage.
|
||||
|
||||
## ComfyUI
|
||||
|
||||
|
||||
+17
-8
@@ -22,8 +22,9 @@ defaults. This page focuses on choosing the right command.
|
||||
| `visible` or `erase` with big-LaMa | `remove-ai-watermarks[lama]` |
|
||||
| `invisible` and `all` (needs CUDA) | `remove-ai-watermarks[qwen-zimage]` |
|
||||
| `video metadata` and `video identify --no-visible` | Default package |
|
||||
| `video identify`, `video visible`, and visible/all batch modes | `remove-ai-watermarks[video]` |
|
||||
| `video invisible` and `video all --invisible` | `remove-ai-watermarks[video,diffusion]` |
|
||||
| `video identify` | `remove-ai-watermarks[video]` |
|
||||
| `video visible`, `video all`, and visible/all batch modes | `remove-ai-watermarks[video]` plus ffmpeg on PATH |
|
||||
| `video invisible` and `video all --invisible` | `remove-ai-watermarks[video,diffusion]` plus ffmpeg on PATH |
|
||||
| HEIC/HEIF/AVIF pixel input | Add `remove-ai-watermarks[heif]` |
|
||||
| Every production command and backend | `remove-ai-watermarks[all]` |
|
||||
|
||||
@@ -67,7 +68,8 @@ remove-ai-watermarks visible image.png -o clean.png
|
||||
The default behavior:
|
||||
|
||||
- checks every registered visible mark;
|
||||
- removes every detected match;
|
||||
- removes every detected match, except the weakly detected Jimeng label pill,
|
||||
which needs corroboration (see [supported signals](supported-signals.md));
|
||||
- selects the best installed fill backend;
|
||||
- strips AI metadata from the output.
|
||||
|
||||
@@ -132,7 +134,8 @@ Two more knobs tune the fill. `--dilate N` (default 3) grows every box by `N`
|
||||
pixels before inpainting, which helps when a mark has a soft edge or a drop
|
||||
shadow just outside the box you measured; it applies to every backend because it
|
||||
shapes the mask. `--inpaint-method telea|ns` selects the classical algorithm and
|
||||
only affects the `cv2` backend.
|
||||
only affects the `cv2` backend. Like `visible`, `erase` strips AI metadata from
|
||||
the output by default; pass `--keep-metadata` to retain it.
|
||||
|
||||
## Strip AI metadata
|
||||
|
||||
@@ -439,10 +442,16 @@ The command runs:
|
||||
|
||||
The visible options and diffusion options are also available on `all`.
|
||||
|
||||
If invisible removal is required but the `qwen-zimage` extra is unavailable, `all` still
|
||||
writes the result of the visible and metadata stages, prints a prominent
|
||||
warning, and exits with code 1. This prevents a partial result from being
|
||||
reported as complete.
|
||||
When the `qwen-zimage` extra is unavailable, `all` still writes the result of
|
||||
the visible and metadata stages, prints a prominent warning, and exits with code
|
||||
1. That happens on every run without the extra: the skipped stage is what would
|
||||
have decided whether a signal was there. This prevents a partial result from
|
||||
being reported as complete.
|
||||
|
||||
If the extra is installed but the machine has no CUDA, which is the usual macOS
|
||||
case, the run fails at engine construction instead: `all` prints
|
||||
`Error: Invisible-watermark removal is CUDA-only ...`, writes no output at all,
|
||||
and exits with code 1.
|
||||
|
||||
## Process a directory
|
||||
|
||||
|
||||
@@ -3,7 +3,10 @@
|
||||
> Research archive. This document records experiments, superseded defaults, and
|
||||
> deployment considerations from the time of the study. It does not define the
|
||||
> current CLI or Python API. See `README.md`, `docs/cli.md`, and
|
||||
> `docs/known-limitations.md` for current behavior.
|
||||
> `docs/known-limitations.md` for current behavior. Everything it names on the
|
||||
> face-restoration path was removed after this study: the `--restore-faces` and
|
||||
> `--face-id` flags, the `instantid`, `photomaker` and `faceid` extras, and their
|
||||
> modules. So were the `controlnet`, `sdxl`, `qwen` and `default` profiles.
|
||||
|
||||
Date: 2026-06-02. Source: a manual primary-source pass (WebSearch + WebFetch over the
|
||||
watermark-removal-attack and SDXL-ControlNet literature). Prompted by issue #35
|
||||
@@ -55,8 +58,7 @@ content it protects. Controlnet is the text/structure PRESERVATION pipeline; rem
|
||||
is set by STRENGTH, separately calibrated, not by the pipeline choice.
|
||||
|
||||
This section is the single consolidated reference for the controlnet pipeline's
|
||||
removal behavior. (Mirrored briefly in `docs/synthid.md` §5.5 and the CLAUDE.md
|
||||
controlnet / face_restore bullets, which point here.)
|
||||
removal behavior. (Mirrored briefly in `docs/synthid.md` §5.5, which points here.)
|
||||
|
||||
### What we measured (real gpt-image + Gemini originals)
|
||||
|
||||
@@ -288,8 +290,8 @@ text-vs-scrub tension as an empirical question to measure, not assume.
|
||||
**Prototype (runs locally on 32 GB MPS — no dedicated GPU required):**
|
||||
|
||||
Compute is NOT the bottleneck. On a 32 GB Apple-silicon machine (M5 here) native SDXL already
|
||||
runs entirely on MPS with no CPU fallback (~155 s at 1122x1402, verified — see `synthid.md` /
|
||||
CLAUDE.md). The prototype runs at **1024** (fewer pixels than that) with SDXL base + an SDXL
|
||||
runs entirely on MPS with no CPU fallback (~155 s at 1122x1402, verified 2026-05-31).
|
||||
The prototype runs at **1024** (fewer pixels than that) with SDXL base + an SDXL
|
||||
ControlNet + activations in **fp32** (MPS fp16 decodes to all-black NaN — issue #29 — confirmed
|
||||
on run 1 below; fp32 is the required default on mps/cpu) — fits the 32 GB budget with vae-tiling +
|
||||
attention-slicing; ~1-2 min/image, so a coarse sweep is a sub-hour background run. A dedicated GPU
|
||||
@@ -534,7 +536,8 @@ https://instantid.github.io/ · https://arxiv.org/abs/2308.06721 (IP-Adapter)
|
||||
|
||||
### FaceID prototype run 1 -- 2026-06-03 (NEGATIVE on dense small-face groups)
|
||||
|
||||
Built and shipped the masked multi-face FaceID layer (`--face-id`, `face_id.py`, `faceid` extra).
|
||||
Built and shipped the masked multi-face FaceID layer (`--face-id`, `face_id.py`, `faceid` extra;
|
||||
all three were removed after this run and never reached the tracked tree).
|
||||
First real run on the gemini_3 group photo (Google, s015, scale 0.6, native 2816 via cap 1536):
|
||||
insightface detected **17 faces**, the masked multi-face pass composed and ran end-to-end (non-black
|
||||
output), so the API is correct. **At s015 the result is a clear FAILURE: every face corrupted --
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ The optional TrustMark decoder downloads weights into its installed package dire
|
||||
|
||||
## CI
|
||||
|
||||
`.github/workflows/test.yml` runs Ruff and a cross-platform supported-Python test matrix with default plus development dependencies. Diffusion and model-running tests skip in that matrix; metadata, identification, visible removal, the DWT-DCT decoder, and the OpenCV eraser remain covered across operating systems.
|
||||
`.github/workflows/test.yml` runs Ruff, a cross-platform test matrix over the supported-Python floor and the latest supported minor with default plus development dependencies, and a separate job that installs ffmpeg on Ubuntu to run the full-clip video test. Diffusion and model-running tests skip in that matrix; metadata, identification, visible removal, the DWT-DCT decoder, and the OpenCV eraser remain covered across operating systems.
|
||||
|
||||
Keep `uv.lock` compatible with `uv sync --frozen`. Dependency pull-request checks use GitHub's merge result against current `main`; if `main` moves, merge it locally and rerun the full gate because a newer linter can expose stale directives in later code.
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ to run the tool. Use the maintainer references only when changing the code.
|
||||
| Page | Purpose |
|
||||
| --- | --- |
|
||||
| [Module internals](module-internals.md) | Current architecture, invariants, and regression guards by module. |
|
||||
| [Development](development.md) | Environment setup, dependency recovery, CI behavior, and fixture policy. |
|
||||
| [Code provenance](code-provenance.md) | Required notices for licensed derivative work. |
|
||||
| [Verification plan](verification-plan.md) | Verification methods, completed measurements, and remaining validation gaps. |
|
||||
| [Release and distribution](release-and-distribution.md) | PyPI, Homebrew, Hugging Face Space, and release workflow. |
|
||||
|
||||
@@ -48,7 +48,10 @@ uv tool install --force "remove-ai-watermarks[visible,heif]"
|
||||
|
||||
## Video processing
|
||||
|
||||
Video metadata inspection and stripping work with the default package. Stable
|
||||
Video metadata inspection works with the default package, and MP4 and MOV
|
||||
stripping uses the in-tree ISOBMFF box walker. Stripping the non-ISOBMFF
|
||||
containers (MKV, WebM, AVI, FLV, and the audio formats) and writing any cleaned
|
||||
video need ffmpeg on PATH, for example `brew install ffmpeg`. Stable
|
||||
visible-mark identification and removal, full video cleaning, and visible/all
|
||||
batch modes need the `video` extra:
|
||||
|
||||
@@ -102,7 +105,7 @@ application actually uses:
|
||||
| `lama` | big-LaMa ONNX fill backend | `visible`, ONNX Runtime | Model download, no Torch |
|
||||
| `qwen-zimage` | Invisible image-watermark removal, both CUDA-only profiles | `diffusion`, DiffSynth | Yes |
|
||||
| `all` | Every production feature | All rows above | Yes |
|
||||
| `dev` | Tests, linting, typing, and upstream parity checks | `visible`, `detect`, upstream invisible-watermark | Yes, for parity tests |
|
||||
| `dev` | Tests, linting, typing, and upstream parity checks | `video`, `detect`, upstream invisible-watermark | Yes, for parity tests |
|
||||
|
||||
Dependency composition:
|
||||
|
||||
@@ -188,8 +191,10 @@ Run the complete project gate:
|
||||
bash maintain.sh
|
||||
```
|
||||
|
||||
The script runs dependency checks, linting, formatting checks, type checking,
|
||||
and the test suite.
|
||||
The script syncs every optional backend on top of the `dev` environment above,
|
||||
then runs dependency checks, linting, formatting, type checking, and the test
|
||||
suite. It applies Ruff fixes and formatting in place rather than only reporting
|
||||
them.
|
||||
|
||||
## Hugging Face authentication
|
||||
|
||||
|
||||
@@ -57,12 +57,12 @@ Invisible removal does not decode and delete a payload. It regenerates the
|
||||
image through a diffusion pipeline. Faces, text, colors, and fine detail can
|
||||
change even when the watermark is successfully disrupted.
|
||||
|
||||
ControlNet is the default compatibility profile. It conditions on edges to
|
||||
preserve structure, but edges do not preserve identity or exact texture.
|
||||
|
||||
The CUDA only `qwen-zimage` profile adds a separate face stage and is the
|
||||
highest fidelity option in the current implementation. It is larger, slower,
|
||||
and still may alter small text or difficult faces.
|
||||
`qwen-zimage` is the default profile and `sdxl-zimage` the only alternative.
|
||||
Both are CUDA only and differ only in the global regeneration model: each
|
||||
conditions that stage on a canny edge map, which preserves structure but not
|
||||
identity or exact texture, and each then runs the same face stage.
|
||||
`qwen-zimage` is the higher fidelity of the two. Both are large, slow, and may
|
||||
still alter small text or difficult faces.
|
||||
|
||||
### Removal cannot be verified locally for proprietary SynthID
|
||||
|
||||
|
||||
@@ -75,12 +75,31 @@ Regression coverage:
|
||||
|
||||
## High-level Python API
|
||||
|
||||
[`api.py`](../src/remove_ai_watermarks/api.py) provides:
|
||||
[`api.py`](../src/remove_ai_watermarks/api.py) provides the visible-mark entry
|
||||
points:
|
||||
|
||||
- `remove_visible`
|
||||
- `visible_provenance`
|
||||
|
||||
The package root exposes both lazily through
|
||||
and the image pipeline that the `all` and `batch` commands are thin wrappers
|
||||
over:
|
||||
|
||||
- `remove_all`, returning a `RemoveAllResult` after the visible, invisible, and
|
||||
metadata stages
|
||||
- `remove_batch`, returning a `BatchSummary` for one directory and one mode
|
||||
- `InvisibleOptions`, the invisible stage's knobs as one immutable value. Every
|
||||
default mirrors `InvisibleEngine`, so a bare `InvisibleOptions()` behaves
|
||||
exactly like calling the engine with no arguments. Two silently stopped:
|
||||
`max_resolution=None` reached `_target_size`'s `max_resolution > 0` and raised
|
||||
`TypeError` on every library call, and `cpu_offload=True` made a library run
|
||||
slower than the identical CLI run. `TestInvisibleOptionsMirrorTheEngine`
|
||||
compares the two signatures field by field
|
||||
- `MetadataStripIncomplete`, raised before any write when AI metadata survives
|
||||
|
||||
`remove_all` reports progress as `(stage, detail)` pairs of stable tokens, not
|
||||
prose the caller has to parse back.
|
||||
|
||||
The package root exposes all of them lazily through
|
||||
[`__getattr__`](../src/remove_ai_watermarks/__init__.py), keeping a plain package
|
||||
import free of the heavier image and model imports.
|
||||
|
||||
@@ -261,9 +280,9 @@ carries `accepts_provenance`, which forces `provenance=False` for Hailuo and Kli
|
||||
— they have no metadata that could confirm them, and the guarantee used to be
|
||||
structural (their wrappers took no `provenance` parameter at all). Provenance can relax a low-contrast run only
|
||||
after recurring visual evidence exists. Sora transition frames follow the
|
||||
nearest confirmed moving position only with Sora provenance. Veo, Seedance,
|
||||
Dola, Hailuo, and Kling additionally require candidates to remain anchored to
|
||||
the start of a run. This rejects slowly drifting scene details that still have
|
||||
nearest confirmed moving position only with Sora provenance. Seedance, Dola,
|
||||
Hailuo, and Kling additionally require candidates to remain anchored to the
|
||||
start of a run. This rejects slowly drifting scene details that still have
|
||||
high frame-to-frame overlap. Hailuo and Kling do not infer provenance from
|
||||
technical encoder tags; their confirmed public samples carried no provider
|
||||
metadata.
|
||||
@@ -784,7 +803,7 @@ everything it has been tested at:
|
||||
| Gemini app | 1.40 MP | -- | 0.1066 (the curve's own value) |
|
||||
|
||||
Read the last two rows before concluding the curve's low end is under-driven. Against
|
||||
the 4.33 MP Gemini boundary the sub-1 MP rungs of 0.084-0.094 look short, but at those
|
||||
the 4.33 MP Gemini boundary the sub-1 MP rungs of 0.084-0.098 look short, but at those
|
||||
sizes the curve's own values verify clean, which is what a resolution-scaled
|
||||
requirement would predict. There is no measured size at which the shipped curve fails,
|
||||
so it is left alone.
|
||||
|
||||
@@ -71,9 +71,10 @@ The source distribution uses an explicit allowlist for `/src`, `/LICENSE`,
|
||||
`[tool.hatch.build.targets.sdist]` in `pyproject.toml`. It also defensively
|
||||
excludes `/data`, `/tmp`, and `/.sc`. Keep both controls: calibration captures,
|
||||
test corpora, generated research outputs, and local session state do not belong
|
||||
in the published package archive. The matching local-root entries in
|
||||
`.gitignore` prevent accidental commits, but are not a substitute for the build
|
||||
boundary because hatchling may include untracked files.
|
||||
in the published package archive. `.gitignore` covers `tmp/` and `.sc/`, so those
|
||||
never reach a commit, but ignore rules are not the build boundary -- hatchling may
|
||||
include untracked files, and `data/` is deliberately tracked, so the sdist exclude
|
||||
is the only control keeping it out of the archive.
|
||||
|
||||
## Build backend
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ when you can select the affected area yourself.
|
||||
| `veo` | Current four-point diamond and legacy `Veo` text | Fixed bottom-right corner | Uses separate silhouettes and requires a recurring match; learned fill is preferable on structured backgrounds. |
|
||||
| `seedance` | Boxed `AI` label | Fixed bottom-right corner | Requires an anchored recurring match; the full localized box is filled because a thinner synthetic shape mask leaves the real translucent rim behind. |
|
||||
| `dola` | `Dola AI` text | Fixed bottom-right corner | Requires an anchored recurring match; ByteDance or BytePlus provenance can relax only an existing visual run. |
|
||||
| `hailuo` | `MINIMAX | hailuo AI` composite label | Fixed lower edge | Uses a synthetic waveform, text, separator, and ring silhouette; the complete recurring label box is filled. |
|
||||
| `hailuo` | `MINIMAX \| hailuo AI` composite label | Fixed lower edge | Uses a synthetic waveform, text, separator, and ring silhouette; the complete recurring label box is filled. |
|
||||
| `kling` | Kling swirl, `KLING AI`, version, and optional `PRO` suffix | Fixed bottom-right edge | Combines a synthetic logo rescue with font variants, an edge gate, a white-label gate, and anchored temporal recurrence. |
|
||||
|
||||
`video identify`, `video visible`, and `video all` share this registry and the
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
# Deep research: SynthID-safe face-identity recovery for SDXL (2026-06-08)
|
||||
|
||||
> Research archive. This dated follow-up records evidence available during the
|
||||
> study. It is not the current command reference.
|
||||
> study. It is not the current command reference. The `instantid` and `photomaker`
|
||||
> extras it discusses, the `--restore-faces` flag, and their modules were all removed
|
||||
> from the library afterwards.
|
||||
|
||||
**Stats:** {"angles": 6, "sourcesFetched": 28, "claimsExtracted": 104, "claimsVerified": 25, "confirmed": 19, "killed": 6, "afterSynthesis": 6, "urlDupes": 1, "budgetDropped": 7, "agentCalls": 111}
|
||||
|
||||
@@ -149,8 +151,9 @@ on Modal A100 in two phases:
|
||||
- v3 (tighter ellipse 0.32*bw x 0.42*bh + per-channel mean color match to
|
||||
local cleaned canvas + softer feather): patchwork visually softened; faces
|
||||
still read as studio portraits inserted into the scene, not as people
|
||||
shot in the scene. Single portrait identity drifted (tatsunari -> "round
|
||||
Asian male" vs original's thin face).
|
||||
shot in the scene. Single portrait identity drifted (the single-portrait
|
||||
fixture regenerated as a generic "round Asian male" vs the original's thin
|
||||
face).
|
||||
2. **Phase 2: InstantID img2img on cleaned crop.** Switched to the upstream
|
||||
`pipeline_stable_diffusion_xl_instantid_img2img` (downloaded at first use
|
||||
from raw.githubusercontent.com; requires `trust_remote_code=True`). Same
|
||||
|
||||
@@ -3,7 +3,8 @@
|
||||
> Research archive. This document evaluates approaches considered during the
|
||||
> study. It does not describe the current public interface. The current
|
||||
> pipelines and limits are documented in `docs/cli.md` and
|
||||
> `docs/known-limitations.md`.
|
||||
> `docs/known-limitations.md`. The path it calls shipped -- `photomaker_restore.py`
|
||||
> and the `photomaker` extra -- was removed from the library afterwards.
|
||||
|
||||
> **Status (2026-06-08): retired.** Every approach described below was empirically
|
||||
> tested and rejected -- see `docs/synthid-robust-identity-research-2026-06-08.md`
|
||||
|
||||
+17
-9
@@ -448,6 +448,14 @@ the payload, reconstituting SynthID in text. The lesson held and shaped the
|
||||
current design: **content is preserved by REGENERATING it under structural
|
||||
conditioning, never by copying original pixels.**
|
||||
|
||||
> **Superseded in 0.24.0.** The `controlnet`, `sdxl`, `qwen` and `default` profiles
|
||||
> were removed, and a retired name is now rejected at parse time rather than routed
|
||||
> onward. `--pipeline` accepts only `qwen-zimage` (the default) and `sdxl-zimage`;
|
||||
> both are CUDA-only, both condition their global stage on a canny edge map, and both
|
||||
> run the same Z-Image face stage. The bullets below are a record of what was measured
|
||||
> on the removed profiles, kept because the oracle verdicts are still the evidence the
|
||||
> current design rests on.
|
||||
|
||||
- **Text + structure:** `--pipeline controlnet` (SDXL img2img + a canny ControlNet) is
|
||||
**THE DEFAULT pipeline since 2026-06-09** (`--pipeline default` opts down to plain
|
||||
SDXL img2img for inputs without text/faces). It conditions the regeneration on the
|
||||
@@ -480,21 +488,21 @@ conditioning, never by copying original pixels.**
|
||||
measured at. See §5.2.**
|
||||
- **Highest-fidelity CUDA option:** `--pipeline qwen-zimage` is the recommended
|
||||
quality mode when preserving face identity matters more than latency, model size,
|
||||
and GPU cost. ControlNet remains the default because it is much cheaper and supports
|
||||
and GPU cost. ControlNet was then the default, because it was much cheaper and ran on
|
||||
CUDA, XPU, MPS, and CPU, but canny conditioning preserves edges rather than identity.
|
||||
On two direct upstream comparisons, `qwen-zimage` retained substantially more
|
||||
ArcFace identity than polished ControlNet. On 2026-07-25 the exact six-output
|
||||
`visible -> qwen-zimage -> metadata` candidate was negative in the corresponding
|
||||
OpenAI and Gemini oracles. This is a quality recommendation for the measured content,
|
||||
not broad removal certification; very small text can still degrade.
|
||||
See `docs/known-limitations.md` for the metrics, runtime, and validation scope.
|
||||
- **Face identity:** canny holds face *structure* but not *identity*. The standard
|
||||
SDXL and ControlNet profiles do not run a separate face-restoration option.
|
||||
Earlier GFPGAN, PhotoMaker, and FaceID experiments were removed after they
|
||||
degraded identity or risked reintroducing source pixels. The separate
|
||||
`qwen-zimage` profile now provides the only shipped face-specific stage:
|
||||
YuNet and SAM locate faces, then Z-Image regenerates the selected original
|
||||
face crops before a feathered composite. See
|
||||
See `docs/qwen-improvement-research.md` for the identity and text metrics and the
|
||||
validation scope of those comparisons.
|
||||
- **Face identity:** canny holds face *structure* but not *identity*. The removed
|
||||
SDXL and ControlNet profiles did not run a separate face-restoration stage, and
|
||||
earlier GFPGAN, PhotoMaker, and FaceID experiments were dropped after they
|
||||
degraded identity or risked reintroducing source pixels. Both shipped profiles
|
||||
now run the same face-specific stage: YuNet and SAM locate faces, then Z-Image
|
||||
regenerates the selected original face crops before a feathered composite. See
|
||||
`docs/controlnet-removal-pipeline-research.md` for the historical experiments.
|
||||
|
||||
### 5.2 Strength setting
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
> Research archive. This page records evaluated ideas, including rejected
|
||||
> prototypes. Current behavior is documented in the user guides and source.
|
||||
> The `text_protector.py` module it describes was removed from the library
|
||||
> afterwards.
|
||||
|
||||
Date: 2026-05-29. Source: a deep-research run (104 agents, 5 search angles, sources
|
||||
fetched and 3-vote adversarially verified). Not committed automatically — saved as a
|
||||
|
||||
@@ -68,7 +68,7 @@ payloads. Removal remuxes either container through ffmpeg with stream copy.
|
||||
Metadata stripping for supported audio containers is a separate implemented
|
||||
path.
|
||||
|
||||
**Box detection window — now handled (v0.6.8):** detection no longer relies on a fixed first-MB read. `metadata.scan_head(path, size)` reads the first `size` bytes and, for ISOBMFF, appends the payloads of late provenance boxes found by `isobmff.scan_c2pa_region` (a file-seeking top-level box walker that skips past `mdat` by size without reading it), so a C2PA/AIGC/IPTC manifest placed AFTER a large `mdat` in a streaming/non-faststart MP4 is now caught. Every C2PA/marker byte scan (`has_ai_metadata`, `aigc_label`, `iptc_ai_system`, `synthid_source`, `exif_generator` XMP, `get_ai_metadata` soft-binding, and `identify`) goes through `scan_head`; it is behavior-neutral for non-ISOBMFF inputs (exactly `f.read(size)`).
|
||||
**Box detection window — now handled (v0.6.8):** detection no longer relies on a fixed first-MB read. `metadata.scan_head(path, size)` reads the first `size` bytes and, for ISOBMFF, appends the payloads of late provenance boxes found by `isobmff.scan_c2pa_region` (a file-seeking top-level box walker that skips past `mdat` by size without reading it), so a C2PA/AIGC/IPTC manifest placed AFTER a large `mdat` in a streaming/non-faststart MP4 is now caught. Every C2PA/marker byte scan (`has_ai_metadata`, `aigc_label`, `iptc_ai_system`, `synthid_source`, `exif_generator` XMP, `get_ai_metadata` soft-binding, and `identify`) goes through `scan_head`; for PNG it likewise appends the payloads of `tEXt` / `iTXt` / `zTXt` / `eXIf` / `iCCP` chunks that start beyond the window (`_png_late_metadata`, seeking past `IDAT`), which is how a TC260 AIGC label appended after the pixel stream is caught; for every other input, and for any file that fits inside `size`, it is exactly `f.read(size)`.
|
||||
|
||||
Native TC260 MP4/MOV tags do not live in those top-level provenance boxes.
|
||||
`tc260_aigc_payloads` separately seeks through `moov.udta.meta.keys/ilst`, so
|
||||
|
||||
@@ -216,8 +216,11 @@ def remove_visible(
|
||||
class InvisibleOptions:
|
||||
"""The invisible stage's knobs, as one value instead of a dozen parameters.
|
||||
|
||||
Mirrors ``InvisibleEngine.remove_watermark``. Immutable so a batch can build it once
|
||||
and reuse it across every image while the engine itself is cached separately.
|
||||
Every default MIRRORS ``InvisibleEngine``, so a bare ``InvisibleOptions()`` behaves
|
||||
exactly like calling the engine with no arguments; two once drifted and broke
|
||||
silently, so ``TestInvisibleOptionsMirrorTheEngine`` compares the two signatures
|
||||
field by field. Immutable so a batch can build it once and reuse it across every
|
||||
image while the engine itself is cached separately.
|
||||
"""
|
||||
|
||||
strength: float | None = None
|
||||
@@ -227,9 +230,9 @@ class InvisibleOptions:
|
||||
humanize: float = 0.0
|
||||
unsharp: float = 0.0
|
||||
adaptive_polish: bool | None = None
|
||||
max_resolution: int | None = None
|
||||
max_resolution: int = 0
|
||||
controlnet_scale: float = 1.0
|
||||
cpu_offload: bool = True
|
||||
cpu_offload: bool = False
|
||||
tile: bool = False
|
||||
tile_size: int = 1024
|
||||
tile_overlap: int = 128
|
||||
|
||||
@@ -195,6 +195,32 @@ class TestRemoveVisibleOutputPath:
|
||||
assert out.exists()
|
||||
|
||||
|
||||
class TestInvisibleOptionsMirrorTheEngine:
|
||||
"""``InvisibleOptions`` forwards to ``InvisibleEngine`` and promises every default
|
||||
mirrors it. Compare the signatures field by field rather than pinning the values we
|
||||
happen to know about, so the next field added on one side and not the other fails
|
||||
here. See the incident record in ``docs/module-internals.md``."""
|
||||
|
||||
def test_every_default_matches_the_engine(self):
|
||||
import dataclasses
|
||||
import inspect
|
||||
|
||||
from remove_ai_watermarks.invisible_engine import InvisibleEngine
|
||||
|
||||
engine = {
|
||||
name: p.default
|
||||
for method in (InvisibleEngine.__init__, InvisibleEngine.remove_watermark)
|
||||
for name, p in inspect.signature(method).parameters.items()
|
||||
if p.default is not inspect.Parameter.empty
|
||||
}
|
||||
# ``force`` is a pipeline decision made before the engine runs, so it has no
|
||||
# engine counterpart; ``remove_watermark`` spells the controlnet knob out in full.
|
||||
renamed = {"controlnet_scale": "controlnet_conditioning_scale"}
|
||||
options = {f.name: f.default for f in dataclasses.fields(api.InvisibleOptions) if f.name != "force"}
|
||||
|
||||
assert options == {name: engine.get(renamed.get(name, name), "<not an engine parameter>") for name in options}
|
||||
|
||||
|
||||
class TestRemoveAllLibrary:
|
||||
"""The three-stage pipeline is a library function, not CLI-only.
|
||||
|
||||
|
||||
@@ -79,6 +79,15 @@ class TestTargetSize:
|
||||
assert _target_size(4096, 4096, 0) is None
|
||||
assert _target_size(123, 456, 0) is None
|
||||
|
||||
def test_the_engine_default_is_itself_a_valid_input(self):
|
||||
# Read the default off the signature rather than restating 0: a caller forwards
|
||||
# whatever ``remove_watermark`` declares, and ``None`` there would reach the
|
||||
# ``max_resolution > 0`` test below as a TypeError instead of "no cap".
|
||||
import inspect
|
||||
|
||||
default = inspect.signature(InvisibleEngine.remove_watermark).parameters["max_resolution"].default
|
||||
assert _target_size(1024, 768, default) is None
|
||||
|
||||
def test_negative_cap_treated_as_native(self):
|
||||
assert _target_size(4096, 4096, -1) is None
|
||||
|
||||
|
||||
Reference in New Issue
Block a user