mirror of
https://github.com/wiltodelta/remove-ai-watermarks.git
synced 2026-10-18 07:20:46 +02:00
Add audio and video watermark benchmark oracles, cohorts, and research studies
Extend the development-only benchmark family from images to audio and video, and close four research directions from the watermark-landscape map with local, revision-pinned evidence. Benchmark schema: media_type audio and video rows, audioseal and videoseal adapters with media-checked adapter binding, sample-domain audio fidelity (SNR with unbounded_identical), frame-domain video fidelity (mean PSNR, changed-frame fraction), and per-frame temporal evidence in videoseal detection records. The report accepts all three media types and carries SNR columns. Audio: shared pinned AudioSeal oracle, synthetic and system-voice speech cohorts with matched negatives, codec, noise, removed, and forged arms, and the provenance experiment proving visual-only video cleaning leaves the audio bitstream and watermark verdict invariant. Video: TorchScript-only VideoSeal oracle (the pip package is unusable on macOS arm64 and its loader resolves paths relative to cwd), video cohort with crf/scale/fps attacks and four-state arms, temporal evaluation measuring the crf decay curve, the frame-aggregation matrix (computed locally after finding the TorchScript build ignores the aggregation argument), and real provider clips as carriers. Forgery: double-embedding study showing the last writer wins completely under both oracles, scoped remover-trace probe, and the forged/ removed cohort arms encoding each scheme's correct expected verdict (presence rule for audioseal, message rule for videoseal). C2PA resolution: measured the five vendors' resolver contracts and the C2PA 2.2 18.10.5 privacy gradient; product decision recorded that this tool never resolves over the network. A registry guard pins the published resolution-API set. Gate: ruff and pyright src/ clean, 2114 passed / 3 skipped; maintain.sh stops only on the known unfixed lightning 2.6.5 CVEs, pre-existing.
This commit is contained in:
29 files changed
+4604
-48
No files matched your search
@@ -0,0 +1,133 @@
|
||||
# Audio provenance experiment (local AudioSeal)
|
||||
|
||||
Development-only study, run on 2026-09-06 from `scripts/audioseal_experiment.py`.
|
||||
It answers the first open item of the original watermark-landscape research:
|
||||
does a video remain provenance-positive through its untouched audio track
|
||||
after a visual-only cleaning pass? AudioSeal (Meta, MIT license) is used as a
|
||||
local matched oracle: the same package embeds the watermark and reads it back,
|
||||
so no provider API is contacted and no external upload happens.
|
||||
|
||||
This page records the design, the pinned inputs, and the measured outcome. It
|
||||
is not a detector-quality claim: carriers are synthetic non-speech signals,
|
||||
and AudioSeal is not any provider's production audio watermark.
|
||||
|
||||
## Design
|
||||
|
||||
The experiment separates three questions and never merges them:
|
||||
|
||||
1. `bitstream_identity`: are the cleaned video's audio packets byte-identical
|
||||
to the source video's audio packets?
|
||||
2. `cleaning_invariance`: does the detector verdict on the decoded audio
|
||||
change across the visual cleaning pass?
|
||||
3. `audio_attack`: how does the watermark respond to audio-path processing
|
||||
only, with no video involved?
|
||||
|
||||
Stages:
|
||||
|
||||
- Three deterministic 8-second 16 kHz mono synthetic carriers (AM tone stack,
|
||||
box-smoothed "pinkish" noise, white noise), a fixed 16-bit message, clean
|
||||
and marked WAVs. Watermark SNR 17.9-34.6 dB.
|
||||
- Three system-voice speech carriers rendered by the local macOS `say` tool
|
||||
(Samantha, Daniel, Milena; no network, byte-deterministic per voice, pinned
|
||||
by voice, text digest, and macOS version), each with clean and marked rows
|
||||
and per-second interval readings on both the marked audio and a variant
|
||||
whose seconds two through four were overwritten with 10 dB-SNR noise while
|
||||
every other sample stays bit-identical.
|
||||
- Audio attacks on the marked tone-stack WAV: AAC-LC 128k at 16 kHz and 48
|
||||
kHz, MP3 128k and 64k, 44.1 kHz resample, 8 kHz lowpass, additive noise at
|
||||
10 dB SNR, 0.5 gain.
|
||||
- Three video arms muxing the same marked WAV under a stamped Sora-like mark
|
||||
(24 frames, 12 fps, 840x480): PCM in MOV (bitstream-preserving), AAC 128k at
|
||||
16 kHz, AAC 128k at 48 kHz. Each arm runs the production
|
||||
`remove_video_visible` path, which transcodes video and stream-copies
|
||||
audio. Audio packets are extracted container-independently and hashed before
|
||||
and after cleaning.
|
||||
|
||||
Weights are pinned to Hugging Face revision
|
||||
`3c19eba53390776cf2cc9ed5f6c9ac67ce72ecba` and verified against pinned SHA-256
|
||||
digests before loading; the loader mirrors `AudioSeal.parse_model` with the
|
||||
checkpoint bytes pinned and now lives in the shared `scripts/audioseal_oracle.py`.
|
||||
Dependency versions at run time: audioseal 0.2.0, torch 2.13.0, Python
|
||||
3.12.13, ffmpeg 9.0.1, macOS 26.6.2 for the system voices. The executed code is
|
||||
bound by hash: experiment script
|
||||
`sha256:cc1f8360403ca1e4f04d72717b4b18ff84ca0718fa01bc1a8316dc189359b5b9`,
|
||||
oracle
|
||||
`sha256:d765e31b4c439fda82536b72bb602b55dac666d13e954809b42f48acfad1d2e8`,
|
||||
case rows
|
||||
`sha256:99b56a68fdb7789f4adc9f321eadf86277354a383270fe6d689b2c87eba83883`.
|
||||
Within one fixed environment two independent runs produced byte-identical
|
||||
case files (verified twice, before and after the oracle refactor). A
|
||||
full-extras `uv sync` between two recorded runs changed the cleaned-video
|
||||
H.264 packet bytes - every detector reading, every audio packet digest, and
|
||||
all source artifacts stayed identical, only the re-encoded video stream
|
||||
moved - so cross-environment comparisons must bind on the audio evidence and
|
||||
readings, not on cleaned-container hashes.
|
||||
|
||||
## Measured outcome, 2026-09-06
|
||||
|
||||
| Stage | Reading |
|
||||
| --- | --- |
|
||||
| Clean carriers (matched negatives) | mean detect 0.003-0.008, frac above threshold 0 |
|
||||
| Marked carriers | mean detect >= 0.9997, bit accuracy 0.94-1.0 |
|
||||
| All video arms, source audio | mean detect >= 0.9999, bit accuracy 1.0 |
|
||||
| All video arms, after visual cleaning | identical readings; audio packets byte-identical |
|
||||
| MP3 128k/64k, AAC 128k (16 and 48 kHz), 44.1 kHz resample, 8 kHz lowpass, 0.5 gain | mean detect >= 0.9999, bit accuracy 1.0 |
|
||||
| Raw ADTS AAC decode | mean detect 0.990, bit accuracy 0.69 |
|
||||
| Additive noise, 10 dB SNR | mean detect 0.30, bit accuracy 0.5 |
|
||||
|
||||
The thesis holds in this model: `remove_video_visible` leaves the audio
|
||||
bitstream untouched in every arm, so the audio watermark verdict is invariant
|
||||
across visual cleaning, and a cleaned video stays provenance-positive through
|
||||
its audio track. The result is scoped to AudioSeal as the mark; it proves the
|
||||
copy semantics of the cleaning path, not that any specific provider embeds a
|
||||
comparable audio mark.
|
||||
|
||||
The speech carriers close the out-of-domain gap the synthetic trio exposed:
|
||||
all three system voices decoded the full 16-bit message (the pinkish synthetic
|
||||
carrier had flipped one bit), with watermark SNR of 27.9-32.2 dB and clean
|
||||
speech far below the decision rule. Per-second interval readings localize the
|
||||
signal in time: overwriting seconds two through four with 10 dB noise killed
|
||||
exactly those two windows (0.19 falling to 0.0009 mean probability) while the
|
||||
untouched windows kept their original readings to the sixth decimal. The
|
||||
final window of a clip is a short remainder (0.1 s here) and its readings are
|
||||
accordingly less stable - visible in the tables, never dropped.
|
||||
|
||||
Three findings qualify the naive readings:
|
||||
|
||||
- **Codecs are not the attack.** At these rates every codec and resample round
|
||||
trip preserved both detection and the full message, on synthetic and speech
|
||||
carriers alike. Full-clip additive noise at 10 dB SNR killed every speech
|
||||
detection while the white and pinkish synthetic carriers survived it: speech
|
||||
has quiet moments where the noise dominates, so the effective removal lever
|
||||
on audio is loud additive noise, and its power depends on the carrier. A
|
||||
removal pipeline that wants an audio path clean must not rely on
|
||||
transcoding alone.
|
||||
- **The decode path is part of the measurement.** Decoding raw ADTS keeps the
|
||||
encoder's 1024-sample priming head in the stream, which alone moved the
|
||||
verdict from 0.9999/full message to 0.990/partial message; a gapless-aware
|
||||
container (MP4/M4A edit list, or MP3 gapless info) decodes sample-identical
|
||||
audio and restores the reading. The experiment carries the raw-ADTS arm
|
||||
(`aac_128k_adts_raw`) explicitly so the artifact stays a named measurement
|
||||
instead of contaminating codec rows.
|
||||
- **Voice names are not carrier identity.** macOS 26.6.2 renders `Alex` and
|
||||
`Samantha` byte-identically, so a naive three-voice set was one carrier
|
||||
counted twice. The shipped script verifies distinct content hashes across
|
||||
configured voices and refuses an aliased set; diversity claims rest on the
|
||||
hash, never the name.
|
||||
|
||||
Detector cost on this host (Apple silicon, CPU): cold first call 176-465 ms
|
||||
across runs, warm median 158-160 ms for 8-second clips. These are diagnostic
|
||||
host timings, not a certified profile.
|
||||
|
||||
An earlier iteration derived per-carrier seeds from Python's `hash()`, which is
|
||||
salted per process; it produced different carrier bytes on every run. The
|
||||
shipped script derives seeds from `zlib.crc32`, and the two recorded final runs
|
||||
are byte-identical.
|
||||
|
||||
## Follow-ups
|
||||
|
||||
- The speech and interval layers are complete; the remaining audio-side
|
||||
upgrade is folding per-second windows into the benchmark schema itself, if a
|
||||
case-level question ever needs them as rows. The forgery/remover-forensics
|
||||
and VideoSeal temporal items remain separate open directions from the
|
||||
original research map.
|
||||
+226
-12
@@ -1,11 +1,15 @@
|
||||
# Watermark benchmark kernel
|
||||
|
||||
`scripts/watermark_benchmark.py` is a development-only runner for reproducible
|
||||
image-watermark experiments. It calls the existing local DWT-DCT and TrustMark
|
||||
detectors through thin adapters. It does not add a runtime command, download a
|
||||
corpus, or contact a provenance oracle or provider API. The optional TrustMark
|
||||
package can fetch its official Adobe model weights when its local package cache
|
||||
is incomplete; prepare that dependency before an offline run.
|
||||
image-, audio-, and video-watermark experiments. It calls the existing local
|
||||
DWT-DCT and TrustMark detectors plus the revision-pinned AudioSeal and
|
||||
VideoSeal oracles through thin adapters. It does not add a runtime command,
|
||||
download a corpus, or contact a provenance oracle or provider API. The
|
||||
optional TrustMark package can fetch its official Adobe model weights when its
|
||||
local package cache is incomplete, the audioseal adapter fetches its pinned
|
||||
checkpoints the same way, and the videoseal adapter downloads one pinned
|
||||
TorchScript file into a user cache; prepare those dependencies before an
|
||||
offline run.
|
||||
|
||||
The kernel answers three different questions and never merges their answers:
|
||||
|
||||
@@ -36,10 +40,10 @@ The fixed fields are:
|
||||
| `schema_version` | Integer `1`. |
|
||||
| `case_id` | Unique case identity. |
|
||||
| `pair_id` | Groups related clean, marked, attacked, and removed cases. |
|
||||
| `media_type` | `image` in schema v1. |
|
||||
| `adapter` | `dwt-dct` or `trustmark`. |
|
||||
| `media_type` | `image`, `audio`, or `video` in schema v1. |
|
||||
| `adapter` | `dwt-dct` or `trustmark` for `image`; `audioseal` for `audio`; `videoseal` for `video`. The loader rejects a known adapter named against the wrong media type. |
|
||||
| `arm` | `positive`, `matched_negative`, `wrong_key`, or `hard_negative`. |
|
||||
| `state` | `clean`, `marked`, `attacked`, or `removed`. |
|
||||
| `state` | `clean`, `marked`, `attacked`, `removed`, or `forged`. `forged` names an artifact carrying a watermark with a message different from the adapter oracle's fixed one; the matched verifier's correct answer there is `not_detected` with the forensic detail carried by the study layer, not the verdict. |
|
||||
| `path`, `sha256` | Artifact path and pinned content digest. |
|
||||
| `reference_path`, `reference_sha256` | Both strings or both `null`; the fidelity reference. |
|
||||
| `source_revision` | Corpus, generator, or acquisition revision. |
|
||||
@@ -85,10 +89,110 @@ pixels produce `psnr_db: null` with
|
||||
`psnr_status: unbounded_identical`, never a misleading numeric zero. Missing or
|
||||
incomparable references remain explicit states rather than fabricated metrics.
|
||||
|
||||
Schema v1 deliberately has no aggregate score, confidence interval, video
|
||||
adapter, automatic attack generator, removal runner, or provider oracle. Add
|
||||
those only after the case-level evidence is large enough to define and test the
|
||||
corresponding statistical or media contract.
|
||||
Schema v1 deliberately has no aggregate score, confidence interval, automatic
|
||||
attack generator, removal runner, or provider oracle. Add those only after the
|
||||
case-level evidence is large enough to define and test the corresponding
|
||||
statistical or media contract.
|
||||
|
||||
## Audio rows
|
||||
|
||||
`media_type: audio` rows decode every artifact and reference to mono 16 kHz
|
||||
float32 through the system `ffmpeg` before any adapter runs, mirroring the
|
||||
image path where decoding also stays outside the detector interval. Audio rows
|
||||
therefore require `ffmpeg` on `PATH`; a missing or failing decode is the same
|
||||
explicit `error` status as an undecodable image.
|
||||
|
||||
The `audioseal` adapter reads the shared revision-pinned oracle in
|
||||
`scripts/audioseal_oracle.py` (Meta AudioSeal, MIT license, Hugging Face
|
||||
revision and checkpoint SHA-256 pinned and verified at load). Its `detected`
|
||||
status is the detector's own documented rule: the fraction of per-sample
|
||||
probabilities above 0.5 reaching 0.5. The `label` is the decoded 16-bit message
|
||||
as a binary string, so a case whose payload differs from the embedded one is
|
||||
still `detected` with a different label, never `not_detected`. A `wrong_key`
|
||||
arm for this adapter is therefore a different-message row, not a clean row.
|
||||
|
||||
Audio fidelity compares the decoded sample arrays against the explicit
|
||||
reference: identical decoded samples produce `snr_db: null` with
|
||||
`snr_status: unbounded_identical`, never a misleading numeric zero; a silent
|
||||
reference reports `zero_reference_power`; a length mismatch stays an explicit
|
||||
`incomparable` state. SNR here is reference signal power over error power, the
|
||||
sample-domain analogue of the image PSNR contract.
|
||||
|
||||
## Audio cohort v1
|
||||
|
||||
`scripts/watermark_benchmark_audio_cohort.py` builds the audio analogue of the
|
||||
synthetic image cohort: three deterministic synthetic carriers (AM tone stack,
|
||||
smoothed noise, white noise), the pinned AudioSeal message, matched clean
|
||||
negatives, MP3 128k, gapless M4A AAC 128k, and deterministic 10 dB additive
|
||||
noise attacks, plus one never-embedded white-noise hard negative. Since v2
|
||||
the audio cohort also carries the four-state arms: a `removed` row where the
|
||||
noise transform measurably destroys the mark (the tone stack only - the
|
||||
synthetic noise carriers survive 10 dB noise, so their noise rows stay open
|
||||
`attacked` measurements), and `forged` rows embedding a foreign message with
|
||||
`expected: detected`, because the audioseal rule is watermark presence and
|
||||
the decoded label is what exposes the foreign payload. 20 cases in total,
|
||||
everything hashed into a strict manifest validated through the
|
||||
benchmark loader.
|
||||
|
||||
```bash
|
||||
uv run --extra dev python scripts/watermark_benchmark_audio_cohort.py \
|
||||
--output-dir .local-eval/watermark-benchmark-audio-cohort-v1
|
||||
uv run --extra dev python scripts/watermark_benchmark.py \
|
||||
.local-eval/watermark-benchmark-audio-cohort-v1/manifest.jsonl \
|
||||
--output .local-eval/audio-cohort-v1-results.jsonl
|
||||
```
|
||||
|
||||
### Development baseline, 2026-09-06
|
||||
|
||||
The first local run used kernel
|
||||
`sha256:2216169ab7c33f7439720f0934e8ac6ba8379e510bfaeabe008f820c7658701d`,
|
||||
oracle `sha256:bd67e2b1be5c486384ab1f64709f661241451ab1ea7592cc56f0eefb91c415ab`,
|
||||
and recipe `sha256:f361163b4fbf23b885c026751c544e97be0bf054906c1aad455276992a2768d2`,
|
||||
producing manifest `sha256:b6c48684f0c85e882159f6d5c4fbeea2f43c3b24d3a2c3a034209c6f531ba73d`
|
||||
and case rows `sha256:66ac23bd926925dc5baf7216c03f8a7246eeb473315a17701358a3803ab7b0d1`.
|
||||
Dependencies were audioseal 0.2.0, torch 2.13.0, and ffmpeg 9.0.1.
|
||||
|
||||
| State and transform | Cases | Detected | Not detected |
|
||||
| --- | ---: | ---: | ---: |
|
||||
| clean matched negatives + hard negative | 4 | 0 | 4 |
|
||||
| marked, audioseal embed | 3 | 3 | 0 |
|
||||
| attacked, MP3 128k | 3 | 3 | 0 |
|
||||
| attacked, AAC 128k (gapless M4A) | 3 | 3 | 0 |
|
||||
| attacked, 10 dB additive noise | 3 | 2 | 1 |
|
||||
|
||||
Every clean and never-embedded negative stayed below the decision rule. All
|
||||
codecs retained detection with the full message on the tone-stack and
|
||||
white-noise carriers; the pinkish-noise carrier decoded one flipped payload bit
|
||||
while remaining detected, matching the experiment's per-carrier bit-accuracy
|
||||
observation. The 10 dB noise arm destroyed the tone-stack detection but not
|
||||
white or pinkish noise: carrier-dependent robustness is exactly what the
|
||||
matched arms exist to expose. Nearest-rank adapter timing: cold first call
|
||||
405.1 ms, warm median 124.2 ms, warm max 157.4 ms for 8-second clips on this
|
||||
host. As with the image baselines, these are diagnostic host rows, not a
|
||||
certified profile.
|
||||
|
||||
### Speech extension
|
||||
|
||||
`--with-speech` adds fifteen more rows: three system-voice speech carriers
|
||||
(Samantha, Daniel, Milena) rendered by the local macOS `say` tool - no
|
||||
network, byte-deterministic per voice on a fixed macOS version - each with a
|
||||
matched clean negative, a marked positive, and the same three attacks. Every
|
||||
speech row's transform parameters carry the full TTS provenance (tool, macOS
|
||||
version, voice, text digest, duration). The builder verifies the configured
|
||||
voices render distinct bytes and refuses an aliased set, because macOS 26.6.2
|
||||
renders `Alex` and `Samantha` byte-identically and a name is not a carrier
|
||||
identity.
|
||||
|
||||
The first `--with-speech` run (2026-09-06, 31 cases total, recipe
|
||||
`sha256:3f291f49ab9ef2499085708fb614e3723ebee6d35f0b509fc36f017ec5a86c3b`,
|
||||
manifest
|
||||
`sha256:b9baa79066deb002402cc20166835887d67a5158ff9ce7bbc1963e5d603db3a5`,
|
||||
case rows
|
||||
`sha256:abc68590f59fbddab1c086371d2854622b41ea756ba61a7a87b76cb6df64c386`)
|
||||
kept every synthetic verdict and added: speech clean 0/3 detected, speech
|
||||
marked 3/3 with the full message, codecs 3/3, and full-clip 10 dB noise 0/3 -
|
||||
unlike the synthetic white and pinkish carriers, speech has quiet moments
|
||||
where the noise dominates. Speech marked fidelity sat at 27.9-32.2 dB SNR.
|
||||
|
||||
## Synthetic cohort v1
|
||||
|
||||
@@ -275,6 +379,116 @@ uncommitted. The exact inputs and implementations are therefore bound by hash:
|
||||
The local timing rows were host-contended and are not used for a new latency
|
||||
claim. The controlled resource profile below remains the performance evidence.
|
||||
|
||||
## Video rows
|
||||
|
||||
`media_type: video` rows decode every artifact and reference to float RGB
|
||||
frames in [0, 1] as (T, H, W, 3) through the system `ffmpeg`, staying outside
|
||||
the detector interval like the image and audio decodes. Video fidelity
|
||||
compares decoded frames against the explicit reference clip: mean PSNR over
|
||||
frames, a changed-frame fraction, `unbounded_identical` for bit-identical
|
||||
pixel sequences, and an explicit `shape_mismatch` state when frame counts or
|
||||
geometry differ - which is why geometry-changing attack arms carry no
|
||||
reference instead of a fabricated comparison.
|
||||
|
||||
The `videoseal` adapter loads Meta's standalone TorchScript release of
|
||||
VideoSeal (MIT license) from `scripts/videoseal_oracle.py` - one pinned file,
|
||||
no `videoseal` package, whose wheel pulls a research dependency chain without
|
||||
macOS arm64 wheels and whose loader resolves cards relative to the working
|
||||
directory. The upstream video evaluation ignores the model's detection head
|
||||
(it prepends a constant) and scores the aggregated 256-bit message decode, so
|
||||
the adapter's `detected` is its own explicit rule: bit accuracy of the
|
||||
averaged decode against the oracle's fixed message at or above 0.9. The
|
||||
`label` is the decoded message as 64 hex characters. Because the rule is
|
||||
message accuracy, a different-message row reads `not_detected` - the matched
|
||||
verifier rejects the foreign payload - unlike the audioseal presence rule
|
||||
above. Every videoseal detection record
|
||||
carries a `temporal` block - frame count, min/mean/max and the full per-frame
|
||||
bit-accuracy series - and the aggregation is computed in the oracle from raw
|
||||
per-frame logits, because the TorchScript build ignores the aggregation
|
||||
argument of `detect_video_and_aggregate`. The aggregation matrix itself
|
||||
(avg versus norm-weighted) is evaluated in
|
||||
[VideoSeal temporal evaluation](videoseal-temporal-evaluation.md), not in the
|
||||
kernel, which keeps plain avg as its canonical rule.
|
||||
|
||||
## Video cohort v1
|
||||
|
||||
`scripts/watermark_benchmark_video_cohort.py` builds the video analogue of
|
||||
the other cohorts: two deterministic synthetic 64-frame 256x256 clips (moving
|
||||
gradient, moving texture), the pinned 256-bit message, matched clean
|
||||
negatives, one never-embedded hard negative, and three attacks - a realistic
|
||||
H.264 re-encode at crf 23, a 75 percent downscale, and a frame-rate halving
|
||||
that drops half the keyframes. Since v2 the video cohort also carries the
|
||||
four-state arms: `removed` rows reusing the crf 23 re-encode that measurably
|
||||
takes both synthetic carriers below the rule, and `forged` rows embedding a
|
||||
foreign message with `expected: not_detected`, because the videoseal rule is
|
||||
message accuracy and the matched verifier rejects a foreign payload. 15
|
||||
cases in total.
|
||||
|
||||
```bash
|
||||
uv run --extra dev python scripts/watermark_benchmark_video_cohort.py \
|
||||
--output-dir .local-eval/watermark-benchmark-video-cohort-v1
|
||||
uv run --extra dev python scripts/watermark_benchmark.py \
|
||||
.local-eval/watermark-benchmark-video-cohort-v1/manifest.jsonl \
|
||||
--output .local-eval/video-cohort-v1-results.jsonl
|
||||
```
|
||||
|
||||
### Development baseline, 2026-09-07
|
||||
|
||||
The first local run used kernel
|
||||
`sha256:7a3906cebc3c7cacbac7e204c014d1d6281615e7acbbc5b3a8fcecc939ca68c7`,
|
||||
oracle `sha256:6c889bf399e8e3bada439eee4a1e9c0b04177be78dfee756d5daa79e73db71b3`,
|
||||
recipe `sha256:dd06ae1c40ef7d7f6fd2b258b20444a3250254a5ef258542a4eaf43cc501342b`,
|
||||
manifest
|
||||
`sha256:bb6a7dda6b3897794f2fbfe47291329bb7b9ff7ee57c0634ebb386e58acd5e7a`,
|
||||
and case rows
|
||||
`sha256:5793b8ba33c55e135e5bef43f33fd2699bb57bae6ca8ead38b1dab0123d1dd12`.
|
||||
The checkpoint is the TorchScript build `y_256b_img.jit`
|
||||
`sha256:5c7a4581c36fc6090aafdcfb3999123bae5172a4847f22e2da4e7fd1a39d1e1b`
|
||||
pinned to upstream commit `870ca7f`.
|
||||
|
||||
| State and transform | Cases | Detected | Not detected |
|
||||
| --- | ---: | ---: | ---: |
|
||||
| clean matched negatives + hard negative | 3 | 0 | 3 |
|
||||
| marked, videoseal embed (crf 8 artifact) | 2 | 2 | 0 |
|
||||
| attacked, H.264 crf 23 | 2 | 0 | 2 |
|
||||
| attacked, 75 percent downscale + crf 18 | 2 | 0 | 2 |
|
||||
| attacked, frame rate halved | 2 | 2 | 0 |
|
||||
|
||||
Direct oracle readings fill in what the threshold hides: the marked clips
|
||||
decode at 0.992-0.996 bit accuracy (48.5 and 41.4 dB mean PSNR against their
|
||||
clean carriers), the crf 23 re-encode collapses to 0.56-0.57, the downscale
|
||||
to 0.69, and the halved frame rate keeps 0.996. Two cautions travel with
|
||||
these numbers: the watermark survives H.264 only at generous quality - a
|
||||
realistic crf 23 re-encode erases it on these smooth synthetic carriers,
|
||||
while a high-bitrate uniform-noise carrier survived crf 18 in the isolated
|
||||
probe, so retention is carrier- and quality-dependent - and dropping half
|
||||
the frames does not touch the message because the secret repeats across
|
||||
keyframes. These are first-baseline observations on synthetic carriers, not
|
||||
robustness curves; a sweep across crf values and real-content carriers is
|
||||
the natural follow-up.
|
||||
|
||||
### Four-state baseline, 2026-09-07 (v2 cohorts)
|
||||
|
||||
The v2 runs used kernel
|
||||
`sha256:22f2c1dd5c3b7aeb96090c4c578c6d1841ee62d90958dd1c5c7ad7db71835eec`
|
||||
with audio recipe
|
||||
`sha256:31c021144212a43565fb3746fadc835bfc37ea8ac98998fad9bba9e72410e82c`
|
||||
(manifest
|
||||
`sha256:43c7f77f1d4537c00d4b0dd5128e9becc6c860cdd196a7ba2f7541f4276784ec`,
|
||||
case rows
|
||||
`sha256:c73d7dddfdb90262e96be4d67ec1567d9046812661928cc816cb9f97f4a01a7e`)
|
||||
and video recipe
|
||||
`sha256:8f25929304ef4f94721667968ee4bb89ebf584a2108de7e02db36c855dc2ddc8`
|
||||
(manifest
|
||||
`sha256:b5659848c0d05e205f80ab36480e70fdf7e5f9df4d8ada6ffadec42c97b88998`,
|
||||
case rows
|
||||
`sha256:c9ad56848bb7efe200aaf1189a8eb03f7571567e6702edd195a62f812fa05815`).
|
||||
Every v1 verdict was unchanged; the new arms read: audio removed 0/1
|
||||
detected, audio forged 3/3 detected under the presence rule, video removed
|
||||
0/2, video forged 0/2 under the message rule - zero expected mismatches.
|
||||
The mechanism underneath these rows, including double embedding, is measured
|
||||
in [Watermark forgery study](watermark-forgery-study.md).
|
||||
|
||||
## Aggregate repeated runs
|
||||
|
||||
`scripts/watermark_benchmark_report.py` turns one or more result JSONL files
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
# C2PA soft-binding resolution research
|
||||
|
||||
Development-only investigation, 2026-09-07, completing the privacy-tiered C2PA
|
||||
resolution item from the original research map. The outcome is a product
|
||||
decision rather than a feature: this tool resolves nothing over the network,
|
||||
and this page records what the resolution ecosystem would learn if it did.
|
||||
|
||||
## The normative shape
|
||||
|
||||
C2PA 2.2 specification section 18.10.5 defines the soft binding resolution
|
||||
API: a standard way of retrieving manifest stores from a resolution endpoint
|
||||
given **a soft binding value, a manifest identifier, or an asset** - three
|
||||
input modes that are exactly three privacy exposure levels. The algorithm
|
||||
registry (`c2pa-org/softbinding-algorithm-list`) carries the endpoint URIs in
|
||||
`softBindingResolutionApis`. As of the packaged revision `c0218cd628c9`,
|
||||
8 of 53 entries publish them.
|
||||
|
||||
## Measured contracts
|
||||
|
||||
| Algorithm | Kind | Endpoint | Input | Content leaves | Content digest leaves |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| `com.aiwatermark.audioseal.1` / `videoseal` / `pixelseal` | watermark | `aiwatermark.com/api/v1/resolve` | `GET ?payload={extracted payload}&alg=` | no | no |
|
||||
| `com.joinmonolith.sha256` | fingerprint | `api.joinmonolith.com/api/c2pa/matches/byBinding` | `GET ?alg&value=base64(SHA-256 of asset)` | no | yes |
|
||||
| `me.deepmark.audio.vigil.128` | watermark | `resolution-api.deepmark.me` | payload-based per vendor site | no | no |
|
||||
| `ai.trufo.pawprint.watermark` / `.fingerprint` | both | `c2pa.trufo.ai/v1` | not published publicly | ? | ? |
|
||||
| `io.blockfact.audio.watermark.32` | watermark | `api.blockfact.io/api/soft-binding/v1` | not published publicly | ? | ? |
|
||||
|
||||
Sources, verified 2026-09-07 against the vendors' own pages: the Monolith
|
||||
soft-binding documentation (lookup endpoints public by design, "a verifier is
|
||||
usually a stranger"; only manifest minting is authenticated), the AIWatermark
|
||||
algorithm page ("after extracting the watermark payload from an audio file,
|
||||
validators can resolve the payload"; model source Meta FAIR AudioSeal), the
|
||||
Deepmark product description ("the signal is detected and its payload
|
||||
resolved"), the Trufo developer documentation and `trufo-py` repository
|
||||
(signing-side only), and the BlockFact API reference (its public verify
|
||||
endpoint takes a Starknet transaction hash, not a soft binding).
|
||||
|
||||
The privacy gradient, concretely:
|
||||
|
||||
- **Payload-only** resolution (AIWatermark, Deepmark) sends a locally
|
||||
extracted watermark payload - opaque bits that identify the mark, not the
|
||||
file. This is the mode our pinned local oracles could serve without ever
|
||||
touching the network with content or a digest.
|
||||
- **Digest** resolution (Monolith) sends base64(SHA-256) of the whole asset.
|
||||
No bytes leave, but the digest is linkable to the exact content: the
|
||||
registry learns you are inspecting that precise file.
|
||||
- **Undocumented** (Trufo, BlockFact): no public contract; any integration
|
||||
would be guesswork and is not attempted.
|
||||
|
||||
## The product decision
|
||||
|
||||
This tool does not resolve anything over the network, at any tier, and no
|
||||
`local_only` / `binding_only` / `full_content` modes ship. The reasoning:
|
||||
|
||||
- The tool's user processes their own generated or edited content and wants
|
||||
to know which provenance signals it carries and to remove them. Local
|
||||
payload extraction - already available through the pinned development
|
||||
oracles - answers the identification question without a network.
|
||||
- Querying a vendor's resolver tells that vendor someone is inspecting the
|
||||
specific content (directly for digest-based lookups, via the payload for
|
||||
payload-based ones). For a removal tool that is a leak in the opposite
|
||||
direction of its purpose, and it contradicts the offline runtime boundary
|
||||
the project already guarantees.
|
||||
- No user scenario of this tool requires manifest recovery from a registry:
|
||||
that is a verifier-of-third-party-content function, which is outside the
|
||||
intended use.
|
||||
|
||||
The runtime boundary stays: identification reports locally decoded evidence
|
||||
only, and `docs/known-limitations.md` states the resolution boundary in user
|
||||
terms. A guard test pins the packaged registry's published-API set, so an
|
||||
upstream ecosystem change surfaces as a conscious decision recorded against
|
||||
this page rather than a silent regeneration.
|
||||
|
||||
## Investigation note
|
||||
|
||||
The first reconnaissance pass concluded "upstream publishes zero resolution
|
||||
APIs" because the probe searched the registry JSON for guessed key names
|
||||
instead of the `softBindingResolutionApis` key the sync actually reads. The
|
||||
drift-guard test written from that wrong premise failed immediately against
|
||||
the packaged snapshot and exposed the error - eight live endpoints, including
|
||||
resolution hosting for the exact AudioSeal and VideoSeal schemes this project
|
||||
benchmarks. The corrected guard now pins the real set. Field names are
|
||||
verified against the consumer's schema, never guessed.
|
||||
@@ -50,4 +50,8 @@ The current behavior is defined by the code, tests, README, and user guides.
|
||||
- [OpenAI SynthID oracle ladders](synthid-oracle-ladders.md) (routing hub)
|
||||
- [SynthID detector and removal plan](synthid-detector-removal-plan.md) (chronological mixed archive)
|
||||
- [Text protection research](text-protection-research.md)
|
||||
- [Audio provenance experiment](audio-provenance-experiment.md) (local AudioSeal matched-oracle study of video audio tracks across visual cleaning)
|
||||
- [VideoSeal temporal evaluation](videoseal-temporal-evaluation.md) (per-frame scores, aggregation matrix, and crf decay on synthetic and real clips)
|
||||
- [Watermark forgery study](watermark-forgery-study.md) (four-state model and double-embedding: the last writer wins under both oracles)
|
||||
- [C2PA resolution research](c2pa-resolution-research.md) (measured resolver contracts, the privacy gradient, and the decision to never resolve over the network)
|
||||
- [Chroma1 engine research](chroma1-engine-research.md)
|
||||
@@ -207,6 +207,15 @@ The public video results make that boundary explicit without decoding audio:
|
||||
the nested audio status reports `copied_if_present` and `unverified`. CLI output
|
||||
uses the same terms. These fields add no media pass or model inference.
|
||||
|
||||
A local matched-oracle study has now measured the boundary concretely:
|
||||
[Audio provenance experiment](audio-provenance-experiment.md) embeds AudioSeal
|
||||
into synthetic carriers, runs the visible video-cleaning path, and shows the
|
||||
audio packets byte-identical and the watermark verdict invariant across
|
||||
cleaning, while additive noise on the audio path is the effective lever. The
|
||||
boundary above still stands for provider marks: this tool still does not
|
||||
decode them, and a `copied_if_present` audio track remains `unverified` in
|
||||
runtime output.
|
||||
|
||||
The shipped engine streams sampled frames in bounded batches, computes its
|
||||
fidelity metrics incrementally, and pipes regenerated pixels directly to
|
||||
ffmpeg. Its frame and latent memory is therefore bounded by `--batch-size`
|
||||
@@ -346,6 +355,18 @@ Memory needs depend on the profile, input size, dtype, and card.
|
||||
|
||||
## Metadata and formats
|
||||
|
||||
### Identification never resolves anything over the network
|
||||
|
||||
C2PA soft bindings can be resolved to manifests through vendor endpoints
|
||||
(the registry carries eight of them), and querying one always tells the
|
||||
vendor that someone is inspecting the specific content - directly through
|
||||
the asset digest for fingerprint lookups, through the watermark payload for
|
||||
payload-based ones. This tool deliberately has no resolution mode, tier, or
|
||||
opt-in: identification reports only locally decoded evidence, and the
|
||||
watermark decoders in this repository run offline against pinned weights.
|
||||
The contracts, the privacy gradient, and the reasoning are recorded in
|
||||
[the resolution research](c2pa-resolution-research.md).
|
||||
|
||||
### Missing metadata does not mean clean
|
||||
|
||||
Screenshots, social platforms, and re-encoding can remove metadata while a
|
||||
|
||||
@@ -432,6 +432,32 @@ resource tier; its CPU throughput makes a six-provider online matrix
|
||||
counterproductive. Store generated outputs and the detailed CSV only under
|
||||
`.local-eval/`.
|
||||
|
||||
The audio side of that contract now has a local matched-oracle study:
|
||||
[Audio provenance experiment](audio-provenance-experiment.md) embeds AudioSeal
|
||||
into synthetic carriers, verifies the copied-audio bitstream identity and a
|
||||
watermark verdict invariant across `remove_video_visible`, and measures the
|
||||
audio-path attacks separately. Its carriers are synthetic non-speech audio
|
||||
and its oracle is AudioSeal, so it validates the copy semantics, not provider
|
||||
audio marks. The audio and video arms are now first-class benchmark rows:
|
||||
[benchmark kernel](benchmark-kernel.md) accepts `media_type: audio` and
|
||||
`media_type: video` with the revision-pinned `audioseal` and `videoseal`
|
||||
adapters, and both cohorts carry matched negatives, codec, and degradation
|
||||
arms as strict manifest cases. The temporal layer is closed too: videoseal
|
||||
detection records carry per-frame bit accuracy, and
|
||||
[VideoSeal temporal evaluation](videoseal-temporal-evaluation.md) measures
|
||||
the aggregation matrix and the crf decay curve on synthetic and real clips.
|
||||
What remains open here is a wider real-content sweep and the learned temporal
|
||||
inpainting comparison.
|
||||
|
||||
The privacy-tiered C2PA resolution item is closed as a product decision:
|
||||
[the resolution research](c2pa-resolution-research.md) measured the five
|
||||
vendors' resolver contracts (payload-only, asset digest, or unpublished),
|
||||
grounded them in C2PA 2.2 section 18.10.5, and concluded that this tool
|
||||
resolves nothing over the network at any tier - querying a resolver tells
|
||||
the vendor someone is inspecting the specific content, which is a leak in
|
||||
the opposite direction of a removal tool's purpose. A registry guard test
|
||||
pins the published-API set so ecosystem changes surface consciously.
|
||||
|
||||
The same full-clip gate runs the public metadata-only path against its real MP4
|
||||
and verifies unchanged file size, decoded frames, stream properties, and AAC
|
||||
packets. A separate synthetic large-`mdat` test rejects full-source
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
# VideoSeal temporal evaluation
|
||||
|
||||
Development-only study, run on 2026-09-07 from
|
||||
`scripts/videoseal_temporal_study.py`, answering the three temporal questions
|
||||
the case-level benchmark verdict aggregates away: how decoded bit accuracy
|
||||
decays across H.264 quality, whether the frame-aggregation choice changes the
|
||||
outcome, and how per-frame accuracy moves in time on real provider clips
|
||||
versus synthetic carriers.
|
||||
|
||||
Carriers: the two synthetic cohort clips (moving gradient, moving texture)
|
||||
plus the committed real provider videos for Veo and Sora, standardized to a
|
||||
64-frame 256x256 prefix (first 64 frames, center-crop, INTER_AREA resize).
|
||||
Attacks: crf 18/23/28/32 re-encodes, 75 percent downscale, and frame-rate
|
||||
halving. Everything is local; the real clips are already-committed project
|
||||
fixtures with public test clearance.
|
||||
|
||||
## The TorchScript aggregation trap
|
||||
|
||||
The first matrix run produced identical numbers under all four aggregations,
|
||||
and that was a measurement artifact, not a finding: the standalone TorchScript
|
||||
build ignores the `aggregation` argument of `detect_video_and_aggregate` and
|
||||
returns the same thresholded bits for every choice (verified by checksum on a
|
||||
single artifact). The oracle now computes the aggregations itself from the raw
|
||||
per-frame logits using the exact upstream `extract_message` formulas - avg,
|
||||
squared_avg, and l1/l2-norm-weighted averages - and the recorded matrix comes
|
||||
from those. The formula port is pinned by a hand-computed unit test.
|
||||
|
||||
## Matrix (bit accuracy of the aggregated 256-bit decode)
|
||||
|
||||
| Carrier | crf18 | crf23 | crf28 | crf32 | scale .75 | fps .5 |
|
||||
| --- | ---: | ---: | ---: | ---: | ---: | ---: |
|
||||
| moving_gradient (clean 0.52) | 0.887 | 0.570 | 0.559 | 0.500 | 0.691 | 0.996 |
|
||||
| moving_texture (clean 0.48) | 0.602 | 0.559 | 0.531 | 0.586 | 0.879 | 0.980 |
|
||||
| real_veo (clean 0.52) | 0.801 | 0.723 | 0.621 | 0.574 | 1.000 | 0.855 |
|
||||
| real_sora (clean 0.51) | 0.996 | 1.000 | 0.992 | 0.902 | 1.000 | 0.996 |
|
||||
|
||||
Readings, not verdicts: the benchmark adapter's rule is 0.9 under plain avg.
|
||||
|
||||
- **Carrier content dominates.** The real Sora clip keeps the message through
|
||||
every attack including crf 32; the real Veo clip decays smoothly with
|
||||
quality; the smooth synthetic gradient dies at crf 23. Any "survives H.264"
|
||||
claim must name the carrier and the crf.
|
||||
- **Downscale is not an attack on real content.** 0.75 downscale plus re-encode
|
||||
scored a perfect 1.000 on both real clips - resize was a training
|
||||
augmentation, and the antialias resample appears to help the detector. On
|
||||
the synthetic gradient it still costs 0.31.
|
||||
- **Frame-rate halving is harmless.** Every carrier kept the message after
|
||||
dropping half the frames: the 256-bit secret repeats across keyframes, so
|
||||
temporal redundancy, not frame count, carries it.
|
||||
- **Aggregation choice matters at the margin.** Norm-weighted averaging
|
||||
recovered 2-6 points on damaged arms (moving_texture scale: 0.879 avg
|
||||
versus 0.941 l1/l2, crossing the 0.9 rule) but never rescued a collapsed
|
||||
case and never lifted a clean negative above chance. The kernel keeps plain
|
||||
avg as its canonical rule; the matrix is the evidence for when that choice
|
||||
is conservative.
|
||||
|
||||
Per-frame accuracy on the marked clips: real_sora sits at 0.988-1.000 on
|
||||
every frame, real_veo spans 0.547-0.910, moving_texture 0.457-0.961 - the
|
||||
temporal spread is itself carrier-dependent, and the kernel's
|
||||
`detection.temporal` block now records it for every videoseal case.
|
||||
|
||||
## Pins
|
||||
|
||||
Study script
|
||||
`sha256:682ccf14b21d6ac17bb6500a7efb364247eebae2d9e0e8895ffac33e6d294d4f`,
|
||||
oracle
|
||||
`sha256:5aa67ceaca1b0050727aa0a75440b1bf4c583435b3bf9e8ef2e97fadd46a035c`,
|
||||
case rows
|
||||
`sha256:5494c9bbe8a6085ecbd27fd9732dadf8d004dab7be159e905051e854dabe64aa`.
|
||||
Checkpoint: TorchScript `y_256b_img.jit`
|
||||
`sha256:5c7a4581c36fc6090aafdcfb3999123bae5172a4847f22e2da4e7fd1a39d1e1b`
|
||||
at upstream commit `870ca7f`. The kernel re-run of the video cohort with
|
||||
temporal records used kernel
|
||||
`sha256:657b0d7e35af75eb97b9bd9b81c15fcae754399f27999e08a5c28c494e69cf05`
|
||||
and produced case rows
|
||||
`sha256:435215e9a55589f6d83e34d62f34469c079db078a0a174ead64ffaa74af664c5`
|
||||
with unchanged verdicts.
|
||||
|
||||
## Follow-ups
|
||||
|
||||
- A crf sweep on more real carriers (the other committed provider clips) to
|
||||
widen the carrier-dominated picture beyond two samples.
|
||||
- Temporal inpainting comparison remains open from the original research map;
|
||||
the per-frame rows recorded by the kernel are the substrate for it.
|
||||
@@ -0,0 +1,76 @@
|
||||
# Watermark forgery study
|
||||
|
||||
Development-only study, run on 2026-09-07 from
|
||||
`scripts/watermark_forgery_study.py`, opening the forgery/remover-forensics
|
||||
direction from the original research map. It measures the four-state model
|
||||
(clean / marked / removed / forged) and the double-embedding question for both
|
||||
pinned oracles: does embedding message B over an A-marked artifact leave A,
|
||||
B, or mutual interference?
|
||||
|
||||
Carriers: the tone-stack audio carrier, the synthetic moving-gradient video
|
||||
clip, and the real committed Sora clip. Message B is the oracle's message
|
||||
generator at seed 8. Everything local; artifacts under `.local-eval/`.
|
||||
|
||||
## The double-embedding answer: the last writer wins, completely
|
||||
|
||||
| Cell | AudioSeal acc vs A | vs B | VideoSeal (gradient) vs A | vs B | VideoSeal (real Sora) vs A | vs B |
|
||||
| --- | ---: | ---: | ---: | ---: | ---: | ---: |
|
||||
| marked A | 1.000 | 0.44 | 1.000 | 0.47 | 1.000 | 0.47 |
|
||||
| removed | 0.50 (presence 0.28) | 0.44 | 0.57 | 0.48 | 1.000 | 0.47 |
|
||||
| forged B on clean | 0.44 | 1.000 | 0.47 | 1.000 | 0.47 | 1.000 |
|
||||
| **forged B on marked A** | **0.44** | **1.000** | **0.47** | **1.000** | **0.48** | **0.99** |
|
||||
| forged B on removed | 0.56 | 0.875 | 0.47 | 1.000 | 0.47 | 1.00 |
|
||||
|
||||
Embedding B over marked A reads as pure B: the original message decays to
|
||||
coin-flip accuracy under both schemes, on synthetic and real content alike.
|
||||
There is no mutual interference and no residue of A. With an unkeyed, openly
|
||||
published embedder this is the expected outcome - and now it is a measurement,
|
||||
not an assumption. The forensic consequence: payload attribution without key
|
||||
control is meaningless against an attacker who can run the same open embedder;
|
||||
only presence detection survives, and even that carries the attacker's
|
||||
payload. Provider schemes that rely on secret keys are a different question
|
||||
this study cannot answer.
|
||||
|
||||
Two boundary observations travel with the table:
|
||||
|
||||
- The removal transform is carrier-dependent, as the cohorts already showed:
|
||||
10 dB noise removes the tone stack (presence 0.28) and crf 23 removes the
|
||||
gradient clip (0.57), but the real Sora clip keeps a perfect 1.000 decode
|
||||
through the same crf 23 - the same number the temporal study recorded.
|
||||
- A forged row reads `detected` under the AudioSeal presence rule (the label
|
||||
carries the foreign payload) and `not_detected` under the VideoSeal
|
||||
message-accuracy rule. Both cohort fixtures encode their scheme's correct
|
||||
expected value, and the benchmark docs state the two rules side by side.
|
||||
|
||||
## Remover-trace probe (scoped first evidence)
|
||||
|
||||
High-to-low frequency band energy ratio on clean versus noise-removed audio:
|
||||
the tone stack jumps from 8.6e-05 to 2.3e-02 (a 269x trace), the pinkish
|
||||
carrier 0.016 to 0.039, and the white-noise carrier does not separate at all
|
||||
(0.330 to 0.332) - a noise carrier absorbs a noise remover. This detects the
|
||||
trace of THIS study's gaussian-noise removal only; it is not a general
|
||||
remover detector, and the probe JSON says so in its scope field.
|
||||
|
||||
## Cohort arms
|
||||
|
||||
The audio cohort (v2, 20 cases) and video cohort (v2, 15 cases) now carry the
|
||||
four states as strict manifest rows: `removed` rows where the transform
|
||||
measurably destroys the mark (audio: noise on tone stack; video: crf 23 on
|
||||
both synthetic carriers), and `forged` rows embedding message B with the
|
||||
scheme-correct expected verdict. Zero expected-result mismatches in both
|
||||
recorded runs.
|
||||
|
||||
## Pins
|
||||
|
||||
Study script
|
||||
`sha256:3985522c862ac44a0c7ef8aa02946a11701439a94d67f5867f00819f638c032b`,
|
||||
case rows
|
||||
`sha256:03d446c08a5daed623a4be2980e097bb8bbc0cd7a6d3ab3f931d25aa58570a16`.
|
||||
Oracles and checkpoints are pinned as in the kernel document.
|
||||
|
||||
## Follow-ups
|
||||
|
||||
- A keyed-scheme perspective needs a scheme with actual key control; WmForger
|
||||
and WMCopier diffusion-attack baselines remain the map's heavyweight item.
|
||||
- A general remover-trace detector would need features beyond one band ratio
|
||||
and validation against removals this project did not produce.
|
||||
Reference in new issue
Block a user