Rewrite internal watermark pipeline and preserve behavior

This commit is contained in:
Victor Kuznetsov
2026-07-31 16:53:41 -07:00
parent 7c922e5133
commit a6c0c1c6f0
54 changed files with 2266 additions and 3771 deletions
+9
View File
@@ -0,0 +1,9 @@
# Code provenance
This page records notices required by source dependencies and licensed derivative work.
## Licensed derivative work
- The DWT-DCT implementation derives from ShieldMnt's
[`invisible-watermark`](https://github.com/ShieldMnt/invisible-watermark), licensed
under MIT. Its notice ships in `src/remove_ai_watermarks/licenses/invisible-watermark-MIT.txt`.
+1 -1
View File
@@ -211,7 +211,7 @@ regeneration so strokes exceed the VAE's ~8 px latent floor), but ctrlregen runs
at LOW res, the opposite. CtrlRegen's paper gives no resolution/tiling spec to contradict this.
**Sources.** the former internal
`src/remove_ai_watermarks/noai/ctrlregen/engine.py` (removed after this study);
`src/remove_ai_watermarks/_internal/ctrlregen/engine.py` (removed after this study);
resolution-omission
confirmed against https://arxiv.org/html/2410.05470v1
+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. |
| [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. |
| [Watermarking landscape](watermarking-landscape.md) | Vendor signals and detection approaches. |
+1 -1
View File
@@ -133,7 +133,7 @@ or a different random seed may change the verifier result.
The base Qwen and `qwen-zimage` profiles have profile specific strength
behavior. Consult `remove-ai-watermarks invisible --help` and the source of
[`watermark_profiles.py`](../src/remove_ai_watermarks/noai/watermark_profiles.py)
[`watermark_profiles.py`](../src/remove_ai_watermarks/_internal/watermark_profiles.py)
for the current resolver.
### Pipelines have different quality tradeoffs
+24 -19
View File
@@ -119,7 +119,7 @@ reported per file without retrying the same multi-GB initialization.
Native MP4/MOV TC260 labels follow TC260-PG-20257A:
`moov.udta.meta.keys` maps an `AIGC` key to a raw JSON value in `ilst`.
[`noai/isobmff.py`](../src/remove_ai_watermarks/noai/isobmff.py) walks those
[`_internal/isobmff.py`](../src/remove_ai_watermarks/_internal/isobmff.py) walks those
nested boxes by seeking, so detection reaches a tail `moov` without reading the
preceding `mdat`. The MP4/MOV/M4V/M4A removal path first validates the top-level
box walk, then copies the source to a sibling temporary file in bounded chunks.
@@ -130,14 +130,14 @@ validated JSON value with same-length spaces. This preserves every box size,
Publication is atomic, and a malformed top-level walk is copied unchanged. A
generic `AIGC` key whose value has no TC260 field is ignored.
[`noai/ebml.py`](../src/remove_ai_watermarks/noai/ebml.py) provides the
[`_internal/ebml.py`](../src/remove_ai_watermarks/_internal/ebml.py) provides the
corresponding bounded Matroska/WebM reader. It seeks over clusters and accepts
only a `Segment.Tags.Tag.SimpleTag` pairing `TagName=AIGC` with a JSON
`TagString` carrying a TC260 field. The existing ffmpeg stream-copy path removes
those container tags without transcoding the encoded streams.
[`noai/riff.py`](../src/remove_ai_watermarks/noai/riff.py) and
[`noai/flv.py`](../src/remove_ai_watermarks/noai/flv.py) implement the remaining
[`_internal/riff.py`](../src/remove_ai_watermarks/_internal/riff.py) and
[`_internal/flv.py`](../src/remove_ai_watermarks/_internal/flv.py) implement the remaining
normative TC260 video placements. The RIFF walker reads only AVI
`LIST/INFO/AIGC` children. The FLV walker skips media tags and parses the AMF0
`script.onMetaData.AIGC` string. Both require a recognized TC260 JSON field and
@@ -284,12 +284,12 @@ Regression coverage:
### C2PA
[`noai/c2pa.py`](../src/remove_ai_watermarks/noai/c2pa.py) reads C2PA with the
[`_internal/c2pa.py`](../src/remove_ai_watermarks/_internal/c2pa.py) reads C2PA with the
official `c2pa-python` reader first. Its byte-level PNG parser remains a fallback
for partial and synthetic fixtures that the official reader rejects.
Vendor attribution comes from the registry in
[`noai/constants.py`](../src/remove_ai_watermarks/noai/constants.py). Derived
[`_internal/constants.py`](../src/remove_ai_watermarks/_internal/constants.py). Derived
issuer and platform maps should not be maintained separately.
### Metadata scanning and stripping
@@ -303,7 +303,7 @@ Key contracts:
- JPEG stripping walks metadata segments and preserves the entropy-coded image
scan.
- ISOBMFF containers use
[`noai/isobmff.py`](../src/remove_ai_watermarks/noai/isobmff.py).
[`_internal/isobmff.py`](../src/remove_ai_watermarks/_internal/isobmff.py).
- Native MP4/MOV TC260 `AIGC` entries are read from
`moov.udta.meta.keys/ilst` and blanked without changing box sizes.
- Native MKV/WebM TC260 `AIGC` entries are read from
@@ -327,7 +327,7 @@ test proves that it no longer appears in the output.
Regression coverage:
- [`test_metadata.py`](../tests/test_metadata.py)
- [`test_noai.py`](../tests/test_noai.py)
- [`test_metadata_internals.py`](../tests/test_metadata_internals.py)
- [`test_security_clamp.py`](../tests/test_security_clamp.py)
### Provenance report
@@ -474,7 +474,7 @@ Regression coverage:
### Profiles and strength
[`noai/watermark_profiles.py`](../src/remove_ai_watermarks/noai/watermark_profiles.py)
[`_internal/watermark_profiles.py`](../src/remove_ai_watermarks/_internal/watermark_profiles.py)
is the source of truth for:
- profile aliases;
@@ -494,12 +494,19 @@ router.
[`invisible_engine.py`](../src/remove_ai_watermarks/invisible_engine.py) handles
image sizing, optional pre-upscaling, postprocessing, and the public engine
interface. It delegates model execution to
[`noai/watermark_remover.py`](../src/remove_ai_watermarks/noai/watermark_remover.py).
[`_internal/watermark_remover.py`](../src/remove_ai_watermarks/_internal/watermark_remover.py).
The Python engine and CLI do not have identical defaults for every optional
postprocessing argument. Integrations that require reproducibility should pass
the relevant values explicitly.
The standard Qwen and ControlNet prompts are calibrated model inputs, and the
ControlNet edge map uses fixed Canny thresholds of 100 and 200. Treat those
values as behavioral compatibility contracts: a refactor must preserve them,
and any deliberate change requires image-quality evaluation rather than only a
unit-test pass. Exact prompt and edge-map regression guards live in
`test_platform.py` and `test_invisible_engine.py`.
Regression coverage:
- [`test_watermark_profiles.py`](../tests/test_watermark_profiles.py)
@@ -519,7 +526,7 @@ Regression coverage:
### Qwen plus Z-Image
[`noai/qwen_zimage_pipeline.py`](../src/remove_ai_watermarks/noai/qwen_zimage_pipeline.py)
[`_internal/qwen_zimage_pipeline.py`](../src/remove_ai_watermarks/_internal/qwen_zimage_pipeline.py)
implements the fixed CUDA-only two-stage profile:
1. Qwen Image with Canny conditioning regenerates the frame.
@@ -530,13 +537,11 @@ The profile rejects a custom model identifier. Its global and face model stack
is fixed by the implementation. When tiling is enabled, only the global stage
is tiled; the face stage runs once after the tiles are blended.
The resolution and largest-face adaptive formulas remain exact ports of the
reference workflow. The face stage applies half the reference result because
this port uses a different sampler and composites regenerated SAM pixels rather
than using the reference latent inpaint mask and noise feather. Paired face
evaluations favored this scale on identity, perceptual distance, and full-image
similarity, and the exact OpenAI and Gemini candidates both passed their
matching provider oracle. The global stage stays unchanged.
The maintained implementation preserves the previously oracle-tested strength,
conditioning, crop, and sampler parameters as compatibility contracts. Its Python
orchestration, YuNet integration, SAM selection, masks, sizing helpers, and pixel
compositing are implemented for this runtime. Changing a calibrated model input
requires the same provider-oracle and identity evaluation as a model change.
Regression coverage:
@@ -545,7 +550,7 @@ Regression coverage:
### Tiling
[`noai/tiling.py`](../src/remove_ai_watermarks/noai/tiling.py) contains pure
[`_internal/tiling.py`](../src/remove_ai_watermarks/_internal/tiling.py) contains pure
tile planning, feather weights, tile orchestration, and region compositing.
Tiling engages only when requested and the long side exceeds the tile size.
+8 -8
View File
@@ -79,13 +79,13 @@ Measured on `gemini_3` (18 faces) at the Gemini scrub floor 0.25 vs base-Qwen 0.
a Qwen face fix. The next distinct architecture was Z-Image-Turbo on original masked face
crops, not another Qwen geometry conditioner.
**Implementation follow-up (2026-07-24):** that distinct architecture now exists as the
manual `qwen-zimage` profile. It ports the upstream Synthid-Bypass v2 graph: Qwen-Image-2512
Lightning + DiffSynth Canny for the full frame, then SAM-masked Z-Image Turbo regeneration
from original face crops. The upstream result supplied by the user was Gemini-oracle negative.
The active upstream face path is YOLO + SAM, not the unconnected MediaPipe node. The port
matches its center-point + box prompts, IoU-0.93 proposal selection, detector-box intersection,
crop factor, and paste feather; YuNet is the intentional detector substitution.
**Implementation follow-up (2026-07-24, revised 2026-07-31):** an early experimental
`qwen-zimage` prototype reproduced a broad two-stage shape demonstrated by a public
experiment: structure-guided Qwen regeneration followed by masked Z-Image face
refinement. The maintained profile was subsequently
rewritten with project-owned prompts, adaptive strength and sizing policies, YuNet face
detection, SAM selection, masks, and compositing. It does not include the upstream workflow
JSON or source code. The upstream result supplied by the user was Gemini-oracle negative.
The first exact-path Modal run completed without SAM fallback or face seams. On one crowded
18-face `gemini_3` fixture, ArcFace identity improved materially over controlnet
@@ -108,7 +108,7 @@ checked all six current outputs in the provider-separated
oracles and confirmed that none retained SynthID or the provider generation signal. The
checked bytes used the complete `visible -> qwen-zimage -> metadata` route, the calibrated
YuNet 0.5 gate, and the shipped prompt-cache/model-residency optimizations. This supersedes
the earlier first-port batch check as the release-candidate result, but it is not a
the earlier prototype batch check as the release-candidate result, but it is not a
certification across seeds, resolutions, and content classes. YuNet's threshold was
calibrated independently from upstream YOLO: 0.5 retained the visible faces in the
comparison fixtures while removing the false and duplicate boxes admitted by the copied
+1 -1
View File
@@ -26,7 +26,7 @@ color still leaves a persistent ghost outline.
Diagnosed why, empirically (cached stacks, `/tmp/doubao_distill`): (1) the mark is a clean white overlay with **no dark halo** -- over glyph pixels ~54% are brighter than the clean bg, only ~4% darker -- so the white-logo model `I=(1-α)O+α·255` is correct; (2) but content backgrounds are almost never dark *under* the mark (median darkest available bg over glyph pixels = **58/255**; only ~13% of mark pixels are ever observed on a bg < 40), so on bright backgrounds the equation is ill-conditioned and `α` is unidentifiable; (3) LaMa's `O` is a plausible **hallucination**, not the true pre-mark background, which compounds the error, and per-pixel regression on ~15 obs overfits into color noise.
**Why Gemini's engine is clean (verified in GeminiWatermarkTool `src/core/watermark_engine.cpp`): its alpha map is the watermark stamped on a PURE-BLACK background**, where `watermarked = α·255 + (1-α)·0 = α·255`, so `alpha = capture/255` exactly -- no estimation. (`gemini_bg_*.png` is literally the sparkle in gray on black.) So the real Doubao unlock is the same controlled capture, **not more content images**. The retained black and gray outputs live in `data/calibration/doubao/`; local solid-color seeds are regenerable and are not committed.
**Why Gemini's engine is clean: its alpha map is the watermark stamped on a PURE-BLACK background**, where `watermarked = α·255 + (1-α)·0 = α·255`, so `alpha = capture/255` exactly -- no estimation. (`gemini_bg_*.png` is literally the sparkle in gray on black.) So the real Doubao unlock is the same controlled capture, **not more content images**. The retained black and gray outputs live in `data/calibration/doubao/`; local solid-color seeds are regenerable and are not committed.
**Until black captures arrive, the shipped direction is precise canonical glyph mask + inpaint (cv2 default, lama optional), NOT reverse-alpha.**
+1 -1
View File
@@ -623,7 +623,7 @@ it; (3) **historical engineering conclusion:** this dated run argued for a
higher ControlNet strength than the then-current default. That proposal was
later superseded. The current resolver intentionally shares the 0.10/0.15
ladder between SDXL and ControlNet and uses a separate Qwen ladder; see
`noai/watermark_profiles.py`.
`_internal/watermark_profiles.py`.
Source images are private (faces / product shots), not committed; reproduce on any
photoreal + flat-graphic gpt-image pair, varying the seed, and re-checking the
oracle.