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:
Victor Kuznetsov
2026-08-04 11:38:04 -07:00
co-authored by Claude Opus 5
parent 0261ec1bd7
commit 13095fb45c
19 changed files with 159 additions and 60 deletions
+6 -1
View File
@@ -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.
+10 -6
View File
@@ -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
View File
@@ -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
+9 -6
View File
@@ -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
View File
@@ -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.
+1
View File
@@ -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. |
+9 -4
View File
@@ -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
+6 -6
View File
@@ -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
+25 -6
View File
@@ -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.
+4 -3
View File
@@ -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
+1 -1
View File
@@ -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
+2 -1
View File
@@ -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
View File
@@ -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
View File
@@ -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
+1 -1
View File
@@ -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
+7 -4
View File
@@ -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
+26
View File
@@ -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.
+9
View File
@@ -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