mirror of
https://github.com/wiltodelta/remove-ai-watermarks.git
synced 2026-08-10 08:00:32 +02:00
Rewrite internal watermark pipeline and preserve behavior
This commit is contained in:
@@ -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`.
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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. |
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user